diff --git a/.storybook-s2/docs/Icons.jsx b/.storybook-s2/docs/Icons.jsx index af7554dc0cc..d16eec4f0c6 100644 --- a/.storybook-s2/docs/Icons.jsx +++ b/.storybook-s2/docs/Icons.jsx @@ -6,7 +6,7 @@ import {highlight} from './highlight' with {type: 'macro'}; export function Icons() { return ( -
+

Workflow icons diff --git a/.storybook-s2/docs/Illustrations.jsx b/.storybook-s2/docs/Illustrations.jsx index 506c1ddcc01..9acd33ca2f1 100644 --- a/.storybook-s2/docs/Illustrations.jsx +++ b/.storybook-s2/docs/Illustrations.jsx @@ -11,7 +11,7 @@ import { useState } from 'react'; export function Illustrations() { let [gradientStyle, setStyle] = useState('generic1'); return ( -
+

Illustrations diff --git a/.storybook-s2/docs/Intro.jsx b/.storybook-s2/docs/Intro.jsx index 21ee083c1bd..dddcc9e2fae 100644 --- a/.storybook-s2/docs/Intro.jsx +++ b/.storybook-s2/docs/Intro.jsx @@ -13,7 +13,7 @@ import {H2, H3, H4, P, Pre, Code, Strong, Link} from './typography'; export function Docs() { return ( -
+

UNSAFE Style Overrides

We highly discourage overriding the styles of React Spectrum components because it may break at any time when we change our implementation, making it difficult for you to update in the future. Consider using React Aria Components with our style macro to build a custom component with Spectrum styles instead.

-

That said, just like in React Spectrum v3, the UNSAFE_className and UNSAFE_style props are supported on Spectrum 2 components as last-resort escape hatches. However, unlike in v3, UNSAFE_classNames must be placed in a special UNSAFE_overrides CSS cascade layer. This guarentees that your overrides will always win over other styles on the page, no matter the order or specificity of the selector.

+

That said, just like in React Spectrum v3, the UNSAFE_className and UNSAFE_style props are supported on Spectrum 2 components as last-resort escape hatches.

{highlight(`/* YourComponent.tsx */
 import {Button} from '@react-spectrum/s2';
 import './YourComponent.css';
@@ -237,12 +237,10 @@ function YourComponent() {
   return ;
 }`)}
{highlight(`/* YourComponent.css */
-@layer UNSAFE_overrides {
-  /* Wrap all UNSAFE_className rules in this layer. */
-  .your-unsafe-class {
-    background: red;
-  }
-}`, 'CSS')}
+.your-unsafe-class { + background: red; +} +`, 'CSS')}
) diff --git a/.storybook-s2/docs/MDXLayout.jsx b/.storybook-s2/docs/MDXLayout.jsx index 79c47bd8322..cf1bf504eef 100644 --- a/.storybook-s2/docs/MDXLayout.jsx +++ b/.storybook-s2/docs/MDXLayout.jsx @@ -19,7 +19,7 @@ const mdxComponents = { export function MDXLayout({children}) { return ( -
+
{children} diff --git a/.storybook-s2/docs/Migrating.jsx b/.storybook-s2/docs/Migrating.jsx index 27af5bb2235..f4861be1bd7 100644 --- a/.storybook-s2/docs/Migrating.jsx +++ b/.storybook-s2/docs/Migrating.jsx @@ -3,7 +3,7 @@ import {P, Code, Pre, H3, H2, Link} from './typography'; export function Migrating() { return ( -
+

Migrating to Spectrum 2 @@ -44,6 +44,17 @@ export function Migrating() {
  • Add allowsMultipleExpanded to allow multiple Disclosure components to be expanded at once (previously default behavior)
  • +

    ActionBar

    +
      +
    • Remove ActionBarContainer and move ActionBar to renderActionBar prop of TableView or CardView
    • +
    • Update Item to ActionButton
    • +
    • Update root level onAction to be called via onPress on each ActionButton
    • +
    • Apply isDisabled directly on each ActionButton or ToggleButton instead of root level disabledKeys
    • +
    • Update key to be id (and keep key if rendered inside array.map)
    • +
    • Convert dynamic collections render function to items.map
    • +
    • [PENDING] Comment out buttonLabelBehavior (it has not been implemented yet)
    • +
    +

    ActionButton

    No updates needed.

    @@ -338,6 +349,17 @@ export function Migrating() {

    Switch

    No updates needed.

    +

    TableView

    +
      +
    • For Column and Row: Update key to be id (and keep key if rendered inside array.map)
    • +
    • For dynamic tables, pass a columns prop into Row
    • +
    • For Row: Update dynamic render function to pass in column instead of columnKey
    • +
    • [PENDING] Comment out UNSTABLE_allowsExpandableRows (it has not been implemented yet)
    • +
    • [PENDING] Comment out UNSTABLE_onExpandedChange (it has not been implemented yet)
    • +
    • [PENDING] Comment out UNSTABLE_expandedKeys (it has not been implemented yet)
    • +
    • [PENDING] Comment out UNSTABLE_defaultExpandedKeys (it has not been implemented yet)
    • +
    +

    Tabs

    • Inside TabList: Update Item to be Tab
    • diff --git a/.storybook-s2/docs/Release Notes.mdx b/.storybook-s2/docs/Release Notes.mdx index 4ee220f0615..af301ba9d8d 100644 --- a/.storybook-s2/docs/Release Notes.mdx +++ b/.storybook-s2/docs/Release Notes.mdx @@ -4,6 +4,25 @@ export default MDXLayout; # Release Notes +## v0.6.0 + +### New Components + +* [ActionBar](?path=/docs/actionbar--docs) + +### Updates + +* [Button](?path=/docs/button--docs): Add `genai` and `premium` gradient variants +* [Menu](?path=/docs/menu--docs): Add `hideLinkOutIcon` prop, update alignment of items in different sections, and show checkmark on selected items that are links. +* Added `staticColor="auto"` option to [ActionButton](?path=/docs/actionbutton--docs), [ToggleButton](?path=/docs/togglebutton--docs), [Divider](?path=/docs/divider--docs), [Meter](?path=/docs/meter--docs), [ProgressBar](?path=/docs/progressbar--docs), and [Link](?path=/docs/link--docs) +* [ContextualHelp](?path=/docs/contextualhelp--docs): Fix alignment with field labels +* [InlineAlert](?path=/docs/inlinealert--docs): Remove maximum width +* [CheckboxGroup](?path=/docs/checkboxgroup--docs): Fix `isRequired` within a Form + +### Codemods + +* Added TableView codemods + ## v0.5.0 In this release we have updated our Dialog and DialogTrigger APIs to improve layout flexibility for custom dialogs and popovers. Dialog has been split into four components: diff --git a/.storybook-s2/docs/StyleMacro.jsx b/.storybook-s2/docs/StyleMacro.jsx index 8501852a82b..9e242ad6f79 100644 --- a/.storybook-s2/docs/StyleMacro.jsx +++ b/.storybook-s2/docs/StyleMacro.jsx @@ -6,7 +6,7 @@ import {Colors} from './Colors'; export function StyleMacro() { return ( -
      +
      +
      ); @@ -549,3 +550,31 @@ function StockRow(props) { function StockCell(props) { return ; } + +function AutocompleteExample() { + let {contains} = useFilter({sensitivity: 'base'}); + return ( +
      + + + +
      + +
      +
      +
      + + + + + + + + {item => } + + +
      +
      +
      + ); +} diff --git a/examples/rsp-cra-18/package.json b/examples/rsp-cra-18/package.json index bac37f332ce..3518a67d985 100644 --- a/examples/rsp-cra-18/package.json +++ b/examples/rsp-cra-18/package.json @@ -21,6 +21,7 @@ "@types/react": "^18", "@types/react-dom": "^18", "react": "^18.1.0", + "react-aria-components": "latest", "react-dom": "^18.1.0", "react-scripts": "5.0.1", "typescript": "5.0.4", diff --git a/examples/rsp-cra-18/src/App.tsx b/examples/rsp-cra-18/src/App.tsx index e2038b32dc2..0abebf7ad5d 100644 --- a/examples/rsp-cra-18/src/App.tsx +++ b/examples/rsp-cra-18/src/App.tsx @@ -15,6 +15,7 @@ import StatusExamples from './sections/StatusExamples'; import ContentExamples from './sections/ContentExamples'; import PickerExamples from './sections/PickerExamples'; import DragAndDropExamples from './sections/DragAndDropExamples'; +import {AutocompleteExample} from './AutocompleteExample'; let columns = [ {name: 'Foo', key: 'foo'}, @@ -61,6 +62,7 @@ function App() { } + diff --git a/examples/rsp-cra-18/src/AutocompleteExample.tsx b/examples/rsp-cra-18/src/AutocompleteExample.tsx new file mode 100644 index 00000000000..4e7a9a443fc --- /dev/null +++ b/examples/rsp-cra-18/src/AutocompleteExample.tsx @@ -0,0 +1,39 @@ +import {UNSTABLE_Autocomplete as Autocomplete, Input, Label, Menu, MenuItem, SearchField, Text, useFilter} from 'react-aria-components' +import {classNames} from '@react-spectrum/utils'; +import styles from './autocomplete.css'; + +interface AutocompleteItem { + id: string, + name: string +} + +let items: AutocompleteItem[] = [{id: '1', name: 'Foo'}, {id: '2', name: 'Bar'}, {id: '3', name: 'Baz'}]; + +export function AutocompleteExample() { + let {contains} = useFilter({sensitivity: 'base'}); + + return ( + +
      + + + + Please select an option below. + + + {item => ( + classNames(styles, 'item', { + focused: isFocused, + selected: isSelected, + open: isOpen + })}> + {item.name} + + )} + +
      +
      + ); +} diff --git a/examples/rsp-cra-18/src/autocomplete.css b/examples/rsp-cra-18/src/autocomplete.css new file mode 100644 index 00000000000..642e45af9c8 --- /dev/null +++ b/examples/rsp-cra-18/src/autocomplete.css @@ -0,0 +1,44 @@ +.react-aria-Menu { + display: block; + min-width: 150px; + width: fit-content; + margin: 4px 0 0 0; + border: 1px solid gray; + background: lightgray; + padding: 0; + list-style: none; + overflow-y: auto; + height: 100px; +} + +.item { + padding: 2px 5px; + outline: none; + cursor: default; + color: black; + background: transparent; +} + +.item[data-disabled] { + opacity: 0.4; +} + +.item.focused { + background: gray; + color: white; +} + +.item.open:not(.focused) { + background: lightslategray; + color: white; +} + +.item.item.hovered { + background: lightsalmon; + color: white; +} + +.item.selected { + background: purple; + color: white; +} diff --git a/examples/rsp-cra-18/tsconfig.json b/examples/rsp-cra-18/tsconfig.json index a273b0cfc0e..d2befe045b8 100644 --- a/examples/rsp-cra-18/tsconfig.json +++ b/examples/rsp-cra-18/tsconfig.json @@ -21,6 +21,7 @@ "jsx": "react-jsx" }, "include": [ - "src" + "src", + "typings.d.ts" ] } diff --git a/examples/rsp-cra-18/typings.d.ts b/examples/rsp-cra-18/typings.d.ts new file mode 100644 index 00000000000..cbe652dbe00 --- /dev/null +++ b/examples/rsp-cra-18/typings.d.ts @@ -0,0 +1 @@ +declare module "*.css"; diff --git a/examples/rsp-next-ts/.yarn/install-state.gz b/examples/rsp-next-ts/.yarn/install-state.gz new file mode 100644 index 00000000000..6ab4b8cdc9c Binary files /dev/null and b/examples/rsp-next-ts/.yarn/install-state.gz differ diff --git a/examples/rsp-next-ts/components/AutocompleteExample.tsx b/examples/rsp-next-ts/components/AutocompleteExample.tsx new file mode 100644 index 00000000000..f333a2442f3 --- /dev/null +++ b/examples/rsp-next-ts/components/AutocompleteExample.tsx @@ -0,0 +1,40 @@ +import {UNSTABLE_Autocomplete as Autocomplete, Input, Label, Menu, MenuItem, SearchField, Text, useFilter} from 'react-aria-components' +import {classNames} from '@react-spectrum/utils'; +import React from 'react'; +import styles from './autocomplete.module.css'; + +interface AutocompleteItem { + id: string, + name: string +} + +let items: AutocompleteItem[] = [{id: '1', name: 'Foo'}, {id: '2', name: 'Bar'}, {id: '3', name: 'Baz'}]; + +export function AutocompleteExample() { + let {contains} = useFilter({sensitivity: 'base'}); + + return ( + +
      + + + + Please select an option below. + + + {item => ( + classNames(styles, 'item', { + focused: isFocused, + selected: isSelected, + open: isOpen + })}> + {item.name} + + )} + +
      +
      + ); +} diff --git a/examples/rsp-next-ts/components/autocomplete.module.css b/examples/rsp-next-ts/components/autocomplete.module.css new file mode 100644 index 00000000000..a1060fc4476 --- /dev/null +++ b/examples/rsp-next-ts/components/autocomplete.module.css @@ -0,0 +1,44 @@ +.menu { + display: block; + min-width: 150px; + width: fit-content; + margin: 4px 0 0 0; + border: 1px solid gray; + background: lightgray; + padding: 0; + list-style: none; + overflow-y: auto; + height: 100px; +} + +.item { + padding: 2px 5px; + outline: none; + cursor: default; + color: black; + background: transparent; +} + +.item[data-disabled] { + opacity: 0.4; +} + +.item.focused { + background: gray; + color: white; +} + +.item.open:not(.focused) { + background: lightslategray; + color: white; +} + +.item.item.hovered { + background: lightsalmon; + color: white; +} + +.item.selected { + background: purple; + color: white; +} diff --git a/examples/rsp-next-ts/package.json b/examples/rsp-next-ts/package.json index 657e756b035..a9b9552f17b 100644 --- a/examples/rsp-next-ts/package.json +++ b/examples/rsp-next-ts/package.json @@ -22,6 +22,7 @@ "glob": "^10.3.12", "next": "^13.4.1", "react": "^18.2.0", + "react-aria-components": "latest", "react-dom": "^18.2.0" }, "devDependencies": { diff --git a/examples/rsp-next-ts/pages/index.tsx b/examples/rsp-next-ts/pages/index.tsx index d3cb776f1fd..21508ba7fbb 100644 --- a/examples/rsp-next-ts/pages/index.tsx +++ b/examples/rsp-next-ts/pages/index.tsx @@ -83,6 +83,7 @@ import { DisclosureTitle, DisclosurePanel } from "@adobe/react-spectrum"; +import {AutocompleteExample} from "../components/AutocompleteExample"; import Edit from "@spectrum-icons/workflow/Edit"; import NotFound from "@spectrum-icons/illustrations/NotFound"; import Section from "../components/Section"; @@ -240,6 +241,7 @@ export default function Home() { } +
      diff --git a/examples/rsp-next-ts/tsconfig.json b/examples/rsp-next-ts/tsconfig.json index 73e4afb1513..dc358701300 100644 --- a/examples/rsp-next-ts/tsconfig.json +++ b/examples/rsp-next-ts/tsconfig.json @@ -16,6 +16,6 @@ "incremental": true, "downlevelIteration": true }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "typing.d.ts"], "exclude": ["node_modules"] } diff --git a/examples/rsp-next-ts/typings.d.ts b/examples/rsp-next-ts/typings.d.ts new file mode 100644 index 00000000000..1ee54e47133 --- /dev/null +++ b/examples/rsp-next-ts/typings.d.ts @@ -0,0 +1 @@ +declare module "*.modules.css"; diff --git a/package.json b/package.json index e544c745549..3f5753d2195 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,8 @@ "bumpVersions": "node scripts/bumpVersions.js", "test:parcel": "jest --testMatch '**/*.parceltest.{ts,tsx}'", "blc:local": "npx git+ssh://git@github.com:ktabors/rsp-blc.git -u http://localhost:1234/", - "blc:prod": "npx git+ssh://git@github.com:ktabors/rsp-blc.git -u https://react-spectrum.adobe.com/" + "blc:prod": "npx git+ssh://git@github.com:ktabors/rsp-blc.git -u https://react-spectrum.adobe.com/", + "createRssFeed": "node scripts/createFeed.mjs" }, "workspaces": [ "packages/react-stately", @@ -210,6 +211,7 @@ "verdaccio": "^5.13.0", "walk-object": "^4.0.0", "wsrun": "^5.0.0", + "xml": "^1.0.1", "yargs": "^17.2.1" }, "resolutions": { diff --git a/packages/@adobe/react-spectrum/package.json b/packages/@adobe/react-spectrum/package.json index 6cedff82ea6..02e459f9e0d 100644 --- a/packages/@adobe/react-spectrum/package.json +++ b/packages/@adobe/react-spectrum/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/react-spectrum", - "version": "3.38.1", + "version": "3.39.0", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -38,65 +38,65 @@ }, "dependencies": { "@internationalized/string": "^3.2.5", - "@react-aria/i18n": "^3.12.4", + "@react-aria/i18n": "^3.12.5", "@react-aria/ssr": "^3.9.7", - "@react-aria/utils": "^3.26.0", - "@react-aria/visually-hidden": "^3.8.18", - "@react-spectrum/accordion": "^3.0.1", - "@react-spectrum/actionbar": "^3.6.2", - "@react-spectrum/actiongroup": "^3.10.10", - "@react-spectrum/avatar": "^3.0.17", - "@react-spectrum/badge": "^3.1.18", - "@react-spectrum/breadcrumbs": "^3.9.12", - "@react-spectrum/button": "^3.16.9", - "@react-spectrum/buttongroup": "^3.6.17", - "@react-spectrum/calendar": "^3.5.0", - "@react-spectrum/checkbox": "^3.9.11", - "@react-spectrum/color": "^3.0.2", - "@react-spectrum/combobox": "^3.14.0", - "@react-spectrum/contextualhelp": "^3.6.16", - "@react-spectrum/datepicker": "^3.11.0", - "@react-spectrum/dialog": "^3.8.16", - "@react-spectrum/divider": "^3.5.18", - "@react-spectrum/dnd": "^3.5.0", - "@react-spectrum/dropzone": "^3.0.6", - "@react-spectrum/filetrigger": "^3.0.6", - "@react-spectrum/form": "^3.7.10", - "@react-spectrum/icon": "^3.8.0", - "@react-spectrum/illustratedmessage": "^3.5.5", - "@react-spectrum/image": "^3.5.6", - "@react-spectrum/inlinealert": "^3.2.10", - "@react-spectrum/labeledvalue": "^3.1.18", - "@react-spectrum/layout": "^3.6.10", - "@react-spectrum/link": "^3.6.12", - "@react-spectrum/list": "^3.9.0", - "@react-spectrum/listbox": "^3.14.0", - "@react-spectrum/menu": "^3.21.0", - "@react-spectrum/meter": "^3.5.5", - "@react-spectrum/numberfield": "^3.9.8", - "@react-spectrum/overlays": "^5.7.0", - "@react-spectrum/picker": "^3.15.4", - "@react-spectrum/progress": "^3.7.11", - "@react-spectrum/provider": "^3.10.0", - "@react-spectrum/radio": "^3.7.11", - "@react-spectrum/searchfield": "^3.8.11", - "@react-spectrum/slider": "^3.7.0", - "@react-spectrum/statuslight": "^3.5.17", - "@react-spectrum/switch": "^3.5.10", - "@react-spectrum/table": "^3.15.0", - "@react-spectrum/tabs": "^3.8.15", - "@react-spectrum/tag": "^3.2.11", - "@react-spectrum/text": "^3.5.10", - "@react-spectrum/textfield": "^3.12.7", - "@react-spectrum/theme-dark": "^3.5.14", - "@react-spectrum/theme-default": "^3.5.14", - "@react-spectrum/theme-light": "^3.4.14", - "@react-spectrum/tooltip": "^3.7.0", - "@react-spectrum/view": "^3.6.14", - "@react-spectrum/well": "^3.4.18", - "@react-stately/collections": "^3.12.0", - "@react-stately/data": "^3.12.0", - "@react-types/shared": "^3.26.0", + "@react-aria/utils": "^3.27.0", + "@react-aria/visually-hidden": "^3.8.19", + "@react-spectrum/accordion": "^3.0.2", + "@react-spectrum/actionbar": "^3.6.3", + "@react-spectrum/actiongroup": "^3.10.11", + "@react-spectrum/avatar": "^3.0.18", + "@react-spectrum/badge": "^3.1.19", + "@react-spectrum/breadcrumbs": "^3.9.13", + "@react-spectrum/button": "^3.16.10", + "@react-spectrum/buttongroup": "^3.6.18", + "@react-spectrum/calendar": "^3.6.0", + "@react-spectrum/checkbox": "^3.9.12", + "@react-spectrum/color": "^3.0.3", + "@react-spectrum/combobox": "^3.14.1", + "@react-spectrum/contextualhelp": "^3.6.17", + "@react-spectrum/datepicker": "^3.12.0", + "@react-spectrum/dialog": "^3.8.17", + "@react-spectrum/divider": "^3.5.19", + "@react-spectrum/dnd": "^3.5.1", + "@react-spectrum/dropzone": "^3.0.7", + "@react-spectrum/filetrigger": "^3.0.7", + "@react-spectrum/form": "^3.7.11", + "@react-spectrum/icon": "^3.8.1", + "@react-spectrum/illustratedmessage": "^3.5.6", + "@react-spectrum/image": "^3.5.7", + "@react-spectrum/inlinealert": "^3.2.11", + "@react-spectrum/labeledvalue": "^3.1.19", + "@react-spectrum/layout": "^3.6.11", + "@react-spectrum/link": "^3.6.13", + "@react-spectrum/list": "^3.9.1", + "@react-spectrum/listbox": "^3.14.1", + "@react-spectrum/menu": "^3.21.1", + "@react-spectrum/meter": "^3.5.6", + "@react-spectrum/numberfield": "^3.9.9", + "@react-spectrum/overlays": "^5.7.1", + "@react-spectrum/picker": "^3.15.5", + "@react-spectrum/progress": "^3.7.12", + "@react-spectrum/provider": "^3.10.1", + "@react-spectrum/radio": "^3.7.12", + "@react-spectrum/searchfield": "^3.8.12", + "@react-spectrum/slider": "^3.7.1", + "@react-spectrum/statuslight": "^3.5.18", + "@react-spectrum/switch": "^3.5.11", + "@react-spectrum/table": "^3.15.1", + "@react-spectrum/tabs": "^3.8.16", + "@react-spectrum/tag": "^3.2.12", + "@react-spectrum/text": "^3.5.11", + "@react-spectrum/textfield": "^3.12.8", + "@react-spectrum/theme-dark": "^3.5.15", + "@react-spectrum/theme-default": "^3.5.15", + "@react-spectrum/theme-light": "^3.4.15", + "@react-spectrum/tooltip": "^3.7.1", + "@react-spectrum/view": "^3.6.15", + "@react-spectrum/well": "^3.4.19", + "@react-stately/collections": "^3.12.1", + "@react-stately/data": "^3.12.1", + "@react-types/shared": "^3.27.0", "client-only": "^0.0.1" }, "publishConfig": { diff --git a/packages/@adobe/spectrum-css-temp/components/menu/index.css b/packages/@adobe/spectrum-css-temp/components/menu/index.css index d15fbdbb353..d9f4477afd5 100644 --- a/packages/@adobe/spectrum-css-temp/components/menu/index.css +++ b/packages/@adobe/spectrum-css-temp/components/menu/index.css @@ -222,6 +222,7 @@ governing permissions and limitations under the License. justify-self: end; align-self: flex-start; padding-inline-start: var(--spectrum-global-dimension-size-250); + box-sizing: content-box; } .spectrum-Menu-icon { grid-area: icon; diff --git a/packages/@adobe/spectrum-css-temp/components/tags/index.css b/packages/@adobe/spectrum-css-temp/components/tags/index.css index 53a3b4e7cbf..119df457163 100644 --- a/packages/@adobe/spectrum-css-temp/components/tags/index.css +++ b/packages/@adobe/spectrum-css-temp/components/tags/index.css @@ -46,6 +46,7 @@ governing permissions and limitations under the License. display: inline-grid; align-items: center; + vertical-align: middle; box-sizing: border-box; position: relative; cursor: default; diff --git a/packages/@internationalized/date/docs/CalendarDate.mdx b/packages/@internationalized/date/docs/CalendarDate.mdx index dea35217c58..ab51b533a97 100644 --- a/packages/@internationalized/date/docs/CalendarDate.mdx +++ b/packages/@internationalized/date/docs/CalendarDate.mdx @@ -315,6 +315,12 @@ startOfWeek(date, 'en-US'); // 2022-01-30 startOfWeek(date, 'fr-FR'); // 2022-01-31 ``` +You can also provide an optional `firstDayOfWeek` argument to override the default first day of the week determined by the locale. It accepts a week day abbreviation, e.g. `sun`, `mon`, `tue`, etc. + +```tsx +startOfWeek(date, 'en-US', 'mon'); // 2022-01-31 +``` + ### Day of week The function returns the day of the week for the given date and locale. Days are numbered from zero to six, where zero is the first day of the week in the given locale. For example, in the United States, the first day of the week is Sunday, but in France it is Monday. @@ -328,6 +334,12 @@ getDayOfWeek(date, 'en-US'); // 0 getDayOfWeek(date, 'fr-FR'); // 6 ``` +You can also provide an optional `firstDayOfWeek` argument to override the default first day of the week determined by the locale. It accepts a week day abbreviation, e.g. `sun`, `mon`, `tue`, etc. + +```tsx +getDayOfWeek(date, 'en-US', 'mon'); // 6 +``` + ### Weekdays and weekends The and functions can be used to determine if a date is weekday or weekend respectively. This depends on the locale. For example, in the United States, weekends are Saturday and Sunday, but in Israel they are Friday and Saturday. @@ -356,3 +368,9 @@ let date = new CalendarDate(2021, 1, 1); getWeeksInMonth(date, 'en-US'); // 6 getWeeksInMonth(date, 'fr-FR'); // 5 ``` + +You can also provide an optional `firstDayOfWeek` argument to override the default first day of the week determined by the locale. It accepts a week day abbreviation, e.g. `sun`, `mon`, `tue`, etc. + +```tsx +getWeeksInMonth(date, 'en-US', 'mon'); // 5 +``` diff --git a/packages/@internationalized/date/docs/CalendarDateTime.mdx b/packages/@internationalized/date/docs/CalendarDateTime.mdx index 946e126cef6..3796777ed49 100644 --- a/packages/@internationalized/date/docs/CalendarDateTime.mdx +++ b/packages/@internationalized/date/docs/CalendarDateTime.mdx @@ -367,6 +367,12 @@ startOfWeek(date, 'en-US'); // 2022-01-30T09:45 startOfWeek(date, 'fr-FR'); // 2022-01-31T09:45 ``` +You can also provide an optional `firstDayOfWeek` argument to override the default first day of the week determined by the locale. It accepts a week day abbreviation, e.g. `sun`, `mon`, `tue`, etc. + +```tsx +startOfWeek(date, 'en-US', 'mon'); // 2022-01-31T09:45 +``` + ### Day of week The function returns the day of the week for the given date and locale. Days are numbered from zero to six, where zero is the first day of the week in the given locale. For example, in the United States, the first day of the week is Sunday, but in France it is Monday. @@ -380,6 +386,12 @@ getDayOfWeek(date, 'en-US'); // 0 getDayOfWeek(date, 'fr-FR'); // 6 ``` +You can also provide an optional `firstDayOfWeek` argument to override the default first day of the week determined by the locale. It accepts a week day abbreviation, e.g. `sun`, `mon`, `tue`, etc. + +```tsx +getDayOfWeek(date, 'en-US', 'mon'); // 6 +``` + ### Weekdays and weekends The and functions can be used to determine if a date is weekday or weekend respectively. This depends on the locale. For example, in the United States, weekends are Saturday and Sunday, but in Israel they are Friday and Saturday. @@ -408,3 +420,9 @@ let date = new CalendarDateTime(2021, 1, 1, 8, 30); getWeeksInMonth(date, 'en-US'); // 6 getWeeksInMonth(date, 'fr-FR'); // 5 ``` + +You can also provide an optional `firstDayOfWeek` argument to override the default first day of the week determined by the locale. It accepts a week day abbreviation, e.g. `sun`, `mon`, `tue`, etc. + +```tsx +getWeeksInMonth(date, 'en-US', 'mon'); // 5 +``` diff --git a/packages/@internationalized/date/docs/ZonedDateTime.mdx b/packages/@internationalized/date/docs/ZonedDateTime.mdx index 1bb2c0ef398..543f89db6dd 100644 --- a/packages/@internationalized/date/docs/ZonedDateTime.mdx +++ b/packages/@internationalized/date/docs/ZonedDateTime.mdx @@ -475,6 +475,12 @@ startOfWeek(date, 'en-US'); // 2022-01-30T09:45[America/Los_Angeles] startOfWeek(date, 'fr-FR'); // 2022-01-31T09:45[America/Los_Angeles] ``` +You can also provide an optional `firstDayOfWeek` argument to override the default first day of the week determined by the locale. It accepts a week day abbreviation, e.g. `sun`, `mon`, `tue`, etc. + +```tsx +startOfWeek(date, 'en-US', 'mon'); // 2022-01-31T09:45[America/Los_Angeles] +``` + ### Day of week The function returns the day of the week for the given date and locale. Days are numbered from zero to six, where zero is the first day of the week in the given locale. For example, in the United States, the first day of the week is Sunday, but in France it is Monday. @@ -488,6 +494,12 @@ getDayOfWeek(date, 'en-US'); // 0 getDayOfWeek(locale, 'fr-FR'); // 6 ``` +You can also provide an optional `firstDayOfWeek` argument to override the default first day of the week determined by the locale. It accepts a week day abbreviation, e.g. `sun`, `mon`, `tue`, etc. + +```tsx +getDayOfWeek(date, 'en-US', 'mon'); // 6 +``` + ### Weekdays and weekends The and functions can be used to determine if a date is weekday or weekend respectively. This depends on the locale. For example, in the United States, weekends are Saturday and Sunday, but in Israel they are Friday and Saturday. @@ -516,3 +528,9 @@ let date = parseZonedDateTime('2023-01-01T08:30[America/Los_Angeles]'); getWeeksInMonth(date, 'en-US'); // 5 getWeeksInMonth(date, 'fr-FR'); // 6 ``` + +You can also provide an optional `firstDayOfWeek` argument to override the default first day of the week determined by the locale. It accepts a week day abbreviation, e.g. `sun`, `mon`, `tue`, etc. + +```tsx +getWeeksInMonth(date, 'en-US', 'mon'); // 6 +``` diff --git a/packages/@internationalized/date/package.json b/packages/@internationalized/date/package.json index d6e2d68acfa..e2f30222553 100644 --- a/packages/@internationalized/date/package.json +++ b/packages/@internationalized/date/package.json @@ -1,6 +1,6 @@ { "name": "@internationalized/date", - "version": "3.6.0", + "version": "3.7.0", "description": "Internationalized calendar, date, and time manipulation utilities", "license": "Apache-2.0", "main": "dist/main.js", diff --git a/packages/@internationalized/date/src/queries.ts b/packages/@internationalized/date/src/queries.ts index 1a50aab51e1..dcf3e8ad0af 100644 --- a/packages/@internationalized/date/src/queries.ts +++ b/packages/@internationalized/date/src/queries.ts @@ -64,17 +64,30 @@ export function isToday(date: DateValue, timeZone: string): boolean { return isSameDay(date, today(timeZone)); } +const DAY_MAP = { + sun: 0, + mon: 1, + tue: 2, + wed: 3, + thu: 4, + fri: 5, + sat: 6 +}; + +type DayOfWeek = 'sun' | 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat'; + /** * Returns the day of week for the given date and locale. Days are numbered from zero to six, * where zero is the first day of the week in the given locale. For example, in the United States, * the first day of the week is Sunday, but in France it is Monday. */ -export function getDayOfWeek(date: DateValue, locale: string): number { +export function getDayOfWeek(date: DateValue, locale: string, firstDayOfWeek?: DayOfWeek): number { let julian = date.calendar.toJulianDay(date); // If julian is negative, then julian % 7 will be negative, so we adjust // accordingly. Julian day 0 is Monday. - let dayOfWeek = Math.ceil(julian + 1 - getWeekStart(locale)) % 7; + let weekStart = firstDayOfWeek ? DAY_MAP[firstDayOfWeek] : getWeekStart(locale); + let dayOfWeek = Math.ceil(julian + 1 - weekStart) % 7; if (dayOfWeek < 0) { dayOfWeek += 7; } @@ -181,22 +194,22 @@ export function getMinimumDayInMonth(date: AnyCalendarDate) { } /** Returns the first date of the week for the given date and locale. */ -export function startOfWeek(date: ZonedDateTime, locale: string): ZonedDateTime; -export function startOfWeek(date: CalendarDateTime, locale: string): CalendarDateTime; -export function startOfWeek(date: CalendarDate, locale: string): CalendarDate; -export function startOfWeek(date: DateValue, locale: string): DateValue; -export function startOfWeek(date: DateValue, locale: string): DateValue { - let dayOfWeek = getDayOfWeek(date, locale); +export function startOfWeek(date: ZonedDateTime, locale: string, firstDayOfWeek?: DayOfWeek): ZonedDateTime; +export function startOfWeek(date: CalendarDateTime, locale: string, firstDayOfWeek?: DayOfWeek): CalendarDateTime; +export function startOfWeek(date: CalendarDate, locale: string, firstDayOfWeek?: DayOfWeek): CalendarDate; +export function startOfWeek(date: DateValue, locale: string, firstDayOfWeek?: DayOfWeek): DateValue; +export function startOfWeek(date: DateValue, locale: string, firstDayOfWeek?: DayOfWeek): DateValue { + let dayOfWeek = getDayOfWeek(date, locale, firstDayOfWeek); return date.subtract({days: dayOfWeek}); } /** Returns the last date of the week for the given date and locale. */ -export function endOfWeek(date: ZonedDateTime, locale: string): ZonedDateTime; -export function endOfWeek(date: CalendarDateTime, locale: string): CalendarDateTime; -export function endOfWeek(date: CalendarDate, locale: string): CalendarDate; -export function endOfWeek(date: DateValue, locale: string): DateValue; -export function endOfWeek(date: DateValue, locale: string): DateValue { - return startOfWeek(date, locale).add({days: 6}); +export function endOfWeek(date: ZonedDateTime, locale: string, firstDayOfWeek?: DayOfWeek): ZonedDateTime; +export function endOfWeek(date: CalendarDateTime, locale: string, firstDayOfWeek?: DayOfWeek): CalendarDateTime; +export function endOfWeek(date: CalendarDate, locale: string, firstDayOfWeek?: DayOfWeek): CalendarDate; +export function endOfWeek(date: DateValue, locale: string, firstDayOfWeek?: DayOfWeek): DateValue; +export function endOfWeek(date: DateValue, locale: string, firstDayOfWeek?: DayOfWeek): DateValue { + return startOfWeek(date, locale, firstDayOfWeek).add({days: 6}); } const cachedRegions = new Map(); @@ -233,9 +246,9 @@ function getWeekStart(locale: string): number { } /** Returns the number of weeks in the given month and locale. */ -export function getWeeksInMonth(date: DateValue, locale: string): number { +export function getWeeksInMonth(date: DateValue, locale: string, firstDayOfWeek?: DayOfWeek): number { let days = date.calendar.getDaysInMonth(date); - return Math.ceil((getDayOfWeek(startOfMonth(date), locale) + days) / 7); + return Math.ceil((getDayOfWeek(startOfMonth(date), locale, firstDayOfWeek) + days) / 7); } /** Returns the lesser of the two provider dates. */ diff --git a/packages/@internationalized/date/tests/queries.test.js b/packages/@internationalized/date/tests/queries.test.js index 8db66b89e58..e13b0e03127 100644 --- a/packages/@internationalized/date/tests/queries.test.js +++ b/packages/@internationalized/date/tests/queries.test.js @@ -246,6 +246,13 @@ describe('queries', function () { it('should return the day of week in fr', function () { expect(getDayOfWeek(new CalendarDate(2021, 8, 4), 'fr')).toBe(2); }); + + it('should return the day of the week with a custom firstDayOfWeek', function () { + expect(getDayOfWeek(new CalendarDate(2021, 8, 4), 'en-US', 'mon')).toBe(2); + expect(getDayOfWeek(new CalendarDate(2021, 8, 4), 'en-US', 'tue')).toBe(1); + expect(getDayOfWeek(new CalendarDate(2021, 8, 4), 'fr-FR', 'mon')).toBe(2); + expect(getDayOfWeek(new CalendarDate(2021, 8, 4), 'fr-FR', 'tue')).toBe(1); + }); }); describe('startOfWeek', function () { @@ -256,16 +263,28 @@ describe('queries', function () { it('should return the start of week in fr-FR', function () { expect(startOfWeek(new CalendarDate(2021, 8, 4), 'fr-FR')).toEqual(new CalendarDate(2021, 8, 2)); }); + + it('should return the start of the week with a custom firstDayOfWeek', function () { + expect(startOfWeek(new CalendarDate(2021, 8, 4), 'en-US', 'mon')).toEqual(new CalendarDate(2021, 8, 2)); + expect(startOfWeek(new CalendarDate(2021, 8, 4), 'en-US', 'tue')).toEqual(new CalendarDate(2021, 8, 3)); + expect(startOfWeek(new CalendarDate(2021, 8, 4), 'fr-FR', 'sun')).toEqual(new CalendarDate(2021, 8, 1)); + }); }); describe('endOfWeek', function () { - it('should return the start of week in en-US', function () { + it('should return the end of week in en-US', function () { expect(endOfWeek(new CalendarDate(2021, 8, 4), 'en-US')).toEqual(new CalendarDate(2021, 8, 7)); }); - it('should return the start of week in fr-FR', function () { + it('should return the end of week in fr-FR', function () { expect(endOfWeek(new CalendarDate(2021, 8, 4), 'fr-FR')).toEqual(new CalendarDate(2021, 8, 8)); }); + + it('should return the end of the week with a custom firstDayOfWeek', function () { + expect(endOfWeek(new CalendarDate(2021, 8, 4), 'en-US', 'mon')).toEqual(new CalendarDate(2021, 8, 8)); + expect(endOfWeek(new CalendarDate(2021, 8, 4), 'en-US', 'tue')).toEqual(new CalendarDate(2021, 8, 9)); + expect(endOfWeek(new CalendarDate(2021, 8, 4), 'fr-FR', 'sun')).toEqual(new CalendarDate(2021, 8, 7)); + }); }); describe('getWeeksInMonth', function () { @@ -280,6 +299,13 @@ describe('queries', function () { it('should work for other calendars', function () { expect(getWeeksInMonth(new CalendarDate(new EthiopicCalendar(), 2013, 13, 4), 'en-US')).toBe(1); }); + + it('should support custom firstDayOfWeek', function () { + expect(getWeeksInMonth(new CalendarDate(2021, 8, 4), 'en-US', 'sun')).toBe(5); + expect(getWeeksInMonth(new CalendarDate(2021, 8, 4), 'en-US', 'mon')).toBe(6); + expect(getWeeksInMonth(new CalendarDate(2021, 10, 4), 'en-US', 'sun')).toBe(6); + expect(getWeeksInMonth(new CalendarDate(2021, 10, 4), 'en-US', 'mon')).toBe(5); + }); }); describe('getMinimumMonthInYear', function () { diff --git a/packages/@react-aria/accordion/package.json b/packages/@react-aria/accordion/package.json index 96a3b9feb49..2f7f7cc92d4 100644 --- a/packages/@react-aria/accordion/package.json +++ b/packages/@react-aria/accordion/package.json @@ -1,6 +1,6 @@ { "name": "@react-aria/accordion", - "version": "3.0.0-alpha.36", + "version": "3.0.0-alpha.37", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -22,12 +22,12 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/button": "^3.11.0", - "@react-aria/selection": "^3.21.0", - "@react-aria/utils": "^3.26.0", - "@react-stately/tree": "^3.8.6", - "@react-types/accordion": "3.0.0-alpha.25", - "@react-types/shared": "^3.26.0", + "@react-aria/button": "^3.11.1", + "@react-aria/selection": "^3.22.0", + "@react-aria/utils": "^3.27.0", + "@react-stately/tree": "^3.8.7", + "@react-types/accordion": "3.0.0-alpha.26", + "@react-types/shared": "^3.27.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { diff --git a/packages/@react-aria/actiongroup/package.json b/packages/@react-aria/actiongroup/package.json index f0775f4d567..8a33bfe9bb0 100644 --- a/packages/@react-aria/actiongroup/package.json +++ b/packages/@react-aria/actiongroup/package.json @@ -1,6 +1,6 @@ { "name": "@react-aria/actiongroup", - "version": "3.7.11", + "version": "3.7.12", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -22,13 +22,13 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/focus": "^3.19.0", - "@react-aria/i18n": "^3.12.4", - "@react-aria/interactions": "^3.22.5", - "@react-aria/utils": "^3.26.0", - "@react-stately/list": "^3.11.1", - "@react-types/actiongroup": "^3.4.13", - "@react-types/shared": "^3.26.0", + "@react-aria/focus": "^3.19.1", + "@react-aria/i18n": "^3.12.5", + "@react-aria/interactions": "^3.23.0", + "@react-aria/utils": "^3.27.0", + "@react-stately/list": "^3.11.2", + "@react-types/actiongroup": "^3.4.14", + "@react-types/shared": "^3.27.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { diff --git a/packages/@react-aria/autocomplete/package.json b/packages/@react-aria/autocomplete/package.json index dc14893b038..37bbdab80c6 100644 --- a/packages/@react-aria/autocomplete/package.json +++ b/packages/@react-aria/autocomplete/package.json @@ -1,6 +1,6 @@ { "name": "@react-aria/autocomplete", - "version": "3.0.0-alpha.36", + "version": "3.0.0-alpha.37", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -22,17 +22,18 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/combobox": "^3.11.0", - "@react-aria/i18n": "^3.12.3", - "@react-aria/interactions": "^3.22.5", - "@react-aria/listbox": "^3.13.6", - "@react-aria/searchfield": "^3.7.11", - "@react-aria/utils": "^3.26.0", - "@react-stately/autocomplete": "3.0.0-alpha.1", - "@react-stately/combobox": "^3.10.1", - "@react-types/autocomplete": "3.0.0-alpha.27", - "@react-types/button": "^3.10.1", - "@react-types/shared": "^3.26.0", + "@react-aria/combobox": "^3.11.1", + "@react-aria/i18n": "^3.12.5", + "@react-aria/interactions": "^3.23.0", + "@react-aria/listbox": "^3.14.0", + "@react-aria/searchfield": "^3.8.0", + "@react-aria/textfield": "^3.16.0", + "@react-aria/utils": "^3.27.0", + "@react-stately/autocomplete": "3.0.0-alpha.0", + "@react-stately/combobox": "^3.10.2", + "@react-types/autocomplete": "3.0.0-alpha.28", + "@react-types/button": "^3.10.2", + "@react-types/shared": "^3.27.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { diff --git a/packages/@react-aria/autocomplete/src/index.ts b/packages/@react-aria/autocomplete/src/index.ts index 3635d09fcf2..91c2e0e3209 100644 --- a/packages/@react-aria/autocomplete/src/index.ts +++ b/packages/@react-aria/autocomplete/src/index.ts @@ -10,7 +10,7 @@ * governing permissions and limitations under the License. */ export {useSearchAutocomplete} from './useSearchAutocomplete'; -export {useAutocomplete} from './useAutocomplete'; +export {UNSTABLE_useAutocomplete} from './useAutocomplete'; export type {AriaSearchAutocompleteOptions, SearchAutocompleteAria} from './useSearchAutocomplete'; export type {AriaSearchAutocompleteProps} from '@react-types/autocomplete'; diff --git a/packages/@react-aria/autocomplete/src/useAutocomplete.ts b/packages/@react-aria/autocomplete/src/useAutocomplete.ts index 12197e24f87..2ebc52d200c 100644 --- a/packages/@react-aria/autocomplete/src/useAutocomplete.ts +++ b/packages/@react-aria/autocomplete/src/useAutocomplete.ts @@ -11,13 +11,13 @@ */ import {AriaLabelingProps, BaseEvent, DOMProps, RefObject} from '@react-types/shared'; +import {AriaTextFieldProps} from '@react-aria/textfield'; import {AutocompleteProps, AutocompleteState} from '@react-stately/autocomplete'; -import {ChangeEvent, InputHTMLAttributes, KeyboardEvent as ReactKeyboardEvent, useCallback, useEffect, useMemo, useRef} from 'react'; -import {CLEAR_FOCUS_EVENT, FOCUS_EVENT, mergeProps, mergeRefs, UPDATE_ACTIVEDESCENDANT, useEffectEvent, useId, useLabels, useObjectRef} from '@react-aria/utils'; +import {CLEAR_FOCUS_EVENT, FOCUS_EVENT, isCtrlKeyPressed, mergeProps, mergeRefs, UPDATE_ACTIVEDESCENDANT, useEffectEvent, useId, useLabels, useObjectRef} from '@react-aria/utils'; // @ts-ignore import intlMessages from '../intl/*.json'; -import {useFilter, useLocalizedStringFormatter} from '@react-aria/i18n'; -import {useKeyboard} from '@react-aria/interactions'; +import {KeyboardEvent as ReactKeyboardEvent, useCallback, useEffect, useMemo, useRef} from 'react'; +import {useLocalizedStringFormatter} from '@react-aria/i18n'; export interface CollectionOptions extends DOMProps, AriaLabelingProps { /** Whether the collection items should use virtual focus instead of being focused directly. */ @@ -27,28 +27,26 @@ export interface CollectionOptions extends DOMProps, AriaLabelingProps { } export interface AriaAutocompleteProps extends AutocompleteProps { /** - * The filter function used to determine if a option should be included in the autocomplete list. - * @default contains + * An optional filter function used to determine if a option should be included in the autocomplete list. + * Include this if the items you are providing to your wrapped collection aren't filtered by default. */ - defaultFilter?: (textValue: string, inputValue: string) => boolean + filter?: (textValue: string, inputValue: string) => boolean } export interface AriaAutocompleteOptions extends Omit { /** The ref for the wrapped collection element. */ - collectionRef: RefObject, - /** The ref for the wrapped input element. */ - inputRef: RefObject + collectionRef: RefObject } export interface AutocompleteAria { - /** Props for the autocomplete input element. */ - inputProps: InputHTMLAttributes, + /** Props for the autocomplete textfield/searchfield element. These should be passed to the textfield/searchfield aria hooks respectively. */ + textFieldProps: AriaTextFieldProps, /** Props for the collection, to be passed to collection's respective aria hook (e.g. useMenu). */ collectionProps: CollectionOptions, /** Ref to attach to the wrapped collection. */ collectionRef: RefObject, /** A filter function that returns if the provided collection node should be filtered out of the collection. */ - filterFn: (nodeTextValue: string) => boolean + filterFn?: (nodeTextValue: string) => boolean } /** @@ -57,27 +55,33 @@ export interface AutocompleteAria { * @param props - Props for the autocomplete. * @param state - State for the autocomplete, as returned by `useAutocompleteState`. */ -export function useAutocomplete(props: AriaAutocompleteOptions, state: AutocompleteState): AutocompleteAria { +export function UNSTABLE_useAutocomplete(props: AriaAutocompleteOptions, state: AutocompleteState): AutocompleteAria { let { collectionRef, - defaultFilter, - inputRef + filter } = props; let collectionId = useId(); let timeout = useRef | undefined>(undefined); let delayNextActiveDescendant = useRef(false); + let queuedActiveDescendant = useRef(null); let lastCollectionNode = useRef(null); let updateActiveDescendant = useEffectEvent((e) => { let {target} = e; + if (queuedActiveDescendant.current === target.id) { + return; + } + clearTimeout(timeout.current); e.stopPropagation(); if (target !== collectionRef.current) { if (delayNextActiveDescendant.current) { + queuedActiveDescendant.current = target.id; timeout.current = setTimeout(() => { state.setFocusedNodeId(target.id); + queuedActiveDescendant.current = null; }, 500); } else { state.setFocusedNodeId(target.id); @@ -130,33 +134,38 @@ export function useAutocomplete(props: AriaAutocompleteOptions, state: Autocompl collectionRef.current?.dispatchEvent(clearFocusEvent); }); - // Tell wrapped collection to focus the first element in the list when typing forward and to clear focused key when deleting text - // for screen reader announcements - let lastInputValue = useRef(null); - useEffect(() => { - if (state.inputValue != null) { - if (lastInputValue.current != null && lastInputValue.current !== state.inputValue && lastInputValue.current?.length <= state.inputValue.length) { - focusFirstItem(); - } else { - clearVirtualFocus(); - } - - lastInputValue.current = state.inputValue; + // TODO: update to see if we can tell what kind of event (paste vs backspace vs typing) is happening instead + let onChange = (value: string) => { + // Tell wrapped collection to focus the first element in the list when typing forward and to clear focused key when deleting text + // for screen reader announcements + if (state.inputValue !== value && state.inputValue.length <= value.length) { + focusFirstItem(); + } else { + clearVirtualFocus(); } - }, [state.inputValue, focusFirstItem, clearVirtualFocus]); + state.setInputValue(value); + }; + + let keyDownTarget = useRef(null); // For textfield specific keydown operations let onKeyDown = (e: BaseEvent>) => { + keyDownTarget.current = e.target as Element; if (e.nativeEvent.isComposing) { return; } switch (e.key) { + case 'a': + if (isCtrlKeyPressed(e)) { + return; + } + break; case 'Escape': // Early return for Escape here so it doesn't leak the Escape event from the simulated collection event below and // close the dialog prematurely. Ideally that should be up to the discretion of the input element hence the check // for isPropagationStopped - if (e.isPropagationStopped()) { + if (e.isDefaultPrevented()) { return; } break; @@ -195,7 +204,13 @@ export function useAutocomplete(props: AriaAutocompleteOptions, state: Autocompl } // Emulate the keyboard events that happen in the input field in the wrapped collection. This is for triggering things like onAction via Enter - // or moving focus from one item to another + // or moving focus from one item to another. Stop propagation on the input event if it isn't already stopped so it doesn't leak out. For events + // like ESC, the dispatched event below will bubble out of the collection and be stopped if handled by useSelectableCollection, otherwise will bubble + // as expected + if (!e.isPropagationStopped()) { + e.stopPropagation(); + } + if (state.focusedNodeId == null) { collectionRef.current?.dispatchEvent( new KeyboardEvent(e.nativeEvent.type, e.nativeEvent) @@ -208,11 +223,11 @@ export function useAutocomplete(props: AriaAutocompleteOptions, state: Autocompl } }; - let onKeyUp = useEffectEvent((e) => { + let onKeyUpCapture = useEffectEvent((e) => { // Dispatch simulated key up events for things like triggering links in listbox // Make sure to stop the propagation of the input keyup event so that the simulated keyup/down pair // is detected by usePress instead of the original keyup originating from the input - if (e.target === inputRef.current) { + if (e.target === keyDownTarget.current) { e.stopImmediatePropagation(); if (state.focusedNodeId == null) { collectionRef.current?.dispatchEvent( @@ -228,13 +243,11 @@ export function useAutocomplete(props: AriaAutocompleteOptions, state: Autocompl }); useEffect(() => { - document.addEventListener('keyup', onKeyUp, true); + document.addEventListener('keyup', onKeyUpCapture, true); return () => { - document.removeEventListener('keyup', onKeyUp, true); + document.removeEventListener('keyup', onKeyUpCapture, true); }; - }, [inputRef, onKeyUp]); - - let {keyboardProps} = useKeyboard({onKeyDown}); + }, [onKeyUpCapture]); let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-aria/autocomplete'); let collectionProps = useLabels({ @@ -242,20 +255,19 @@ export function useAutocomplete(props: AriaAutocompleteOptions, state: Autocompl 'aria-label': stringFormatter.format('collectionLabel') }); - let {contains} = useFilter({sensitivity: 'base'}); let filterFn = useCallback((nodeTextValue: string) => { - if (defaultFilter) { - return defaultFilter(nodeTextValue, state.inputValue); + if (filter) { + return filter(nodeTextValue, state.inputValue); } - return contains(nodeTextValue, state.inputValue); - }, [state.inputValue, defaultFilter, contains]) ; + return true; + }, [state.inputValue, filter]); return { - inputProps: { + textFieldProps: { value: state.inputValue, - onChange: (e: ChangeEvent) => state.setInputValue(e.target.value), - ...keyboardProps, + onChange, + onKeyDown, autoComplete: 'off', 'aria-haspopup': 'listbox', 'aria-controls': collectionId, @@ -273,6 +285,6 @@ export function useAutocomplete(props: AriaAutocompleteOptions, state: Autocompl disallowTypeAhead: true }), collectionRef: mergedCollectionRef, - filterFn + filterFn: filter != null ? filterFn : undefined }; } diff --git a/packages/@react-aria/breadcrumbs/package.json b/packages/@react-aria/breadcrumbs/package.json index 703a1c7c193..496028fbc40 100644 --- a/packages/@react-aria/breadcrumbs/package.json +++ b/packages/@react-aria/breadcrumbs/package.json @@ -1,6 +1,6 @@ { "name": "@react-aria/breadcrumbs", - "version": "3.5.19", + "version": "3.5.20", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -22,15 +22,16 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/i18n": "^3.12.4", - "@react-aria/link": "^3.7.7", - "@react-aria/utils": "^3.26.0", - "@react-types/breadcrumbs": "^3.7.9", - "@react-types/shared": "^3.26.0", + "@react-aria/i18n": "^3.12.5", + "@react-aria/link": "^3.7.8", + "@react-aria/utils": "^3.27.0", + "@react-types/breadcrumbs": "^3.7.10", + "@react-types/shared": "^3.27.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" }, "publishConfig": { "access": "public" diff --git a/packages/@react-aria/button/package.json b/packages/@react-aria/button/package.json index 04cb9e605a2..419549dfb97 100644 --- a/packages/@react-aria/button/package.json +++ b/packages/@react-aria/button/package.json @@ -1,6 +1,6 @@ { "name": "@react-aria/button", - "version": "3.11.0", + "version": "3.11.1", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -22,17 +22,18 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/focus": "^3.19.0", - "@react-aria/interactions": "^3.22.5", - "@react-aria/toolbar": "3.0.0-beta.11", - "@react-aria/utils": "^3.26.0", - "@react-stately/toggle": "^3.8.0", - "@react-types/button": "^3.10.1", - "@react-types/shared": "^3.26.0", + "@react-aria/focus": "^3.19.1", + "@react-aria/interactions": "^3.23.0", + "@react-aria/toolbar": "3.0.0-beta.12", + "@react-aria/utils": "^3.27.0", + "@react-stately/toggle": "^3.8.1", + "@react-types/button": "^3.10.2", + "@react-types/shared": "^3.27.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" }, "publishConfig": { "access": "public" diff --git a/packages/@react-aria/calendar/docs/useCalendar.mdx b/packages/@react-aria/calendar/docs/useCalendar.mdx index 9c0dafcf919..65d64177ebe 100644 --- a/packages/@react-aria/calendar/docs/useCalendar.mdx +++ b/packages/@react-aria/calendar/docs/useCalendar.mdx @@ -129,7 +129,7 @@ function Calendar(props) {
      - +
    ); } @@ -152,7 +152,7 @@ function CalendarGrid({state, ...props}) { let {gridProps, headerProps, weekDays} = useCalendarGrid(props, state); // Get the number of weeks in the month so we can render the proper number of rows. - let weeksInMonth = getWeeksInMonth(state.visibleRange.start, locale); + let weeksInMonth = getWeeksInMonth(state.visibleRange.start, locale, props.firstDayOfWeek); return ( @@ -458,6 +458,14 @@ The `isReadOnly` boolean prop makes the Calendar's value immutable. Unlike `isDi ``` +### Custom first day of week + +By default, the first day of the week is automatically set based on the current locale. This can be changed by setting the `firstDayOfWeek` prop to `'sun'`, `'mon'`, `'tue'`, `'wed'`, `'thu'`, `'fri'`, or `'sat'`. + +```tsx example + +``` + ### Labeling An aria-label must be provided to the `Calendar` for accessibility. If it is labeled by a separate element, an `aria-labelledby` prop must be provided using the `id` of the labeling element instead. diff --git a/packages/@react-aria/calendar/docs/useRangeCalendar.mdx b/packages/@react-aria/calendar/docs/useRangeCalendar.mdx index 3900cdc08a5..f34e455c1af 100644 --- a/packages/@react-aria/calendar/docs/useRangeCalendar.mdx +++ b/packages/@react-aria/calendar/docs/useRangeCalendar.mdx @@ -129,7 +129,7 @@ function RangeCalendar(props) { - + ); } @@ -152,7 +152,7 @@ function CalendarGrid({state, ...props}) { let {gridProps, headerProps, weekDays} = useCalendarGrid(props, state); // Get the number of weeks in the month so we can render the proper number of rows. - let weeksInMonth = getWeeksInMonth(state.visibleRange.start, locale); + let weeksInMonth = getWeeksInMonth(state.visibleRange.start, locale, props.firstDayOfWeek); return (
    @@ -477,6 +477,14 @@ The `isReadOnly` boolean prop makes the RangeCalendar's value immutable. Unlike ``` +### Custom first day of week + +By default, the first day of the week is automatically set based on the current locale. This can be changed by setting the `firstDayOfWeek` prop to `'sun'`, `'mon'`, `'tue'`, `'wed'`, `'thu'`, `'fri'`, or `'sat'`. + +```tsx example + +``` + ### Labeling An aria-label must be provided to the `RangeCalendar` for accessibility. If it is labeled by a separate element, an `aria-labelledby` prop must be provided using the `id` of the labeling element instead. diff --git a/packages/@react-aria/calendar/package.json b/packages/@react-aria/calendar/package.json index c4ea6fae7dd..a38894a52bf 100644 --- a/packages/@react-aria/calendar/package.json +++ b/packages/@react-aria/calendar/package.json @@ -1,6 +1,6 @@ { "name": "@react-aria/calendar", - "version": "3.6.0", + "version": "3.7.0", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -22,15 +22,15 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@internationalized/date": "^3.6.0", - "@react-aria/i18n": "^3.12.4", - "@react-aria/interactions": "^3.22.5", + "@internationalized/date": "^3.7.0", + "@react-aria/i18n": "^3.12.5", + "@react-aria/interactions": "^3.23.0", "@react-aria/live-announcer": "^3.4.1", - "@react-aria/utils": "^3.26.0", - "@react-stately/calendar": "^3.6.0", - "@react-types/button": "^3.10.1", - "@react-types/calendar": "^3.5.0", - "@react-types/shared": "^3.26.0", + "@react-aria/utils": "^3.27.0", + "@react-stately/calendar": "^3.7.0", + "@react-types/button": "^3.10.2", + "@react-types/calendar": "^3.6.0", + "@react-types/shared": "^3.27.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { diff --git a/packages/@react-aria/calendar/src/useCalendarGrid.ts b/packages/@react-aria/calendar/src/useCalendarGrid.ts index ec91c10075b..0da6e54feb8 100644 --- a/packages/@react-aria/calendar/src/useCalendarGrid.ts +++ b/packages/@react-aria/calendar/src/useCalendarGrid.ts @@ -36,7 +36,11 @@ export interface AriaCalendarGridProps { * e.g. single letter, abbreviation, or full day name. * @default "narrow" */ - weekdayStyle?: 'narrow' | 'short' | 'long' + weekdayStyle?: 'narrow' | 'short' | 'long', + /** + * The day that starts the week. + */ + firstDayOfWeek?: 'sun' | 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat' } export interface CalendarGridAria { @@ -56,7 +60,8 @@ export interface CalendarGridAria { export function useCalendarGrid(props: AriaCalendarGridProps, state: CalendarState | RangeCalendarState): CalendarGridAria { let { startDate = state.visibleRange.start, - endDate = state.visibleRange.end + endDate = state.visibleRange.end, + firstDayOfWeek } = props; let {direction} = useLocale(); @@ -137,13 +142,13 @@ export function useCalendarGrid(props: AriaCalendarGridProps, state: CalendarSta let dayFormatter = useDateFormatter({weekday: props.weekdayStyle || 'narrow', timeZone: state.timeZone}); let {locale} = useLocale(); let weekDays = useMemo(() => { - let weekStart = startOfWeek(today(state.timeZone), locale); + let weekStart = startOfWeek(today(state.timeZone), locale, firstDayOfWeek); return [...new Array(7).keys()].map((index) => { let date = weekStart.add({days: index}); let dateDay = date.toDate(state.timeZone); return dayFormatter.format(dateDay); }); - }, [locale, state.timeZone, dayFormatter]); + }, [locale, state.timeZone, dayFormatter, firstDayOfWeek]); return { gridProps: mergeProps(labelProps, { diff --git a/packages/@react-aria/calendar/stories/Example.tsx b/packages/@react-aria/calendar/stories/Example.tsx index 583a5b847f2..0f93b75f6d1 100644 --- a/packages/@react-aria/calendar/stories/Example.tsx +++ b/packages/@react-aria/calendar/stories/Example.tsx @@ -97,3 +97,49 @@ function Cell(props) { ); } + +export function ExampleCustomFirstDay(props) { + let {locale} = useLocale(); + const {firstDayOfWeek} = props; + + let state = useCalendarState({ + ...props, + locale, + createCalendar + }); + + let {calendarProps, prevButtonProps, nextButtonProps} = useCalendar(props, state); + + return ( +
    +
    + {calendarProps['aria-label']} +
    +
    + +
    +
    + + +
    +
    + ); +} + +function ExampleFirstDayCalendarGrid({state, firstDayOfWeek}: {state: CalendarState | RangeCalendarState, firstDayOfWeek?: 'sun' | 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat'}) { + let {locale} = useLocale(); + let {gridProps} = useCalendarGrid({firstDayOfWeek}, state); + let startDate = state.visibleRange.start; + let weeksInMonth = getWeeksInMonth(startDate, locale, firstDayOfWeek); + return ( +
    + {[...new Array(weeksInMonth).keys()].map(weekIndex => ( +
    + {state.getDatesInWeek(weekIndex, startDate).map((date, i) => ( + + ))} +
    + ))} +
    + ); +} diff --git a/packages/@react-aria/calendar/test/useCalendar.test.js b/packages/@react-aria/calendar/test/useCalendar.test.js index db34fd92e66..415219f2389 100644 --- a/packages/@react-aria/calendar/test/useCalendar.test.js +++ b/packages/@react-aria/calendar/test/useCalendar.test.js @@ -12,7 +12,8 @@ import {act, pointerMap, render} from '@react-spectrum/test-utils-internal'; import {CalendarDate} from '@internationalized/date'; -import {Example} from '../stories/Example'; +import {Example, ExampleCustomFirstDay} from '../stories/Example'; +import {I18nProvider} from '@react-aria/i18n'; import React from 'react'; import userEvent from '@testing-library/user-event'; @@ -63,6 +64,17 @@ describe('useCalendar', () => { unmount(); } + async function testFirstDayOfWeek(defaultValue, firstDayOfWeek, expectedFirstDay, locale = 'en-US') { + let {getAllByRole, unmount} = render( + + + + ); + let cells = getAllByRole('gridcell'); + expect(cells[0].children[0]).toHaveAttribute('aria-label', expectedFirstDay); + unmount(); + } + describe('visibleDuration: 3 days', () => { it('should move the focused date by one day with the left/right arrows', async () => { await testKeyboard(new CalendarDate(2019, 6, 5), 'June 4 to 6, 2019', 'ArrowLeft', 1, 'Tuesday, June 4, 2019', 'June 4 to 6, 2019', {visibleDuration: {days: 3}}); @@ -227,4 +239,28 @@ describe('useCalendar', () => { await testPagination(defaultValue, rangeBefore, rangeAfter, rel, count, visibleDuration, pageBehavior); }); }); + + describe('firstDayOfWeek', () => { + it.each` + Name | defaultValue | firstDayOfWeek | expectedFirstDay | locale + ${'default'} | ${new CalendarDate(2024, 1, 1)} | ${undefined} | ${'Sunday, December 31, 2023'} | ${'en-US'} + ${'Sunday'} | ${new CalendarDate(2024, 1, 1)} | ${'sun'} | ${'Sunday, December 31, 2023'} | ${'en-US'} + ${'Monday'} | ${new CalendarDate(2024, 1, 1)} | ${'mon'} | ${'Monday, January 1, 2024 selected'} | ${'en-US'} + ${'Tuesday'} | ${new CalendarDate(2024, 1, 1)} | ${'tue'} | ${'Tuesday, December 26, 2023'} | ${'en-US'} + ${'Wednesday'} | ${new CalendarDate(2024, 1, 1)} | ${'wed'} | ${'Wednesday, December 27, 2023'} | ${'en-US'} + ${'Thursday'} | ${new CalendarDate(2024, 1, 1)} | ${'thu'} | ${'Thursday, December 28, 2023'} | ${'en-US'} + ${'Friday'} | ${new CalendarDate(2024, 1, 1)} | ${'fri'} | ${'Friday, December 29, 2023'} | ${'en-US'} + ${'Saturday'} | ${new CalendarDate(2024, 1, 1)} | ${'sat'} | ${'Saturday, December 30, 2023'} | ${'en-US'} + ${'default (fr-FR)'} | ${new CalendarDate(2024, 1, 1)} | ${undefined} | ${'lundi 1 janvier 2024 sélectionné'} | ${'fr-FR'} + ${'Sunday (fr-FR)'} | ${new CalendarDate(2024, 1, 1)} | ${'sun'} | ${'dimanche 31 décembre 2023'} | ${'fr-FR'} + ${'Monday (fr-FR)'} | ${new CalendarDate(2024, 1, 1)} | ${'mon'} | ${'lundi 1 janvier 2024 sélectionné'} | ${'fr-FR'} + ${'Tuesday (fr-FR)'} | ${new CalendarDate(2024, 1, 1)} | ${'tue'} | ${'mardi 26 décembre 2023'} | ${'fr-FR'} + ${'Wednesday (fr-FR)'} | ${new CalendarDate(2024, 1, 1)} | ${'wed'} | ${'mercredi 27 décembre 2023'} | ${'fr-FR'} + ${'Thursday (fr-FR)'} | ${new CalendarDate(2024, 1, 1)} | ${'thu'} | ${'jeudi 28 décembre 2023'} | ${'fr-FR'} + ${'Friday (fr-FR)'} | ${new CalendarDate(2024, 1, 1)} | ${'fri'} | ${'vendredi 29 décembre 2023'} | ${'fr-FR'} + ${'Saturday (fr-FR)'} | ${new CalendarDate(2024, 1, 1)} | ${'sat'} | ${'samedi 30 décembre 2023'} | ${'fr-FR'} + `('should use firstDayOfWeek $Name', async ({defaultValue, firstDayOfWeek, expectedFirstDay, locale}) => { + await testFirstDayOfWeek(defaultValue, firstDayOfWeek, expectedFirstDay, locale); + }); + }); }); diff --git a/packages/@react-aria/checkbox/package.json b/packages/@react-aria/checkbox/package.json index d030a42d7e9..0b3913ae74c 100644 --- a/packages/@react-aria/checkbox/package.json +++ b/packages/@react-aria/checkbox/package.json @@ -1,6 +1,6 @@ { "name": "@react-aria/checkbox", - "version": "3.15.0", + "version": "3.15.1", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -22,20 +22,21 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/form": "^3.0.11", - "@react-aria/interactions": "^3.22.5", - "@react-aria/label": "^3.7.13", - "@react-aria/toggle": "^3.10.10", - "@react-aria/utils": "^3.26.0", - "@react-stately/checkbox": "^3.6.10", - "@react-stately/form": "^3.1.0", - "@react-stately/toggle": "^3.8.0", - "@react-types/checkbox": "^3.9.0", - "@react-types/shared": "^3.26.0", + "@react-aria/form": "^3.0.12", + "@react-aria/interactions": "^3.23.0", + "@react-aria/label": "^3.7.14", + "@react-aria/toggle": "^3.10.11", + "@react-aria/utils": "^3.27.0", + "@react-stately/checkbox": "^3.6.11", + "@react-stately/form": "^3.1.1", + "@react-stately/toggle": "^3.8.1", + "@react-types/checkbox": "^3.9.1", + "@react-types/shared": "^3.27.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" }, "publishConfig": { "access": "public" diff --git a/packages/@react-aria/collections/package.json b/packages/@react-aria/collections/package.json index 70c8df4c93e..ded791e995e 100644 --- a/packages/@react-aria/collections/package.json +++ b/packages/@react-aria/collections/package.json @@ -1,6 +1,6 @@ { "name": "@react-aria/collections", - "version": "3.0.0-alpha.6", + "version": "3.0.0-alpha.7", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -23,8 +23,8 @@ }, "dependencies": { "@react-aria/ssr": "^3.9.7", - "@react-aria/utils": "^3.26.0", - "@react-types/shared": "^3.26.0", + "@react-aria/utils": "^3.27.0", + "@react-types/shared": "^3.27.0", "@swc/helpers": "^0.5.0", "use-sync-external-store": "^1.2.0" }, diff --git a/packages/@react-aria/color/package.json b/packages/@react-aria/color/package.json index e9973af9ad1..49e76bf89b9 100644 --- a/packages/@react-aria/color/package.json +++ b/packages/@react-aria/color/package.json @@ -1,6 +1,6 @@ { "name": "@react-aria/color", - "version": "3.0.2", + "version": "3.0.3", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -22,18 +22,18 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/i18n": "^3.12.4", - "@react-aria/interactions": "^3.22.5", - "@react-aria/numberfield": "^3.11.9", - "@react-aria/slider": "^3.7.14", - "@react-aria/spinbutton": "^3.6.10", - "@react-aria/textfield": "^3.15.0", - "@react-aria/utils": "^3.26.0", - "@react-aria/visually-hidden": "^3.8.18", - "@react-stately/color": "^3.8.1", - "@react-stately/form": "^3.1.0", - "@react-types/color": "^3.0.1", - "@react-types/shared": "^3.26.0", + "@react-aria/i18n": "^3.12.5", + "@react-aria/interactions": "^3.23.0", + "@react-aria/numberfield": "^3.11.10", + "@react-aria/slider": "^3.7.15", + "@react-aria/spinbutton": "^3.6.11", + "@react-aria/textfield": "^3.16.0", + "@react-aria/utils": "^3.27.0", + "@react-aria/visually-hidden": "^3.8.19", + "@react-stately/color": "^3.8.2", + "@react-stately/form": "^3.1.1", + "@react-types/color": "^3.0.2", + "@react-types/shared": "^3.27.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { diff --git a/packages/@react-aria/combobox/package.json b/packages/@react-aria/combobox/package.json index 30a9eb3c3c7..85f134a06a1 100644 --- a/packages/@react-aria/combobox/package.json +++ b/packages/@react-aria/combobox/package.json @@ -1,6 +1,6 @@ { "name": "@react-aria/combobox", - "version": "3.11.0", + "version": "3.11.1", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -22,20 +22,20 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/i18n": "^3.12.4", - "@react-aria/listbox": "^3.13.6", + "@react-aria/i18n": "^3.12.5", + "@react-aria/listbox": "^3.14.0", "@react-aria/live-announcer": "^3.4.1", - "@react-aria/menu": "^3.16.0", - "@react-aria/overlays": "^3.24.0", - "@react-aria/selection": "^3.21.0", - "@react-aria/textfield": "^3.15.0", - "@react-aria/utils": "^3.26.0", - "@react-stately/collections": "^3.12.0", - "@react-stately/combobox": "^3.10.1", - "@react-stately/form": "^3.1.0", - "@react-types/button": "^3.10.1", - "@react-types/combobox": "^3.13.1", - "@react-types/shared": "^3.26.0", + "@react-aria/menu": "^3.17.0", + "@react-aria/overlays": "^3.25.0", + "@react-aria/selection": "^3.22.0", + "@react-aria/textfield": "^3.16.0", + "@react-aria/utils": "^3.27.0", + "@react-stately/collections": "^3.12.1", + "@react-stately/combobox": "^3.10.2", + "@react-stately/form": "^3.1.1", + "@react-types/button": "^3.10.2", + "@react-types/combobox": "^3.13.2", + "@react-types/shared": "^3.27.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { diff --git a/packages/@react-aria/datepicker/docs/useDatePicker.mdx b/packages/@react-aria/datepicker/docs/useDatePicker.mdx index 190cc48a9db..044e94dbb0f 100644 --- a/packages/@react-aria/datepicker/docs/useDatePicker.mdx +++ b/packages/@react-aria/datepicker/docs/useDatePicker.mdx @@ -106,7 +106,7 @@ function DatePicker(props) { {state.isOpen && - + } @@ -346,7 +346,7 @@ function Calendar(props) { - + ); } @@ -690,3 +690,11 @@ By default, `useDatePicker` displays times in either 12 or 24 hour hour format d granularity="minute" hourCycle={24} /> ``` + +### Custom first day of week + +By default, the first day of the week is automatically set based on the current locale. This can be changed by setting the `firstDayOfWeek` prop to `'sun'`, `'mon'`, `'tue'`, `'wed'`, `'thu'`, `'fri'`, or `'sat'`. + +```tsx example + +``` \ No newline at end of file diff --git a/packages/@react-aria/datepicker/docs/useDateRangePicker.mdx b/packages/@react-aria/datepicker/docs/useDateRangePicker.mdx index 84d4a829840..50d20d7b25d 100644 --- a/packages/@react-aria/datepicker/docs/useDateRangePicker.mdx +++ b/packages/@react-aria/datepicker/docs/useDateRangePicker.mdx @@ -113,7 +113,7 @@ function DateRangePicker(props) { {state.isOpen && - + } @@ -359,7 +359,7 @@ function RangeCalendar(props) { - + ); } @@ -369,7 +369,7 @@ function CalendarGrid({state, ...props}) { let {gridProps, headerProps, weekDays} = useCalendarGrid(props, state); // Get the number of weeks in the month so we can render the proper number of rows. - let weeksInMonth = getWeeksInMonth(state.visibleRange.start, locale); + let weeksInMonth = getWeeksInMonth(state.visibleRange.start, locale, props.firstDayOfWeek); return (
    @@ -755,3 +755,11 @@ By default, `useDateRangePicker` displays times in either 12 or 24 hour hour for granularity="minute" hourCycle={24} /> ``` + +### Custom first day of week + +By default, the first day of the week is automatically set based on the current locale. This can be changed by setting the `firstDayOfWeek` prop to `'sun'`, `'mon'`, `'tue'`, `'wed'`, `'thu'`, `'fri'`, or `'sat'`. + +```tsx example + +``` \ No newline at end of file diff --git a/packages/@react-aria/datepicker/package.json b/packages/@react-aria/datepicker/package.json index b7558a1d78a..27f92c09cca 100644 --- a/packages/@react-aria/datepicker/package.json +++ b/packages/@react-aria/datepicker/package.json @@ -1,6 +1,6 @@ { "name": "@react-aria/datepicker", - "version": "3.12.0", + "version": "3.13.0", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -22,23 +22,23 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@internationalized/date": "^3.6.0", + "@internationalized/date": "^3.7.0", "@internationalized/number": "^3.6.0", "@internationalized/string": "^3.2.5", - "@react-aria/focus": "^3.19.0", - "@react-aria/form": "^3.0.11", - "@react-aria/i18n": "^3.12.4", - "@react-aria/interactions": "^3.22.5", - "@react-aria/label": "^3.7.13", - "@react-aria/spinbutton": "^3.6.10", - "@react-aria/utils": "^3.26.0", - "@react-stately/datepicker": "^3.11.0", - "@react-stately/form": "^3.1.0", - "@react-types/button": "^3.10.1", - "@react-types/calendar": "^3.5.0", - "@react-types/datepicker": "^3.9.0", - "@react-types/dialog": "^3.5.14", - "@react-types/shared": "^3.26.0", + "@react-aria/focus": "^3.19.1", + "@react-aria/form": "^3.0.12", + "@react-aria/i18n": "^3.12.5", + "@react-aria/interactions": "^3.23.0", + "@react-aria/label": "^3.7.14", + "@react-aria/spinbutton": "^3.6.11", + "@react-aria/utils": "^3.27.0", + "@react-stately/datepicker": "^3.12.0", + "@react-stately/form": "^3.1.1", + "@react-types/button": "^3.10.2", + "@react-types/calendar": "^3.6.0", + "@react-types/datepicker": "^3.10.0", + "@react-types/dialog": "^3.5.15", + "@react-types/shared": "^3.27.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { diff --git a/packages/@react-aria/dialog/package.json b/packages/@react-aria/dialog/package.json index 396bf085471..806d3ab71b2 100644 --- a/packages/@react-aria/dialog/package.json +++ b/packages/@react-aria/dialog/package.json @@ -1,6 +1,6 @@ { "name": "@react-aria/dialog", - "version": "3.5.20", + "version": "3.5.21", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -26,11 +26,11 @@ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" }, "dependencies": { - "@react-aria/focus": "^3.19.0", - "@react-aria/overlays": "^3.24.0", - "@react-aria/utils": "^3.26.0", - "@react-types/dialog": "^3.5.14", - "@react-types/shared": "^3.26.0", + "@react-aria/focus": "^3.19.1", + "@react-aria/overlays": "^3.25.0", + "@react-aria/utils": "^3.27.0", + "@react-types/dialog": "^3.5.15", + "@react-types/shared": "^3.27.0", "@swc/helpers": "^0.5.0" }, "publishConfig": { diff --git a/packages/@react-aria/disclosure/package.json b/packages/@react-aria/disclosure/package.json index f8db80284d6..3a30773460f 100644 --- a/packages/@react-aria/disclosure/package.json +++ b/packages/@react-aria/disclosure/package.json @@ -1,6 +1,6 @@ { "name": "@react-aria/disclosure", - "version": "3.0.0", + "version": "3.0.1", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -23,9 +23,9 @@ }, "dependencies": { "@react-aria/ssr": "^3.9.7", - "@react-aria/utils": "^3.26.0", - "@react-stately/disclosure": "^3.0.0", - "@react-types/button": "^3.10.1", + "@react-aria/utils": "^3.27.0", + "@react-stately/disclosure": "^3.0.1", + "@react-types/button": "^3.10.2", "@swc/helpers": "^0.5.0" }, "peerDependencies": { diff --git a/packages/@react-aria/dnd/package.json b/packages/@react-aria/dnd/package.json index 45c8fc56803..373fae0313e 100644 --- a/packages/@react-aria/dnd/package.json +++ b/packages/@react-aria/dnd/package.json @@ -1,6 +1,6 @@ { "name": "@react-aria/dnd", - "version": "3.8.0", + "version": "3.8.1", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -23,14 +23,14 @@ }, "dependencies": { "@internationalized/string": "^3.2.5", - "@react-aria/i18n": "^3.12.4", - "@react-aria/interactions": "^3.22.5", + "@react-aria/i18n": "^3.12.5", + "@react-aria/interactions": "^3.23.0", "@react-aria/live-announcer": "^3.4.1", - "@react-aria/overlays": "^3.24.0", - "@react-aria/utils": "^3.26.0", - "@react-stately/dnd": "^3.5.0", - "@react-types/button": "^3.10.1", - "@react-types/shared": "^3.26.0", + "@react-aria/overlays": "^3.25.0", + "@react-aria/utils": "^3.27.0", + "@react-stately/dnd": "^3.5.1", + "@react-types/button": "^3.10.2", + "@react-types/shared": "^3.27.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { diff --git a/packages/@react-aria/focus/package.json b/packages/@react-aria/focus/package.json index 1e9f5c34a9f..45a3b24aedb 100644 --- a/packages/@react-aria/focus/package.json +++ b/packages/@react-aria/focus/package.json @@ -1,6 +1,6 @@ { "name": "@react-aria/focus", - "version": "3.19.0", + "version": "3.19.1", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -22,14 +22,15 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/interactions": "^3.22.5", - "@react-aria/utils": "^3.26.0", - "@react-types/shared": "^3.26.0", + "@react-aria/interactions": "^3.23.0", + "@react-aria/utils": "^3.27.0", + "@react-types/shared": "^3.27.0", "@swc/helpers": "^0.5.0", "clsx": "^2.0.0" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" }, "publishConfig": { "access": "public" diff --git a/packages/@react-aria/focus/src/FocusScope.tsx b/packages/@react-aria/focus/src/FocusScope.tsx index 002f94f981a..e9c48a7ec1b 100644 --- a/packages/@react-aria/focus/src/FocusScope.tsx +++ b/packages/@react-aria/focus/src/FocusScope.tsx @@ -12,7 +12,8 @@ import {FocusableElement, RefObject} from '@react-types/shared'; import {focusSafely} from './focusSafely'; -import {getOwnerDocument, isFocusable, isTabbable, useLayoutEffect} from '@react-aria/utils'; +import {getInteractionModality} from '@react-aria/interactions'; +import {getOwnerDocument, isAndroid, isChrome, isFocusable, isTabbable, useLayoutEffect} from '@react-aria/utils'; import {isElementVisible} from './isElementVisible'; import React, {ReactNode, useContext, useEffect, useMemo, useRef} from 'react'; @@ -356,8 +357,14 @@ function useFocusContainment(scopeRef: RefObject, contain?: bo cancelAnimationFrame(raf.current); } raf.current = requestAnimationFrame(() => { + // Patches infinite focus coersion loop for Android Talkback where the user isn't able to move the virtual cursor + // if within a containing focus scope. Bug filed against Chrome: https://issuetracker.google.com/issues/384844019. + // Note that this means focus can leave focus containing modals due to this, but it is isolated to Chrome Talkback. + let modality = getInteractionModality(); + let shouldSkipFocusRestore = (modality === 'virtual' || modality === null) && isAndroid() && isChrome(); + // Use document.activeElement instead of e.relatedTarget so we can tell if user clicked into iframe - if (ownerDocument.activeElement && shouldContainFocus(scopeRef) && !isElementInChildScope(ownerDocument.activeElement, scopeRef)) { + if (!shouldSkipFocusRestore && ownerDocument.activeElement && shouldContainFocus(scopeRef) && !isElementInChildScope(ownerDocument.activeElement, scopeRef)) { activeScope = scopeRef; if (ownerDocument.body.contains(e.target)) { focusedNode.current = e.target; diff --git a/packages/@react-aria/form/package.json b/packages/@react-aria/form/package.json index a9533c173fc..2326747a82d 100644 --- a/packages/@react-aria/form/package.json +++ b/packages/@react-aria/form/package.json @@ -1,6 +1,6 @@ { "name": "@react-aria/form", - "version": "3.0.11", + "version": "3.0.12", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -22,14 +22,15 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/interactions": "^3.22.5", - "@react-aria/utils": "^3.26.0", - "@react-stately/form": "^3.1.0", - "@react-types/shared": "^3.26.0", + "@react-aria/interactions": "^3.23.0", + "@react-aria/utils": "^3.27.0", + "@react-stately/form": "^3.1.1", + "@react-types/shared": "^3.27.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" }, "publishConfig": { "access": "public" diff --git a/packages/@react-aria/grid/package.json b/packages/@react-aria/grid/package.json index cae10fdaf6a..47cd3606851 100644 --- a/packages/@react-aria/grid/package.json +++ b/packages/@react-aria/grid/package.json @@ -1,6 +1,6 @@ { "name": "@react-aria/grid", - "version": "3.11.0", + "version": "3.11.1", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -22,18 +22,18 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/focus": "^3.19.0", - "@react-aria/i18n": "^3.12.4", - "@react-aria/interactions": "^3.22.5", + "@react-aria/focus": "^3.19.1", + "@react-aria/i18n": "^3.12.5", + "@react-aria/interactions": "^3.23.0", "@react-aria/live-announcer": "^3.4.1", - "@react-aria/selection": "^3.21.0", - "@react-aria/utils": "^3.26.0", - "@react-stately/collections": "^3.12.0", - "@react-stately/grid": "^3.10.0", - "@react-stately/selection": "^3.18.0", - "@react-types/checkbox": "^3.9.0", - "@react-types/grid": "^3.2.10", - "@react-types/shared": "^3.26.0", + "@react-aria/selection": "^3.22.0", + "@react-aria/utils": "^3.27.0", + "@react-stately/collections": "^3.12.1", + "@react-stately/grid": "^3.10.1", + "@react-stately/selection": "^3.19.0", + "@react-types/checkbox": "^3.9.1", + "@react-types/grid": "^3.2.11", + "@react-types/shared": "^3.27.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { diff --git a/packages/@react-aria/gridlist/package.json b/packages/@react-aria/gridlist/package.json index e35afe4427d..d941ccc8aa2 100644 --- a/packages/@react-aria/gridlist/package.json +++ b/packages/@react-aria/gridlist/package.json @@ -1,6 +1,6 @@ { "name": "@react-aria/gridlist", - "version": "3.10.0", + "version": "3.10.1", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -22,16 +22,16 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/focus": "^3.19.0", - "@react-aria/grid": "^3.11.0", - "@react-aria/i18n": "^3.12.4", - "@react-aria/interactions": "^3.22.5", - "@react-aria/selection": "^3.21.0", - "@react-aria/utils": "^3.26.0", - "@react-stately/collections": "^3.12.0", - "@react-stately/list": "^3.11.1", - "@react-stately/tree": "^3.8.6", - "@react-types/shared": "^3.26.0", + "@react-aria/focus": "^3.19.1", + "@react-aria/grid": "^3.11.1", + "@react-aria/i18n": "^3.12.5", + "@react-aria/interactions": "^3.23.0", + "@react-aria/selection": "^3.22.0", + "@react-aria/utils": "^3.27.0", + "@react-stately/collections": "^3.12.1", + "@react-stately/list": "^3.11.2", + "@react-stately/tree": "^3.8.7", + "@react-types/shared": "^3.27.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { diff --git a/packages/@react-aria/i18n/package.json b/packages/@react-aria/i18n/package.json index 48242779a12..323d29466b3 100644 --- a/packages/@react-aria/i18n/package.json +++ b/packages/@react-aria/i18n/package.json @@ -1,6 +1,6 @@ { "name": "@react-aria/i18n", - "version": "3.12.4", + "version": "3.12.5", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -47,17 +47,18 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@internationalized/date": "^3.6.0", + "@internationalized/date": "^3.7.0", "@internationalized/message": "^3.1.6", "@internationalized/number": "^3.6.0", "@internationalized/string": "^3.2.5", "@react-aria/ssr": "^3.9.7", - "@react-aria/utils": "^3.26.0", - "@react-types/shared": "^3.26.0", + "@react-aria/utils": "^3.27.0", + "@react-types/shared": "^3.27.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" }, "publishConfig": { "access": "public" diff --git a/packages/@react-aria/interactions/package.json b/packages/@react-aria/interactions/package.json index 2ff29f149ab..d0dfaf988d7 100644 --- a/packages/@react-aria/interactions/package.json +++ b/packages/@react-aria/interactions/package.json @@ -1,6 +1,6 @@ { "name": "@react-aria/interactions", - "version": "3.22.5", + "version": "3.23.0", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -23,8 +23,8 @@ }, "dependencies": { "@react-aria/ssr": "^3.9.7", - "@react-aria/utils": "^3.26.0", - "@react-types/shared": "^3.26.0", + "@react-aria/utils": "^3.27.0", + "@react-types/shared": "^3.27.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { diff --git a/packages/@react-aria/interactions/src/createEventHandler.ts b/packages/@react-aria/interactions/src/createEventHandler.ts index e518816d783..e0517140260 100644 --- a/packages/@react-aria/interactions/src/createEventHandler.ts +++ b/packages/@react-aria/interactions/src/createEventHandler.ts @@ -32,10 +32,17 @@ export function createEventHandler(handler?: (e: BaseE return e.isDefaultPrevented(); }, stopPropagation() { - console.error('stopPropagation is now the default behavior for events in React Spectrum. You can use continuePropagation() to revert this behavior.'); + if (shouldStopPropagation) { + console.error('stopPropagation is now the default behavior for events in React Spectrum. You can use continuePropagation() to revert this behavior.'); + } else { + shouldStopPropagation = true; + } }, continuePropagation() { shouldStopPropagation = false; + }, + isPropagationStopped() { + return shouldStopPropagation; } }; diff --git a/packages/@react-aria/label/package.json b/packages/@react-aria/label/package.json index eed77f2ed07..0f225d23fe4 100644 --- a/packages/@react-aria/label/package.json +++ b/packages/@react-aria/label/package.json @@ -1,6 +1,6 @@ { "name": "@react-aria/label", - "version": "3.7.13", + "version": "3.7.14", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -22,12 +22,13 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/utils": "^3.26.0", - "@react-types/shared": "^3.26.0", + "@react-aria/utils": "^3.27.0", + "@react-types/shared": "^3.27.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" }, "publishConfig": { "access": "public" diff --git a/packages/@react-aria/landmark/package.json b/packages/@react-aria/landmark/package.json index b219c581469..da54fe9a76f 100644 --- a/packages/@react-aria/landmark/package.json +++ b/packages/@react-aria/landmark/package.json @@ -1,6 +1,6 @@ { "name": "@react-aria/landmark", - "version": "3.0.0-beta.17", + "version": "3.0.0-beta.18", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -22,13 +22,14 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/utils": "^3.26.0", - "@react-types/shared": "^3.26.0", + "@react-aria/utils": "^3.27.0", + "@react-types/shared": "^3.27.0", "@swc/helpers": "^0.5.0", "use-sync-external-store": "^1.2.0" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" }, "publishConfig": { "access": "public" diff --git a/packages/@react-aria/link/package.json b/packages/@react-aria/link/package.json index d2f8713110b..65100b7be4a 100644 --- a/packages/@react-aria/link/package.json +++ b/packages/@react-aria/link/package.json @@ -1,6 +1,6 @@ { "name": "@react-aria/link", - "version": "3.7.7", + "version": "3.7.8", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -22,15 +22,16 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/focus": "^3.19.0", - "@react-aria/interactions": "^3.22.5", - "@react-aria/utils": "^3.26.0", - "@react-types/link": "^3.5.9", - "@react-types/shared": "^3.26.0", + "@react-aria/focus": "^3.19.1", + "@react-aria/interactions": "^3.23.0", + "@react-aria/utils": "^3.27.0", + "@react-types/link": "^3.5.10", + "@react-types/shared": "^3.27.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" }, "publishConfig": { "access": "public" diff --git a/packages/@react-aria/listbox/package.json b/packages/@react-aria/listbox/package.json index ec6f211dc7d..d5b00aad03a 100644 --- a/packages/@react-aria/listbox/package.json +++ b/packages/@react-aria/listbox/package.json @@ -1,6 +1,6 @@ { "name": "@react-aria/listbox", - "version": "3.13.6", + "version": "3.14.0", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -22,14 +22,14 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/interactions": "^3.22.5", - "@react-aria/label": "^3.7.13", - "@react-aria/selection": "^3.21.0", - "@react-aria/utils": "^3.26.0", - "@react-stately/collections": "^3.12.0", - "@react-stately/list": "^3.11.1", - "@react-types/listbox": "^3.5.3", - "@react-types/shared": "^3.26.0", + "@react-aria/interactions": "^3.23.0", + "@react-aria/label": "^3.7.14", + "@react-aria/selection": "^3.22.0", + "@react-aria/utils": "^3.27.0", + "@react-stately/collections": "^3.12.1", + "@react-stately/list": "^3.11.2", + "@react-types/listbox": "^3.5.4", + "@react-types/shared": "^3.27.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { diff --git a/packages/@react-aria/menu/package.json b/packages/@react-aria/menu/package.json index e45d82c953e..a5bc80bc93e 100644 --- a/packages/@react-aria/menu/package.json +++ b/packages/@react-aria/menu/package.json @@ -1,6 +1,6 @@ { "name": "@react-aria/menu", - "version": "3.16.0", + "version": "3.17.0", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -22,19 +22,19 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/focus": "^3.19.0", - "@react-aria/i18n": "^3.12.4", - "@react-aria/interactions": "^3.22.5", - "@react-aria/overlays": "^3.24.0", - "@react-aria/selection": "^3.21.0", - "@react-aria/utils": "^3.26.0", - "@react-stately/collections": "^3.12.0", - "@react-stately/menu": "^3.9.0", - "@react-stately/selection": "^3.18.0", - "@react-stately/tree": "^3.8.6", - "@react-types/button": "^3.10.1", - "@react-types/menu": "^3.9.13", - "@react-types/shared": "^3.26.0", + "@react-aria/focus": "^3.19.1", + "@react-aria/i18n": "^3.12.5", + "@react-aria/interactions": "^3.23.0", + "@react-aria/overlays": "^3.25.0", + "@react-aria/selection": "^3.22.0", + "@react-aria/utils": "^3.27.0", + "@react-stately/collections": "^3.12.1", + "@react-stately/menu": "^3.9.1", + "@react-stately/selection": "^3.19.0", + "@react-stately/tree": "^3.8.7", + "@react-types/button": "^3.10.2", + "@react-types/menu": "^3.9.14", + "@react-types/shared": "^3.27.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { diff --git a/packages/@react-aria/meter/package.json b/packages/@react-aria/meter/package.json index a1aa2a67fed..8615d425d37 100644 --- a/packages/@react-aria/meter/package.json +++ b/packages/@react-aria/meter/package.json @@ -1,6 +1,6 @@ { "name": "@react-aria/meter", - "version": "3.4.18", + "version": "3.4.19", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -22,9 +22,9 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/progress": "^3.4.18", - "@react-types/meter": "^3.4.5", - "@react-types/shared": "^3.26.0", + "@react-aria/progress": "^3.4.19", + "@react-types/meter": "^3.4.6", + "@react-types/shared": "^3.27.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { diff --git a/packages/@react-aria/numberfield/package.json b/packages/@react-aria/numberfield/package.json index 98603cbd9cf..a53989355fe 100644 --- a/packages/@react-aria/numberfield/package.json +++ b/packages/@react-aria/numberfield/package.json @@ -1,6 +1,6 @@ { "name": "@react-aria/numberfield", - "version": "3.11.9", + "version": "3.11.10", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -22,16 +22,16 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/i18n": "^3.12.4", - "@react-aria/interactions": "^3.22.5", - "@react-aria/spinbutton": "^3.6.10", - "@react-aria/textfield": "^3.15.0", - "@react-aria/utils": "^3.26.0", - "@react-stately/form": "^3.1.0", - "@react-stately/numberfield": "^3.9.8", - "@react-types/button": "^3.10.1", - "@react-types/numberfield": "^3.8.7", - "@react-types/shared": "^3.26.0", + "@react-aria/i18n": "^3.12.5", + "@react-aria/interactions": "^3.23.0", + "@react-aria/spinbutton": "^3.6.11", + "@react-aria/textfield": "^3.16.0", + "@react-aria/utils": "^3.27.0", + "@react-stately/form": "^3.1.1", + "@react-stately/numberfield": "^3.9.9", + "@react-types/button": "^3.10.2", + "@react-types/numberfield": "^3.8.8", + "@react-types/shared": "^3.27.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { diff --git a/packages/@react-aria/overlays/package.json b/packages/@react-aria/overlays/package.json index ea6ea01e486..a91532d0f2c 100644 --- a/packages/@react-aria/overlays/package.json +++ b/packages/@react-aria/overlays/package.json @@ -1,6 +1,6 @@ { "name": "@react-aria/overlays", - "version": "3.24.0", + "version": "3.25.0", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -22,16 +22,16 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/focus": "^3.19.0", - "@react-aria/i18n": "^3.12.4", - "@react-aria/interactions": "^3.22.5", + "@react-aria/focus": "^3.19.1", + "@react-aria/i18n": "^3.12.5", + "@react-aria/interactions": "^3.23.0", "@react-aria/ssr": "^3.9.7", - "@react-aria/utils": "^3.26.0", - "@react-aria/visually-hidden": "^3.8.18", - "@react-stately/overlays": "^3.6.12", - "@react-types/button": "^3.10.1", - "@react-types/overlays": "^3.8.11", - "@react-types/shared": "^3.26.0", + "@react-aria/utils": "^3.27.0", + "@react-aria/visually-hidden": "^3.8.19", + "@react-stately/overlays": "^3.6.13", + "@react-types/button": "^3.10.2", + "@react-types/overlays": "^3.8.12", + "@react-types/shared": "^3.27.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { diff --git a/packages/@react-aria/overlays/src/useCloseOnScroll.ts b/packages/@react-aria/overlays/src/useCloseOnScroll.ts index 09588b09a5b..1f9aac1c213 100644 --- a/packages/@react-aria/overlays/src/useCloseOnScroll.ts +++ b/packages/@react-aria/overlays/src/useCloseOnScroll.ts @@ -23,7 +23,7 @@ export const onCloseMap: WeakMap void> = new WeakMap(); interface CloseOnScrollOptions { triggerRef: RefObject, isOpen?: boolean, - onClose?: () => void + onClose?: (() => void) | null } /** @private */ diff --git a/packages/@react-aria/overlays/src/useOverlayPosition.ts b/packages/@react-aria/overlays/src/useOverlayPosition.ts index bb1ad72f5e6..d5bf2ee1e06 100644 --- a/packages/@react-aria/overlays/src/useOverlayPosition.ts +++ b/packages/@react-aria/overlays/src/useOverlayPosition.ts @@ -48,7 +48,7 @@ export interface AriaPositionProps extends PositionProps { */ shouldUpdatePosition?: boolean, /** Handler that is called when the overlay should close. */ - onClose?: () => void, + onClose?: (() => void) | null, /** * The maxHeight specified for the overlay element. * By default, it will take all space up to the current viewport height. diff --git a/packages/@react-aria/overlays/src/usePopover.ts b/packages/@react-aria/overlays/src/usePopover.ts index 3577dff3be6..903af187fae 100644 --- a/packages/@react-aria/overlays/src/usePopover.ts +++ b/packages/@react-aria/overlays/src/usePopover.ts @@ -98,7 +98,7 @@ export function usePopover(props: AriaPopoverProps, state: OverlayTriggerState): targetRef: triggerRef, overlayRef: popoverRef, isOpen: state.isOpen, - onClose: isNonModal ? state.close : undefined + onClose: isNonModal ? state.close : null }); usePreventScroll({ diff --git a/packages/@react-aria/overlays/test/usePopover.test.tsx b/packages/@react-aria/overlays/test/usePopover.test.tsx new file mode 100644 index 00000000000..1b65f9edf23 --- /dev/null +++ b/packages/@react-aria/overlays/test/usePopover.test.tsx @@ -0,0 +1,41 @@ +/* + * Copyright 2024 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import {fireEvent, render} from '@react-spectrum/test-utils-internal'; +import {type OverlayTriggerProps, useOverlayTriggerState} from '@react-stately/overlays'; +import React, {useRef} from 'react'; +import {useOverlayTrigger, usePopover} from '../'; + +function Example(props: OverlayTriggerProps) { + const triggerRef = useRef(null); + const popoverRef = useRef(null); + const state = useOverlayTriggerState(props); + useOverlayTrigger({type: 'listbox'}, state, triggerRef); + const {popoverProps} = usePopover({triggerRef, popoverRef}, state); + + return ( +
    +
    +
    +
    + ); +} + +describe('usePopover', () => { + it('should not close popover on scroll', () => { + const onOpenChange = jest.fn(); + render(); + + fireEvent.scroll(document.body); + expect(onOpenChange).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/@react-aria/progress/package.json b/packages/@react-aria/progress/package.json index 098d32d8bb0..7bb05b8b6b9 100644 --- a/packages/@react-aria/progress/package.json +++ b/packages/@react-aria/progress/package.json @@ -1,6 +1,6 @@ { "name": "@react-aria/progress", - "version": "3.4.18", + "version": "3.4.19", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/index.js", @@ -22,15 +22,16 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/i18n": "^3.12.4", - "@react-aria/label": "^3.7.13", - "@react-aria/utils": "^3.26.0", - "@react-types/progress": "^3.5.8", - "@react-types/shared": "^3.26.0", + "@react-aria/i18n": "^3.12.5", + "@react-aria/label": "^3.7.14", + "@react-aria/utils": "^3.27.0", + "@react-types/progress": "^3.5.9", + "@react-types/shared": "^3.27.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" }, "publishConfig": { "access": "public" diff --git a/packages/@react-aria/radio/package.json b/packages/@react-aria/radio/package.json index cd3ec7f748b..ad106e58a4c 100644 --- a/packages/@react-aria/radio/package.json +++ b/packages/@react-aria/radio/package.json @@ -1,6 +1,6 @@ { "name": "@react-aria/radio", - "version": "3.10.10", + "version": "3.10.11", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -22,19 +22,20 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/focus": "^3.19.0", - "@react-aria/form": "^3.0.11", - "@react-aria/i18n": "^3.12.4", - "@react-aria/interactions": "^3.22.5", - "@react-aria/label": "^3.7.13", - "@react-aria/utils": "^3.26.0", - "@react-stately/radio": "^3.10.9", - "@react-types/radio": "^3.8.5", - "@react-types/shared": "^3.26.0", + "@react-aria/focus": "^3.19.1", + "@react-aria/form": "^3.0.12", + "@react-aria/i18n": "^3.12.5", + "@react-aria/interactions": "^3.23.0", + "@react-aria/label": "^3.7.14", + "@react-aria/utils": "^3.27.0", + "@react-stately/radio": "^3.10.10", + "@react-types/radio": "^3.8.6", + "@react-types/shared": "^3.27.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" }, "publishConfig": { "access": "public" diff --git a/packages/@react-aria/searchfield/package.json b/packages/@react-aria/searchfield/package.json index c597f4543a6..beb2f7d8d89 100644 --- a/packages/@react-aria/searchfield/package.json +++ b/packages/@react-aria/searchfield/package.json @@ -1,6 +1,6 @@ { "name": "@react-aria/searchfield", - "version": "3.7.11", + "version": "3.8.0", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -22,17 +22,18 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/i18n": "^3.12.4", - "@react-aria/textfield": "^3.15.0", - "@react-aria/utils": "^3.26.0", - "@react-stately/searchfield": "^3.5.8", - "@react-types/button": "^3.10.1", - "@react-types/searchfield": "^3.5.10", - "@react-types/shared": "^3.26.0", + "@react-aria/i18n": "^3.12.5", + "@react-aria/textfield": "^3.16.0", + "@react-aria/utils": "^3.27.0", + "@react-stately/searchfield": "^3.5.9", + "@react-types/button": "^3.10.2", + "@react-types/searchfield": "^3.5.11", + "@react-types/shared": "^3.27.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" }, "publishConfig": { "access": "public" diff --git a/packages/@react-aria/searchfield/src/useSearchField.ts b/packages/@react-aria/searchfield/src/useSearchField.ts index a8110c41e0e..15aae4a1a67 100644 --- a/packages/@react-aria/searchfield/src/useSearchField.ts +++ b/packages/@react-aria/searchfield/src/useSearchField.ts @@ -73,9 +73,12 @@ export function useSearchField( } if (key === 'Escape') { - if (state.value === '') { + // Also check the inputRef value for the case where the value was set directly on the input element instead of going through + // the hook + if (state.value === '' && (!inputRef.current || inputRef.current.value === '')) { e.continuePropagation(); } else { + e.preventDefault(); state.setValue(''); if (onClear) { onClear(); diff --git a/packages/@react-aria/select/package.json b/packages/@react-aria/select/package.json index 8cb27bee9ec..7d3b56e3752 100644 --- a/packages/@react-aria/select/package.json +++ b/packages/@react-aria/select/package.json @@ -1,6 +1,6 @@ { "name": "@react-aria/select", - "version": "3.15.0", + "version": "3.15.1", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -22,19 +22,19 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/form": "^3.0.11", - "@react-aria/i18n": "^3.12.4", - "@react-aria/interactions": "^3.22.5", - "@react-aria/label": "^3.7.13", - "@react-aria/listbox": "^3.13.6", - "@react-aria/menu": "^3.16.0", - "@react-aria/selection": "^3.21.0", - "@react-aria/utils": "^3.26.0", - "@react-aria/visually-hidden": "^3.8.18", - "@react-stately/select": "^3.6.9", - "@react-types/button": "^3.10.1", - "@react-types/select": "^3.9.8", - "@react-types/shared": "^3.26.0", + "@react-aria/form": "^3.0.12", + "@react-aria/i18n": "^3.12.5", + "@react-aria/interactions": "^3.23.0", + "@react-aria/label": "^3.7.14", + "@react-aria/listbox": "^3.14.0", + "@react-aria/menu": "^3.17.0", + "@react-aria/selection": "^3.22.0", + "@react-aria/utils": "^3.27.0", + "@react-aria/visually-hidden": "^3.8.19", + "@react-stately/select": "^3.6.10", + "@react-types/button": "^3.10.2", + "@react-types/select": "^3.9.9", + "@react-types/shared": "^3.27.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { diff --git a/packages/@react-aria/selection/package.json b/packages/@react-aria/selection/package.json index 15cb3ea42f7..56ea0d1c6d8 100644 --- a/packages/@react-aria/selection/package.json +++ b/packages/@react-aria/selection/package.json @@ -1,6 +1,6 @@ { "name": "@react-aria/selection", - "version": "3.21.0", + "version": "3.22.0", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -22,12 +22,12 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/focus": "^3.19.0", - "@react-aria/i18n": "^3.12.4", - "@react-aria/interactions": "^3.22.5", - "@react-aria/utils": "^3.26.0", - "@react-stately/selection": "^3.18.0", - "@react-types/shared": "^3.26.0", + "@react-aria/focus": "^3.19.1", + "@react-aria/i18n": "^3.12.5", + "@react-aria/interactions": "^3.23.0", + "@react-aria/utils": "^3.27.0", + "@react-stately/selection": "^3.19.0", + "@react-types/shared": "^3.27.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { diff --git a/packages/@react-aria/selection/src/useSelectableCollection.ts b/packages/@react-aria/selection/src/useSelectableCollection.ts index 0d67ea18faf..b8fd9498a27 100644 --- a/packages/@react-aria/selection/src/useSelectableCollection.ts +++ b/packages/@react-aria/selection/src/useSelectableCollection.ts @@ -10,13 +10,13 @@ * governing permissions and limitations under the License. */ -import {CLEAR_FOCUS_EVENT, FOCUS_EVENT, focusWithoutScrolling, mergeProps, scrollIntoView, scrollIntoViewport, UPDATE_ACTIVEDESCENDANT, useEvent, useRouter} from '@react-aria/utils'; +import {CLEAR_FOCUS_EVENT, FOCUS_EVENT, focusWithoutScrolling, isCtrlKeyPressed, mergeProps, scrollIntoView, scrollIntoViewport, UPDATE_ACTIVEDESCENDANT, useEffectEvent, useEvent, useRouter, useUpdateLayoutEffect} from '@react-aria/utils'; import {DOMAttributes, FocusableElement, FocusStrategy, Key, KeyboardDelegate, RefObject} from '@react-types/shared'; import {flushSync} from 'react-dom'; import {FocusEvent, KeyboardEvent, useEffect, useRef} from 'react'; import {focusSafely, getFocusableTreeWalker} from '@react-aria/focus'; import {getInteractionModality} from '@react-aria/interactions'; -import {isCtrlKeyPressed, isNonContiguousSelectionModifier} from './utils'; +import {isNonContiguousSelectionModifier} from './utils'; import {MultipleSelectionManager} from '@react-stately/selection'; import {useLocale} from '@react-aria/i18n'; import {useTypeSelect} from './useTypeSelect'; @@ -342,12 +342,11 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions } manager.setFocused(true); - if (manager.focusedKey == null) { - let navigateToFirstKey = (key: Key | undefined | null) => { + let navigateToKey = (key: Key | undefined | null) => { if (key != null) { manager.setFocusedKey(key); - if (selectOnFocus) { + if (selectOnFocus && !manager.isSelected(key)) { manager.replaceSelection(key); } } @@ -357,9 +356,9 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions // and either focus the first or last item accordingly. let relatedTarget = e.relatedTarget as Element; if (relatedTarget && (e.currentTarget.compareDocumentPosition(relatedTarget) & Node.DOCUMENT_POSITION_FOLLOWING)) { - navigateToFirstKey(manager.lastSelectedKey ?? delegate.getLastKey?.()); + navigateToKey(manager.lastSelectedKey ?? delegate.getLastKey?.()); } else { - navigateToFirstKey(manager.firstSelectedKey ?? delegate.getFirstKey?.()); + navigateToKey(manager.firstSelectedKey ?? delegate.getFirstKey?.()); } } else if (!isVirtualized && scrollRef.current) { // Restore the scroll position to what it was before. @@ -391,6 +390,10 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions } }; + // Ref to track whether the first item in the collection should be automatically focused. Specifically used for autocomplete when user types + // to focus the first key AFTER the collection updates. + // TODO: potentially expand the usage of this + let shouldVirtualFocusFirst = useRef(false); // Add event listeners for custom virtual events. These handle updating the focused key in response to various keyboard events // at the autocomplete level // TODO: fix type later @@ -401,21 +404,56 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions // If the user is typing forwards, autofocus the first option in the list. if (detail?.focusStrategy === 'first') { - let keyToFocus = delegate.getFirstKey?.() ?? null; - // If no focusable items exist in the list, make sure to clear any activedescendant that may still exist - if (keyToFocus == null) { - ref.current?.dispatchEvent( - new CustomEvent(UPDATE_ACTIVEDESCENDANT, { - cancelable: true, - bubbles: true - }) - ); - } + shouldVirtualFocusFirst.current = true; + } + }); + let updateActiveDescendant = useEffectEvent(() => { + let keyToFocus = delegate.getFirstKey?.() ?? null; + + // If no focusable items exist in the list, make sure to clear any activedescendant that may still exist + if (keyToFocus == null) { + ref.current?.dispatchEvent( + new CustomEvent(UPDATE_ACTIVEDESCENDANT, { + cancelable: true, + bubbles: true + }) + ); + + // If there wasn't a focusable key but the collection had items, then that means we aren't in an intermediate load state and all keys are disabled. + // Reset shouldVirtualFocusFirst so that we don't erronously autofocus an item when the collection is filtered again. + if (manager.collection.size > 0) { + shouldVirtualFocusFirst.current = false; + } + } else { manager.setFocusedKey(keyToFocus); + // Only set shouldVirtualFocusFirst to false if we've successfully set the first key as the focused key + // If there wasn't a key to focus, we might be in a temporary loading state so we'll want to still focus the first key + // after the collection updates after load + shouldVirtualFocusFirst.current = false; + } + }); + + useUpdateLayoutEffect(() => { + if (shouldVirtualFocusFirst.current) { + updateActiveDescendant(); + } + + }, [manager.collection, updateActiveDescendant]); + + let resetFocusFirstFlag = useEffectEvent(() => { + // If user causes the focused key to change in any other way, clear shouldVirtualFocusFirst so we don't + // accidentally move focus from under them. Skip this if the collection was empty because we might be in a load + // state and will still want to focus the first item after load + if (manager.collection.size > 0) { + shouldVirtualFocusFirst.current = false; } }); + useUpdateLayoutEffect(() => { + resetFocusFirstFlag(); + }, [manager.focusedKey, resetFocusFirstFlag]); + useEvent(ref, CLEAR_FOCUS_EVENT, !shouldUseVirtualFocus ? undefined : (e) => { e.stopPropagation(); manager.setFocused(false); diff --git a/packages/@react-aria/selection/src/useSelectableItem.ts b/packages/@react-aria/selection/src/useSelectableItem.ts index ebe9fb766ae..8e97ca7825d 100644 --- a/packages/@react-aria/selection/src/useSelectableItem.ts +++ b/packages/@react-aria/selection/src/useSelectableItem.ts @@ -12,8 +12,8 @@ import {DOMAttributes, DOMProps, FocusableElement, Key, LongPressEvent, PointerType, PressEvent, RefObject} from '@react-types/shared'; import {focusSafely} from '@react-aria/focus'; -import {isCtrlKeyPressed, isNonContiguousSelectionModifier} from './utils'; -import {mergeProps, openLink, UPDATE_ACTIVEDESCENDANT, useId, useRouter} from '@react-aria/utils'; +import {isCtrlKeyPressed, mergeProps, openLink, UPDATE_ACTIVEDESCENDANT, useId, useRouter} from '@react-aria/utils'; +import {isNonContiguousSelectionModifier} from './utils'; import {MultipleSelectionManager} from '@react-stately/selection'; import {PressProps, useLongPress, usePress} from '@react-aria/interactions'; import {useEffect, useRef} from 'react'; @@ -160,6 +160,9 @@ export function useSelectableItem(options: SelectableItemOptions): SelectableIte }; // Focus the associated DOM node when this item becomes the focusedKey + // TODO: can't make this useLayoutEffect bacause it breaks menus inside dialogs + // However, if this is a useEffect, it runs twice and dispatches two UPDATE_ACTIVEDESCENDANT and immediately sets + // aria-activeDescendant in useAutocomplete... I've worked around this for now useEffect(() => { let isFocused = key === manager.focusedKey; if (isFocused && manager.isFocused) { diff --git a/packages/@react-aria/selection/src/utils.ts b/packages/@react-aria/selection/src/utils.ts index d0aace06543..6fabc5593f4 100644 --- a/packages/@react-aria/selection/src/utils.ts +++ b/packages/@react-aria/selection/src/utils.ts @@ -10,7 +10,7 @@ * governing permissions and limitations under the License. */ -import {isAppleDevice, isMac} from '@react-aria/utils'; +import {isAppleDevice} from '@react-aria/utils'; interface Event { altKey: boolean, @@ -23,11 +23,3 @@ export function isNonContiguousSelectionModifier(e: Event) { // On Windows and Ubuntu, Alt + Space has a system wide meaning. return isAppleDevice() ? e.altKey : e.ctrlKey; } - -export function isCtrlKeyPressed(e: Event) { - if (isMac()) { - return e.metaKey; - } - - return e.ctrlKey; -} diff --git a/packages/@react-aria/selection/test/useSelectableCollection.test.js b/packages/@react-aria/selection/test/useSelectableCollection.test.js index f9c38787581..b83705e5248 100644 --- a/packages/@react-aria/selection/test/useSelectableCollection.test.js +++ b/packages/@react-aria/selection/test/useSelectableCollection.test.js @@ -10,7 +10,7 @@ * governing permissions and limitations under the License. */ -import {fireEvent, installPointerEvent, pointerMap, render, simulateDesktop, simulateMobile} from '@react-spectrum/test-utils-internal'; +import {fireEvent, installPointerEvent, pointerMap, render, simulateDesktop, simulateMobile, within} from '@react-spectrum/test-utils-internal'; import {Item} from '@react-stately/collections'; import {List} from '../stories/List'; import React from 'react'; @@ -113,5 +113,43 @@ describe('useSelectableCollection', () => { expect(options[1]).not.toHaveAttribute('aria-selected'); expect(options[2]).toHaveAttribute('aria-selected', 'true'); }); + + it('focuses first/last selected item on focus enter and does not change the selection', async () => { + let onSelectionChange1 = jest.fn(); + let onSelectionChange2 = jest.fn(); + let {getByText, getAllByRole} = render( + <> + + Paco de Lucia + Vicente Amigo + Gerardo Nunez + + + + Paco de Lucia + Vicente Amigo + Gerardo Nunez + + + ); + let [firstList, secondList] = getAllByRole('listbox'); + await user.click(getByText('focus stop')); + await user.tab(); + expect(document.activeElement).toBe(within(secondList).getByRole('option', {name: 'Vicente Amigo'})); + await user.click(getByText('focus stop')); + await user.tab({shift: true}); + expect(document.activeElement).toBe(within(firstList).getByRole('option', {name: 'Gerardo Nunez'})); + + expect(onSelectionChange1).not.toHaveBeenCalled(); + expect(onSelectionChange2).not.toHaveBeenCalled(); + }); }); }); diff --git a/packages/@react-aria/separator/package.json b/packages/@react-aria/separator/package.json index 06c750c3ae3..a54628bf536 100644 --- a/packages/@react-aria/separator/package.json +++ b/packages/@react-aria/separator/package.json @@ -1,6 +1,6 @@ { "name": "@react-aria/separator", - "version": "3.4.4", + "version": "3.4.5", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -22,12 +22,13 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/utils": "^3.26.0", - "@react-types/shared": "^3.26.0", + "@react-aria/utils": "^3.27.0", + "@react-types/shared": "^3.27.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" }, "publishConfig": { "access": "public" diff --git a/packages/@react-aria/slider/docs/useSlider.mdx b/packages/@react-aria/slider/docs/useSlider.mdx index ecdd4728e66..5760c1ecb58 100644 --- a/packages/@react-aria/slider/docs/useSlider.mdx +++ b/packages/@react-aria/slider/docs/useSlider.mdx @@ -375,7 +375,7 @@ function Example() { ### Custom value scale -By default, slider values are precentages between 0 and 100. A different scale can be used by setting the `minValue` and `maxValue` props. +By default, slider values are percentages between 0 and 100. A different scale can be used by setting the `minValue` and `maxValue` props. ```tsx example { + /** + * Set the interaction type used by the combobox tester. + */ + setInteractionType(type: UserOpts['interactionType']) { this._interactionType = type; - }; + } - open = async (opts: {triggerBehavior?: 'focus' | 'manual', interactionType?: UserOpts['interactionType']} = {}) => { + /** + * Opens the combobox dropdown. Defaults to using the interaction type set on the combobox tester. + */ + async open(opts: ComboBoxOpenOpts = {}) { let {triggerBehavior = 'manual', interactionType = this._interactionType} = opts; let trigger = this.trigger; let combobox = this.combobox; @@ -96,18 +116,51 @@ export class ComboBoxTester { return true; } }); - }; + } + + /** + * Returns an option matching the specified index or text content. + */ + findOption(opts: {optionIndexOrText: number | string}): HTMLElement { + let { + optionIndexOrText + } = opts; + + let option; + let options = this.options(); + let listbox = this.listbox; - selectOption = async (opts: {option?: HTMLElement, optionText?: string, triggerBehavior?: 'focus' | 'manual', interactionType?: UserOpts['interactionType']} = {}) => { - let {optionText, option, triggerBehavior, interactionType = this._interactionType} = opts; + if (typeof optionIndexOrText === 'number') { + option = options[optionIndexOrText]; + } else if (typeof optionIndexOrText === 'string' && listbox != null) { + option = (within(listbox!).getByText(optionIndexOrText).closest('[role=option]'))! as HTMLElement; + } + + return option; + } + + /** + * Selects the desired combobox option. Defaults to using the interaction type set on the combobox tester. If necessary, will open the combobox dropdown beforehand. + * The desired option can be targeted via the option's node, the option's text, or the option's index. + */ + async selectOption(opts: ComboBoxSelectOpts) { + let {option, triggerBehavior, interactionType = this._interactionType} = opts; if (!this.combobox.getAttribute('aria-controls')) { await this.open({triggerBehavior}); } let listbox = this.listbox; + if (!listbox) { + throw new Error('Combobox\'s listbox not found.'); + } + if (listbox) { - if (!option && optionText) { - option = within(listbox).getByText(optionText); + if (typeof option === 'string' || typeof option === 'number') { + option = this.findOption({optionIndexOrText: option}); + } + + if (!option) { + throw new Error('Target option not found in the listbox.'); } // TODO: keyboard method of selecting the the option is a bit tricky unless I simply simulate the user pressing the down arrow @@ -118,7 +171,7 @@ export class ComboBoxTester { await this.user.pointer({target: option, keys: '[TouchA]'}); } - if (option && option.getAttribute('href') == null) { + if (option.getAttribute('href') == null) { await waitFor(() => { if (document.contains(listbox)) { throw new Error('Expected listbox element to not be in the document after selecting an option'); @@ -130,9 +183,12 @@ export class ComboBoxTester { } else { throw new Error("Attempted to select a option in the combobox, but the listbox wasn't found."); } - }; + } - close = async () => { + /** + * Closes the combobox dropdown. + */ + async close() { let listbox = this.listbox; if (listbox) { act(() => this.combobox.focus()); @@ -146,43 +202,56 @@ export class ComboBoxTester { } }); } - }; + } - get combobox() { + /** + * Returns the combobox. + */ + get combobox(): HTMLElement { return this._combobox; } - get trigger() { + /** + * Returns the combobox trigger button. + */ + get trigger(): HTMLElement { return this._trigger; } - get listbox() { + /** + * Returns the combobox's listbox if present. + */ + get listbox(): HTMLElement | null { let listBoxId = this.combobox.getAttribute('aria-controls'); - return listBoxId ? document.getElementById(listBoxId) || undefined : undefined; + return listBoxId ? document.getElementById(listBoxId) || null : null; + } + + /** + * Returns the combobox's sections if present. + */ + get sections(): HTMLElement[] { + let listbox = this.listbox; + return listbox ? within(listbox).queryAllByRole('group') : []; } - options = (opts: {element?: HTMLElement} = {}): HTMLElement[] | never[] => { - let {element} = opts; - element = element || this.listbox; + /** + * Returns the combobox's options if present. Can be filtered to a subsection of the listbox if provided via `element`. + */ + options(opts: {element?: HTMLElement} = {}): HTMLElement[] { + let {element = this.listbox} = opts; let options = []; if (element) { options = within(element).queryAllByRole('option'); } return options; - }; - - get sections() { - let listbox = this.listbox; - if (listbox) { - return within(listbox).queryAllByRole('group'); - } else { - return []; - } } - get focusedOption() { + /** + * Returns the currently focused option in the combobox's dropdown if any. + */ + get focusedOption(): HTMLElement | null { let focusedOptionId = this.combobox.getAttribute('aria-activedescendant'); - return focusedOptionId ? document.getElementById(focusedOptionId) : undefined; + return focusedOptionId ? document.getElementById(focusedOptionId) : null; } } diff --git a/packages/@react-aria/test-utils/src/events.ts b/packages/@react-aria/test-utils/src/events.ts index 957fb8630fc..73a048de8d8 100644 --- a/packages/@react-aria/test-utils/src/events.ts +++ b/packages/@react-aria/test-utils/src/events.ts @@ -11,7 +11,7 @@ */ import {act, fireEvent} from '@testing-library/react'; -import {UserOpts} from './user'; +import {UserOpts} from './types'; export const DEFAULT_LONG_PRESS_TIME = 500; diff --git a/packages/@react-aria/test-utils/src/gridlist.ts b/packages/@react-aria/test-utils/src/gridlist.ts index eb6decdf004..2052131f678 100644 --- a/packages/@react-aria/test-utils/src/gridlist.ts +++ b/packages/@react-aria/test-utils/src/gridlist.ts @@ -11,106 +11,193 @@ */ import {act, within} from '@testing-library/react'; -import {BaseTesterOpts, UserOpts} from './user'; -import {pressElement} from './events'; +import {GridListTesterOpts, GridRowActionOpts, ToggleGridRowOpts, UserOpts} from './types'; +import {pressElement, triggerLongPress} from './events'; + +interface GridListToggleRowOpts extends ToggleGridRowOpts {} +interface GridListRowActionOpts extends GridRowActionOpts {} -export interface GridListOptions extends UserOpts, BaseTesterOpts { - user?: any -} export class GridListTester { private user; private _interactionType: UserOpts['interactionType']; + private _advanceTimer: UserOpts['advanceTimer']; private _gridlist: HTMLElement; - - constructor(opts: GridListOptions) { - let {root, user, interactionType} = opts; + constructor(opts: GridListTesterOpts) { + let {root, user, interactionType, advanceTimer} = opts; this.user = user; this._interactionType = interactionType || 'mouse'; + this._advanceTimer = advanceTimer; this._gridlist = root; } - setInteractionType = (type: UserOpts['interactionType']) => { + /** + * Set the interaction type used by the gridlist tester. + */ + setInteractionType(type: UserOpts['interactionType']) { this._interactionType = type; + } + + /** + * Returns a row matching the specified index or text content. + */ + findRow(opts: {rowIndexOrText: number | string}): HTMLElement { + let { + rowIndexOrText + } = opts; + + let row; + if (typeof rowIndexOrText === 'number') { + row = this.rows[rowIndexOrText]; + } else if (typeof rowIndexOrText === 'string') { + row = (within(this.gridlist!).getByText(rowIndexOrText).closest('[role=row]'))! as HTMLElement; + } + + return row; + } + + // TODO: RTL + private async keyboardNavigateToRow(opts: {row: HTMLElement}) { + let {row} = opts; + let rows = this.rows; + let targetIndex = rows.indexOf(row); + if (targetIndex === -1) { + throw new Error('Option provided is not in the gridlist'); + } + if (document.activeElement === this._gridlist) { + await this.user.keyboard('[ArrowDown]'); + } else if (this._gridlist.contains(document.activeElement) && document.activeElement!.getAttribute('role') !== 'row') { + do { + await this.user.keyboard('[ArrowLeft]'); + } while (document.activeElement!.getAttribute('role') !== 'row'); + } + let currIndex = rows.indexOf(document.activeElement as HTMLElement); + if (currIndex === -1) { + throw new Error('ActiveElement is not in the gridlist'); + } + let direction = targetIndex > currIndex ? 'down' : 'up'; + + for (let i = 0; i < Math.abs(targetIndex - currIndex); i++) { + await this.user.keyboard(`[${direction === 'down' ? 'ArrowDown' : 'ArrowUp'}]`); + } }; - // TODO: support long press? This is also pretty much the same as table's toggleRowSelection so maybe can share - // For now, don't include long press, see if people need it or if we should just expose long press as a separate util if it isn't very common - // If the current way of passing in the user specified advance timers is ok, then I'd be find including long press - // Maybe also support an option to force the click to happen on a specific part of the element (checkbox or row). That way - // the user can test a specific type of interaction? - toggleRowSelection = async (opts: {index?: number, text?: string, interactionType?: UserOpts['interactionType']} = {}) => { - let {index, text, interactionType = this._interactionType} = opts; + /** + * Toggles the selection for the specified gridlist row. Defaults to using the interaction type set on the gridlist tester. + */ + async toggleRowSelection(opts: GridListToggleRowOpts) { + let { + row, + needsLongPress, + checkboxSelection = true, + interactionType = this._interactionType + } = opts; + + if (typeof row === 'string' || typeof row === 'number') { + row = this.findRow({rowIndexOrText: row}); + } + + if (!row) { + throw new Error('Target row not found in the gridlist.'); + } - let row = this.findRow({index, text}); let rowCheckbox = within(row).queryByRole('checkbox'); - if (rowCheckbox) { + + // TODO: we early return here because the checkbox/row can't be keyboard navigated to if the row is disabled usually + // but we may to check for disabledBehavior (aka if the disable row gets skipped when keyboard navigating or not) + if (interactionType === 'keyboard' && (rowCheckbox?.getAttribute('disabled') === '' || row?.getAttribute('aria-disabled') === 'true')) { + return; + } + + // this would be better than the check to do nothing in events.ts + // also, it'd be good to be able to trigger selection on the row instead of having to go to the checkbox directly + if (interactionType === 'keyboard' && !checkboxSelection) { + await this.keyboardNavigateToRow({row}); + await this.user.keyboard('{Space}'); + return; + } + if (rowCheckbox && checkboxSelection) { await pressElement(this.user, rowCheckbox, interactionType); } else { let cell = within(row).getAllByRole('gridcell')[0]; - await pressElement(this.user, cell, interactionType); - } - }; + if (needsLongPress && interactionType === 'touch') { + if (this._advanceTimer == null) { + throw new Error('No advanceTimers provided for long press.'); + } - // TODO: pretty much the same as table except it uses this.gridlist. Make common between the two by accepting an option for - // an element? - findRow = (opts: {index?: number, text?: string}) => { - let { - index, - text - } = opts; + // Note that long press interactions with rows is strictly touch only for grid rows + await triggerLongPress({element: cell, advanceTimer: this._advanceTimer, pointerOpts: {pointerType: 'touch'}}); - let row; - if (index != null) { - row = this.rows[index]; - } else if (text != null) { - row = within(this?.gridlist).getByText(text); - while (row && row.getAttribute('role') !== 'row') { - row = row.parentElement; + } else { + await pressElement(this.user, cell, interactionType); } } - - return row; - }; + } // TODO: There is a more difficult use case where the row has/behaves as link, don't think we have a good way to determine that unless the // user specificlly tells us - triggerRowAction = async (opts: {index?: number, text?: string, needsDoubleClick?: boolean, interactionType?: UserOpts['interactionType']}) => { + /** + * Triggers the action for the specified gridlist row. Defaults to using the interaction type set on the gridlist tester. + */ + async triggerRowAction(opts: GridListRowActionOpts) { let { - index, - text, + row, needsDoubleClick, interactionType = this._interactionType } = opts; - let row = this.findRow({index, text}); - if (row) { - if (needsDoubleClick) { - await this.user.dblClick(row); - } else if (interactionType === 'keyboard') { - act(() => row.focus()); - await this.user.keyboard('[Enter]'); - } else { - await pressElement(this.user, row, interactionType); + if (typeof row === 'string' || typeof row === 'number') { + row = this.findRow({rowIndexOrText: row}); + } + + if (!row) { + throw new Error('Target row not found in the gridlist.'); + } + + if (needsDoubleClick) { + await this.user.dblClick(row); + } else if (interactionType === 'keyboard') { + if (row?.getAttribute('aria-disabled') === 'true') { + return; + } + + if (document.activeElement !== this._gridlist || !this._gridlist.contains(document.activeElement)) { + act(() => this._gridlist.focus()); } + + await this.keyboardNavigateToRow({row}); + await this.user.keyboard('[Enter]'); + } else { + await pressElement(this.user, row, interactionType); } - }; + } - // TODO: do we really need this getter? Theoretically the user already has the reference to the gridlist - get gridlist() { + /** + * Returns the gridlist. + */ + get gridlist(): HTMLElement { return this._gridlist; } - get rows() { + /** + * Returns the gridlist's rows if any. + */ + get rows(): HTMLElement[] { return within(this?.gridlist).queryAllByRole('row'); } - get selectedRows() { + /** + * Returns the gridlist's selected rows if any. + */ + get selectedRows(): HTMLElement[] { return this.rows.filter(row => row.getAttribute('aria-selected') === 'true'); } - cells = (opts: {element?: HTMLElement} = {}) => { - let {element} = opts; - return within(element || this.gridlist).queryAllByRole('gridcell'); - }; + /** + * Returns the gridlist's cells if any. Can be filtered against a specific row if provided via `element`. + */ + cells(opts: {element?: HTMLElement} = {}): HTMLElement[] { + let {element = this.gridlist} = opts; + return within(element).queryAllByRole('gridcell'); + } } diff --git a/packages/@react-aria/test-utils/src/index.ts b/packages/@react-aria/test-utils/src/index.ts index 962abbf72c3..b5b7da34492 100644 --- a/packages/@react-aria/test-utils/src/index.ts +++ b/packages/@react-aria/test-utils/src/index.ts @@ -15,4 +15,4 @@ export {installMouseEvent, installPointerEvent} from './testSetup'; export {pointerMap} from './userEventMaps'; export {User} from './user'; -export type {UserOpts} from './user'; +export type {UserOpts} from './types'; diff --git a/packages/@react-aria/test-utils/src/listbox.ts b/packages/@react-aria/test-utils/src/listbox.ts new file mode 100644 index 00000000000..8fbec312b7b --- /dev/null +++ b/packages/@react-aria/test-utils/src/listbox.ts @@ -0,0 +1,226 @@ +/* + * Copyright 2024 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import {act, within} from '@testing-library/react'; +import {ListBoxTesterOpts, UserOpts} from './types'; +import {pressElement, triggerLongPress} from './events'; + +interface ListBoxToggleOptionOpts { + /** + * What interaction type to use when toggling selection for an option. Defaults to the interaction type set on the tester. + */ + interactionType?: UserOpts['interactionType'], + /** + * The index, text, or node of the option to toggle selection for. + */ + option: number | string | HTMLElement, + /** + * Whether the option should be triggered by Space or Enter in keyboard modality. + * @default 'Enter' + */ + keyboardActivation?: 'Space' | 'Enter', + /** + * Whether the option needs to be long pressed to be selected. Depends on the listbox's implementation. + */ + needsLongPress?: boolean +} + +interface ListBoxOptionActionOpts extends Omit { + /** + * Whether or not the option needs a double click to trigger the option action. Depends on the listbox's implementation. + */ + needsDoubleClick?: boolean +} + +export class ListBoxTester { + private user; + private _interactionType: UserOpts['interactionType']; + private _advanceTimer: UserOpts['advanceTimer']; + private _listbox: HTMLElement; + + constructor(opts: ListBoxTesterOpts) { + let {root, user, interactionType, advanceTimer} = opts; + this.user = user; + this._interactionType = interactionType || 'mouse'; + this._listbox = root; + this._advanceTimer = advanceTimer; + } + + /** + * Set the interaction type used by the listbox tester. + */ + setInteractionType(type: UserOpts['interactionType']) { + this._interactionType = type; + } + + // TODO: now that we have listbox, perhaps select can make use of this tester internally + /** + * Returns a option matching the specified index or text content. + */ + findOption(opts: {optionIndexOrText: number | string}): HTMLElement { + let { + optionIndexOrText + } = opts; + + let option; + let options = this.options(); + + if (typeof optionIndexOrText === 'number') { + option = options[optionIndexOrText]; + } else if (typeof optionIndexOrText === 'string') { + option = (within(this.listbox!).getByText(optionIndexOrText).closest('[role=option]'))! as HTMLElement; + } + + return option; + } + + // TODO: this is basically the same as menu except for the error message, refactor later so that they share + // TODO: this also doesn't support grid layout yet + private async keyboardNavigateToOption(opts: {option: HTMLElement}) { + let {option} = opts; + let options = this.options(); + let targetIndex = options.indexOf(option); + if (targetIndex === -1) { + throw new Error('Option provided is not in the listbox'); + } + + if (document.activeElement === this._listbox) { + await this.user.keyboard('[ArrowDown]'); + } + + // TODO: not sure about doing same while loop that exists in other implementations of keyboardNavigateToOption, + // feels like it could break easily + if (document.activeElement?.getAttribute('role') !== 'option') { + await act(async () => { + option.focus(); + }); + } + + let currIndex = options.indexOf(document.activeElement as HTMLElement); + if (currIndex === -1) { + throw new Error('ActiveElement is not in the listbox'); + } + let direction = targetIndex > currIndex ? 'down' : 'up'; + + for (let i = 0; i < Math.abs(targetIndex - currIndex); i++) { + await this.user.keyboard(`[${direction === 'down' ? 'ArrowDown' : 'ArrowUp'}]`); + } + }; + + /** + * Toggles the selection for the specified listbox option. Defaults to using the interaction type set on the listbox tester. + */ + async toggleOptionSelection(opts: ListBoxToggleOptionOpts) { + let {option, needsLongPress, keyboardActivation = 'Enter', interactionType = this._interactionType} = opts; + + if (typeof option === 'string' || typeof option === 'number') { + option = this.findOption({optionIndexOrText: option}); + } + + if (!option) { + throw new Error('Target option not found in the listbox.'); + } + + if (interactionType === 'keyboard') { + if (option?.getAttribute('aria-disabled') === 'true') { + return; + } + + if (document.activeElement !== this._listbox || !this._listbox.contains(document.activeElement)) { + act(() => this._listbox.focus()); + } + + await this.keyboardNavigateToOption({option}); + await this.user.keyboard(`[${keyboardActivation}]`); + } else { + if (needsLongPress && interactionType === 'touch') { + if (this._advanceTimer == null) { + throw new Error('No advanceTimers provided for long press.'); + } + + await triggerLongPress({element: option, advanceTimer: this._advanceTimer, pointerOpts: {pointerType: 'touch'}}); + } else { + await pressElement(this.user, option, interactionType); + } + } + } + + /** + * Triggers the action for the specified listbox option. Defaults to using the interaction type set on the listbox tester. + */ + async triggerOptionAction(opts: ListBoxOptionActionOpts) { + let { + option, + needsDoubleClick, + interactionType = this._interactionType + } = opts; + + if (typeof option === 'string' || typeof option === 'number') { + option = this.findOption({optionIndexOrText: option}); + } + + if (!option) { + throw new Error('Target option not found in the listbox.'); + } + + if (needsDoubleClick) { + await this.user.dblClick(option); + } else if (interactionType === 'keyboard') { + if (option?.getAttribute('aria-disabled') === 'true') { + return; + } + + if (document.activeElement !== this._listbox || !this._listbox.contains(document.activeElement)) { + act(() => this._listbox.focus()); + } + + await this.keyboardNavigateToOption({option}); + await this.user.keyboard('[Enter]'); + } else { + await pressElement(this.user, option, interactionType); + } + } + + /** + * Returns the listbox. + */ + get listbox(): HTMLElement { + return this._listbox; + } + + /** + * Returns the listbox options. Can be filtered to a subsection of the listbox if provided via `element`. + */ + options(opts: {element?: HTMLElement} = {}): HTMLElement[] { + let {element = this._listbox} = opts; + let options = []; + if (element) { + options = within(element).queryAllByRole('option'); + } + + return options; + } + + /** + * Returns the listbox's selected options if any. + */ + get selectedOptions(): HTMLElement[] { + return this.options().filter(row => row.getAttribute('aria-selected') === 'true'); + } + + /** + * Returns the listbox's sections if any. + */ + get sections(): HTMLElement[] { + return within(this._listbox).queryAllByRole('group'); + } +} diff --git a/packages/@react-aria/test-utils/src/menu.ts b/packages/@react-aria/test-utils/src/menu.ts index 73a1afd895c..f8cf4a346cf 100644 --- a/packages/@react-aria/test-utils/src/menu.ts +++ b/packages/@react-aria/test-utils/src/menu.ts @@ -11,13 +11,53 @@ */ import {act, waitFor, within} from '@testing-library/react'; -import {BaseTesterOpts, UserOpts} from './user'; +import {MenuTesterOpts, UserOpts} from './types'; import {triggerLongPress} from './events'; -export interface MenuOptions extends UserOpts, BaseTesterOpts { - user?: any, - isSubmenu?: boolean +interface MenuOpenOpts { + /** + * Whether the menu needs to be long pressed to open. + */ + needsLongPress?: boolean, + /** + * What interaction type to use when opening the menu. Defaults to the interaction type set on the tester. + */ + interactionType?: UserOpts['interactionType'], + /** + * Whether to open the menu via ArrowUp or ArrowDown if in keyboard modality. + */ + direction?: 'up' | 'down' } + +interface MenuSelectOpts extends MenuOpenOpts { + /** + * The index, text, or node of the option to select. Option nodes can be sourced via `options()`. + */ + option: number | string | HTMLElement, + /** + * The menu's selection mode. Will affect whether or not the menu is expected to be closed upon option selection. + * @default 'single' + */ + menuSelectionMode?: 'single' | 'multiple', + /** + * Whether or not the menu closes on select. Depends on menu implementation and configuration. + * @default true + */ + closesOnSelect?: boolean, + /** + * Whether the option should be triggered by Space or Enter in keyboard modality. + * @default 'Enter' + */ + keyboardActivation?: 'Space' | 'Enter' +} + +interface MenuOpenSubmenuOpts extends MenuOpenOpts { + /** + * The text or node of the submenu trigger to open. Available submenu trigger nodes can be sourced via `submenuTriggers`. + */ + submenuTrigger: string | HTMLElement +} + export class MenuTester { private user; private _interactionType: UserOpts['interactionType']; @@ -25,7 +65,7 @@ export class MenuTester { private _trigger: HTMLElement | undefined; private _isSubmenu: boolean = false; - constructor(opts: MenuOptions) { + constructor(opts: MenuTesterOpts) { let {root, user, interactionType, advanceTimer, isSubmenu} = opts; this.user = user; this._interactionType = interactionType || 'mouse'; @@ -47,13 +87,19 @@ export class MenuTester { this._isSubmenu = isSubmenu || false; } - setInteractionType = (type: UserOpts['interactionType']) => { + /** + * Set the interaction type used by the menu tester. + */ + setInteractionType(type: UserOpts['interactionType']) { this._interactionType = type; - }; + } // TODO: this has been common to select as well, maybe make select use it? Or make a generic method. Will need to make error messages generic // One difference will be that it supports long press as well - open = async (opts: {needsLongPress?: boolean, interactionType?: UserOpts['interactionType'], direction?: 'up' | 'down'} = {}) => { + /** + * Opens the menu. Defaults to using the interaction type set on the menu tester. + */ + async open(opts: MenuOpenOpts = {}) { let { needsLongPress, interactionType = this._interactionType, @@ -103,21 +149,37 @@ export class MenuTester { } }); } - }; + } + + /** + * Returns a option matching the specified index or text content. + */ + findOption(opts: {optionIndexOrText: number | string}): HTMLElement { + let { + optionIndexOrText + } = opts; + + let option; + let options = this.options(); + let menu = this.menu; + + if (typeof optionIndexOrText === 'number') { + option = options[optionIndexOrText]; + } else if (typeof optionIndexOrText === 'string' && menu != null) { + option = (within(menu!).getByText(optionIndexOrText).closest('[role=menuitem], [role=menuitemradio], [role=menuitemcheckbox]'))! as HTMLElement; + } + + return option; + } // TODO: also very similar to select, barring potential long press support // Close on select is also kinda specific? - selectOption = async (opts: { - option?: HTMLElement, - optionText?: string, - menuSelectionMode?: 'single' | 'multiple', - needsLongPress?: boolean, - closesOnSelect?: boolean, - interactionType?: UserOpts['interactionType'], - keyboardActivation?: 'Space' | 'Enter' - }) => { + /** + * Selects the desired menu option. Defaults to using the interaction type set on the menu tester. If necessary, will open the menu dropdown beforehand. + * The desired option can be targeted via the option's node, the option's text, or the option's index. + */ + async selectOption(opts: MenuSelectOpts) { let { - optionText, menuSelectionMode = 'single', needsLongPress, closesOnSelect = true, @@ -132,15 +194,25 @@ export class MenuTester { } let menu = this.menu; + + if (!menu) { + throw new Error('Menu not found.'); + } + if (menu) { - if (!option && optionText) { - option = (within(menu!).getByText(optionText).closest('[role=menuitem], [role=menuitemradio], [role=menuitemcheckbox]'))! as HTMLElement; + if (typeof option === 'string' || typeof option === 'number') { + option = this.findOption({optionIndexOrText: option}); } + if (!option) { - throw new Error('No option found in the menu.'); + throw new Error('Target option not found in the menu.'); } if (interactionType === 'keyboard') { + if (option?.getAttribute('aria-disabled') === 'true') { + return; + } + if (document.activeElement !== menu || !menu.contains(document.activeElement)) { act(() => menu.focus()); } @@ -156,7 +228,7 @@ export class MenuTester { } act(() => {jest.runAllTimers();}); - if (option && option.getAttribute('href') == null && option.getAttribute('aria-haspopup') == null && menuSelectionMode === 'single' && closesOnSelect && keyboardActivation !== 'Space' && !this._isSubmenu) { + if (option.getAttribute('href') == null && option.getAttribute('aria-haspopup') == null && menuSelectionMode === 'single' && closesOnSelect && keyboardActivation !== 'Space' && !this._isSubmenu) { await waitFor(() => { if (document.activeElement !== trigger) { throw new Error(`Expected the document.activeElement after selecting an option to be the menu trigger but got ${document.activeElement}`); @@ -172,13 +244,15 @@ export class MenuTester { } else { throw new Error("Attempted to select a option in the menu, but menu wasn't found."); } - }; + } // TODO: update this to remove needsLongPress if we wanna make the user call open first always - openSubmenu = async (opts: {submenuTrigger?: HTMLElement, submenuTriggerText?: string, needsLongPress?: boolean, interactionType?: UserOpts['interactionType']}): Promise => { + /** + * Opens the submenu. Defaults to using the interaction type set on the menu tester. The submenu trigger can be targeted via the trigger's node or the trigger's text. + */ + async openSubmenu(opts: MenuOpenSubmenuOpts): Promise { let { submenuTrigger, - submenuTriggerText, needsLongPress, interactionType = this._interactionType } = opts; @@ -191,19 +265,16 @@ export class MenuTester { if (!isDisabled) { let menu = this.menu; if (menu) { - let submenu; - if (submenuTrigger) { - submenu = submenuTrigger; - } else if (submenuTriggerText) { - submenu = within(menu).getByText(submenuTriggerText); + if (typeof submenuTrigger === 'string') { + submenuTrigger = (within(menu!).getByText(submenuTrigger).closest('[role=menuitem]'))! as HTMLElement; } - let submenuTriggerTester = new MenuTester({user: this.user, interactionType: this._interactionType, root: submenu, isSubmenu: true}); + let submenuTriggerTester = new MenuTester({user: this.user, interactionType: this._interactionType, root: submenuTrigger, isSubmenu: true}); if (interactionType === 'mouse') { - await this.user.pointer({target: submenu}); + await this.user.pointer({target: submenuTrigger}); act(() => {jest.runAllTimers();}); } else if (interactionType === 'keyboard') { - await this.keyboardNavigateToOption({option: submenu}); + await this.keyboardNavigateToOption({option: submenuTrigger}); await this.user.keyboard('[ArrowRight]'); act(() => {jest.runAllTimers();}); } else { @@ -216,12 +287,13 @@ export class MenuTester { } return null; - }; + } - keyboardNavigateToOption = async (opts: {option: HTMLElement}) => { + private async keyboardNavigateToOption(opts: {option: HTMLElement}) { let {option} = opts; - let options = this.options; - let targetIndex = options.indexOf(option); + let options = this.options(); + let targetIndex = options.findIndex(opt => (opt === option) || opt.contains(option)); + if (targetIndex === -1) { throw new Error('Option provided is not in the menu'); } @@ -229,7 +301,7 @@ export class MenuTester { await this.user.keyboard('[ArrowDown]'); } let currIndex = options.indexOf(document.activeElement as HTMLElement); - if (targetIndex === -1) { + if (currIndex === -1) { throw new Error('ActiveElement is not in the menu'); } let direction = targetIndex > currIndex ? 'down' : 'up'; @@ -239,8 +311,10 @@ export class MenuTester { } }; - - close = async () => { + /** + * Closes the menu. + */ + async close() { let menu = this.menu; if (menu) { act(() => menu.focus()); @@ -258,29 +332,51 @@ export class MenuTester { throw new Error('Expected the menu to not be in the document after closing it.'); } } - }; + } - get trigger() { + /** + * Returns the menu's trigger. + */ + get trigger(): HTMLElement { if (!this._trigger) { throw new Error('No trigger element found for menu.'); } + return this._trigger; } - get menu() { + /** + * Returns the menu if present. + */ + get menu(): HTMLElement | null { let menuId = this.trigger.getAttribute('aria-controls'); - return menuId ? document.getElementById(menuId) : undefined; + return menuId ? document.getElementById(menuId) : null; } - get options(): HTMLElement[] { + /** + * Returns the menu's sections if any. + */ + get sections(): HTMLElement[] { let menu = this.menu; - let options: HTMLElement[] = []; if (menu) { - options = within(menu).queryAllByRole('menuitem'); + return within(menu).queryAllByRole('group'); + } else { + return []; + } + } + + /** + * Returns the menu's options if present. Can be filtered to a subsection of the menu if provided via `element`. + */ + options(opts: {element?: HTMLElement} = {}): HTMLElement[] { + let {element = this.menu} = opts; + let options: HTMLElement[] = []; + if (element) { + options = within(element).queryAllByRole('menuitem'); if (options.length === 0) { - options = within(menu).queryAllByRole('menuitemradio'); + options = within(element).queryAllByRole('menuitemradio'); if (options.length === 0) { - options = within(menu).queryAllByRole('menuitemcheckbox'); + options = within(element).queryAllByRole('menuitemcheckbox'); } } } @@ -288,19 +384,13 @@ export class MenuTester { return options; } - get sections() { - let menu = this.menu; - if (menu) { - return within(menu).queryAllByRole('group'); - } else { - return []; - } - } - - get submenuTriggers() { - let options = this.options; + /** + * Returns the menu's submenu triggers if any. + */ + get submenuTriggers(): HTMLElement[] { + let options = this.options(); if (options.length > 0) { - return this.options.filter(item => item.getAttribute('aria-haspopup') != null); + return options.filter(item => item.getAttribute('aria-haspopup') != null); } return []; diff --git a/packages/@react-aria/test-utils/src/select.ts b/packages/@react-aria/test-utils/src/select.ts index 1e6ff7fb665..3eddaaa6583 100644 --- a/packages/@react-aria/test-utils/src/select.ts +++ b/packages/@react-aria/test-utils/src/select.ts @@ -11,18 +11,28 @@ */ import {act, waitFor, within} from '@testing-library/react'; -import {BaseTesterOpts, UserOpts} from './user'; +import {SelectTesterOpts, UserOpts} from './types'; -export interface SelectOptions extends UserOpts, BaseTesterOpts { - // TODO: I think the type grabbed from the testing library dist for UserEvent is breaking the build, will need to figure out a better place to grab from - user?: any +interface SelectOpenOpts { + /** + * What interaction type to use when opening the select. Defaults to the interaction type set on the tester. + */ + interactionType?: UserOpts['interactionType'] } + +interface SelectTriggerOptionOpts extends SelectOpenOpts { + /** + * The index, text, or node of the option to select. Option nodes can be sourced via `options()`. + */ + option: number | string | HTMLElement +} + export class SelectTester { private user; private _interactionType: UserOpts['interactionType']; private _trigger: HTMLElement; - constructor(opts: SelectOptions) { + constructor(opts: SelectTesterOpts) { let {root, user, interactionType} = opts; this.user = user; this._interactionType = interactionType || 'mouse'; @@ -33,12 +43,17 @@ export class SelectTester { } this._trigger = triggerButton; } - - setInteractionType = (type: UserOpts['interactionType']) => { + /** + * Set the interaction type used by the select tester. + */ + setInteractionType(type: UserOpts['interactionType']) { this._interactionType = type; - }; + } - open = async (opts: {interactionType?: UserOpts['interactionType']} = {}) => { + /** + * Opens the select. Defaults to using the interaction type set on the select tester. + */ + async open(opts: SelectOpenOpts = {}) { let { interactionType = this._interactionType } = opts; @@ -69,11 +84,80 @@ export class SelectTester { return true; } }); + } + + /** + * Closes the select. + */ + async close() { + let listbox = this.listbox; + if (listbox) { + act(() => listbox.focus()); + await this.user.keyboard('[Escape]'); + } + + await waitFor(() => { + if (document.activeElement !== this._trigger) { + throw new Error(`Expected the document.activeElement after closing the select dropdown to be the select component trigger but got ${document.activeElement}`); + } else { + return true; + } + }); + + if (listbox && document.contains(listbox)) { + throw new Error('Expected the select element listbox to not be in the document after closing the dropdown.'); + } + } + + /** + * Returns a option matching the specified index or text content. + */ + findOption(opts: {optionIndexOrText: number | string}): HTMLElement { + let { + optionIndexOrText + } = opts; + + let option; + let options = this.options(); + let listbox = this.listbox; + + if (typeof optionIndexOrText === 'number') { + option = options[optionIndexOrText]; + } else if (typeof optionIndexOrText === 'string' && listbox != null) { + option = (within(listbox!).getByText(optionIndexOrText).closest('[role=option]'))! as HTMLElement; + } + + return option; + } + + private async keyboardNavigateToOption(opts: {option: HTMLElement}) { + let {option} = opts; + let options = this.options(); + let targetIndex = options.indexOf(option); + if (targetIndex === -1) { + throw new Error('Option provided is not in the listbox'); + } + if (document.activeElement === this.listbox) { + await this.user.keyboard('[ArrowDown]'); + } + let currIndex = options.indexOf(document.activeElement as HTMLElement); + if (currIndex === -1) { + throw new Error('ActiveElement is not in the listbox'); + } + let direction = targetIndex > currIndex ? 'down' : 'up'; + + for (let i = 0; i < Math.abs(targetIndex - currIndex); i++) { + await this.user.keyboard(`[${direction === 'down' ? 'ArrowDown' : 'ArrowUp'}]`); + } }; - selectOption = async (opts: {optionText: string, interactionType?: UserOpts['interactionType']}) => { + /** + * Selects the desired select option. Defaults to using the interaction type set on the select tester. If necessary, will open the select dropdown beforehand. + * The desired option can be targeted via the option's node, the option's text, or the option's index. + */ + async selectOption(opts: SelectTriggerOptionOpts) { let { - optionText, + option, interactionType = this._interactionType } = opts || {}; let trigger = this.trigger; @@ -81,16 +165,28 @@ export class SelectTester { await this.open(); } let listbox = this.listbox; + if (!listbox) { + throw new Error('Select\'s listbox not found.'); + } + if (listbox) { - let option = within(listbox).getByText(optionText); + if (typeof option === 'string' || typeof option === 'number') { + option = this.findOption({optionIndexOrText: option}); + } + + if (!option) { + throw new Error('Target option not found in the listbox.'); + } + if (interactionType === 'keyboard') { + if (option?.getAttribute('aria-disabled') === 'true') { + return; + } + if (document.activeElement !== listbox || !listbox.contains(document.activeElement)) { act(() => listbox.focus()); } - - // TODO: this simulates typeahead, do we want to add a helper util for that? Not sure if users would really need that for - // their test - await this.user.keyboard(optionText); + await this.keyboardNavigateToOption({option}); await this.user.keyboard('[Enter]'); } else { // TODO: what if the user needs to scroll the list to find the option? What if there are multiple matches for text (hopefully the picker options are pretty unique) @@ -101,7 +197,7 @@ export class SelectTester { } } - if (option.getAttribute('href') == null) { + if (option?.getAttribute('href') == null) { await waitFor(() => { if (document.activeElement !== this._trigger) { throw new Error(`Expected the document.activeElement after selecting an option to be the select component trigger but got ${document.activeElement}`); @@ -115,43 +211,40 @@ export class SelectTester { } } } - }; + } - close = async () => { - let listbox = this.listbox; - if (listbox) { - act(() => listbox.focus()); - await this.user.keyboard('[Escape]'); + /** + * Returns the select's options if present. Can be filtered to a subsection of the listbox if provided via `element`. + */ + options(opts: {element?: HTMLElement} = {}): HTMLElement[] { + let {element = this.listbox} = opts; + let options = []; + if (element) { + options = within(element).queryAllByRole('option'); } - await waitFor(() => { - if (document.activeElement !== this._trigger) { - throw new Error(`Expected the document.activeElement after closing the select dropdown to be the select component trigger but got ${document.activeElement}`); - } else { - return true; - } - }); - - if (listbox && document.contains(listbox)) { - throw new Error('Expected the select element listbox to not be in the document after closing the dropdown.'); - } - }; + return options; + } - get trigger() { + /** + * Returns the select's trigger. + */ + get trigger(): HTMLElement { return this._trigger; } - get listbox() { + /** + * Returns the select's listbox if present. + */ + get listbox(): HTMLElement | null { let listBoxId = this.trigger.getAttribute('aria-controls'); - return listBoxId ? document.getElementById(listBoxId) : undefined; - } - - get options() { - let listbox = this.listbox; - return listbox ? within(listbox).queryAllByRole('option') : []; + return listBoxId ? document.getElementById(listBoxId) : null; } - get sections() { + /** + * Returns the select's sections if present. + */ + get sections(): HTMLElement[] { let listbox = this.listbox; return listbox ? within(listbox).queryAllByRole('group') : []; } diff --git a/packages/@react-aria/test-utils/src/table.ts b/packages/@react-aria/test-utils/src/table.ts index c56a2020089..a7a14aaf0cf 100644 --- a/packages/@react-aria/test-utils/src/table.ts +++ b/packages/@react-aria/test-utils/src/table.ts @@ -11,23 +11,29 @@ */ import {act, waitFor, within} from '@testing-library/react'; -import {BaseTesterOpts, UserOpts} from './user'; +import {GridRowActionOpts, TableTesterOpts, ToggleGridRowOpts, UserOpts} from './types'; import {pressElement, triggerLongPress} from './events'; -export interface TableOptions extends UserOpts, BaseTesterOpts { - user?: any, - advanceTimer: UserOpts['advanceTimer'] + +interface TableToggleRowOpts extends ToggleGridRowOpts {} +interface TableToggleSortOpts { + /** + * The index, text, or node of the column to toggle selection for. + */ + column: number | string | HTMLElement, + /** + * What interaction type to use when sorting the column. Defaults to the interaction type set on the tester. + */ + interactionType?: UserOpts['interactionType'] } +interface TableRowActionOpts extends GridRowActionOpts {} -// TODO: Previously used logic like https://github.com/testing-library/react-testing-library/blame/c63b873072d62c858959c2a19e68f8e2cc0b11be/src/pure.js#L16 -// but https://github.com/testing-library/dom-testing-library/issues/987#issuecomment-891901804 indicates that it may falsely indicate that fake timers are enabled -// when they aren't export class TableTester { private user; private _interactionType: UserOpts['interactionType']; private _advanceTimer: UserOpts['advanceTimer']; private _table: HTMLElement; - constructor(opts: TableOptions) { + constructor(opts: TableTesterOpts) { let {root, user, interactionType, advanceTimer} = opts; this.user = user; this._interactionType = interactionType || 'mouse'; @@ -35,21 +41,43 @@ export class TableTester { this._table = root; } - setInteractionType = (type: UserOpts['interactionType']) => { + /** + * Set the interaction type used by the table tester. + */ + setInteractionType(type: UserOpts['interactionType']) { this._interactionType = type; - }; + } - toggleRowSelection = async (opts: {index?: number, text?: string, needsLongPress?: boolean, interactionType?: UserOpts['interactionType']} = {}) => { + /** + * Toggles the selection for the specified table row. Defaults to using the interaction type set on the table tester. + */ + async toggleRowSelection(opts: TableToggleRowOpts) { let { - index, - text, + row, needsLongPress, + checkboxSelection = true, interactionType = this._interactionType } = opts; - let row = this.findRow({index, text}); + if (typeof row === 'string' || typeof row === 'number') { + row = this.findRow({rowIndexOrText: row}); + } + + if (!row) { + throw new Error('Target row not found in the table.'); + } + let rowCheckbox = within(row).queryByRole('checkbox'); - if (rowCheckbox) { + + if (interactionType === 'keyboard' && !checkboxSelection) { + // TODO: for now focus the row directly until I add keyboard navigation + await act(async () => { + row.focus(); + }); + await this.user.keyboard('{Space}'); + return; + } + if (rowCheckbox && checkboxSelection) { await pressElement(this.user, rowCheckbox, interactionType); } else { let cell = within(row).getAllByRole('gridcell')[0]; @@ -66,21 +94,25 @@ export class TableTester { } }; - toggleSort = async (opts: {index?: number, text?: string, interactionType?: UserOpts['interactionType']} = {}) => { + /** + * Toggles the sort order for the specified table column. Defaults to using the interaction type set on the table tester. + */ + async toggleSort(opts: TableToggleSortOpts) { let { - index, - text, + column, interactionType = this._interactionType } = opts; let columnheader; - if (index != null) { - columnheader = this.columns[index]; - } else if (text != null) { - columnheader = within(this.rowGroups[0]).getByText(text); + if (typeof column === 'number') { + columnheader = this.columns[column]; + } else if (typeof column === 'string') { + columnheader = within(this.rowGroups[0]).getByText(column); while (columnheader && !/columnheader/.test(columnheader.getAttribute('role'))) { columnheader = columnheader.parentElement; } + } else { + columnheader = column; } let menuButton = within(columnheader).queryByRole('button'); @@ -150,38 +182,46 @@ export class TableTester { } else { await pressElement(this.user, columnheader, interactionType); } - }; - // TODO: should there be a util for triggering a row action? Perhaps there should be but it would rely on the user teling us the config of the - // table. Maybe we could rely on the user knowing to trigger a press/double click? We could have the user pass in "needsDoubleClick" - // It is also iffy if there is any row selected because then the table is in selectionMode and the below actions will simply toggle row selection - triggerRowAction = async (opts: {index?: number, text?: string, needsDoubleClick?: boolean, interactionType?: UserOpts['interactionType']} = {}) => { + } + + /** + * Triggers the action for the specified table row. Defaults to using the interaction type set on the table tester. + */ + async triggerRowAction(opts: TableRowActionOpts) { let { - index, - text, + row, needsDoubleClick, interactionType = this._interactionType } = opts; - let row = this.findRow({index, text}); - if (row) { - if (needsDoubleClick) { - await this.user.dblClick(row); - } else if (interactionType === 'keyboard') { - act(() => row.focus()); - await this.user.keyboard('[Enter]'); - } else { - await pressElement(this.user, row, interactionType); - } + if (typeof row === 'string' || typeof row === 'number') { + row = this.findRow({rowIndexOrText: row}); } - }; + + if (!row) { + throw new Error('Target row not found in the table.'); + } + + if (needsDoubleClick) { + await this.user.dblClick(row); + } else if (interactionType === 'keyboard') { + // TODO: add keyboard navigation instead of focusing the row directly. Will need to consider if the focus in in the columns + act(() => row.focus()); + await this.user.keyboard('[Enter]'); + } else { + await pressElement(this.user, row, interactionType); + } + } // TODO: should there be utils for drag and drop and column resizing? For column resizing, I'm not entirely convinced that users will be doing that in their tests. // For DnD, it might be tricky to do for keyboard DnD since we wouldn't know what valid drop zones there are... Similarly, for simulating mouse drag and drop the coordinates depend // on the mocks the user sets up for their row height/etc. // Additionally, should we also support keyboard navigation/typeahead? Those felt like they could be very easily replicated by the user via user.keyboard already and don't really // add much value if we provide that to them - - toggleSelectAll = async (opts: {interactionType?: UserOpts['interactionType']} = {}) => { + /** + * Toggle selection for all rows in the table. Defaults to using the interaction type set on the table tester. + */ + async toggleSelectAll(opts: {interactionType?: UserOpts['interactionType']} = {}) { let { interactionType = this._interactionType } = opts; @@ -192,30 +232,35 @@ export class TableTester { } else { await pressElement(this.user, checkbox, interactionType); } - }; + } - findRow = (opts: {index?: number, text?: string} = {}) => { + /** + * Returns a row matching the specified index or text content. + */ + findRow(opts: {rowIndexOrText: number | string}): HTMLElement { let { - index, - text + rowIndexOrText } = opts; let row; let rows = this.rows; let bodyRowGroup = this.rowGroups[1]; - if (index != null) { - row = rows[index]; - } else if (text != null) { - row = within(bodyRowGroup).getByText(text); + if (typeof rowIndexOrText === 'number') { + row = rows[rowIndexOrText]; + } else if (typeof rowIndexOrText === 'string') { + row = within(bodyRowGroup).getByText(rowIndexOrText); while (row && row.getAttribute('role') !== 'row') { row = row.parentElement; } } return row; - }; + } - findCell = (opts: {text: string}) => { + /** + * Returns a cell matching the specified text content. + */ + findCell(opts: {text: string}) { let { text } = opts; @@ -232,38 +277,58 @@ export class TableTester { } return cell; - }; + } - get table() { + /** + * Returns the table. + */ + get table(): HTMLElement { return this._table; } - get rowGroups() { + /** + * Returns the row groups within the table. + */ + get rowGroups(): HTMLElement[] { let table = this._table; return table ? within(table).queryAllByRole('rowgroup') : []; } - get columns() { + /** + * Returns the columns within the table. + */ + get columns(): HTMLElement[] { let headerRowGroup = this.rowGroups[0]; return headerRowGroup ? within(headerRowGroup).queryAllByRole('columnheader') : []; } - get rows() { + /** + * Returns the rows within the table if any. + */ + get rows(): HTMLElement[] { let bodyRowGroup = this.rowGroups[1]; return bodyRowGroup ? within(bodyRowGroup).queryAllByRole('row') : []; } - get selectedRows() { + /** + * Returns the currently selected rows within the table if any. + */ + get selectedRows(): HTMLElement[] { return this.rows.filter(row => row.getAttribute('aria-selected') === 'true'); } - get rowHeaders() { - let table = this.table; - return table ? within(table).queryAllByRole('rowheader') : []; + /** + * Returns the row headers within the table if any. + */ + get rowHeaders(): HTMLElement[] { + return within(this.table).queryAllByRole('rowheader'); } - get cells() { - let table = this.table; - return table ? within(table).queryAllByRole('gridcell') : []; + /** + * Returns the cells within the table if any. Can be filtered against a specific row if provided via `element`. + */ + cells(opts: {element?: HTMLElement} = {}): HTMLElement[] { + let {element = this.table} = opts; + return within(element).queryAllByRole('gridcell'); } } diff --git a/packages/@react-aria/test-utils/src/tabs.ts b/packages/@react-aria/test-utils/src/tabs.ts new file mode 100644 index 00000000000..8a940d4f1d3 --- /dev/null +++ b/packages/@react-aria/test-utils/src/tabs.ts @@ -0,0 +1,198 @@ +/* + * Copyright 2024 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import {act, within} from '@testing-library/react'; +import {Direction, Orientation, TabsTesterOpts, UserOpts} from './types'; +import {pressElement} from './events'; + +interface TriggerTabOptions { + /** + * What interaction type to use when triggering a tab. Defaults to the interaction type set on the tester. + */ + interactionType?: UserOpts['interactionType'], + /** + * The index, text, or node of the tab to toggle selection for. + */ + tab: number | string | HTMLElement, + /** + * Whether the tab needs to be activated manually rather than on focus. + */ + manualActivation?: boolean +} + +export class TabsTester { + private user; + private _interactionType: UserOpts['interactionType']; + private _tablist: HTMLElement; + private _direction: Direction; + + constructor(opts: TabsTesterOpts) { + let {root, user, interactionType, direction} = opts; + this.user = user; + this._interactionType = interactionType || 'mouse'; + this._direction = direction || 'ltr'; + + this._tablist = root; + let tablist = within(root).queryAllByRole('tablist'); + if (tablist.length > 0) { + this._tablist = tablist[0]; + } + } + + /** + * Set the interaction type used by the tabs tester. + */ + setInteractionType(type: UserOpts['interactionType']) { + this._interactionType = type; + } + + // TODO: This is pretty similar across most the utils, refactor to make it generic? + /** + * Returns a tab matching the specified index or text content. + */ + findTab(opts: {tabIndexOrText: number | string}): HTMLElement { + let { + tabIndexOrText + } = opts; + + let tab; + let tabs = this.tabs; + if (typeof tabIndexOrText === 'number') { + tab = tabs[tabIndexOrText]; + } else if (typeof tabIndexOrText === 'string') { + tab = (within(this._tablist).getByText(tabIndexOrText).closest('[role=tab]'))! as HTMLElement; + } + + return tab; + } + + // TODO: also quite similar across more utils albeit with orientation, refactor to make generic + private async keyboardNavigateToTab(opts: {tab: HTMLElement, orientation?: Orientation}) { + let {tab, orientation = 'vertical'} = opts; + let tabs = this.tabs; + let targetIndex = tabs.indexOf(tab); + if (targetIndex === -1) { + throw new Error('Tab provided is not in the tablist'); + } + + if (!this._tablist.contains(document.activeElement)) { + let selectedTab = this.selectedTab; + if (selectedTab != null) { + act(() => selectedTab.focus()); + } else { + act(() => tabs.find(tab => !(tab.hasAttribute('disabled') || tab.getAttribute('aria-disabled') === 'true'))?.focus()); + } + } + + let currIndex = this.tabs.indexOf(document.activeElement as HTMLElement); + if (currIndex === -1) { + throw new Error('ActiveElement is not in the tablist'); + } + + let arrowUp = 'ArrowUp'; + let arrowDown = 'ArrowDown'; + if (orientation === 'horizontal') { + if (this._direction === 'ltr') { + arrowUp = 'ArrowLeft'; + arrowDown = 'ArrowRight'; + } else { + arrowUp = 'ArrowRight'; + arrowDown = 'ArrowLeft'; + } + } + + let movementDirection = targetIndex > currIndex ? 'down' : 'up'; + for (let i = 0; i < Math.abs(targetIndex - currIndex); i++) { + await this.user.keyboard(`[${movementDirection === 'down' ? arrowDown : arrowUp}]`); + } + }; + + /** + * Triggers the specified tab. Defaults to using the interaction type set on the tabs tester. + */ + async triggerTab(opts: TriggerTabOptions) { + let { + tab, + interactionType = this._interactionType, + manualActivation + } = opts; + + if (typeof tab === 'string' || typeof tab === 'number') { + tab = this.findTab({tabIndexOrText: tab}); + } + + if (!tab) { + throw new Error('Target tab not found in the tablist.'); + } else if (tab.hasAttribute('disabled')) { + throw new Error('Target tab is disabled.'); + } + + if (interactionType === 'keyboard') { + if (document.activeElement !== this._tablist || !this._tablist.contains(document.activeElement)) { + act(() => this._tablist.focus()); + } + + let tabsOrientation = this._tablist.getAttribute('aria-orientation') || 'horizontal'; + await this.keyboardNavigateToTab({tab, orientation: tabsOrientation as Orientation}); + if (manualActivation) { + await this.user.keyboard('[Enter]'); + } + } else { + await pressElement(this.user, tab, interactionType); + } + } + + /** + * Returns the tablist. + */ + get tablist(): HTMLElement { + return this._tablist; + } + + /** + * Returns the tabpanels. + */ + get tabpanels(): HTMLElement[] { + let tabpanels = [] as HTMLElement[]; + for (let tab of this.tabs) { + let controlId = tab.getAttribute('aria-controls'); + let panel = controlId != null ? document.getElementById(controlId) : null; + if (panel != null) { + tabpanels.push(panel); + } + } + + return tabpanels; + } + + /** + * Returns the tabs in the tablist. + */ + get tabs(): HTMLElement[] { + return within(this.tablist).queryAllByRole('tab'); + } + + /** + * Returns the currently selected tab in the tablist if any. + */ + get selectedTab(): HTMLElement | null { + return this.tabs.find(tab => tab.getAttribute('aria-selected') === 'true') || null; + } + + /** + * Returns the currently active tabpanel if any. + */ + get activeTabpanel(): HTMLElement | null { + let activeTabpanelId = this.selectedTab?.getAttribute('aria-controls'); + return activeTabpanelId ? document.getElementById(activeTabpanelId) : null; + } +} diff --git a/packages/@react-aria/test-utils/src/tree.ts b/packages/@react-aria/test-utils/src/tree.ts new file mode 100644 index 00000000000..93e4ec65ebf --- /dev/null +++ b/packages/@react-aria/test-utils/src/tree.ts @@ -0,0 +1,248 @@ +/* + * Copyright 2024 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import {act, within} from '@testing-library/react'; +import {BaseGridRowInteractionOpts, GridRowActionOpts, ToggleGridRowOpts, TreeTesterOpts, UserOpts} from './types'; +import {pressElement, triggerLongPress} from './events'; + +interface TreeToggleExpansionOpts extends BaseGridRowInteractionOpts {} +interface TreeToggleRowOpts extends ToggleGridRowOpts {} +interface TreeRowActionOpts extends GridRowActionOpts {} + +// TODO: this ended up being pretty much the same as gridlist, refactor so it extends from gridlist +export class TreeTester { + private user; + private _interactionType: UserOpts['interactionType']; + private _advanceTimer: UserOpts['advanceTimer']; + private _tree: HTMLElement; + + constructor(opts: TreeTesterOpts) { + let {root, user, interactionType, advanceTimer} = opts; + this.user = user; + this._interactionType = interactionType || 'mouse'; + this._advanceTimer = advanceTimer; + this._tree = root; + // TODO: should all helpers do this? + let tree = within(root).queryByRole('treegrid'); + if (root.getAttribute('role') !== 'treegrid' && tree) { + this._tree = tree; + } + } + + /** + * Set the interaction type used by the tree tester. + */ + setInteractionType(type: UserOpts['interactionType']) { + this._interactionType = type; + }; + + /** + * Returns a row matching the specified index or text content. + */ + findRow(opts: {rowIndexOrText: number | string}): HTMLElement { + let { + rowIndexOrText + } = opts; + + let row; + if (typeof rowIndexOrText === 'number') { + row = this.rows[rowIndexOrText]; + } else if (typeof rowIndexOrText === 'string') { + row = (within(this.tree!).getByText(rowIndexOrText).closest('[role=row]'))! as HTMLElement; + } + + return row; + } + + // TODO: RTL + private async keyboardNavigateToRow(opts: {row: HTMLElement}) { + let {row} = opts; + let rows = this.rows; + let targetIndex = rows.indexOf(row); + if (targetIndex === -1) { + throw new Error('Option provided is not in the tree'); + } + if (document.activeElement === this.tree) { + await this.user.keyboard('[ArrowDown]'); + } else if (this._tree.contains(document.activeElement) && document.activeElement!.getAttribute('role') !== 'row') { + do { + await this.user.keyboard('[ArrowLeft]'); + } while (document.activeElement!.getAttribute('role') !== 'row'); + } + let currIndex = rows.indexOf(document.activeElement as HTMLElement); + if (currIndex === -1) { + throw new Error('ActiveElement is not in the tree'); + } + let direction = targetIndex > currIndex ? 'down' : 'up'; + + for (let i = 0; i < Math.abs(targetIndex - currIndex); i++) { + await this.user.keyboard(`[${direction === 'down' ? 'ArrowDown' : 'ArrowUp'}]`); + } + }; + + /** + * Toggles the selection for the specified tree row. Defaults to using the interaction type set on the tree tester. + */ + async toggleRowSelection(opts: TreeToggleRowOpts) { + let { + row, + needsLongPress, + checkboxSelection = true, + interactionType = this._interactionType + } = opts; + + if (typeof row === 'string' || typeof row === 'number') { + row = this.findRow({rowIndexOrText: row}); + } + + if (!row) { + throw new Error('Target row not found in the tree.'); + } + + let rowCheckbox = within(row).queryByRole('checkbox'); + + // TODO: we early return here because the checkbox can't be keyboard navigated to if the row is disabled usually + // but we may to check for disabledBehavior (aka if the disable row gets skipped when keyboard navigating or not) + if (interactionType === 'keyboard' && (rowCheckbox?.getAttribute('disabled') === '' || row?.getAttribute('aria-disabled') === 'true')) { + return; + } + + // this would be better than the check to do nothing in events.ts + // also, it'd be good to be able to trigger selection on the row instead of having to go to the checkbox directly + if (interactionType === 'keyboard' && !checkboxSelection) { + await this.keyboardNavigateToRow({row}); + await this.user.keyboard('{Space}'); + return; + } + if (rowCheckbox && checkboxSelection) { + await pressElement(this.user, rowCheckbox, interactionType); + } else { + let cell = within(row).getAllByRole('gridcell')[0]; + if (needsLongPress && interactionType === 'touch') { + if (this._advanceTimer == null) { + throw new Error('No advanceTimers provided for long press.'); + } + + // Note that long press interactions with rows is strictly touch only for grid rows + await triggerLongPress({element: cell, advanceTimer: this._advanceTimer, pointerOpts: {pointerType: 'touch'}}); + } else { + await pressElement(this.user, cell, interactionType); + } + } + }; + + /** + * Toggles the expansion for the specified tree row. Defaults to using the interaction type set on the tree tester. + */ + async toggleRowExpansion(opts: TreeToggleExpansionOpts) { + let { + row, + interactionType = this._interactionType + } = opts; + if (!this.tree.contains(document.activeElement)) { + await act(async () => { + this.tree.focus(); + }); + } + + if (typeof row === 'string' || typeof row === 'number') { + row = this.findRow({rowIndexOrText: row}); + } + + if (!row) { + throw new Error('Target row not found in the tree.'); + } else if (row.getAttribute('aria-expanded') == null) { + throw new Error('Target row is not expandable.'); + } + + if (interactionType === 'mouse' || interactionType === 'touch') { + let rowExpander = within(row).getAllByRole('button')[0]; // what happens if the button is not first? how can we differentiate? + await pressElement(this.user, rowExpander, interactionType); + } else if (interactionType === 'keyboard') { + if (row?.getAttribute('aria-disabled') === 'true') { + return; + } + + await this.keyboardNavigateToRow({row}); + if (row.getAttribute('aria-expanded') === 'true') { + await this.user.keyboard('[ArrowLeft]'); + } else { + await this.user.keyboard('[ArrowRight]'); + } + } + }; + + /** + * Triggers the action for the specified tree row. Defaults to using the interaction type set on the tree tester. + */ + async triggerRowAction(opts: TreeRowActionOpts) { + let { + row, + needsDoubleClick, + interactionType = this._interactionType + } = opts; + + if (typeof row === 'string' || typeof row === 'number') { + row = this.findRow({rowIndexOrText: row}); + } + + if (!row) { + throw new Error('Target row not found in the tree.'); + } + + if (needsDoubleClick) { + await this.user.dblClick(row); + } else if (interactionType === 'keyboard') { + if (row?.getAttribute('aria-disabled') === 'true') { + return; + } + + if (document.activeElement !== this._tree || !this._tree.contains(document.activeElement)) { + act(() => this._tree.focus()); + } + + await this.keyboardNavigateToRow({row}); + await this.user.keyboard('[Enter]'); + } else { + await pressElement(this.user, row, interactionType); + } + }; + + /** + * Returns the tree. + */ + get tree(): HTMLElement { + return this._tree; + } + + /** + * Returns the tree's rows if any. + */ + get rows(): HTMLElement[] { + return within(this?.tree).queryAllByRole('row'); + } + + /** + * Returns the tree's selected rows if any. + */ + get selectedRows(): HTMLElement[] { + return this.rows.filter(row => row.getAttribute('aria-selected') === 'true'); + } + + /** + * Returns the tree's cells if any. Can be filtered against a specific row if provided via `element`. + */ + cells(opts: {element?: HTMLElement} = {}): HTMLElement[] { + let {element = this.tree} = opts; + return within(element).queryAllByRole('gridcell'); + } +} diff --git a/packages/@react-aria/test-utils/src/types.ts b/packages/@react-aria/test-utils/src/types.ts new file mode 100644 index 00000000000..3c58dbc61f3 --- /dev/null +++ b/packages/@react-aria/test-utils/src/types.ts @@ -0,0 +1,133 @@ +/* + * Copyright 2024 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +export type Orientation = 'horizontal' | 'vertical'; +export type Direction = 'ltr' | 'rtl'; + +// https://github.com/testing-library/dom-testing-library/issues/939#issuecomment-830771708 is an interesting way of allowing users to configure the timers +// curent way is like https://testing-library.com/docs/user-event/options/#advancetimers, +export interface UserOpts { + /** + * The interaction type (mouse, touch, keyboard) that the test util user will use when interacting with a component. This can be overridden + * at the aria pattern tester level if needed. + * @default mouse + */ + interactionType?: 'mouse' | 'touch' | 'keyboard', + // If using fake timers user should provide something like (time) => jest.advanceTimersByTime(time))} + // A real timer user would pass async () => await new Promise((resolve) => setTimeout(resolve, waitTime)) + // Time is in ms. + /** + * A function used by the test utils to advance timers during interactions. Required for certain aria patterns (e.g. table). This can be overridden + * at the aria pattern tester level if needed. + */ + advanceTimer?: (time?: number) => void | Promise +} + +export interface BaseTesterOpts extends UserOpts { + /** @private */ + user?: any, + /** The base element for the given tester (e.g. the table, menu trigger button, etc). */ + root: HTMLElement +} + +export interface ComboBoxTesterOpts extends BaseTesterOpts { + /** + * The base element for the combobox. If provided the wrapping element around the target combobox (as is the the case with a ref provided to RSP ComboBox), + * will automatically search for the combobox element within. + */ + root: HTMLElement, + /** + * The node of the combobox trigger button if any. If not provided, we will try to automatically use any button + * within the `root` provided or that the `root` serves as the trigger. + */ + trigger?: HTMLElement +} + +export interface GridListTesterOpts extends BaseTesterOpts {} + +export interface ListBoxTesterOpts extends BaseTesterOpts { + /** + * A function used by the test utils to advance timers during interactions. + */ + advanceTimer?: UserOpts['advanceTimer'] +} + +export interface MenuTesterOpts extends BaseTesterOpts { + /** + * The trigger element for the menu. + */ + root: HTMLElement, + /** + * Whether the current menu is a submenu. + */ + isSubmenu?: boolean +} + +export interface SelectTesterOpts extends BaseTesterOpts { + /** + * The trigger element for the select. If provided the wrapping element around the target select (as is the case with a ref provided to RSP Select), + * will automatically search for the select's trigger element within. + */ + root: HTMLElement +} + +export interface TableTesterOpts extends BaseTesterOpts { + /** + * A function used by the test utils to advance timers during interactions. + */ + advanceTimer?: UserOpts['advanceTimer'] +} + +export interface TabsTesterOpts extends BaseTesterOpts { + /** + * The horizontal layout direction, typically affected by locale. + * @default 'ltr' + */ + direction?: Direction +} + +export interface TreeTesterOpts extends BaseTesterOpts { + /** + * A function used by the test utils to advance timers during interactions. + */ + advanceTimer?: UserOpts['advanceTimer'] +} + +export interface BaseGridRowInteractionOpts { + /** + * The index, text, or node of the row to target. + */ + row: number | string | HTMLElement, + /** + * What interaction type to use when interacting with the row. Defaults to the interaction type set on the tester. + */ + interactionType?: UserOpts['interactionType'] +} + +export interface ToggleGridRowOpts extends BaseGridRowInteractionOpts { + /** + * Whether the row needs to be long pressed to be selected. Depends on the components implementation. + */ + needsLongPress?: boolean, + /** + * Whether the checkbox should be used to select the row. If false, will attempt to select the row via press. + * @default 'true' + */ + checkboxSelection?: boolean +} + +export interface GridRowActionOpts extends BaseGridRowInteractionOpts { + /** + * Whether or not the row needs a double click to trigger the row action. Depends on the components implementation. + */ + needsDoubleClick?: boolean +} diff --git a/packages/@react-aria/test-utils/src/user.ts b/packages/@react-aria/test-utils/src/user.ts index abae908181a..09b40fdde1a 100644 --- a/packages/@react-aria/test-utils/src/user.ts +++ b/packages/@react-aria/test-utils/src/user.ts @@ -10,54 +10,76 @@ * governing permissions and limitations under the License. */ -import {ComboBoxOptions, ComboBoxTester} from './combobox'; -import {GridListOptions, GridListTester} from './gridlist'; -import {MenuOptions, MenuTester} from './menu'; +import {ComboBoxTester} from './combobox'; +import { + ComboBoxTesterOpts, + GridListTesterOpts, + ListBoxTesterOpts, + MenuTesterOpts, + SelectTesterOpts, + TableTesterOpts, + TabsTesterOpts, + TreeTesterOpts, + UserOpts +} from './types'; +import {GridListTester} from './gridlist'; +import {ListBoxTester} from './listbox'; +import {MenuTester} from './menu'; import {pointerMap} from './'; -import {SelectOptions, SelectTester} from './select'; -import {TableOptions, TableTester} from './table'; +import {SelectTester} from './select'; +import {TableTester} from './table'; +import {TabsTester} from './tabs'; +import {TreeTester} from './tree'; import userEvent from '@testing-library/user-event'; -// https://github.com/testing-library/dom-testing-library/issues/939#issuecomment-830771708 is an interesting way of allowing users to configure the timers -// curent way is like https://testing-library.com/docs/user-event/options/#advancetimers, -export interface UserOpts { - interactionType?: 'mouse' | 'touch' | 'keyboard', - // If using fake timers user should provide something like (time) => jest.advanceTimersByTime(time))} - // A real timer user would pass async () => await new Promise((resolve) => setTimeout(resolve, waitTime)) - // Time is in ms. - advanceTimer?: (time?: number) => void | Promise -} - -export interface BaseTesterOpts { - // The base element for the given tester (e.g. the table, menu trigger, etc) - root: HTMLElement -} - -let keyToUtil = {'Select': SelectTester, 'Table': TableTester, 'Menu': MenuTester, 'ComboBox': ComboBoxTester, 'GridList': GridListTester} as const; +let keyToUtil = { + 'Select': SelectTester, + 'Table': TableTester, + 'Menu': MenuTester, + 'ComboBox': ComboBoxTester, + 'GridList': GridListTester, + 'ListBox': ListBoxTester, + 'Tabs': TabsTester, + 'Tree': TreeTester +} as const; export type PatternNames = keyof typeof keyToUtil; // Conditional type: https://www.typescriptlang.org/docs/handbook/2/conditional-types.html -type ObjectType = - T extends 'Select' ? SelectTester : - T extends 'Table' ? TableTester : - T extends 'Menu' ? MenuTester : - T extends 'ComboBox' ? ComboBoxTester : - T extends 'GridList' ? GridListTester : - never; +type Tester = + T extends 'ComboBox' ? ComboBoxTester : + T extends 'GridList' ? GridListTester : + T extends 'ListBox' ? ListBoxTester : + T extends 'Menu' ? MenuTester : + T extends 'Select' ? SelectTester : + T extends 'Table' ? TableTester : + T extends 'Tabs' ? TabsTester : + T extends 'Tree' ? TreeTester : + never; -type ObjectOptionsTypes = - T extends 'Select' ? SelectOptions : - T extends 'Table' ? TableOptions : - T extends 'Menu' ? MenuOptions : - T extends 'ComboBox' ? ComboBoxOptions : - T extends 'GridList' ? GridListOptions : +type TesterOpts = + T extends 'ComboBox' ? ComboBoxTesterOpts : + T extends 'GridList' ? GridListTesterOpts : + T extends 'ListBox' ? ListBoxTesterOpts : + T extends 'Menu' ? MenuTesterOpts : + T extends 'Select' ? SelectTesterOpts : + T extends 'Table' ? TableTesterOpts : + T extends 'Tabs' ? TabsTesterOpts : + T extends 'Tree' ? TreeTesterOpts : never; let defaultAdvanceTimer = async (waitTime: number | undefined) => await new Promise((resolve) => setTimeout(resolve, waitTime)); export class User { - user; + private user; + /** + * The interaction type (mouse, touch, keyboard) that the test util user will use when interacting with a component. This can be overridden + * at the aria pattern util level if needed. + * @default mouse + */ interactionType: UserOpts['interactionType']; + /** + * A function used by the test utils to advance timers during interactions. Required for certain aria patterns (e.g. table). + */ advanceTimer: UserOpts['advanceTimer']; constructor(opts: UserOpts = {}) { @@ -67,7 +89,10 @@ export class User { this.advanceTimer = advanceTimer || defaultAdvanceTimer; } - createTester(patternName: T, opts: ObjectOptionsTypes): ObjectType { - return new (keyToUtil)[patternName]({user: this.user, interactionType: this.interactionType, advanceTimer: this.advanceTimer, ...opts}) as ObjectType; + /** + * Creates an aria pattern tester, inheriting the options provided to the original user. + */ + createTester(patternName: T, opts: TesterOpts): Tester { + return new (keyToUtil)[patternName]({interactionType: this.interactionType, advanceTimer: this.advanceTimer, ...opts, user: this.user}) as Tester; } } diff --git a/packages/@react-aria/textfield/package.json b/packages/@react-aria/textfield/package.json index 1f3da18b9b6..f4b5c897171 100644 --- a/packages/@react-aria/textfield/package.json +++ b/packages/@react-aria/textfield/package.json @@ -1,6 +1,6 @@ { "name": "@react-aria/textfield", - "version": "3.15.0", + "version": "3.16.0", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -22,18 +22,19 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/focus": "^3.19.0", - "@react-aria/form": "^3.0.11", - "@react-aria/label": "^3.7.13", - "@react-aria/utils": "^3.26.0", - "@react-stately/form": "^3.1.0", + "@react-aria/focus": "^3.19.1", + "@react-aria/form": "^3.0.12", + "@react-aria/label": "^3.7.14", + "@react-aria/utils": "^3.27.0", + "@react-stately/form": "^3.1.1", "@react-stately/utils": "^3.10.5", - "@react-types/shared": "^3.26.0", - "@react-types/textfield": "^3.10.0", + "@react-types/shared": "^3.27.0", + "@react-types/textfield": "^3.11.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" }, "publishConfig": { "access": "public" diff --git a/packages/@react-aria/textfield/src/useTextField.ts b/packages/@react-aria/textfield/src/useTextField.ts index 26be9bf62f3..330df22764a 100644 --- a/packages/@react-aria/textfield/src/useTextField.ts +++ b/packages/@react-aria/textfield/src/useTextField.ts @@ -174,6 +174,7 @@ export function useTextField) => setValue(e.target.value), autoComplete: props.autoComplete, @@ -183,6 +184,8 @@ export function useTextField, isReady: boolean = true) { let [isEntering, setEntering] = useState(true); - useAnimation(ref, isEntering && isReady, useCallback(() => setEntering(false), [])); - return isEntering && isReady; + let isAnimationReady = isEntering && isReady; + + // There are two cases for entry animations: + // 1. CSS @keyframes. The `animation` property is set during the isEntering state, and it is removed after the animation finishes. + // 2. CSS transitions. The initial styles are applied during the isEntering state, and removed immediately, causing the transition to occur. + // + // In the second case, cancel any transitions that were triggered prior to the isEntering = false state (when the transition is supposed to start). + // This can happen when isReady starts as false (e.g. popovers prior to placement calculation). + useLayoutEffect(() => { + if (isAnimationReady && ref.current && 'getAnimations' in ref.current) { + for (let animation of ref.current.getAnimations()) { + if (animation instanceof CSSTransition) { + animation.cancel(); + } + } + } + }, [ref, isAnimationReady]); + + useAnimation(ref, isAnimationReady, useCallback(() => setEntering(false), [])); + return isAnimationReady; } export function useExitAnimation(ref: RefObject, isOpen: boolean) { - // State to trigger a re-render after animation is complete, which causes the element to be removed from the DOM. - // Ref to track the state we're in, so we don't immediately reset isExiting to true after the animation. - let [isExiting, setExiting] = useState(false); - let [exitState, setExitState] = useState('idle'); + let [exitState, setExitState] = useState<'closed' | 'open' | 'exiting'>(isOpen ? 'open' : 'closed'); - // If isOpen becomes false, set isExiting to true. - if (!isOpen && ref.current && exitState === 'idle') { - isExiting = true; - setExiting(true); - setExitState('exiting'); - } - - // If we exited, and the element has been removed, reset exit state to idle. - if (!ref.current && exitState === 'exited') { - setExitState('idle'); + switch (exitState) { + case 'open': + // If isOpen becomes false, set the state to exiting. + if (!isOpen) { + setExitState('exiting'); + } + break; + case 'closed': + case 'exiting': + // If we are exiting and isOpen becomes true, the animation was interrupted. + // Reset the state to open. + if (isOpen) { + setExitState('open'); + } + break; } + let isExiting = exitState === 'exiting'; useAnimation( ref, isExiting, useCallback(() => { - setExitState('exited'); - setExiting(false); + // Set the state to closed, which will cause the element to be unmounted. + setExitState(state => state === 'exiting' ? 'closed' : state); }, []) ); @@ -51,35 +72,32 @@ export function useExitAnimation(ref: RefObject, isOpen: boo } function useAnimation(ref: RefObject, isActive: boolean, onEnd: () => void) { - let prevAnimation = useRef(null); - if (isActive && ref.current) { - // This is ok because we only read it in the layout effect below, immediately after the commit phase. - // We could move this to another effect that runs every render, but this would be unnecessarily slow. - // We only need the computed style right before the animation becomes active. - // eslint-disable-next-line rulesdir/pure-render - prevAnimation.current = window.getComputedStyle(ref.current).animation; - } - useLayoutEffect(() => { if (isActive && ref.current) { - // Make sure there's actually an animation, and it wasn't there before we triggered the update. - let computedStyle = window.getComputedStyle(ref.current); - if (computedStyle.animationName && computedStyle.animationName !== 'none' && computedStyle.animation !== prevAnimation.current) { - let onAnimationEnd = (e: AnimationEvent) => { - if (e.target === ref.current) { - element.removeEventListener('animationend', onAnimationEnd); - flushSync(() => {onEnd();}); - } - }; - - let element = ref.current; - element.addEventListener('animationend', onAnimationEnd); - return () => { - element.removeEventListener('animationend', onAnimationEnd); - }; - } else { + if (!('getAnimations' in ref.current)) { + // JSDOM onEnd(); + return; } + + let animations = ref.current.getAnimations(); + if (animations.length === 0) { + onEnd(); + return; + } + + let canceled = false; + Promise.all(animations.map(a => a.finished)).then(() => { + if (!canceled) { + flushSync(() => { + onEnd(); + }); + } + }).catch(() => {}); + + return () => { + canceled = true; + }; } }, [ref, isActive, onEnd]); } diff --git a/packages/@react-aria/utils/src/index.ts b/packages/@react-aria/utils/src/index.ts index 785342fc310..260f16a176c 100644 --- a/packages/@react-aria/utils/src/index.ts +++ b/packages/@react-aria/utils/src/index.ts @@ -24,6 +24,7 @@ export {useGlobalListeners} from './useGlobalListeners'; export {useLabels} from './useLabels'; export {useObjectRef} from './useObjectRef'; export {useUpdateEffect} from './useUpdateEffect'; +export {useUpdateLayoutEffect} from './useUpdateLayoutEffect'; export {useLayoutEffect} from './useLayoutEffect'; export {useResizeObserver} from './useResizeObserver'; export {useSyncRef} from './useSyncRef'; @@ -42,6 +43,8 @@ export {useEffectEvent} from './useEffectEvent'; export {useDeepMemo} from './useDeepMemo'; export {useFormReset} from './useFormReset'; export {useLoadMore} from './useLoadMore'; +export {inertValue} from './inertValue'; export {CLEAR_FOCUS_EVENT, FOCUS_EVENT, UPDATE_ACTIVEDESCENDANT} from './constants'; +export {isCtrlKeyPressed} from './keyboard'; export {useEnterAnimation, useExitAnimation} from './animation'; export {isFocusable, isTabbable} from './isFocusable'; diff --git a/packages/@react-aria/utils/src/inertValue.ts b/packages/@react-aria/utils/src/inertValue.ts new file mode 100644 index 00000000000..1dce213b3b5 --- /dev/null +++ b/packages/@react-aria/utils/src/inertValue.ts @@ -0,0 +1,11 @@ +import {version} from 'react'; + +export function inertValue(value?: boolean) { + const pieces = version.split('.'); + const major = parseInt(pieces[0], 10); + if (major >= 19) { + return value; + } + // compatibility with React < 19 + return value ? 'true' : undefined; +} diff --git a/packages/@react-aria/utils/src/keyboard.tsx b/packages/@react-aria/utils/src/keyboard.tsx new file mode 100644 index 00000000000..2fffc5c470a --- /dev/null +++ b/packages/@react-aria/utils/src/keyboard.tsx @@ -0,0 +1,27 @@ +/* + * Copyright 2024 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import {isMac} from './platform'; + +interface Event { + altKey: boolean, + ctrlKey: boolean, + metaKey: boolean +} + +export function isCtrlKeyPressed(e: Event) { + if (isMac()) { + return e.metaKey; + } + + return e.ctrlKey; +} diff --git a/packages/@react-aria/utils/src/useUpdateLayoutEffect.ts b/packages/@react-aria/utils/src/useUpdateLayoutEffect.ts new file mode 100644 index 00000000000..d3a5ff49f0a --- /dev/null +++ b/packages/@react-aria/utils/src/useUpdateLayoutEffect.ts @@ -0,0 +1,37 @@ +/* + * Copyright 2024 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import {EffectCallback, useRef} from 'react'; +import {useLayoutEffect} from './useLayoutEffect'; + +// Like useLayoutEffect, but only called for updates after the initial render. +export function useUpdateLayoutEffect(effect: EffectCallback, dependencies: any[]) { + const isInitialMount = useRef(true); + const lastDeps = useRef(null); + + useLayoutEffect(() => { + isInitialMount.current = true; + return () => { + isInitialMount.current = false; + }; + }, []); + + useLayoutEffect(() => { + if (isInitialMount.current) { + isInitialMount.current = false; + } else if (!lastDeps.current || dependencies.some((dep, i) => !Object.is(dep, lastDeps[i]))) { + effect(); + } + lastDeps.current = dependencies; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, dependencies); +} diff --git a/packages/@react-aria/virtualizer/package.json b/packages/@react-aria/virtualizer/package.json index 106af05fce2..8b0c943aabf 100644 --- a/packages/@react-aria/virtualizer/package.json +++ b/packages/@react-aria/virtualizer/package.json @@ -1,6 +1,6 @@ { "name": "@react-aria/virtualizer", - "version": "4.1.0", + "version": "4.1.1", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -22,11 +22,11 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/i18n": "^3.12.4", - "@react-aria/interactions": "^3.22.5", - "@react-aria/utils": "^3.26.0", - "@react-stately/virtualizer": "^4.2.0", - "@react-types/shared": "^3.26.0", + "@react-aria/i18n": "^3.12.5", + "@react-aria/interactions": "^3.23.0", + "@react-aria/utils": "^3.27.0", + "@react-stately/virtualizer": "^4.2.1", + "@react-types/shared": "^3.27.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { diff --git a/packages/@react-aria/visually-hidden/package.json b/packages/@react-aria/visually-hidden/package.json index 93d9d0ee2ac..237700e5cbd 100644 --- a/packages/@react-aria/visually-hidden/package.json +++ b/packages/@react-aria/visually-hidden/package.json @@ -1,6 +1,6 @@ { "name": "@react-aria/visually-hidden", - "version": "3.8.18", + "version": "3.8.19", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -24,13 +24,14 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/interactions": "^3.22.5", - "@react-aria/utils": "^3.26.0", - "@react-types/shared": "^3.26.0", + "@react-aria/interactions": "^3.23.0", + "@react-aria/utils": "^3.27.0", + "@react-types/shared": "^3.27.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" }, "publishConfig": { "access": "public" diff --git a/packages/@react-spectrum/accordion/package.json b/packages/@react-spectrum/accordion/package.json index 9a949e14e0b..cab4754f220 100644 --- a/packages/@react-spectrum/accordion/package.json +++ b/packages/@react-spectrum/accordion/package.json @@ -1,6 +1,6 @@ { "name": "@react-spectrum/accordion", - "version": "3.0.1", + "version": "3.0.2", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -36,12 +36,12 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/i18n": "^3.12.4", - "@react-spectrum/utils": "^3.12.0", - "@react-types/shared": "^3.26.0", - "@spectrum-icons/ui": "^3.6.11", + "@react-aria/i18n": "^3.12.5", + "@react-spectrum/utils": "^3.12.1", + "@react-types/shared": "^3.27.0", + "@spectrum-icons/ui": "^3.6.12", "@swc/helpers": "^0.5.0", - "react-aria-components": "^1.5.0" + "react-aria-components": "^1.6.0" }, "devDependencies": { "@adobe/spectrum-css-temp": "3.0.0-alpha.1" diff --git a/packages/@react-spectrum/actionbar/package.json b/packages/@react-spectrum/actionbar/package.json index 22c01aa9665..527b72c67ca 100644 --- a/packages/@react-spectrum/actionbar/package.json +++ b/packages/@react-spectrum/actionbar/package.json @@ -1,6 +1,6 @@ { "name": "@react-spectrum/actionbar", - "version": "3.6.2", + "version": "3.6.3", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -36,20 +36,20 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/focus": "^3.19.0", - "@react-aria/i18n": "^3.12.4", - "@react-aria/interactions": "^3.22.5", + "@react-aria/focus": "^3.19.1", + "@react-aria/i18n": "^3.12.5", + "@react-aria/interactions": "^3.23.0", "@react-aria/live-announcer": "^3.4.1", - "@react-aria/utils": "^3.26.0", - "@react-spectrum/actiongroup": "^3.10.10", - "@react-spectrum/button": "^3.16.9", - "@react-spectrum/overlays": "^5.7.0", - "@react-spectrum/text": "^3.5.10", - "@react-spectrum/utils": "^3.12.0", - "@react-stately/collections": "^3.12.0", - "@react-types/actionbar": "^3.1.11", - "@react-types/shared": "^3.26.0", - "@spectrum-icons/ui": "^3.6.11", + "@react-aria/utils": "^3.27.0", + "@react-spectrum/actiongroup": "^3.10.11", + "@react-spectrum/button": "^3.16.10", + "@react-spectrum/overlays": "^5.7.1", + "@react-spectrum/text": "^3.5.11", + "@react-spectrum/utils": "^3.12.1", + "@react-stately/collections": "^3.12.1", + "@react-types/actionbar": "^3.1.12", + "@react-types/shared": "^3.27.0", + "@spectrum-icons/ui": "^3.6.12", "@swc/helpers": "^0.5.0" }, "devDependencies": { diff --git a/packages/@react-spectrum/actiongroup/package.json b/packages/@react-spectrum/actiongroup/package.json index 082efb1e308..6e97594c90e 100644 --- a/packages/@react-spectrum/actiongroup/package.json +++ b/packages/@react-spectrum/actiongroup/package.json @@ -1,6 +1,6 @@ { "name": "@react-spectrum/actiongroup", - "version": "3.10.10", + "version": "3.10.11", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -36,21 +36,21 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/actiongroup": "^3.7.11", - "@react-aria/focus": "^3.19.0", - "@react-aria/interactions": "^3.22.5", - "@react-aria/utils": "^3.26.0", - "@react-spectrum/button": "^3.16.9", - "@react-spectrum/menu": "^3.21.0", - "@react-spectrum/text": "^3.5.10", - "@react-spectrum/tooltip": "^3.7.0", - "@react-spectrum/utils": "^3.12.0", - "@react-stately/collections": "^3.12.0", - "@react-stately/list": "^3.11.1", - "@react-types/actiongroup": "^3.4.13", - "@react-types/shared": "^3.26.0", - "@spectrum-icons/ui": "^3.6.11", - "@spectrum-icons/workflow": "^4.2.16", + "@react-aria/actiongroup": "^3.7.12", + "@react-aria/focus": "^3.19.1", + "@react-aria/interactions": "^3.23.0", + "@react-aria/utils": "^3.27.0", + "@react-spectrum/button": "^3.16.10", + "@react-spectrum/menu": "^3.21.1", + "@react-spectrum/text": "^3.5.11", + "@react-spectrum/tooltip": "^3.7.1", + "@react-spectrum/utils": "^3.12.1", + "@react-stately/collections": "^3.12.1", + "@react-stately/list": "^3.11.2", + "@react-types/actiongroup": "^3.4.14", + "@react-types/shared": "^3.27.0", + "@spectrum-icons/ui": "^3.6.12", + "@spectrum-icons/workflow": "^4.2.17", "@swc/helpers": "^0.5.0" }, "devDependencies": { diff --git a/packages/@react-spectrum/autocomplete/package.json b/packages/@react-spectrum/autocomplete/package.json index 4848101e402..b647dbef94a 100644 --- a/packages/@react-spectrum/autocomplete/package.json +++ b/packages/@react-spectrum/autocomplete/package.json @@ -1,6 +1,6 @@ { "name": "@react-spectrum/autocomplete", - "version": "3.0.0-alpha.38", + "version": "3.0.0-alpha.39", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -36,30 +36,30 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/autocomplete": "3.0.0-alpha.36", - "@react-aria/button": "^3.11.0", - "@react-aria/dialog": "^3.5.20", - "@react-aria/focus": "^3.19.0", - "@react-aria/form": "^3.0.11", - "@react-aria/i18n": "^3.12.4", - "@react-aria/interactions": "^3.22.5", - "@react-aria/label": "^3.7.13", - "@react-aria/overlays": "^3.24.0", - "@react-aria/utils": "^3.26.0", - "@react-spectrum/button": "^3.16.9", - "@react-spectrum/form": "^3.7.10", - "@react-spectrum/label": "^3.16.10", - "@react-spectrum/listbox": "^3.14.0", - "@react-spectrum/overlays": "^5.7.0", - "@react-spectrum/progress": "^3.7.11", - "@react-spectrum/textfield": "^3.12.7", - "@react-spectrum/utils": "^3.12.0", - "@react-stately/collections": "^3.12.0", - "@react-stately/combobox": "^3.10.1", - "@react-types/autocomplete": "3.0.0-alpha.27", - "@react-types/button": "^3.10.1", - "@react-types/shared": "^3.26.0", - "@spectrum-icons/ui": "^3.6.11", + "@react-aria/autocomplete": "3.0.0-alpha.37", + "@react-aria/button": "^3.11.1", + "@react-aria/dialog": "^3.5.21", + "@react-aria/focus": "^3.19.1", + "@react-aria/form": "^3.0.12", + "@react-aria/i18n": "^3.12.5", + "@react-aria/interactions": "^3.23.0", + "@react-aria/label": "^3.7.14", + "@react-aria/overlays": "^3.25.0", + "@react-aria/utils": "^3.27.0", + "@react-spectrum/button": "^3.16.10", + "@react-spectrum/form": "^3.7.11", + "@react-spectrum/label": "^3.16.11", + "@react-spectrum/listbox": "^3.14.1", + "@react-spectrum/overlays": "^5.7.1", + "@react-spectrum/progress": "^3.7.12", + "@react-spectrum/textfield": "^3.12.8", + "@react-spectrum/utils": "^3.12.1", + "@react-stately/collections": "^3.12.1", + "@react-stately/combobox": "^3.10.2", + "@react-types/autocomplete": "3.0.0-alpha.28", + "@react-types/button": "^3.10.2", + "@react-types/shared": "^3.27.0", + "@spectrum-icons/ui": "^3.6.12", "@swc/helpers": "^0.5.0" }, "devDependencies": { diff --git a/packages/@react-spectrum/avatar/package.json b/packages/@react-spectrum/avatar/package.json index 5a79625fab3..35a36e1b07e 100644 --- a/packages/@react-spectrum/avatar/package.json +++ b/packages/@react-spectrum/avatar/package.json @@ -1,6 +1,6 @@ { "name": "@react-spectrum/avatar", - "version": "3.0.17", + "version": "3.0.18", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -36,10 +36,10 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/utils": "^3.26.0", - "@react-spectrum/utils": "^3.12.0", - "@react-types/avatar": "^3.0.11", - "@react-types/shared": "^3.26.0", + "@react-aria/utils": "^3.27.0", + "@react-spectrum/utils": "^3.12.1", + "@react-types/avatar": "^3.0.12", + "@react-types/shared": "^3.27.0", "@swc/helpers": "^0.5.0" }, "devDependencies": { @@ -47,7 +47,8 @@ }, "peerDependencies": { "@react-spectrum/provider": "^3.2.1", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" }, "publishConfig": { "access": "public" diff --git a/packages/@react-spectrum/badge/package.json b/packages/@react-spectrum/badge/package.json index 3827c87b7d9..5655c3db3ad 100644 --- a/packages/@react-spectrum/badge/package.json +++ b/packages/@react-spectrum/badge/package.json @@ -1,6 +1,6 @@ { "name": "@react-spectrum/badge", - "version": "3.1.18", + "version": "3.1.19", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -36,11 +36,11 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/utils": "^3.26.0", - "@react-spectrum/text": "^3.5.10", - "@react-spectrum/utils": "^3.12.0", - "@react-types/badge": "^3.1.13", - "@react-types/shared": "^3.26.0", + "@react-aria/utils": "^3.27.0", + "@react-spectrum/text": "^3.5.11", + "@react-spectrum/utils": "^3.12.1", + "@react-types/badge": "^3.1.14", + "@react-types/shared": "^3.27.0", "@swc/helpers": "^0.5.0" }, "devDependencies": { @@ -48,7 +48,8 @@ }, "peerDependencies": { "@react-spectrum/provider": "^3.0.0", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" }, "publishConfig": { "access": "public" diff --git a/packages/@react-spectrum/breadcrumbs/package.json b/packages/@react-spectrum/breadcrumbs/package.json index 96feb27baeb..939d8e274d4 100644 --- a/packages/@react-spectrum/breadcrumbs/package.json +++ b/packages/@react-spectrum/breadcrumbs/package.json @@ -1,6 +1,6 @@ { "name": "@react-spectrum/breadcrumbs", - "version": "3.9.12", + "version": "3.9.13", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -36,18 +36,18 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/breadcrumbs": "^3.5.19", - "@react-aria/focus": "^3.19.0", - "@react-aria/i18n": "^3.12.4", - "@react-aria/interactions": "^3.22.5", - "@react-aria/utils": "^3.26.0", - "@react-spectrum/button": "^3.16.9", - "@react-spectrum/menu": "^3.21.0", - "@react-spectrum/utils": "^3.12.0", - "@react-stately/collections": "^3.12.0", - "@react-types/breadcrumbs": "^3.7.9", - "@react-types/shared": "^3.26.0", - "@spectrum-icons/ui": "^3.6.11", + "@react-aria/breadcrumbs": "^3.5.20", + "@react-aria/focus": "^3.19.1", + "@react-aria/i18n": "^3.12.5", + "@react-aria/interactions": "^3.23.0", + "@react-aria/utils": "^3.27.0", + "@react-spectrum/button": "^3.16.10", + "@react-spectrum/menu": "^3.21.1", + "@react-spectrum/utils": "^3.12.1", + "@react-stately/collections": "^3.12.1", + "@react-types/breadcrumbs": "^3.7.10", + "@react-types/shared": "^3.27.0", + "@spectrum-icons/ui": "^3.6.12", "@swc/helpers": "^0.5.0" }, "devDependencies": { diff --git a/packages/@react-spectrum/button/package.json b/packages/@react-spectrum/button/package.json index abcba1114ba..acd5203d483 100644 --- a/packages/@react-spectrum/button/package.json +++ b/packages/@react-spectrum/button/package.json @@ -1,6 +1,6 @@ { "name": "@react-spectrum/button", - "version": "3.16.9", + "version": "3.16.10", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -36,18 +36,18 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/button": "^3.11.0", - "@react-aria/focus": "^3.19.0", - "@react-aria/i18n": "^3.12.4", - "@react-aria/interactions": "^3.22.5", - "@react-aria/utils": "^3.26.0", - "@react-spectrum/progress": "^3.7.11", - "@react-spectrum/text": "^3.5.10", - "@react-spectrum/utils": "^3.12.0", - "@react-stately/toggle": "^3.8.0", - "@react-types/button": "^3.10.1", - "@react-types/shared": "^3.26.0", - "@spectrum-icons/ui": "^3.6.11", + "@react-aria/button": "^3.11.1", + "@react-aria/focus": "^3.19.1", + "@react-aria/i18n": "^3.12.5", + "@react-aria/interactions": "^3.23.0", + "@react-aria/utils": "^3.27.0", + "@react-spectrum/progress": "^3.7.12", + "@react-spectrum/text": "^3.5.11", + "@react-spectrum/utils": "^3.12.1", + "@react-stately/toggle": "^3.8.1", + "@react-types/button": "^3.10.2", + "@react-types/shared": "^3.27.0", + "@spectrum-icons/ui": "^3.6.12", "@swc/helpers": "^0.5.0" }, "devDependencies": { diff --git a/packages/@react-spectrum/buttongroup/package.json b/packages/@react-spectrum/buttongroup/package.json index 01610df533d..b22a2f53004 100644 --- a/packages/@react-spectrum/buttongroup/package.json +++ b/packages/@react-spectrum/buttongroup/package.json @@ -1,6 +1,6 @@ { "name": "@react-spectrum/buttongroup", - "version": "3.6.17", + "version": "3.6.18", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -36,10 +36,10 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/utils": "^3.26.0", - "@react-spectrum/utils": "^3.12.0", - "@react-types/buttongroup": "^3.3.13", - "@react-types/shared": "^3.26.0", + "@react-aria/utils": "^3.27.0", + "@react-spectrum/utils": "^3.12.1", + "@react-types/buttongroup": "^3.3.14", + "@react-types/shared": "^3.27.0", "@swc/helpers": "^0.5.0" }, "devDependencies": { @@ -48,7 +48,8 @@ }, "peerDependencies": { "@react-spectrum/provider": "^3.0.0", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" }, "publishConfig": { "access": "public" diff --git a/packages/@react-spectrum/calendar/chromatic/Calendar.stories.tsx b/packages/@react-spectrum/calendar/chromatic/Calendar.stories.tsx index f29c9903877..e7b84703c2d 100644 --- a/packages/@react-spectrum/calendar/chromatic/Calendar.stories.tsx +++ b/packages/@react-spectrum/calendar/chromatic/Calendar.stories.tsx @@ -45,3 +45,5 @@ export const Invalid = () => ; export const ErrorMessage = () => ; export const UnavailableInvalid = () => d.compare(date) === 0} />; export const DisabledInvalid = () => ; +export const CustomWeekStartMonday = () => ; +export const CustomWeekStartSaturday = () => ; diff --git a/packages/@react-spectrum/calendar/chromatic/RangeCalendar.stories.tsx b/packages/@react-spectrum/calendar/chromatic/RangeCalendar.stories.tsx index ea27789d5b0..4e71d2b573f 100644 --- a/packages/@react-spectrum/calendar/chromatic/RangeCalendar.stories.tsx +++ b/packages/@react-spectrum/calendar/chromatic/RangeCalendar.stories.tsx @@ -69,3 +69,5 @@ export const NonContiguousInvalid = () => { ); }; +export const CustomWeekStartMonday = () => ; +export const CustomWeekStartSaturday = () => ; diff --git a/packages/@react-spectrum/calendar/docs/Calendar.mdx b/packages/@react-spectrum/calendar/docs/Calendar.mdx index 97fc6f8baee..cdbb0f56eaf 100644 --- a/packages/@react-spectrum/calendar/docs/Calendar.mdx +++ b/packages/@react-spectrum/calendar/docs/Calendar.mdx @@ -220,3 +220,13 @@ By default, when pressing the next or previous buttons, pagination will advance
    ``` + +### Custom first day of week + +By default, the first day of the week is automatically set based on the current locale. This can be changed by setting the `firstDayOfWeek` prop to `'sun'`, `'mon'`, `'tue'`, `'wed'`, `'thu'`, `'fri'`, or `'sat'`. + +```tsx example +
    + +
    +``` diff --git a/packages/@react-spectrum/calendar/docs/RangeCalendar.mdx b/packages/@react-spectrum/calendar/docs/RangeCalendar.mdx index 2ad4e6243f3..13f034ed9de 100644 --- a/packages/@react-spectrum/calendar/docs/RangeCalendar.mdx +++ b/packages/@react-spectrum/calendar/docs/RangeCalendar.mdx @@ -252,3 +252,13 @@ By default, when pressing the next or previous buttons, pagination will advance
    ``` + +### Custom first day of week + +By default, the first day of the week is automatically set based on the current locale. This can be changed by setting the `firstDayOfWeek` prop to `'sun'`, `'mon'`, `'tue'`, `'wed'`, `'thu'`, `'fri'`, or `'sat'`. + +```tsx example +
    + +
    +``` diff --git a/packages/@react-spectrum/calendar/package.json b/packages/@react-spectrum/calendar/package.json index 04e899ce518..380159301f2 100644 --- a/packages/@react-spectrum/calendar/package.json +++ b/packages/@react-spectrum/calendar/package.json @@ -1,6 +1,6 @@ { "name": "@react-spectrum/calendar", - "version": "3.5.0", + "version": "3.6.0", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -36,21 +36,21 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@internationalized/date": "^3.6.0", - "@react-aria/calendar": "^3.6.0", - "@react-aria/focus": "^3.19.0", - "@react-aria/i18n": "^3.12.4", - "@react-aria/interactions": "^3.22.5", - "@react-aria/utils": "^3.26.0", - "@react-aria/visually-hidden": "^3.8.18", - "@react-spectrum/button": "^3.16.9", - "@react-spectrum/label": "^3.16.10", - "@react-spectrum/utils": "^3.12.0", - "@react-stately/calendar": "^3.6.0", - "@react-types/button": "^3.10.1", - "@react-types/calendar": "^3.5.0", - "@react-types/shared": "^3.26.0", - "@spectrum-icons/ui": "^3.6.11", + "@internationalized/date": "^3.7.0", + "@react-aria/calendar": "^3.7.0", + "@react-aria/focus": "^3.19.1", + "@react-aria/i18n": "^3.12.5", + "@react-aria/interactions": "^3.23.0", + "@react-aria/utils": "^3.27.0", + "@react-aria/visually-hidden": "^3.8.19", + "@react-spectrum/button": "^3.16.10", + "@react-spectrum/label": "^3.16.11", + "@react-spectrum/utils": "^3.12.1", + "@react-stately/calendar": "^3.7.0", + "@react-types/button": "^3.10.2", + "@react-types/calendar": "^3.6.0", + "@react-types/shared": "^3.27.0", + "@spectrum-icons/ui": "^3.6.12", "@swc/helpers": "^0.5.0" }, "devDependencies": { diff --git a/packages/@react-spectrum/calendar/src/CalendarBase.tsx b/packages/@react-spectrum/calendar/src/CalendarBase.tsx index 37388db5da3..82e965fe82a 100644 --- a/packages/@react-spectrum/calendar/src/CalendarBase.tsx +++ b/packages/@react-spectrum/calendar/src/CalendarBase.tsx @@ -45,7 +45,8 @@ export function CalendarBase(props prevButtonProps, errorMessageProps, calendarRef: ref, - visibleMonths = 1 + visibleMonths = 1, + firstDayOfWeek } = props; let {styleProps} = useStyleProps(props); let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-spectrum/calendar'); @@ -97,7 +98,8 @@ export function CalendarBase(props {...props} key={i} state={state} - startDate={d} /> + startDate={d} + firstDayOfWeek={firstDayOfWeek} /> ); } diff --git a/packages/@react-spectrum/calendar/src/CalendarCell.tsx b/packages/@react-spectrum/calendar/src/CalendarCell.tsx index 24f5552a775..4f391a7489d 100644 --- a/packages/@react-spectrum/calendar/src/CalendarCell.tsx +++ b/packages/@react-spectrum/calendar/src/CalendarCell.tsx @@ -23,10 +23,11 @@ import {useLocale} from '@react-aria/i18n'; interface CalendarCellProps extends AriaCalendarCellProps { state: CalendarState | RangeCalendarState, - currentMonth: CalendarDate + currentMonth: CalendarDate, + firstDayOfWeek?: 'sun' | 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat' } -export function CalendarCell({state, currentMonth, ...props}: CalendarCellProps) { +export function CalendarCell({state, currentMonth, firstDayOfWeek, ...props}: CalendarCellProps) { let ref = useRef(null); let { cellProps, @@ -48,7 +49,7 @@ export function CalendarCell({state, currentMonth, ...props}: CalendarCellProps) let isSelectionStart = isSelected && highlightedRange && isSameDay(props.date, highlightedRange.start); let isSelectionEnd = isSelected && highlightedRange && isSameDay(props.date, highlightedRange.end); let {locale} = useLocale(); - let dayOfWeek = getDayOfWeek(props.date, locale); + let dayOfWeek = getDayOfWeek(props.date, locale, firstDayOfWeek); let isRangeStart = isSelected && (isFirstSelectedAfterDisabled || dayOfWeek === 0 || props.date.day === 1); let isRangeEnd = isSelected && (isLastSelectedBeforeDisabled || dayOfWeek === 6 || props.date.day === currentMonth.calendar.getDaysInMonth(currentMonth)); let {focusProps, isFocusVisible} = useFocusRing(); diff --git a/packages/@react-spectrum/calendar/src/CalendarMonth.tsx b/packages/@react-spectrum/calendar/src/CalendarMonth.tsx index d768360c81b..c81a8bf42a9 100644 --- a/packages/@react-spectrum/calendar/src/CalendarMonth.tsx +++ b/packages/@react-spectrum/calendar/src/CalendarMonth.tsx @@ -29,7 +29,8 @@ interface CalendarMonthProps extends CalendarPropsBase, DOMProps, StyleProps { export function CalendarMonth(props: CalendarMonthProps) { let { state, - startDate + startDate, + firstDayOfWeek } = props; let { gridProps, @@ -41,7 +42,7 @@ export function CalendarMonth(props: CalendarMonthProps) { }, state); let {locale} = useLocale(); - let weeksInMonth = getWeeksInMonth(startDate, locale); + let weeksInMonth = getWeeksInMonth(startDate, locale, firstDayOfWeek); return (
    + currentMonth={startDate} + firstDayOfWeek={firstDayOfWeek} /> ) : diff --git a/packages/@react-spectrum/calendar/stories/Calendar.stories.tsx b/packages/@react-spectrum/calendar/stories/Calendar.stories.tsx index b198657a1f5..649ff4962e7 100644 --- a/packages/@react-spectrum/calendar/stories/Calendar.stories.tsx +++ b/packages/@react-spectrum/calendar/stories/Calendar.stories.tsx @@ -80,6 +80,10 @@ export default { control: 'select', options: [null, 'single', 'visible'] }, + firstDayOfWeek: { + control: 'select', + options: [undefined, 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'] + }, isInvalid: { control: 'boolean' }, diff --git a/packages/@react-spectrum/calendar/stories/RangeCalendar.stories.tsx b/packages/@react-spectrum/calendar/stories/RangeCalendar.stories.tsx index 2e998f502ed..d4195943524 100644 --- a/packages/@react-spectrum/calendar/stories/RangeCalendar.stories.tsx +++ b/packages/@react-spectrum/calendar/stories/RangeCalendar.stories.tsx @@ -74,6 +74,10 @@ export default { control: 'select', options: [null, 'single', 'visible'] }, + firstDayOfWeek: { + control: 'select', + options: [undefined, 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'] + }, isInvalid: { control: 'boolean' }, diff --git a/packages/@react-spectrum/calendar/test/CalendarBase.test.js b/packages/@react-spectrum/calendar/test/CalendarBase.test.js index 3642696e895..1680a007b86 100644 --- a/packages/@react-spectrum/calendar/test/CalendarBase.test.js +++ b/packages/@react-spectrum/calendar/test/CalendarBase.test.js @@ -789,5 +789,108 @@ describe('CalendarBase', () => { await user.keyboard('{ArrowRight}'); expect(document.activeElement).toBe(selected); }); + + it.each` + Name | Calendar | props | locale + ${'v3 Calendar'} | ${Calendar} | ${{defaultValue: new CalendarDate(2024, 1, 1)}} | ${'en-US'}} + ${'v3 RangeCalendar'} | ${RangeCalendar} | ${{defaultValue: {start: new CalendarDate(2024, 1, 1), end: new CalendarDate(2024, 1, 1)}}} | ${'en-US'}} + `('$Name should override start of week with firstDayOfWeek="mon" (en-US)', ({Calendar, props, locale = 'en-US'}) => { + let {getAllByRole, getByRole} = render( + + + + ); + + let grid = getByRole('grid'); + let headers = getAllByRole('columnheader', {hidden: true}); + expect(headers.map(h => h.textContent)).toEqual(['M', 'T', 'W', 'T', 'F', 'S', 'S']); + + let cells = within(grid).getAllByRole('gridcell'); + expect(cells[0]).toHaveTextContent('1'); + }); + + it.each` + Name | Calendar | props | locale + ${'v3 Calendar'} | ${Calendar} | ${{defaultValue: new CalendarDate(2024, 1, 1)}} | ${'en-US'}} + ${'v3 RangeCalendar'} | ${RangeCalendar} | ${{defaultValue: {start: new CalendarDate(2024, 1, 1), end: new CalendarDate(2024, 1, 1)}}} | ${'en-US'}} + `('$Name should override start of week with firstDayOfWeek="sat" (en-US)', ({Calendar, props, locale}) => { + let {getAllByRole, getByRole} = render( + + + + ); + + let grid = getByRole('grid'); + let headers = getAllByRole('columnheader', {hidden: true}); + expect(headers.map(h => h.textContent)).toEqual(['S', 'S', 'M', 'T', 'W', 'T', 'F']); + + let cells = within(grid).getAllByRole('gridcell'); + expect(cells[2]).toHaveTextContent('1'); + }); + + it.each` + Name | Calendar | props | locale + ${'v3 Calendar'} | ${Calendar} | ${{defaultValue: new CalendarDate(2024, 1, 1)}} | ${'fr-FR'}} + ${'v3 RangeCalendar'} | ${RangeCalendar} | ${{defaultValue: {start: new CalendarDate(2024, 1, 1), end: new CalendarDate(2024, 1, 1)}}} | ${'fr-FR'}} + `('$Name should override start of week with firstDayOfWeek="mon" (fr-FR)', ({Calendar, props, locale}) => { + let {getAllByRole, getByRole} = render( + + + + ); + + let grid = getByRole('grid'); + let headers = getAllByRole('columnheader', {hidden: true}); + expect(headers.map(h => h.textContent)).toEqual(['L', 'M', 'M', 'J', 'V', 'S', 'D']); + + let cells = within(grid).getAllByRole('gridcell'); + expect(cells[0]).toHaveTextContent('1'); + }); + + it.each` + Name | Calendar | props | locale + ${'v3 Calendar'} | ${Calendar} | ${{defaultValue: new CalendarDate(2024, 1, 1)}} | ${'fr-FR'}} + ${'v3 RangeCalendar'} | ${RangeCalendar} | ${{defaultValue: {start: new CalendarDate(2024, 1, 1), end: new CalendarDate(2024, 1, 1)}}} | ${'fr-FR'}} + `('$Name should override start of week with firstDayOfWeek="sat" (fr-FR)', ({Calendar, props, locale}) => { + let {getAllByRole, getByRole} = render( + + + + ); + + let grid = getByRole('grid'); + let headers = getAllByRole('columnheader', {hidden: true}); + expect(headers.map(h => h.textContent)).toEqual(['S', 'D', 'L', 'M', 'M', 'J', 'V']); + + let cells = within(grid).getAllByRole('gridcell'); + expect(cells[2]).toHaveTextContent('1'); + }); + + it.each` + Name | Calendar | props + ${'v3 Calendar'} | ${Calendar} | ${{defaultValue: new CalendarDate(2025, 1, 1)}} + ${'v3 RangeCalendar'} | ${RangeCalendar} | ${{defaultValue: {start: new CalendarDate(2025, 1, 1), end: new CalendarDate(2025, 1, 1)}}} + `('should render enough weeks to display all days in month for firstDayOfWeek', ({Calendar, props}) => { + let {getAllByRole, getByRole} = render( + + + + ); + + let grid = getByRole('grid'); + let headers = getAllByRole('columnheader', {hidden: true}); + expect(headers.map(h => h.textContent)).toEqual(['T', 'F', 'S', 'S', 'M', 'T', 'W']); + + let rows = within(grid).getAllByRole('row'); + expect(rows).toHaveLength(6); + + let cells = within(grid).getAllByRole('gridcell'); + expect(cells).toHaveLength(42); + + expect(cells[35]).toHaveTextContent('30'); + expect(cells[36]).toHaveTextContent('31'); + expect(cells[cells.length - 1]).toHaveAttribute('aria-disabled', 'true'); + expect(cells[cells.length - 1]).toHaveTextContent('5'); + }); }); }); diff --git a/packages/@react-spectrum/card/package.json b/packages/@react-spectrum/card/package.json index 3a0fbad7a29..1948b25bc51 100644 --- a/packages/@react-spectrum/card/package.json +++ b/packages/@react-spectrum/card/package.json @@ -1,6 +1,6 @@ { "name": "@react-spectrum/card", - "version": "3.0.0-alpha.38", + "version": "3.0.0-alpha.39", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -36,22 +36,22 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/focus": "^3.19.0", - "@react-aria/grid": "^3.11.0", - "@react-aria/i18n": "^3.12.4", - "@react-aria/interactions": "^3.22.5", - "@react-aria/utils": "^3.26.0", - "@react-aria/virtualizer": "^4.1.0", - "@react-spectrum/checkbox": "^3.9.11", - "@react-spectrum/progress": "^3.7.11", - "@react-spectrum/utils": "^3.12.0", - "@react-stately/collections": "^3.12.0", - "@react-stately/grid": "^3.10.0", - "@react-stately/list": "^3.11.1", - "@react-stately/virtualizer": "^4.2.0", - "@react-types/card": "3.0.0-alpha.31", - "@react-types/provider": "^3.8.5", - "@react-types/shared": "^3.26.0", + "@react-aria/focus": "^3.19.1", + "@react-aria/grid": "^3.11.1", + "@react-aria/i18n": "^3.12.5", + "@react-aria/interactions": "^3.23.0", + "@react-aria/utils": "^3.27.0", + "@react-aria/virtualizer": "^4.1.1", + "@react-spectrum/checkbox": "^3.9.12", + "@react-spectrum/progress": "^3.7.12", + "@react-spectrum/utils": "^3.12.1", + "@react-stately/collections": "^3.12.1", + "@react-stately/grid": "^3.10.1", + "@react-stately/list": "^3.11.2", + "@react-stately/virtualizer": "^4.2.1", + "@react-types/card": "3.0.0-alpha.32", + "@react-types/provider": "^3.8.6", + "@react-types/shared": "^3.27.0", "@swc/helpers": "^0.5.0" }, "devDependencies": { diff --git a/packages/@react-spectrum/checkbox/package.json b/packages/@react-spectrum/checkbox/package.json index 3f8ab9df5c6..2cb947d0192 100644 --- a/packages/@react-spectrum/checkbox/package.json +++ b/packages/@react-spectrum/checkbox/package.json @@ -1,6 +1,6 @@ { "name": "@react-spectrum/checkbox", - "version": "3.9.11", + "version": "3.9.12", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -36,19 +36,19 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/checkbox": "^3.15.0", - "@react-aria/focus": "^3.19.0", - "@react-aria/interactions": "^3.22.5", - "@react-spectrum/form": "^3.7.10", - "@react-spectrum/label": "^3.16.10", - "@react-spectrum/utils": "^3.12.0", - "@react-stately/checkbox": "^3.6.10", - "@react-stately/toggle": "^3.8.0", - "@react-types/checkbox": "^3.9.0", - "@react-types/shared": "^3.26.0", - "@spectrum-icons/ui": "^3.6.11", + "@react-aria/checkbox": "^3.15.1", + "@react-aria/focus": "^3.19.1", + "@react-aria/interactions": "^3.23.0", + "@react-spectrum/form": "^3.7.11", + "@react-spectrum/label": "^3.16.11", + "@react-spectrum/utils": "^3.12.1", + "@react-stately/checkbox": "^3.6.11", + "@react-stately/toggle": "^3.8.1", + "@react-types/checkbox": "^3.9.1", + "@react-types/shared": "^3.27.0", + "@spectrum-icons/ui": "^3.6.12", "@swc/helpers": "^0.5.0", - "react-aria-components": "^1.5.0" + "react-aria-components": "^1.6.0" }, "devDependencies": { "@adobe/spectrum-css-temp": "3.0.0-alpha.1", diff --git a/packages/@react-spectrum/color/package.json b/packages/@react-spectrum/color/package.json index dd76d8e9e18..66d28b1c879 100644 --- a/packages/@react-spectrum/color/package.json +++ b/packages/@react-spectrum/color/package.json @@ -1,6 +1,6 @@ { "name": "@react-spectrum/color", - "version": "3.0.2", + "version": "3.0.3", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -36,25 +36,25 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/color": "^3.0.2", - "@react-aria/focus": "^3.19.0", - "@react-aria/i18n": "^3.12.4", - "@react-aria/interactions": "^3.22.5", - "@react-aria/utils": "^3.26.0", - "@react-spectrum/dialog": "^3.8.16", - "@react-spectrum/form": "^3.7.10", - "@react-spectrum/label": "^3.16.10", - "@react-spectrum/overlays": "^5.7.0", - "@react-spectrum/picker": "^3.15.4", - "@react-spectrum/textfield": "^3.12.7", - "@react-spectrum/utils": "^3.12.0", - "@react-spectrum/view": "^3.6.14", - "@react-stately/color": "^3.8.1", - "@react-types/color": "^3.0.1", - "@react-types/shared": "^3.26.0", - "@react-types/textfield": "^3.10.0", + "@react-aria/color": "^3.0.3", + "@react-aria/focus": "^3.19.1", + "@react-aria/i18n": "^3.12.5", + "@react-aria/interactions": "^3.23.0", + "@react-aria/utils": "^3.27.0", + "@react-spectrum/dialog": "^3.8.17", + "@react-spectrum/form": "^3.7.11", + "@react-spectrum/label": "^3.16.11", + "@react-spectrum/overlays": "^5.7.1", + "@react-spectrum/picker": "^3.15.5", + "@react-spectrum/textfield": "^3.12.8", + "@react-spectrum/utils": "^3.12.1", + "@react-spectrum/view": "^3.6.15", + "@react-stately/color": "^3.8.2", + "@react-types/color": "^3.0.2", + "@react-types/shared": "^3.27.0", + "@react-types/textfield": "^3.11.0", "@swc/helpers": "^0.5.0", - "react-aria-components": "^1.5.0" + "react-aria-components": "^1.6.0" }, "devDependencies": { "@adobe/spectrum-css-temp": "3.0.0-alpha.1", diff --git a/packages/@react-spectrum/combobox/docs/ComboBox.mdx b/packages/@react-spectrum/combobox/docs/ComboBox.mdx index 9b2f1f9e5e0..b4000f4de81 100644 --- a/packages/@react-spectrum/combobox/docs/ComboBox.mdx +++ b/packages/@react-spectrum/combobox/docs/ComboBox.mdx @@ -11,8 +11,9 @@ import {Layout} from '@react-spectrum/docs'; export default Layout; import docs from 'docs:@react-spectrum/combobox'; +import comboboxUtils from 'docs:@react-aria/test-utils/src/combobox.ts'; import packageData from '@react-spectrum/combobox/package.json'; -import {HeaderInfo, PropTable, PageDescription} from '@react-spectrum/docs'; +import {HeaderInfo, PropTable, PageDescription, ClassAPI, VersionBadge} from '@react-spectrum/docs'; ```jsx import import Add from '@spectrum-icons/workflow/Add'; @@ -992,3 +993,40 @@ behaviors in your test suite. Please also refer to [React Spectrum's test suite](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-spectrum/combobox/test/ComboBox.test.js) if you find that the above isn't sufficient when resolving issues in your own test cases. + +### Test utils + +`@react-spectrum/test-utils` offers common combobox interaction utilities which you may find helpful when writing tests. See [here](../react-aria/testing.html#react-aria-test-utils) for more information on how to setup these utilities +in your tests. Below is the full definition of the combobox tester and a sample of how you could use it in your test suite. + +```ts +// Combobox.test.ts +import {render, within} from '@testing-library/react'; +import {theme} from '@react-spectrum/theme-default'; +import {User} from '@react-spectrum/test-utils'; + +let testUtilUser = new User({interactionType: 'mouse'}); +// ... + +it('ComboBox can select an option via keyboard', async function () { + // Render your test component/app and initialize the combobox tester + let {getByTestId} = render( + + + ... + + + ); + let comboboxTester = testUtilUser.createTester('ComboBox', {root: getByTestId('test-combobox'), interactionType: 'keyboard'}); + + await comboboxTester.open(); + expect(comboboxTester.listbox).toBeInTheDocument(); + + let options = comboboxTester.options(); + await comboboxTester.selectOption({option: options[0]}); + expect(comboboxTester.combobox.value).toBe('One'); + expect(comboboxTester.listbox).not.toBeInTheDocument(); +}); +``` + + diff --git a/packages/@react-spectrum/combobox/package.json b/packages/@react-spectrum/combobox/package.json index 24b295f46d7..84e00335e0b 100644 --- a/packages/@react-spectrum/combobox/package.json +++ b/packages/@react-spectrum/combobox/package.json @@ -1,6 +1,6 @@ { "name": "@react-spectrum/combobox", - "version": "3.14.0", + "version": "3.14.1", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -36,30 +36,30 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/button": "^3.11.0", - "@react-aria/combobox": "^3.11.0", - "@react-aria/dialog": "^3.5.20", - "@react-aria/focus": "^3.19.0", - "@react-aria/form": "^3.0.11", - "@react-aria/i18n": "^3.12.4", - "@react-aria/interactions": "^3.22.5", - "@react-aria/label": "^3.7.13", - "@react-aria/overlays": "^3.24.0", - "@react-aria/utils": "^3.26.0", - "@react-spectrum/button": "^3.16.9", - "@react-spectrum/form": "^3.7.10", - "@react-spectrum/label": "^3.16.10", - "@react-spectrum/listbox": "^3.14.0", - "@react-spectrum/overlays": "^5.7.0", - "@react-spectrum/progress": "^3.7.11", - "@react-spectrum/textfield": "^3.12.7", - "@react-spectrum/utils": "^3.12.0", - "@react-stately/collections": "^3.12.0", - "@react-stately/combobox": "^3.10.1", - "@react-types/button": "^3.10.1", - "@react-types/combobox": "^3.13.1", - "@react-types/shared": "^3.26.0", - "@spectrum-icons/ui": "^3.6.11", + "@react-aria/button": "^3.11.1", + "@react-aria/combobox": "^3.11.1", + "@react-aria/dialog": "^3.5.21", + "@react-aria/focus": "^3.19.1", + "@react-aria/form": "^3.0.12", + "@react-aria/i18n": "^3.12.5", + "@react-aria/interactions": "^3.23.0", + "@react-aria/label": "^3.7.14", + "@react-aria/overlays": "^3.25.0", + "@react-aria/utils": "^3.27.0", + "@react-spectrum/button": "^3.16.10", + "@react-spectrum/form": "^3.7.11", + "@react-spectrum/label": "^3.16.11", + "@react-spectrum/listbox": "^3.14.1", + "@react-spectrum/overlays": "^5.7.1", + "@react-spectrum/progress": "^3.7.12", + "@react-spectrum/textfield": "^3.12.8", + "@react-spectrum/utils": "^3.12.1", + "@react-stately/collections": "^3.12.1", + "@react-stately/combobox": "^3.10.2", + "@react-types/button": "^3.10.2", + "@react-types/combobox": "^3.13.2", + "@react-types/shared": "^3.27.0", + "@spectrum-icons/ui": "^3.6.12", "@swc/helpers": "^0.5.0" }, "devDependencies": { diff --git a/packages/@react-spectrum/combobox/test/ComboBox.test.js b/packages/@react-spectrum/combobox/test/ComboBox.test.js index 2d6c9215199..ebdb76e8f54 100644 --- a/packages/@react-spectrum/combobox/test/ComboBox.test.js +++ b/packages/@react-spectrum/combobox/test/ComboBox.test.js @@ -457,7 +457,7 @@ describe('ComboBox', function () { expect(comboboxTester.listbox).toBeFalsy(); await comboboxTester.open(); - expect(comboboxTester.listbox).toBeTruthy(); + expect(comboboxTester.listbox).toBeInTheDocument(); expect(document.activeElement).toBe(comboboxTester.combobox); await user.click(comboboxTester.trigger); @@ -494,7 +494,7 @@ describe('ComboBox', function () { comboboxTester.setInteractionType('touch'); await comboboxTester.open(); expect(document.activeElement).toBe(comboboxTester.combobox); - expect(comboboxTester.listbox).toBeTruthy(); + expect(comboboxTester.listbox).toBeInTheDocument(); let button = comboboxTester.trigger; fireEvent.touchStart(button, {targetTouches: [{identifier: 1}]}); @@ -870,7 +870,7 @@ describe('ComboBox', function () { expect(combobox.value).toBe('Tw'); expect(comboboxTester.options().length).toBe(1); - await comboboxTester.selectOption({optionText: 'Two'}); + await comboboxTester.selectOption({option: 'Two'}); expect(comboboxTester.listbox).toBeFalsy(); expect(combobox.value).toBe('Two'); // selectionManager.select from useSingleSelectListState always calls onSelectionChange even if the key is the same diff --git a/packages/@react-spectrum/contextualhelp/package.json b/packages/@react-spectrum/contextualhelp/package.json index 4cfc4a6a09b..f0408d1459d 100644 --- a/packages/@react-spectrum/contextualhelp/package.json +++ b/packages/@react-spectrum/contextualhelp/package.json @@ -1,6 +1,6 @@ { "name": "@react-spectrum/contextualhelp", - "version": "3.6.16", + "version": "3.6.17", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -36,14 +36,14 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/i18n": "^3.12.4", - "@react-aria/utils": "^3.26.0", - "@react-spectrum/button": "^3.16.9", - "@react-spectrum/dialog": "^3.8.16", - "@react-spectrum/utils": "^3.12.0", - "@react-types/contextualhelp": "^3.2.14", - "@react-types/shared": "^3.26.0", - "@spectrum-icons/workflow": "^4.2.16", + "@react-aria/i18n": "^3.12.5", + "@react-aria/utils": "^3.27.0", + "@react-spectrum/button": "^3.16.10", + "@react-spectrum/dialog": "^3.8.17", + "@react-spectrum/utils": "^3.12.1", + "@react-types/contextualhelp": "^3.2.15", + "@react-types/shared": "^3.27.0", + "@spectrum-icons/workflow": "^4.2.17", "@swc/helpers": "^0.5.0" }, "devDependencies": { diff --git a/packages/@react-spectrum/datepicker/docs/DatePicker.mdx b/packages/@react-spectrum/datepicker/docs/DatePicker.mdx index ec1ee6ab226..508a63dd091 100644 --- a/packages/@react-spectrum/datepicker/docs/DatePicker.mdx +++ b/packages/@react-spectrum/datepicker/docs/DatePicker.mdx @@ -441,6 +441,14 @@ By default, `DatePicker` displays times in either 12 or 24 hour hour format depe hourCycle={24} /> ``` +### Custom first day of week + +By default, the first day of the week is automatically set based on the current locale. This can be changed by setting the `firstDayOfWeek` prop to `'sun'`, `'mon'`, `'tue'`, `'wed'`, `'thu'`, `'fri'`, or `'sat'`. + +```tsx example + +``` + ## Testing The DatePicker features an overlay that transitions in and out of the page as it is opened and closed. Depending on diff --git a/packages/@react-spectrum/datepicker/docs/DateRangePicker.mdx b/packages/@react-spectrum/datepicker/docs/DateRangePicker.mdx index 68be8285a29..c8fb2b73024 100644 --- a/packages/@react-spectrum/datepicker/docs/DateRangePicker.mdx +++ b/packages/@react-spectrum/datepicker/docs/DateRangePicker.mdx @@ -478,6 +478,14 @@ By default, `DateRangePicker` displays times in either 12 or 24 hour hour format hourCycle={24} /> ``` +### Custom first day of week + +By default, the first day of the week is automatically set based on the current locale. This can be changed by setting the `firstDayOfWeek` prop to `'sun'`, `'mon'`, `'tue'`, `'wed'`, `'thu'`, `'fri'`, or `'sat'`. + +```tsx example + +``` + ## Testing The DateRangePicker features an overlay that transitions in and out of the page as it is opened and closed. Depending on diff --git a/packages/@react-spectrum/datepicker/package.json b/packages/@react-spectrum/datepicker/package.json index 208b05db96a..9aba7ed67b8 100644 --- a/packages/@react-spectrum/datepicker/package.json +++ b/packages/@react-spectrum/datepicker/package.json @@ -1,6 +1,6 @@ { "name": "@react-spectrum/datepicker", - "version": "3.11.0", + "version": "3.12.0", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -36,25 +36,25 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@internationalized/date": "^3.6.0", - "@react-aria/datepicker": "^3.12.0", - "@react-aria/focus": "^3.19.0", - "@react-aria/i18n": "^3.12.4", - "@react-aria/interactions": "^3.22.5", - "@react-aria/utils": "^3.26.0", - "@react-spectrum/button": "^3.16.9", - "@react-spectrum/calendar": "^3.5.0", - "@react-spectrum/dialog": "^3.8.16", - "@react-spectrum/form": "^3.7.10", - "@react-spectrum/label": "^3.16.10", - "@react-spectrum/layout": "^3.6.10", - "@react-spectrum/utils": "^3.12.0", - "@react-spectrum/view": "^3.6.14", - "@react-stately/datepicker": "^3.11.0", - "@react-types/datepicker": "^3.9.0", - "@react-types/shared": "^3.26.0", - "@spectrum-icons/ui": "^3.6.11", - "@spectrum-icons/workflow": "^4.2.16", + "@internationalized/date": "^3.7.0", + "@react-aria/datepicker": "^3.13.0", + "@react-aria/focus": "^3.19.1", + "@react-aria/i18n": "^3.12.5", + "@react-aria/interactions": "^3.23.0", + "@react-aria/utils": "^3.27.0", + "@react-spectrum/button": "^3.16.10", + "@react-spectrum/calendar": "^3.6.0", + "@react-spectrum/dialog": "^3.8.17", + "@react-spectrum/form": "^3.7.11", + "@react-spectrum/label": "^3.16.11", + "@react-spectrum/layout": "^3.6.11", + "@react-spectrum/utils": "^3.12.1", + "@react-spectrum/view": "^3.6.15", + "@react-stately/datepicker": "^3.12.0", + "@react-types/datepicker": "^3.10.0", + "@react-types/shared": "^3.27.0", + "@spectrum-icons/ui": "^3.6.12", + "@spectrum-icons/workflow": "^4.2.17", "@swc/helpers": "^0.5.0" }, "devDependencies": { diff --git a/packages/@react-spectrum/datepicker/src/DatePicker.tsx b/packages/@react-spectrum/datepicker/src/DatePicker.tsx index 3faf0437fec..229d11ef07e 100644 --- a/packages/@react-spectrum/datepicker/src/DatePicker.tsx +++ b/packages/@react-spectrum/datepicker/src/DatePicker.tsx @@ -50,7 +50,8 @@ export const DatePicker = React.forwardRef(function DatePicker(null); @@ -171,6 +172,7 @@ export const DatePicker = React.forwardRef(function DatePicker {showTimeField &&
    diff --git a/packages/@react-spectrum/datepicker/src/DateRangePicker.tsx b/packages/@react-spectrum/datepicker/src/DateRangePicker.tsx index 0cd78290f6a..359df856ae2 100644 --- a/packages/@react-spectrum/datepicker/src/DateRangePicker.tsx +++ b/packages/@react-spectrum/datepicker/src/DateRangePicker.tsx @@ -51,7 +51,8 @@ export const DateRangePicker = React.forwardRef(function DateRangePicker(null); @@ -187,6 +188,7 @@ export const DateRangePicker = React.forwardRef(function DateRangePicker {showTimeField && diff --git a/packages/@react-spectrum/datepicker/stories/DatePicker.stories.tsx b/packages/@react-spectrum/datepicker/stories/DatePicker.stories.tsx index 58740157c8c..7af1d82c90d 100644 --- a/packages/@react-spectrum/datepicker/stories/DatePicker.stories.tsx +++ b/packages/@react-spectrum/datepicker/stories/DatePicker.stories.tsx @@ -182,6 +182,10 @@ export default { }, isOpen: { control: 'boolean' + }, + firstDayOfWeek: { + control: 'select', + options: [undefined, 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'] } } } as ComponentMeta; diff --git a/packages/@react-spectrum/dialog/package.json b/packages/@react-spectrum/dialog/package.json index 091b98aecb1..d14b1c4147b 100644 --- a/packages/@react-spectrum/dialog/package.json +++ b/packages/@react-spectrum/dialog/package.json @@ -1,6 +1,6 @@ { "name": "@react-spectrum/dialog", - "version": "3.8.16", + "version": "3.8.17", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -36,24 +36,24 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/dialog": "^3.5.20", - "@react-aria/i18n": "^3.12.4", - "@react-aria/interactions": "^3.22.5", - "@react-aria/overlays": "^3.24.0", - "@react-aria/utils": "^3.26.0", - "@react-spectrum/button": "^3.16.9", - "@react-spectrum/buttongroup": "^3.6.17", - "@react-spectrum/divider": "^3.5.18", - "@react-spectrum/layout": "^3.6.10", - "@react-spectrum/overlays": "^5.7.0", - "@react-spectrum/text": "^3.5.10", - "@react-spectrum/utils": "^3.12.0", - "@react-spectrum/view": "^3.6.14", - "@react-stately/overlays": "^3.6.12", - "@react-types/button": "^3.10.1", - "@react-types/dialog": "^3.5.14", - "@react-types/shared": "^3.26.0", - "@spectrum-icons/ui": "^3.6.11", + "@react-aria/dialog": "^3.5.21", + "@react-aria/i18n": "^3.12.5", + "@react-aria/interactions": "^3.23.0", + "@react-aria/overlays": "^3.25.0", + "@react-aria/utils": "^3.27.0", + "@react-spectrum/button": "^3.16.10", + "@react-spectrum/buttongroup": "^3.6.18", + "@react-spectrum/divider": "^3.5.19", + "@react-spectrum/layout": "^3.6.11", + "@react-spectrum/overlays": "^5.7.1", + "@react-spectrum/text": "^3.5.11", + "@react-spectrum/utils": "^3.12.1", + "@react-spectrum/view": "^3.6.15", + "@react-stately/overlays": "^3.6.13", + "@react-types/button": "^3.10.2", + "@react-types/dialog": "^3.5.15", + "@react-types/shared": "^3.27.0", + "@spectrum-icons/ui": "^3.6.12", "@swc/helpers": "^0.5.0" }, "devDependencies": { diff --git a/packages/@react-spectrum/divider/package.json b/packages/@react-spectrum/divider/package.json index 189124857d3..bd50c19a3d1 100644 --- a/packages/@react-spectrum/divider/package.json +++ b/packages/@react-spectrum/divider/package.json @@ -1,6 +1,6 @@ { "name": "@react-spectrum/divider", - "version": "3.5.18", + "version": "3.5.19", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -36,10 +36,10 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/separator": "^3.4.4", - "@react-spectrum/utils": "^3.12.0", - "@react-types/divider": "^3.3.13", - "@react-types/shared": "^3.26.0", + "@react-aria/separator": "^3.4.5", + "@react-spectrum/utils": "^3.12.1", + "@react-types/divider": "^3.3.14", + "@react-types/shared": "^3.27.0", "@swc/helpers": "^0.5.0" }, "devDependencies": { diff --git a/packages/@react-spectrum/dnd/package.json b/packages/@react-spectrum/dnd/package.json index e924ac0493b..2af56d56ba7 100644 --- a/packages/@react-spectrum/dnd/package.json +++ b/packages/@react-spectrum/dnd/package.json @@ -1,6 +1,6 @@ { "name": "@react-spectrum/dnd", - "version": "3.5.0", + "version": "3.5.1", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -36,9 +36,9 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/dnd": "^3.8.0", - "@react-stately/dnd": "^3.5.0", - "@react-types/shared": "^3.26.0", + "@react-aria/dnd": "^3.8.1", + "@react-stately/dnd": "^3.5.1", + "@react-types/shared": "^3.27.0", "@swc/helpers": "^0.5.0" }, "devDependencies": { diff --git a/packages/@react-spectrum/dropzone/package.json b/packages/@react-spectrum/dropzone/package.json index d92a5aee5ad..4ab50ee2351 100644 --- a/packages/@react-spectrum/dropzone/package.json +++ b/packages/@react-spectrum/dropzone/package.json @@ -1,6 +1,6 @@ { "name": "@react-spectrum/dropzone", - "version": "3.0.6", + "version": "3.0.7", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -36,12 +36,12 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/i18n": "^3.12.4", - "@react-aria/utils": "^3.26.0", - "@react-spectrum/utils": "^3.12.0", - "@react-types/shared": "^3.26.0", + "@react-aria/i18n": "^3.12.5", + "@react-aria/utils": "^3.27.0", + "@react-spectrum/utils": "^3.12.1", + "@react-types/shared": "^3.27.0", "@swc/helpers": "^0.5.0", - "react-aria-components": "^1.5.0" + "react-aria-components": "^1.6.0" }, "devDependencies": { "@adobe/spectrum-css-temp": "3.0.0-alpha.1", diff --git a/packages/@react-spectrum/filetrigger/package.json b/packages/@react-spectrum/filetrigger/package.json index a1ad3cbd1c6..8d3c0c07775 100644 --- a/packages/@react-spectrum/filetrigger/package.json +++ b/packages/@react-spectrum/filetrigger/package.json @@ -1,6 +1,6 @@ { "name": "@react-spectrum/filetrigger", - "version": "3.0.6", + "version": "3.0.7", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -37,7 +37,7 @@ }, "dependencies": { "@swc/helpers": "^0.5.0", - "react-aria-components": "^1.5.0" + "react-aria-components": "^1.6.0" }, "peerDependencies": { "@react-spectrum/provider": "^3.0.0", diff --git a/packages/@react-spectrum/form/package.json b/packages/@react-spectrum/form/package.json index d9959726c00..f792449a4d8 100644 --- a/packages/@react-spectrum/form/package.json +++ b/packages/@react-spectrum/form/package.json @@ -1,6 +1,6 @@ { "name": "@react-spectrum/form", - "version": "3.7.10", + "version": "3.7.11", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -36,11 +36,11 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/utils": "^3.26.0", - "@react-spectrum/utils": "^3.12.0", - "@react-stately/form": "^3.1.0", - "@react-types/form": "^3.7.8", - "@react-types/shared": "^3.26.0", + "@react-aria/utils": "^3.27.0", + "@react-spectrum/utils": "^3.12.1", + "@react-stately/form": "^3.1.1", + "@react-types/form": "^3.7.9", + "@react-types/shared": "^3.27.0", "@swc/helpers": "^0.5.0" }, "devDependencies": { @@ -48,7 +48,8 @@ }, "peerDependencies": { "@react-spectrum/provider": "^3.0.0", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" }, "publishConfig": { "access": "public" diff --git a/packages/@react-spectrum/icon/package.json b/packages/@react-spectrum/icon/package.json index a098df00d2e..573d506d928 100644 --- a/packages/@react-spectrum/icon/package.json +++ b/packages/@react-spectrum/icon/package.json @@ -1,6 +1,6 @@ { "name": "@react-spectrum/icon", - "version": "3.8.0", + "version": "3.8.1", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -36,9 +36,9 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/utils": "^3.26.0", - "@react-spectrum/utils": "^3.12.0", - "@react-types/shared": "^3.26.0", + "@react-aria/utils": "^3.27.0", + "@react-spectrum/utils": "^3.12.1", + "@react-types/shared": "^3.27.0", "@swc/helpers": "^0.5.0" }, "devDependencies": { @@ -46,7 +46,8 @@ }, "peerDependencies": { "@react-spectrum/provider": "^3.0.0", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" }, "publishConfig": { "access": "public" diff --git a/packages/@react-spectrum/illustratedmessage/package.json b/packages/@react-spectrum/illustratedmessage/package.json index 272c8cc1397..4fb0c7acea4 100644 --- a/packages/@react-spectrum/illustratedmessage/package.json +++ b/packages/@react-spectrum/illustratedmessage/package.json @@ -1,6 +1,6 @@ { "name": "@react-spectrum/illustratedmessage", - "version": "3.5.5", + "version": "3.5.6", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -36,11 +36,11 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/utils": "^3.26.0", - "@react-spectrum/layout": "^3.6.10", - "@react-spectrum/utils": "^3.12.0", - "@react-types/illustratedmessage": "^3.3.13", - "@react-types/shared": "^3.26.0", + "@react-aria/utils": "^3.27.0", + "@react-spectrum/layout": "^3.6.11", + "@react-spectrum/utils": "^3.12.1", + "@react-types/illustratedmessage": "^3.3.14", + "@react-types/shared": "^3.27.0", "@swc/helpers": "^0.5.0" }, "devDependencies": { @@ -48,7 +48,8 @@ }, "peerDependencies": { "@react-spectrum/provider": "^3.0.0", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" }, "publishConfig": { "access": "public" diff --git a/packages/@react-spectrum/image/package.json b/packages/@react-spectrum/image/package.json index 48c7dade18e..4e18ca224e4 100644 --- a/packages/@react-spectrum/image/package.json +++ b/packages/@react-spectrum/image/package.json @@ -1,6 +1,6 @@ { "name": "@react-spectrum/image", - "version": "3.5.6", + "version": "3.5.7", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -36,10 +36,10 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/utils": "^3.26.0", - "@react-spectrum/utils": "^3.12.0", - "@react-types/image": "^3.4.5", - "@react-types/shared": "^3.26.0", + "@react-aria/utils": "^3.27.0", + "@react-spectrum/utils": "^3.12.1", + "@react-types/image": "^3.4.6", + "@react-types/shared": "^3.27.0", "@swc/helpers": "^0.5.0" }, "devDependencies": { @@ -47,7 +47,8 @@ }, "peerDependencies": { "@react-spectrum/provider": "^3.0.0", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" }, "publishConfig": { "access": "public" diff --git a/packages/@react-spectrum/inlinealert/package.json b/packages/@react-spectrum/inlinealert/package.json index d446a0282bc..d5e3083ba1c 100644 --- a/packages/@react-spectrum/inlinealert/package.json +++ b/packages/@react-spectrum/inlinealert/package.json @@ -1,6 +1,6 @@ { "name": "@react-spectrum/inlinealert", - "version": "3.2.10", + "version": "3.2.11", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -36,13 +36,13 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/focus": "^3.19.0", - "@react-aria/i18n": "^3.12.4", - "@react-aria/utils": "^3.26.0", - "@react-spectrum/layout": "^3.6.10", - "@react-spectrum/utils": "^3.12.0", - "@react-types/shared": "^3.26.0", - "@spectrum-icons/ui": "^3.6.11", + "@react-aria/focus": "^3.19.1", + "@react-aria/i18n": "^3.12.5", + "@react-aria/utils": "^3.27.0", + "@react-spectrum/layout": "^3.6.11", + "@react-spectrum/utils": "^3.12.1", + "@react-types/shared": "^3.27.0", + "@spectrum-icons/ui": "^3.6.12", "@swc/helpers": "^0.5.0" }, "devDependencies": { @@ -50,7 +50,8 @@ }, "peerDependencies": { "@react-spectrum/provider": "^3.0.0", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" }, "publishConfig": { "access": "public" diff --git a/packages/@react-spectrum/label/package.json b/packages/@react-spectrum/label/package.json index 87324dfa12f..6b5e233e23e 100644 --- a/packages/@react-spectrum/label/package.json +++ b/packages/@react-spectrum/label/package.json @@ -1,6 +1,6 @@ { "name": "@react-spectrum/label", - "version": "3.16.10", + "version": "3.16.11", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -36,14 +36,14 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/i18n": "^3.12.4", - "@react-aria/utils": "^3.26.0", - "@react-spectrum/form": "^3.7.10", - "@react-spectrum/layout": "^3.6.10", - "@react-spectrum/utils": "^3.12.0", - "@react-types/label": "^3.9.7", - "@react-types/shared": "^3.26.0", - "@spectrum-icons/ui": "^3.6.11", + "@react-aria/i18n": "^3.12.5", + "@react-aria/utils": "^3.27.0", + "@react-spectrum/form": "^3.7.11", + "@react-spectrum/layout": "^3.6.11", + "@react-spectrum/utils": "^3.12.1", + "@react-types/label": "^3.9.8", + "@react-types/shared": "^3.27.0", + "@spectrum-icons/ui": "^3.6.12", "@swc/helpers": "^0.5.0" }, "devDependencies": { @@ -51,7 +51,8 @@ }, "peerDependencies": { "@react-spectrum/provider": "^3.0.0", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" }, "publishConfig": { "access": "public" diff --git a/packages/@react-spectrum/labeledvalue/package.json b/packages/@react-spectrum/labeledvalue/package.json index 37c79a9c53a..77275be9489 100644 --- a/packages/@react-spectrum/labeledvalue/package.json +++ b/packages/@react-spectrum/labeledvalue/package.json @@ -1,6 +1,6 @@ { "name": "@react-spectrum/labeledvalue", - "version": "3.1.18", + "version": "3.1.19", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -36,12 +36,12 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@internationalized/date": "^3.6.0", - "@react-aria/i18n": "^3.12.4", - "@react-aria/utils": "^3.26.0", - "@react-spectrum/label": "^3.16.10", - "@react-spectrum/utils": "^3.12.0", - "@react-types/shared": "^3.26.0", + "@internationalized/date": "^3.7.0", + "@react-aria/i18n": "^3.12.5", + "@react-aria/utils": "^3.27.0", + "@react-spectrum/label": "^3.16.11", + "@react-spectrum/utils": "^3.12.1", + "@react-types/shared": "^3.27.0", "@swc/helpers": "^0.5.0" }, "devDependencies": { @@ -49,7 +49,8 @@ }, "peerDependencies": { "@react-spectrum/provider": "^3.0.0", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" }, "publishConfig": { "access": "public" diff --git a/packages/@react-spectrum/layout/package.json b/packages/@react-spectrum/layout/package.json index e3107126117..7a63caead89 100644 --- a/packages/@react-spectrum/layout/package.json +++ b/packages/@react-spectrum/layout/package.json @@ -1,6 +1,6 @@ { "name": "@react-spectrum/layout", - "version": "3.6.10", + "version": "3.6.11", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -36,10 +36,10 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/utils": "^3.26.0", - "@react-spectrum/utils": "^3.12.0", - "@react-types/layout": "^3.3.19", - "@react-types/shared": "^3.26.0", + "@react-aria/utils": "^3.27.0", + "@react-spectrum/utils": "^3.12.1", + "@react-types/layout": "^3.3.20", + "@react-types/shared": "^3.27.0", "@swc/helpers": "^0.5.0" }, "devDependencies": { @@ -47,7 +47,8 @@ }, "peerDependencies": { "@react-spectrum/provider": "^3.0.0", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" }, "publishConfig": { "access": "public" diff --git a/packages/@react-spectrum/link/package.json b/packages/@react-spectrum/link/package.json index acfaa2b8ebc..c83cabd3e62 100644 --- a/packages/@react-spectrum/link/package.json +++ b/packages/@react-spectrum/link/package.json @@ -1,6 +1,6 @@ { "name": "@react-spectrum/link", - "version": "3.6.12", + "version": "3.6.13", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -36,13 +36,13 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/focus": "^3.19.0", - "@react-aria/interactions": "^3.22.5", - "@react-aria/link": "^3.7.7", - "@react-aria/utils": "^3.26.0", - "@react-spectrum/utils": "^3.12.0", - "@react-types/link": "^3.5.9", - "@react-types/shared": "^3.26.0", + "@react-aria/focus": "^3.19.1", + "@react-aria/interactions": "^3.23.0", + "@react-aria/link": "^3.7.8", + "@react-aria/utils": "^3.27.0", + "@react-spectrum/utils": "^3.12.1", + "@react-types/link": "^3.5.10", + "@react-types/shared": "^3.27.0", "@swc/helpers": "^0.5.0" }, "devDependencies": { @@ -50,7 +50,8 @@ }, "peerDependencies": { "@react-spectrum/provider": "^3.0.0", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" }, "publishConfig": { "access": "public" diff --git a/packages/@react-spectrum/list/docs/ListView.mdx b/packages/@react-spectrum/list/docs/ListView.mdx index 362fa71631f..73c326ebae5 100644 --- a/packages/@react-spectrum/list/docs/ListView.mdx +++ b/packages/@react-spectrum/list/docs/ListView.mdx @@ -14,7 +14,8 @@ import Anatomy from './anatomy.svg'; import ChevronRight from '@spectrum-icons/workflow/ChevronRight'; import docs from 'docs:@react-spectrum/list'; import dndDocs from 'docs:@react-spectrum/dnd'; -import {HeaderInfo, PropTable, PageDescription, TypeLink} from '@react-spectrum/docs'; +import gridlistUtil from 'docs:@react-aria/test-utils/src/gridlist.ts'; +import {HeaderInfo, PropTable, PageDescription, TypeLink, ClassAPI, VersionBadge} from '@react-spectrum/docs'; import {Keyboard} from '@react-spectrum/text'; import packageData from '@react-spectrum/list/package.json'; @@ -1191,3 +1192,44 @@ behaviors in your test suite. Please also refer to [React Spectrum's test suite](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-spectrum/list/test/ListView.test.js) if you find that the above isn't sufficient when resolving issues in your own test cases. + +### Test utils + +`@react-spectrum/test-utils` offers common gridlist interaction utilities which you may find helpful when writing tests. See [here](../react-aria/testing.html#react-aria-test-utils) for more information on how to setup these utilities +in your tests. Below is the full definition of the gridlist tester and a sample of how you could use it in your test suite. + +```ts +// ListView.test.ts +import {render, within} from '@testing-library/react'; +import {theme} from '@react-spectrum/theme-default'; +import {User} from '@react-spectrum/test-utils'; + +let testUtilUser = new User({interactionType: 'mouse'}); +// ... + +it('ListView can select a row via keyboard', async function () { + // Render your test component/app and initialize the gridlist tester + let {getByTestId} = render( + + + ... + + + ); + let gridListTester = testUtilUser.createTester('GridList', {root: getByTestId('test-gridlist'), interactionType: 'keyboard'}); + + let row = gridListTester.rows[0]; + expect(within(row).getByRole('checkbox')).not.toBeChecked(); + expect(gridListTester.selectedRows).toHaveLength(0); + + await gridListTester.toggleRowSelection({row: 0}); + expect(within(row).getByRole('checkbox')).toBeChecked(); + expect(gridListTester.selectedRows).toHaveLength(1); + + await gridListTester.toggleRowSelection({row: 0}); + expect(within(row).getByRole('checkbox')).not.toBeChecked(); + expect(gridListTester.selectedRows).toHaveLength(0); +}); +``` + + diff --git a/packages/@react-spectrum/list/package.json b/packages/@react-spectrum/list/package.json index a2c6c998d78..4af5a3a9f0b 100644 --- a/packages/@react-spectrum/list/package.json +++ b/packages/@react-spectrum/list/package.json @@ -1,6 +1,6 @@ { "name": "@react-spectrum/list", - "version": "3.9.0", + "version": "3.9.1", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -36,28 +36,28 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/button": "^3.11.0", - "@react-aria/focus": "^3.19.0", - "@react-aria/gridlist": "^3.10.0", - "@react-aria/i18n": "^3.12.4", - "@react-aria/interactions": "^3.22.5", - "@react-aria/selection": "^3.21.0", - "@react-aria/utils": "^3.26.0", - "@react-aria/virtualizer": "^4.1.0", - "@react-aria/visually-hidden": "^3.8.18", - "@react-spectrum/checkbox": "^3.9.11", - "@react-spectrum/dnd": "^3.5.0", - "@react-spectrum/layout": "^3.6.10", - "@react-spectrum/progress": "^3.7.11", - "@react-spectrum/text": "^3.5.10", - "@react-spectrum/utils": "^3.12.0", - "@react-stately/collections": "^3.12.0", - "@react-stately/layout": "^4.1.0", - "@react-stately/list": "^3.11.1", - "@react-stately/virtualizer": "^4.2.0", - "@react-types/grid": "^3.2.10", - "@react-types/shared": "^3.26.0", - "@spectrum-icons/ui": "^3.6.11", + "@react-aria/button": "^3.11.1", + "@react-aria/focus": "^3.19.1", + "@react-aria/gridlist": "^3.10.1", + "@react-aria/i18n": "^3.12.5", + "@react-aria/interactions": "^3.23.0", + "@react-aria/selection": "^3.22.0", + "@react-aria/utils": "^3.27.0", + "@react-aria/virtualizer": "^4.1.1", + "@react-aria/visually-hidden": "^3.8.19", + "@react-spectrum/checkbox": "^3.9.12", + "@react-spectrum/dnd": "^3.5.1", + "@react-spectrum/layout": "^3.6.11", + "@react-spectrum/progress": "^3.7.12", + "@react-spectrum/text": "^3.5.11", + "@react-spectrum/utils": "^3.12.1", + "@react-stately/collections": "^3.12.1", + "@react-stately/layout": "^4.1.1", + "@react-stately/list": "^3.11.2", + "@react-stately/virtualizer": "^4.2.1", + "@react-types/grid": "^3.2.11", + "@react-types/shared": "^3.27.0", + "@spectrum-icons/ui": "^3.6.12", "@swc/helpers": "^0.5.0", "react-transition-group": "^4.4.5" }, diff --git a/packages/@react-spectrum/list/stories/ListViewActions.stories.tsx b/packages/@react-spectrum/list/stories/ListViewActions.stories.tsx index 44c6fbfdc15..322d2538dd6 100644 --- a/packages/@react-spectrum/list/stories/ListViewActions.stories.tsx +++ b/packages/@react-spectrum/list/stories/ListViewActions.stories.tsx @@ -30,37 +30,27 @@ export default { }, argTypes: { selectionMode: { - control: { - type: 'radio', - options: ['none', 'single', 'multiple'] - } + control: 'radio', + options: ['none', 'single', 'multiple'] }, selectionStyle: { - control: { - type: 'radio', - options: ['checkbox', 'highlight'] - } + control: 'radio', + options: ['checkbox', 'highlight'] }, isQuiet: { - control: {type: 'boolean'} + control: 'boolean' }, density: { - control: { - type: 'select', - options: ['compact', 'regular', 'spacious'] - } + control: 'select', + options: ['compact', 'regular', 'spacious'] }, overflowMode: { - control: { - type: 'radio', - options: ['truncate', 'wrap'] - } + control: 'radio', + options: ['truncate', 'wrap'] }, disabledBehavior: { - control: { - type: 'radio', - options: ['selection', 'all'] - } + control: 'radio', + options: ['selection', 'all'] } } } as ComponentMeta; diff --git a/packages/@react-spectrum/list/stories/ListViewDnDUtil.stories.tsx b/packages/@react-spectrum/list/stories/ListViewDnDUtil.stories.tsx index 3be13f1b0cc..d2c56d02abe 100644 --- a/packages/@react-spectrum/list/stories/ListViewDnDUtil.stories.tsx +++ b/packages/@react-spectrum/list/stories/ListViewDnDUtil.stories.tsx @@ -19,37 +19,27 @@ export default { }, argTypes: { selectionMode: { - control: { - type: 'radio', - options: ['none', 'single', 'multiple'] - } + control: 'radio', + options: ['none', 'single', 'multiple'] }, selectionStyle: { - control: { - type: 'radio', - options: ['checkbox', 'highlight'] - } + control: 'radio', + options: ['checkbox', 'highlight'] }, isQuiet: { - control: {type: 'boolean'} + control: 'boolean' }, density: { - control: { - type: 'select', - options: ['compact', 'regular', 'spacious'] - } + control: 'select', + options: ['compact', 'regular', 'spacious'] }, overflowMode: { - control: { - type: 'radio', - options: ['truncate', 'wrap'] - } + control: 'radio', + options: ['truncate', 'wrap'] }, disabledBehavior: { - control: { - type: 'radio', - options: ['selection', 'all'] - } + control: 'radio', + options: ['selection', 'all'] } } } as ComponentMeta; diff --git a/packages/@react-spectrum/list/stories/ListViewSelection.stories.tsx b/packages/@react-spectrum/list/stories/ListViewSelection.stories.tsx index 3f2829374b0..bd8dd8cb558 100644 --- a/packages/@react-spectrum/list/stories/ListViewSelection.stories.tsx +++ b/packages/@react-spectrum/list/stories/ListViewSelection.stories.tsx @@ -26,37 +26,27 @@ export default { }, argTypes: { selectionMode: { - control: { - type: 'radio', - options: ['none', 'single', 'multiple'] - } + control: 'radio', + options: ['none', 'single', 'multiple'] }, selectionStyle: { - control: { - type: 'radio', - options: ['checkbox', 'highlight'] - } + control: 'radio', + options: ['checkbox', 'highlight'] }, isQuiet: { - control: {type: 'boolean'} + control: 'boolean' }, density: { - control: { - type: 'select', - options: ['compact', 'regular', 'spacious'] - } + control: 'select', + options: ['compact', 'regular', 'spacious'] }, overflowMode: { - control: { - type: 'radio', - options: ['truncate', 'wrap'] - } + control: 'radio', + options: ['truncate', 'wrap'] }, disabledBehavior: { - control: { - type: 'radio', - options: ['selection', 'all'] - } + control: 'radio', + options: ['selection', 'all'] } } } as ComponentMeta; diff --git a/packages/@react-spectrum/list/test/ListView.test.js b/packages/@react-spectrum/list/test/ListView.test.js index 9ef8d4abfdb..ba9f0abf1e3 100644 --- a/packages/@react-spectrum/list/test/ListView.test.js +++ b/packages/@react-spectrum/list/test/ListView.test.js @@ -841,7 +841,7 @@ describe('ListView', function () { let gridListTester = testUtilUser.createTester('GridList', {root: grid}); let rows = gridListTester.rows; - await gridListTester.toggleRowSelection({index: 0}); + await gridListTester.toggleRowSelection({row: 0}); checkSelection(onSelectionChange, ['foo']); onSelectionChange.mockClear(); expect(announce).toHaveBeenLastCalledWith('Foo selected.'); @@ -872,17 +872,17 @@ describe('ListView', function () { let tree = renderSelectionList({onSelectionChange, selectionMode: 'multiple', onAction}); let gridListTester = testUtilUser.createTester('GridList', {root: tree.getByRole('grid')}); - await gridListTester.triggerRowAction({index: 1}); + await gridListTester.triggerRowAction({row: 1}); expect(onSelectionChange).not.toHaveBeenCalled(); expect(onAction).toHaveBeenCalledTimes(1); expect(onAction).toHaveBeenLastCalledWith('bar'); - await gridListTester.toggleRowSelection({index: 1}); + await gridListTester.toggleRowSelection({row: 1}); expect(onSelectionChange).toHaveBeenCalledTimes(1); checkSelection(onSelectionChange, ['bar']); onSelectionChange.mockReset(); - await gridListTester.toggleRowSelection({index: 2}); + await gridListTester.toggleRowSelection({row: 2}); expect(onSelectionChange).toHaveBeenCalledTimes(1); checkSelection(onSelectionChange, ['bar', 'baz']); }); @@ -893,18 +893,18 @@ describe('ListView', function () { let tree = renderSelectionList({onSelectionChange, selectionMode: 'multiple', onAction}); let gridListTester = testUtilUser.createTester('GridList', {root: tree.getByRole('grid')}); - await gridListTester.triggerRowAction({index: 1}); + await gridListTester.triggerRowAction({row: 1}); expect(onSelectionChange).not.toHaveBeenCalled(); expect(onAction).toHaveBeenCalledTimes(1); expect(onAction).toHaveBeenLastCalledWith('bar'); - await gridListTester.toggleRowSelection({index: 1}); + await gridListTester.toggleRowSelection({row: 1}); expect(onSelectionChange).toHaveBeenCalledTimes(1); checkSelection(onSelectionChange, ['bar']); onSelectionChange.mockReset(); gridListTester.setInteractionType('touch'); - await gridListTester.toggleRowSelection({index: 2}); + await gridListTester.toggleRowSelection({row: 2}); expect(onSelectionChange).toHaveBeenCalledTimes(1); checkSelection(onSelectionChange, ['bar', 'baz']); }); @@ -951,13 +951,13 @@ describe('ListView', function () { let gridListTester = testUtilUser.createTester('GridList', {root: tree.getByRole('grid')}); gridListTester.setInteractionType('keyboard'); - await gridListTester.triggerRowAction({index: 1}); + await gridListTester.triggerRowAction({row: 1}); expect(onSelectionChange).not.toHaveBeenCalled(); expect(onAction).toHaveBeenCalledTimes(1); expect(onAction).toHaveBeenLastCalledWith('bar'); onAction.mockReset(); - await gridListTester.toggleRowSelection({index: 2}); + await gridListTester.toggleRowSelection({row: 2}); expect(onSelectionChange).toHaveBeenCalledTimes(1); expect(onAction).not.toHaveBeenCalled(); checkSelection(onSelectionChange, ['baz']); diff --git a/packages/@react-spectrum/listbox/docs/ListBox.mdx b/packages/@react-spectrum/listbox/docs/ListBox.mdx index bcbe38cbf0e..56de8d1e1e1 100644 --- a/packages/@react-spectrum/listbox/docs/ListBox.mdx +++ b/packages/@react-spectrum/listbox/docs/ListBox.mdx @@ -11,8 +11,9 @@ import {Layout} from '@react-spectrum/docs'; export default Layout; import docs from 'docs:@react-spectrum/listbox'; -import {HeaderInfo, PropTable, PageDescription} from '@react-spectrum/docs'; +import {HeaderInfo, PropTable, PageDescription, ClassAPI, VersionBadge} from '@react-spectrum/docs'; import packageData from '@react-spectrum/listbox/package.json'; +import listboxUtils from 'docs:@react-aria/test-utils/src/listbox.ts'; ```jsx import import {ListBox, Section, Item} from '@react-spectrum/listbox'; @@ -409,3 +410,35 @@ Please see the following sections in the testing docs for more information on ho Please also refer to [React Spectrum's test suite](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-spectrum/listbox/test/ListBox.test.js) if you find that the above isn't sufficient when resolving issues in your own test cases. + +### Test utils + +`@react-spectrum/test-utils` offers common listbox interaction utilities which you may find helpful when writing tests. See [here](../react-aria/testing.html#react-aria-test-utils) for more information on how to setup these utilities +in your tests. Below is the full definition of the listbox tester and a sample of how you could use it in your test suite. + +```ts +// ListBox.test.ts +import {render} from '@testing-library/react'; +import {theme} from '@react-spectrum/theme-default'; +import {User} from '@react-spectrum/test-utils'; + +let testUtilUser = new User({interactionType: 'mouse'}); +// ... + +it('ListBox can select an option via keyboard', async function () { + // Render your test component/app and initialize the listbox tester + let {getByTestId} = render( + + + ... + + + ); + let listboxTester = testUtilUser.createTester('ListBox', {root: getByTestId('test-listbox'), interactionType: 'keyboard'}); + + await listboxTester.toggleOptionSelection({option: 4}); + expect(listboxTester.options()[4]).toHaveAttribute('aria-selected', 'true'); +}); +``` + + diff --git a/packages/@react-spectrum/listbox/package.json b/packages/@react-spectrum/listbox/package.json index 6c9423b2917..ac6ecd882c6 100644 --- a/packages/@react-spectrum/listbox/package.json +++ b/packages/@react-spectrum/listbox/package.json @@ -1,6 +1,6 @@ { "name": "@react-spectrum/listbox", - "version": "3.14.0", + "version": "3.14.1", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -36,23 +36,23 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/focus": "^3.19.0", - "@react-aria/i18n": "^3.12.4", - "@react-aria/interactions": "^3.22.5", - "@react-aria/listbox": "^3.13.6", - "@react-aria/utils": "^3.26.0", - "@react-aria/virtualizer": "^4.1.0", - "@react-spectrum/layout": "^3.6.10", - "@react-spectrum/progress": "^3.7.11", - "@react-spectrum/text": "^3.5.10", - "@react-spectrum/utils": "^3.12.0", - "@react-stately/collections": "^3.12.0", - "@react-stately/layout": "^4.1.0", - "@react-stately/list": "^3.11.1", - "@react-stately/virtualizer": "^4.2.0", - "@react-types/listbox": "^3.5.3", - "@react-types/shared": "^3.26.0", - "@spectrum-icons/ui": "^3.6.11", + "@react-aria/focus": "^3.19.1", + "@react-aria/i18n": "^3.12.5", + "@react-aria/interactions": "^3.23.0", + "@react-aria/listbox": "^3.14.0", + "@react-aria/utils": "^3.27.0", + "@react-aria/virtualizer": "^4.1.1", + "@react-spectrum/layout": "^3.6.11", + "@react-spectrum/progress": "^3.7.12", + "@react-spectrum/text": "^3.5.11", + "@react-spectrum/utils": "^3.12.1", + "@react-stately/collections": "^3.12.1", + "@react-stately/layout": "^4.1.1", + "@react-stately/list": "^3.11.2", + "@react-stately/virtualizer": "^4.2.1", + "@react-types/listbox": "^3.5.4", + "@react-types/shared": "^3.27.0", + "@spectrum-icons/ui": "^3.6.12", "@swc/helpers": "^0.5.0" }, "devDependencies": { diff --git a/packages/@react-spectrum/listbox/test/ListBox.test.js b/packages/@react-spectrum/listbox/test/ListBox.test.js index 2a7c07ca6bb..56658103c55 100644 --- a/packages/@react-spectrum/listbox/test/ListBox.test.js +++ b/packages/@react-spectrum/listbox/test/ListBox.test.js @@ -19,6 +19,7 @@ import React from 'react'; import {Text} from '@react-spectrum/text'; import {theme} from '@react-spectrum/theme-default'; import {useAsyncList} from '@react-stately/data'; +import {User} from '@react-aria/test-utils'; import userEvent from '@testing-library/user-event'; let withSection = [ @@ -69,6 +70,7 @@ function renderComponent(props) { describe('ListBox', function () { let offsetWidth, offsetHeight, scrollHeight; let onSelectionChange = jest.fn(); + let testUtilUser = new User(); beforeAll(function () { offsetWidth = jest.spyOn(window.HTMLElement.prototype, 'clientWidth', 'get').mockImplementation(() => 1000); @@ -89,11 +91,12 @@ describe('ListBox', function () { it('renders properly', function () { let tree = renderComponent(); - let listbox = tree.getByRole('listbox'); + let listboxTester = testUtilUser.createTester('ListBox', {root: tree.getByRole('listbox')}); + let listbox = listboxTester.listbox; expect(listbox).toBeTruthy(); expect(listbox).toHaveAttribute('aria-labelledby', 'label'); - let sections = within(listbox).getAllByRole('group'); + let sections = listboxTester.sections; expect(sections.length).toBe(withSection.length); for (let section of sections) { @@ -110,28 +113,22 @@ describe('ListBox', function () { } } - let items = within(listbox).getAllByRole('option'); - expect(items.length).toBe(withSection.reduce((acc, curr) => (acc + curr.children.length), 0)); + let options = listboxTester.options(); + expect(options.length).toBe(withSection.reduce((acc, curr) => (acc + curr.children.length), 0)); let i = 1; - for (let item of items) { - expect(item).toHaveAttribute('tabindex'); - expect(item).not.toHaveAttribute('aria-selected'); - expect(item).not.toHaveAttribute('aria-disabled'); - expect(item).toHaveAttribute('aria-posinset', '' + i++); - expect(item).toHaveAttribute('aria-setsize'); + for (let option of options) { + expect(option).toHaveAttribute('tabindex'); + expect(option).not.toHaveAttribute('aria-selected'); + expect(option).not.toHaveAttribute('aria-disabled'); + expect(option).toHaveAttribute('aria-posinset', '' + i++); + expect(option).toHaveAttribute('aria-setsize'); } - let item1 = within(listbox).getByText('Foo'); - let item2 = within(listbox).getByText('Bar'); - let item3 = within(listbox).getByText('Baz'); - let item4 = within(listbox).getByText('Blah'); - let item5 = within(listbox).getByText('Bleh'); - - expect(item1).toBeTruthy(); - expect(item2).toBeTruthy(); - expect(item3).toBeTruthy(); - expect(item4).toBeTruthy(); - expect(item5).toBeTruthy(); - expect(item3).toBeTruthy(); + + expect(listboxTester.findOption({optionIndexOrText: 'Foo'})).toBeTruthy(); + expect(listboxTester.findOption({optionIndexOrText: 'Bar'})).toBeTruthy(); + expect(listboxTester.findOption({optionIndexOrText: 'Baz'})).toBeTruthy(); + expect(listboxTester.findOption({optionIndexOrText: 'Blah'})).toBeTruthy(); + expect(listboxTester.findOption({optionIndexOrText: 'Bleh'})).toBeTruthy(); }); it('renders with falsy id', function () { @@ -190,30 +187,32 @@ describe('ListBox', function () { }); describe('supports single selection', function () { - it('supports defaultSelectedKeys (uncontrolled)', function () { - // Check that correct menu item is selected by default + it('supports defaultSelectedKeys (uncontrolled)', async function () { + // Check that correct listbox item is selected by default let tree = renderComponent({onSelectionChange, defaultSelectedKeys: ['Blah'], autoFocus: 'first', selectionMode: 'single'}); - let listbox = tree.getByRole('listbox'); - let options = within(listbox).getAllByRole('option'); - let selectedItem = options[3]; - expect(selectedItem).toBe(document.activeElement); - expect(selectedItem).toHaveAttribute('aria-selected', 'true'); - expect(selectedItem).toHaveAttribute('tabindex', '0'); - let itemText = within(selectedItem).getByText('Blah'); + let listboxTester = testUtilUser.createTester('ListBox', {root: tree.getByRole('listbox')}); + + let selectedOptions = listboxTester.selectedOptions; + expect(selectedOptions).toHaveLength(1); + expect(selectedOptions[0]).toBe(document.activeElement); + expect(selectedOptions[0]).toHaveAttribute('aria-selected', 'true'); + expect(selectedOptions[0]).toHaveAttribute('tabindex', '0'); + let itemText = within(selectedOptions[0]).getByText('Blah'); expect(itemText).toBeTruthy(); - let checkmark = within(selectedItem).getByRole('img', {hidden: true}); + let checkmark = within(selectedOptions[0]).getByRole('img', {hidden: true}); expect(checkmark).toBeTruthy(); - // Select a different menu item via enter - let nextSelectedItem = options[4]; - fireEvent.keyDown(nextSelectedItem, {key: 'Enter', code: 13, charCode: 13}); - expect(nextSelectedItem).toHaveAttribute('aria-selected', 'true'); - itemText = within(nextSelectedItem).getByText('Bleh'); + // Select a different listbox item via enter + await listboxTester.toggleOptionSelection({option: 4, interactionType: 'keyboard'}); + selectedOptions = listboxTester.selectedOptions; + expect(selectedOptions[0]).toHaveAttribute('aria-selected', 'true'); + itemText = within(selectedOptions[0]).getByText('Bleh'); expect(itemText).toBeTruthy(); - checkmark = within(nextSelectedItem).getByRole('img', {hidden: true}); + checkmark = within(selectedOptions[0]).getByRole('img', {hidden: true}); expect(checkmark).toBeTruthy(); + expect(selectedOptions).toHaveLength(1); - // Make sure there is only a single checkmark in the entire menu + // Make sure there is only a single checkmark in the entire listbox let checkmarks = tree.getAllByRole('img', {hidden: true}); expect(checkmarks.length).toBe(1); @@ -251,16 +250,15 @@ describe('ListBox', function () { expect(onSelectionChange.mock.calls[0][0].has('Bleh')).toBeTruthy(); }); - it('supports using space key to change item selection', function () { + it('supports using space key to change item selection', async function () { let tree = renderComponent({onSelectionChange, selectionMode: 'single'}); - let listbox = tree.getByRole('listbox'); - let options = within(listbox).getAllByRole('option'); + let listboxTester = testUtilUser.createTester('ListBox', {root: tree.getByRole('listbox')}); // Trigger a menu item via space - let item = options[4]; - fireEvent.keyDown(item, {key: ' ', code: 32, charCode: 32}); - expect(item).toHaveAttribute('aria-selected', 'true'); - let checkmark = within(item).getByRole('img', {hidden: true}); + let options = listboxTester.options(); + await listboxTester.toggleOptionSelection({option: 4, keyboardActivation: 'Space', interactionType: 'keyboard'}); + expect(options[4]).toHaveAttribute('aria-selected', 'true'); + let checkmark = within(options[4]).getByRole('img', {hidden: true}); expect(checkmark).toBeTruthy(); // Make sure there is only a single checkmark in the entire menu @@ -762,16 +760,16 @@ describe('ListBox', function () { it('should handle when an item changes sections', function () { let sections = [ { - id: 'foo', - title: 'Foo', + id: 'sect1', + title: 'Section 1', children: [ {id: 'foo-1', title: 'Foo 1'}, {id: 'foo-2', title: 'Foo 2'} ] }, { - id: 'bar', - title: 'Bar', + id: 'sect2', + title: 'Section 2', children: [ {id: 'bar-1', title: 'Bar 1'}, {id: 'bar-2', title: 'Bar 2'} @@ -793,9 +791,12 @@ describe('ListBox', function () { ); } - let {getByText, rerender} = render(); - let item = getByText('Foo 1'); - expect(document.getElementById(item.closest('[role=group]').getAttribute('aria-labelledby'))).toHaveTextContent('Foo'); + let {rerender, getByRole, getByLabelText} = render(); + let listboxTester = testUtilUser.createTester('ListBox', {root: getByRole('listbox')}); + let item = listboxTester.findOption({optionIndexOrText: 'Foo 1'}); + let listboxSections = listboxTester.sections; + expect(listboxTester.options({element: listboxSections[0]})).toContain(item); + expect(listboxSections[0]).toBe(getByLabelText('Section 1')); let sections2 = [ { @@ -809,8 +810,11 @@ describe('ListBox', function () { ]; rerender(); - item = getByText('Foo 1'); - expect(document.getElementById(item.closest('[role=group]').getAttribute('aria-labelledby'))).toHaveTextContent('Bar'); + listboxTester = testUtilUser.createTester('ListBox', {root: getByRole('listbox')}); + item = listboxTester.findOption({optionIndexOrText: 'Foo 1'}); + listboxSections = listboxTester.sections; + expect(listboxTester.options({element: listboxSections[1]})).toContain(item); + expect(listboxSections[1]).toBe(getByLabelText('Section 2')); }); describe('async loading', function () { diff --git a/packages/@react-spectrum/menu/docs/MenuTrigger.mdx b/packages/@react-spectrum/menu/docs/MenuTrigger.mdx index ffcaade3523..4641da4f53e 100644 --- a/packages/@react-spectrum/menu/docs/MenuTrigger.mdx +++ b/packages/@react-spectrum/menu/docs/MenuTrigger.mdx @@ -11,7 +11,8 @@ import {Layout} from '@react-spectrum/docs'; export default Layout; import docs from 'docs:@react-spectrum/menu'; -import {HeaderInfo, PropTable, PageDescription} from '@react-spectrum/docs'; +import menuUtil from 'docs:@react-aria/test-utils/src/menu.ts'; +import {HeaderInfo, PropTable, PageDescription, ClassAPI, VersionBadge} from '@react-spectrum/docs'; import packageData from '@react-spectrum/menu/package.json'; import {Keyboard} from '@react-spectrum/text'; @@ -256,3 +257,45 @@ behaviors in your test suite. Please also refer to [React Spectrum's test suite](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-spectrum/menu/test/MenuTrigger.test.js) if you find that the above isn't sufficient when resolving issues in your own test cases. + +### Test utils + +`@react-spectrum/test-utils` offers common menu interaction utilities which you may find helpful when writing tests. See [here](../react-aria/testing.html#react-aria-test-utils) for more information on how to setup these utilities +in your tests. Below is the full definition of the menu tester and a sample of how you could use it in your test suite. + +```ts +// Menu.test.ts +import {render} from '@testing-library/react'; +import {theme} from '@react-spectrum/theme-default'; +import {User} from '@react-spectrum/test-utils'; + +let testUtilUser = new User({interactionType: 'mouse'}); +// ... + +it('Menu can open its submenu via keyboard', async function () { + // Render your test component/app and initialize the menu tester + let {getByTestId} = render( + + + + ... + + + ); + let menuTester = testUtilUser.createTester('Menu', {root: getByTestId('test-menutrigger'), interactionType: 'keyboard'}); + + await menuTester.open(); + expect(menuTester.menu).toBeInTheDocument(); + let submenuTriggers = menuTester.submenuTriggers; + expect(submenuTriggers).toHaveLength(1); + + let submenuTester = await menuTester.openSubmenu({submenuTrigger: 'Share…'}); + expect(submenuTester.menu).toBeInTheDocument(); + + await submenuTester.selectOption({option: submenuTester.options()[0]}); + expect(submenuTester.menu).not.toBeInTheDocument(); + expect(menuTester.menu).not.toBeInTheDocument(); +}); +``` + + diff --git a/packages/@react-spectrum/menu/package.json b/packages/@react-spectrum/menu/package.json index cd8fa1d6cf5..d9453f035bc 100644 --- a/packages/@react-spectrum/menu/package.json +++ b/packages/@react-spectrum/menu/package.json @@ -1,6 +1,6 @@ { "name": "@react-spectrum/menu", - "version": "3.21.0", + "version": "3.21.1", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -36,27 +36,27 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/focus": "^3.19.0", - "@react-aria/i18n": "^3.12.4", - "@react-aria/interactions": "^3.22.5", - "@react-aria/menu": "^3.16.0", - "@react-aria/overlays": "^3.24.0", - "@react-aria/separator": "^3.4.4", - "@react-aria/utils": "^3.26.0", - "@react-spectrum/button": "^3.16.9", - "@react-spectrum/layout": "^3.6.10", - "@react-spectrum/overlays": "^5.7.0", - "@react-spectrum/text": "^3.5.10", - "@react-spectrum/utils": "^3.12.0", - "@react-stately/collections": "^3.12.0", - "@react-stately/menu": "^3.9.0", - "@react-stately/overlays": "^3.6.12", - "@react-stately/tree": "^3.8.6", - "@react-types/menu": "^3.9.13", - "@react-types/overlays": "^3.8.11", - "@react-types/shared": "^3.26.0", - "@spectrum-icons/ui": "^3.6.11", - "@spectrum-icons/workflow": "^4.2.16", + "@react-aria/focus": "^3.19.1", + "@react-aria/i18n": "^3.12.5", + "@react-aria/interactions": "^3.23.0", + "@react-aria/menu": "^3.17.0", + "@react-aria/overlays": "^3.25.0", + "@react-aria/separator": "^3.4.5", + "@react-aria/utils": "^3.27.0", + "@react-spectrum/button": "^3.16.10", + "@react-spectrum/layout": "^3.6.11", + "@react-spectrum/overlays": "^5.7.1", + "@react-spectrum/text": "^3.5.11", + "@react-spectrum/utils": "^3.12.1", + "@react-stately/collections": "^3.12.1", + "@react-stately/menu": "^3.9.1", + "@react-stately/overlays": "^3.6.13", + "@react-stately/tree": "^3.8.7", + "@react-types/menu": "^3.9.14", + "@react-types/overlays": "^3.8.12", + "@react-types/shared": "^3.27.0", + "@spectrum-icons/ui": "^3.6.12", + "@spectrum-icons/workflow": "^4.2.17", "@swc/helpers": "^0.5.0" }, "devDependencies": { diff --git a/packages/@react-spectrum/menu/test/MenuTrigger.test.js b/packages/@react-spectrum/menu/test/MenuTrigger.test.js index c176f504f3a..2bf317064ce 100644 --- a/packages/@react-spectrum/menu/test/MenuTrigger.test.js +++ b/packages/@react-spectrum/menu/test/MenuTrigger.test.js @@ -117,15 +117,15 @@ describe('MenuTrigger', function () { act(() => {jest.runAllTimers();}); let menu = menuTester.menu; - expect(menu).toBeTruthy(); + expect(menu).toBeInTheDocument(); expect(menu).toHaveAttribute('aria-labelledby', triggerButton.id); let menuItem1 = within(menu).getByText('Foo'); let menuItem2 = within(menu).getByText('Bar'); let menuItem3 = within(menu).getByText('Baz'); - expect(menuItem1).toBeTruthy(); - expect(menuItem2).toBeTruthy(); - expect(menuItem3).toBeTruthy(); + expect(menuItem1).toBeInTheDocument(); + expect(menuItem2).toBeInTheDocument(); + expect(menuItem3).toBeInTheDocument(); expect(triggerButton).toHaveAttribute('aria-expanded', 'true'); expect(triggerButton).toHaveAttribute('aria-controls', menu.id); @@ -160,13 +160,13 @@ describe('MenuTrigger', function () { expect(onOpenChange).toBeCalledTimes(0); let menu = tree.getByRole('menu'); - expect(menu).toBeTruthy(); + expect(menu).toBeInTheDocument(); let triggerButton = tree.getByText('Menu Button'); await user.click(triggerButton); act(() => {jest.runAllTimers();}); - expect(menu).toBeTruthy(); + expect(menu).toBeInTheDocument(); expect(onOpenChange).toBeCalledTimes(1); }); @@ -180,7 +180,7 @@ describe('MenuTrigger', function () { expect(onOpenChange).toBeCalledTimes(0); let menu = tree.getByRole('menu'); - expect(menu).toBeTruthy(); + expect(menu).toBeInTheDocument(); let triggerButton = tree.getByText('Menu Button'); await user.click(triggerButton); @@ -202,7 +202,7 @@ describe('MenuTrigger', function () { async function openAndTriggerMenuItem(tree, role, selectionMode, triggerEvent) { let menuTester = testUtilUser.createTester('Menu', {root: tree.container}); await menuTester.open(); - let menuItems = menuTester.options; + let menuItems = menuTester.options(); let itemToAction = menuItems[1]; await triggerEvent(itemToAction); act(() => {jest.runAllTimers();}); // FocusScope useLayoutEffect cleanup @@ -228,7 +228,7 @@ describe('MenuTrigger', function () { expect(onSelect).toBeCalledTimes(0); } - await menuTester.selectOption({optionText: 'Foo', menuSelectionMode: 'single', closesOnSelect: false}); + await menuTester.selectOption({option: 'Foo', menuSelectionMode: 'single', closesOnSelect: false}); if (Component === MenuTrigger) { expect(onSelectionChange).toBeCalledTimes(1); @@ -260,7 +260,7 @@ describe('MenuTrigger', function () { expect(onOpenChange).toBeCalledTimes(1); expect(onSelectionChange).toBeCalledTimes(0); menuTester.setInteractionType('keyboard'); - await menuTester.selectOption({optionText: 'Foo', menuSelectionMode: 'single', closesOnSelect: false}); + await menuTester.selectOption({option: 'Foo', menuSelectionMode: 'single', closesOnSelect: false}); expect(menuTester.menu).toBeInTheDocument(); expect(menuTester.trigger).toHaveAttribute('aria-expanded', 'true'); @@ -345,7 +345,7 @@ describe('MenuTrigger', function () { const getMenuOrThrow = (tree, button) => { try { let menu = tree.getByRole('menu'); - expect(menu).toBeTruthy(); + expect(menu).toBeInTheDocument(); expect(menu).toHaveAttribute('aria-labelledby', button.id); } catch { throw ERROR_MENU_NOT_FOUND; @@ -435,7 +435,7 @@ describe('MenuTrigger', function () { menuItemRole = 'menuitemradio'; } let menu = tree.getByRole('menu'); - expect(menu).toBeTruthy(); + expect(menu).toBeInTheDocument(); let menuItems = within(menu).getAllByRole(menuItemRole); let selectedItem = menuItems[idx < 0 ? menuItems.length + idx : idx]; expect(selectedItem).toBe(document.activeElement); @@ -935,7 +935,7 @@ AriaMenuTests({ SMS - Twitter + X Delete… @@ -1123,4 +1123,3 @@ AriaMenuTests({ ) } }); - diff --git a/packages/@react-spectrum/meter/package.json b/packages/@react-spectrum/meter/package.json index 0ed7066fc34..5483609c006 100644 --- a/packages/@react-spectrum/meter/package.json +++ b/packages/@react-spectrum/meter/package.json @@ -1,6 +1,6 @@ { "name": "@react-spectrum/meter", - "version": "3.5.5", + "version": "3.5.6", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -36,11 +36,11 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/meter": "^3.4.18", - "@react-spectrum/progress": "^3.7.11", - "@react-spectrum/utils": "^3.12.0", - "@react-types/meter": "^3.4.5", - "@react-types/shared": "^3.26.0", + "@react-aria/meter": "^3.4.19", + "@react-spectrum/progress": "^3.7.12", + "@react-spectrum/utils": "^3.12.1", + "@react-types/meter": "^3.4.6", + "@react-types/shared": "^3.27.0", "@swc/helpers": "^0.5.0" }, "devDependencies": { @@ -48,7 +48,8 @@ }, "peerDependencies": { "@react-spectrum/provider": "^3.0.0", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" }, "publishConfig": { "access": "public" diff --git a/packages/@react-spectrum/numberfield/package.json b/packages/@react-spectrum/numberfield/package.json index a4d493e02f7..77fa66f8044 100644 --- a/packages/@react-spectrum/numberfield/package.json +++ b/packages/@react-spectrum/numberfield/package.json @@ -1,6 +1,6 @@ { "name": "@react-spectrum/numberfield", - "version": "3.9.8", + "version": "3.9.9", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -36,22 +36,22 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/button": "^3.11.0", - "@react-aria/focus": "^3.19.0", - "@react-aria/i18n": "^3.12.4", - "@react-aria/interactions": "^3.22.5", - "@react-aria/numberfield": "^3.11.9", - "@react-aria/utils": "^3.26.0", - "@react-spectrum/form": "^3.7.10", - "@react-spectrum/label": "^3.16.10", - "@react-spectrum/textfield": "^3.12.7", - "@react-spectrum/utils": "^3.12.0", - "@react-stately/numberfield": "^3.9.8", - "@react-types/button": "^3.10.1", - "@react-types/numberfield": "^3.8.7", - "@react-types/shared": "^3.26.0", - "@spectrum-icons/ui": "^3.6.11", - "@spectrum-icons/workflow": "^4.2.16", + "@react-aria/button": "^3.11.1", + "@react-aria/focus": "^3.19.1", + "@react-aria/i18n": "^3.12.5", + "@react-aria/interactions": "^3.23.0", + "@react-aria/numberfield": "^3.11.10", + "@react-aria/utils": "^3.27.0", + "@react-spectrum/form": "^3.7.11", + "@react-spectrum/label": "^3.16.11", + "@react-spectrum/textfield": "^3.12.8", + "@react-spectrum/utils": "^3.12.1", + "@react-stately/numberfield": "^3.9.9", + "@react-types/button": "^3.10.2", + "@react-types/numberfield": "^3.8.8", + "@react-types/shared": "^3.27.0", + "@spectrum-icons/ui": "^3.6.12", + "@spectrum-icons/workflow": "^4.2.17", "@swc/helpers": "^0.5.0" }, "devDependencies": { diff --git a/packages/@react-spectrum/overlays/package.json b/packages/@react-spectrum/overlays/package.json index 12c5cefbcfa..5875307ea20 100644 --- a/packages/@react-spectrum/overlays/package.json +++ b/packages/@react-spectrum/overlays/package.json @@ -1,6 +1,6 @@ { "name": "@react-spectrum/overlays", - "version": "5.7.0", + "version": "5.7.1", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -36,13 +36,13 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/interactions": "^3.22.5", - "@react-aria/overlays": "^3.24.0", - "@react-aria/utils": "^3.26.0", - "@react-spectrum/utils": "^3.12.0", - "@react-stately/overlays": "^3.6.12", - "@react-types/overlays": "^3.8.11", - "@react-types/shared": "^3.26.0", + "@react-aria/interactions": "^3.23.0", + "@react-aria/overlays": "^3.25.0", + "@react-aria/utils": "^3.27.0", + "@react-spectrum/utils": "^3.12.1", + "@react-stately/overlays": "^3.6.13", + "@react-types/overlays": "^3.8.12", + "@react-types/shared": "^3.27.0", "@swc/helpers": "^0.5.0", "react-transition-group": "^4.4.5" }, diff --git a/packages/@react-spectrum/picker/docs/Picker.mdx b/packages/@react-spectrum/picker/docs/Picker.mdx index ba1ef556db1..323384fcc5f 100644 --- a/packages/@react-spectrum/picker/docs/Picker.mdx +++ b/packages/@react-spectrum/picker/docs/Picker.mdx @@ -11,7 +11,8 @@ import {Layout} from '@react-spectrum/docs'; export default Layout; import docs from 'docs:@react-spectrum/picker'; -import {HeaderInfo, PropTable, PageDescription} from '@react-spectrum/docs'; +import selectUtil from 'docs:@react-aria/test-utils/src/select.ts'; +import {HeaderInfo, PropTable, PageDescription, ClassAPI, VersionBadge} from '@react-spectrum/docs'; import packageData from '@react-spectrum/picker/package.json'; ```jsx import @@ -588,3 +589,37 @@ for more information on how to handle these behaviors in your test suite. Please also refer to [React Spectrum's test suite](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-spectrum/picker/test/Picker.test.js) if you find that the above isn't sufficient when resolving issues in your own test cases. + +### Test utils + +`@react-spectrum/test-utils` offers common select interaction utilities which you may find helpful when writing tests. See [here](../react-aria/testing.html#react-aria-test-utils) for more information on how to setup these utilities +in your tests. Below is the full definition of the select tester and a sample of how you could use it in your test suite. + +```ts +// Picker.test.ts +import {render} from '@testing-library/react'; +import {theme} from '@react-spectrum/theme-default'; +import {User} from '@react-spectrum/test-utils'; + +let testUtilUser = new User({interactionType: 'mouse'}); +// ... + +it('Picker can select an option via keyboard', async function () { + // Render your test component/app and initialize the select tester + let {getByTestId} = render( + + + ... + + + ); + let selectTester = testUtilUser.createTester('Select', {root: getByTestId('test-select'), interactionType: 'keyboard'}); + let trigger = selectTester.trigger; + expect(trigger).toHaveTextContent('Select…'); + + await selectTester.selectOption({option: 'Cat'}); + expect(trigger).toHaveTextContent('Cat'); +}); +``` + + diff --git a/packages/@react-spectrum/picker/package.json b/packages/@react-spectrum/picker/package.json index 2d5cc416040..9c342e19c66 100644 --- a/packages/@react-spectrum/picker/package.json +++ b/packages/@react-spectrum/picker/package.json @@ -1,6 +1,6 @@ { "name": "@react-spectrum/picker", - "version": "3.15.4", + "version": "3.15.5", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -36,23 +36,23 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/i18n": "^3.12.4", - "@react-aria/interactions": "^3.22.5", - "@react-aria/select": "^3.15.0", - "@react-aria/utils": "^3.26.0", - "@react-spectrum/button": "^3.16.9", - "@react-spectrum/form": "^3.7.10", - "@react-spectrum/label": "^3.16.10", - "@react-spectrum/listbox": "^3.14.0", - "@react-spectrum/overlays": "^5.7.0", - "@react-spectrum/progress": "^3.7.11", - "@react-spectrum/text": "^3.5.10", - "@react-spectrum/utils": "^3.12.0", - "@react-stately/collections": "^3.12.0", - "@react-stately/select": "^3.6.9", - "@react-types/select": "^3.9.8", - "@react-types/shared": "^3.26.0", - "@spectrum-icons/ui": "^3.6.11", + "@react-aria/i18n": "^3.12.5", + "@react-aria/interactions": "^3.23.0", + "@react-aria/select": "^3.15.1", + "@react-aria/utils": "^3.27.0", + "@react-spectrum/button": "^3.16.10", + "@react-spectrum/form": "^3.7.11", + "@react-spectrum/label": "^3.16.11", + "@react-spectrum/listbox": "^3.14.1", + "@react-spectrum/overlays": "^5.7.1", + "@react-spectrum/progress": "^3.7.12", + "@react-spectrum/text": "^3.5.11", + "@react-spectrum/utils": "^3.12.1", + "@react-stately/collections": "^3.12.1", + "@react-stately/select": "^3.6.10", + "@react-types/select": "^3.9.9", + "@react-types/shared": "^3.27.0", + "@spectrum-icons/ui": "^3.6.12", "@swc/helpers": "^0.5.0" }, "devDependencies": { diff --git a/packages/@react-spectrum/picker/test/Picker.test.js b/packages/@react-spectrum/picker/test/Picker.test.js index 24877ec2c0f..36421aaf844 100644 --- a/packages/@react-spectrum/picker/test/Picker.test.js +++ b/packages/@react-spectrum/picker/test/Picker.test.js @@ -100,7 +100,7 @@ describe('Picker', function () { expect(picker).toHaveAttribute('aria-expanded', 'true'); expect(picker).toHaveAttribute('aria-controls', listbox.id); - let items = selectTester.options; + let items = selectTester.options(); expect(items.length).toBe(3); expect(items[0]).toHaveTextContent('One'); expect(items[1]).toHaveTextContent('Two'); @@ -211,7 +211,7 @@ describe('Picker', function () { expect(picker).toHaveAttribute('aria-expanded', 'true'); expect(picker).toHaveAttribute('aria-controls', listbox.id); - let items = selectTester.options; + let items = selectTester.options(); expect(items.length).toBe(3); expect(items[0]).toHaveTextContent('One'); expect(items[1]).toHaveTextContent('Two'); @@ -249,7 +249,7 @@ describe('Picker', function () { expect(picker).toHaveAttribute('aria-expanded', 'true'); expect(picker).toHaveAttribute('aria-controls', listbox.id); - let items = selectTester.options; + let items = selectTester.options(); expect(items.length).toBe(3); expect(items[0]).toHaveTextContent('One'); expect(items[1]).toHaveTextContent('Two'); @@ -979,7 +979,7 @@ describe('Picker', function () { await selectTester.open(); let listbox = selectTester.listbox; - let items = selectTester.options; + let items = selectTester.options(); expect(items.length).toBe(3); expect(items[0]).toHaveTextContent('One'); expect(items[1]).toHaveTextContent('Two'); @@ -987,7 +987,7 @@ describe('Picker', function () { expect(document.activeElement).toBe(listbox); - await selectTester.selectOption({optionText: 'Three'}); + await selectTester.selectOption({option: 'Three'}); expect(onSelectionChange).toHaveBeenCalledTimes(1); expect(onSelectionChange).toHaveBeenLastCalledWith('three'); @@ -1011,7 +1011,7 @@ describe('Picker', function () { await selectTester.open(); let listbox = selectTester.listbox; - let items = selectTester.options; + let items = selectTester.options(); expect(items.length).toBe(3); expect(items[0]).toHaveTextContent('Empty'); expect(items[1]).toHaveTextContent('Zero'); @@ -1019,19 +1019,19 @@ describe('Picker', function () { expect(document.activeElement).toBe(listbox); - await selectTester.selectOption({optionText: 'Empty'}); + await selectTester.selectOption({option: 'Empty'}); expect(onSelectionChange).toHaveBeenCalledTimes(1); expect(onSelectionChange).toHaveBeenLastCalledWith(''); expect(document.activeElement).toBe(picker); expect(picker).toHaveTextContent('Empty'); - await selectTester.selectOption({optionText: 'Zero'}); + await selectTester.selectOption({option: 'Zero'}); expect(onSelectionChange).toHaveBeenCalledTimes(2); expect(onSelectionChange).toHaveBeenLastCalledWith('0'); expect(document.activeElement).toBe(picker); expect(picker).toHaveTextContent('Zero'); - await selectTester.selectOption({optionText: 'False'}); + await selectTester.selectOption({option: 'False'}); expect(onSelectionChange).toHaveBeenCalledTimes(3); expect(onSelectionChange).toHaveBeenLastCalledWith('false'); expect(document.activeElement).toBe(picker); @@ -1102,13 +1102,13 @@ describe('Picker', function () { expect(picker).toHaveTextContent('Select…'); await selectTester.open(); - let items = selectTester.options; + let items = selectTester.options(); expect(items.length).toBe(3); expect(items[0]).toHaveTextContent('One'); expect(items[1]).toHaveTextContent('Two'); expect(items[2]).toHaveTextContent('Three'); - await selectTester.selectOption({optionText: 'Two'}); + await selectTester.selectOption({option: 'Two'}); expect(onSelectionChange).toHaveBeenCalledTimes(1); expect(onSelectionChange).toHaveBeenLastCalledWith('two'); @@ -1184,12 +1184,12 @@ describe('Picker', function () { expect(listbox).toBeVisible(); expect(listbox).toHaveAttribute('aria-labelledby', label.id); - let items = selectTester.options; + let items = selectTester.options(); expect(items[0]).toHaveTextContent('One'); expect(items[1]).toHaveTextContent('Two'); expect(items[2]).toHaveTextContent('Three'); - await selectTester.selectOption({optionText: 'Three'}); + await selectTester.selectOption({option: 'Three'}); expect(onSelectionChange).toHaveBeenCalledTimes(1); expect(onOpenChangeSpy).toHaveBeenCalledTimes(2); @@ -1382,7 +1382,7 @@ describe('Picker', function () { await selectTester.open(); let listbox = selectTester.listbox; - let items = selectTester.options; + let items = selectTester.options(); expect(items.length).toBe(6); let groups = selectTester.sections; @@ -1434,7 +1434,7 @@ describe('Picker', function () { await selectTester.open(); listbox = selectTester.listbox; - items = selectTester.options; + items = selectTester.options(); expect(items.length).toBe(6); expect(document.activeElement).toBe(items[1]); @@ -1543,10 +1543,10 @@ describe('Picker', function () { expect(picker).toHaveTextContent('Two'); await selectTester.open(); - let items = selectTester.options; + let items = selectTester.options(); expect(document.activeElement).toBe(items[1]); - await selectTester.selectOption({optionText: 'Two'}); + await selectTester.selectOption({option: 'Two'}); expect(onSelectionChange).toHaveBeenCalledTimes(1); expect(onSelectionChange).toHaveBeenCalledWith('two'); @@ -2133,7 +2133,7 @@ describe('Picker', function () { expect(document.getElementById(picker.getAttribute('aria-describedby'))).toHaveTextContent('Constraints not satisfied'); expect(document.activeElement).toBe(picker); - await selectTester.selectOption({optionText: 'One'}); + await selectTester.selectOption({option: 'One'}); expect(picker).not.toHaveAttribute('aria-describedby'); }); @@ -2161,7 +2161,7 @@ describe('Picker', function () { expect(document.getElementById(picker.getAttribute('aria-describedby'))).toHaveTextContent('Invalid value'); expect(document.activeElement).toBe(picker); - await selectTester.selectOption({optionText: 'One'}); + await selectTester.selectOption({option: 'One'}); expect(picker).not.toHaveAttribute('aria-describedby'); }); @@ -2201,7 +2201,7 @@ describe('Picker', function () { expect(document.getElementById(picker.getAttribute('aria-describedby'))).toHaveTextContent('Invalid value.'); expect(input.validity.valid).toBe(false); - await selectTester.selectOption({optionText: 'One'}); + await selectTester.selectOption({option: 'One'}); expect(picker).not.toHaveAttribute('aria-describedby'); expect(input.validity.valid).toBe(true); }); @@ -2252,7 +2252,7 @@ describe('Picker', function () { expect(picker).toHaveAttribute('aria-describedby'); expect(document.getElementById(picker.getAttribute('aria-describedby'))).toHaveTextContent('Constraints not satisfied'); - await selectTester.selectOption({optionText: 'One'}); + await selectTester.selectOption({option: 'One'}); expect(picker).not.toHaveAttribute('aria-describedby'); await user.click(getByTestId('reset')); @@ -2280,7 +2280,7 @@ describe('Picker', function () { expect(document.getElementById(picker.getAttribute('aria-describedby'))).toHaveTextContent('Invalid value'); expect(input.validity.valid).toBe(true); - await selectTester.selectOption({optionText: 'One'}); + await selectTester.selectOption({option: 'One'}); expect(picker).not.toHaveAttribute('aria-describedby'); }); @@ -2301,7 +2301,7 @@ describe('Picker', function () { expect(picker).toHaveAttribute('aria-describedby'); expect(document.getElementById(picker.getAttribute('aria-describedby'))).toHaveTextContent('Invalid value'); - await selectTester.selectOption({optionText: 'One'}); + await selectTester.selectOption({option: 'One'}); expect(picker).not.toHaveAttribute('aria-describedby'); }); }); diff --git a/packages/@react-spectrum/picker/test/TempUtilTest.test.js b/packages/@react-spectrum/picker/test/TempUtilTest.test.js index 237409e0a8c..a9fbc25eba4 100644 --- a/packages/@react-spectrum/picker/test/TempUtilTest.test.js +++ b/packages/@react-spectrum/picker/test/TempUtilTest.test.js @@ -100,7 +100,7 @@ describe('Picker/Select ', function () { ); let selectTester = testUtilUser.createTester('Select', {root: screen.getByTestId('test')}); - await selectTester.selectOption({optionText: 'Three'}); + await selectTester.selectOption({option: 'Three'}); expect(selectTester.trigger).toHaveTextContent('Three'); expect(onSelectionChange).toHaveBeenCalledTimes(1); expect(onSelectionChange).toHaveBeenLastCalledWith('three'); @@ -126,7 +126,7 @@ describe('Picker/Select ', function () { ); let selectTester = testUtilUser.createTester('Select', {root: screen.getByTestId('test')}); - await selectTester.selectOption({optionText: 'Cat'}); + await selectTester.selectOption({option: 'Cat'}); expect(selectTester.trigger).toHaveTextContent('Cat'); expect(onSelectionChange).toHaveBeenCalledTimes(1); expect(onSelectionChange).toHaveBeenLastCalledWith('cat'); @@ -205,7 +205,7 @@ describe('Picker/Select ', function () { ); let selectTester = testUtilUser.createTester('Select', {root: screen.getByTestId('test')}); - await selectTester.selectOption({optionText: 'Three'}); + await selectTester.selectOption({option: 'Three'}); expect(selectTester.trigger).toHaveTextContent('Three'); expect(onSelectionChange).toHaveBeenCalledTimes(1); expect(onSelectionChange).toHaveBeenLastCalledWith('three'); @@ -231,7 +231,7 @@ describe('Picker/Select ', function () { ); let selectTester = testUtilUser.createTester('Select', {root: screen.getAllByTestId('test')[0]}); - await selectTester.selectOption({optionText: 'Cat'}); + await selectTester.selectOption({option: 'Cat'}); expect(selectTester.trigger).toHaveTextContent('Cat'); expect(onSelectionChange).toHaveBeenCalledTimes(1); expect(onSelectionChange).toHaveBeenLastCalledWith('cat'); diff --git a/packages/@react-spectrum/progress/package.json b/packages/@react-spectrum/progress/package.json index 343ec10616c..0c5830d4443 100644 --- a/packages/@react-spectrum/progress/package.json +++ b/packages/@react-spectrum/progress/package.json @@ -1,6 +1,6 @@ { "name": "@react-spectrum/progress", - "version": "3.7.11", + "version": "3.7.12", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -36,11 +36,11 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/progress": "^3.4.18", - "@react-aria/utils": "^3.26.0", - "@react-spectrum/utils": "^3.12.0", - "@react-types/progress": "^3.5.8", - "@react-types/shared": "^3.26.0", + "@react-aria/progress": "^3.4.19", + "@react-aria/utils": "^3.27.0", + "@react-spectrum/utils": "^3.12.1", + "@react-types/progress": "^3.5.9", + "@react-types/shared": "^3.27.0", "@swc/helpers": "^0.5.0" }, "devDependencies": { @@ -48,7 +48,8 @@ }, "peerDependencies": { "@react-spectrum/provider": "^3.0.0", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" }, "publishConfig": { "access": "public" diff --git a/packages/@react-spectrum/provider/package.json b/packages/@react-spectrum/provider/package.json index 54f5c558e4a..46d22b6d089 100644 --- a/packages/@react-spectrum/provider/package.json +++ b/packages/@react-spectrum/provider/package.json @@ -1,6 +1,6 @@ { "name": "@react-spectrum/provider", - "version": "3.10.0", + "version": "3.10.1", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -36,12 +36,12 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/i18n": "^3.12.4", - "@react-aria/overlays": "^3.24.0", - "@react-aria/utils": "^3.26.0", - "@react-spectrum/utils": "^3.12.0", - "@react-types/provider": "^3.8.5", - "@react-types/shared": "^3.26.0", + "@react-aria/i18n": "^3.12.5", + "@react-aria/overlays": "^3.25.0", + "@react-aria/utils": "^3.27.0", + "@react-spectrum/utils": "^3.12.1", + "@react-types/provider": "^3.8.6", + "@react-types/shared": "^3.27.0", "@swc/helpers": "^0.5.0", "clsx": "^2.0.0" }, diff --git a/packages/@react-spectrum/radio/package.json b/packages/@react-spectrum/radio/package.json index d5458079d51..5d764078fdc 100644 --- a/packages/@react-spectrum/radio/package.json +++ b/packages/@react-spectrum/radio/package.json @@ -1,6 +1,6 @@ { "name": "@react-spectrum/radio", - "version": "3.7.11", + "version": "3.7.12", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -36,15 +36,15 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/focus": "^3.19.0", - "@react-aria/interactions": "^3.22.5", - "@react-aria/radio": "^3.10.10", - "@react-spectrum/form": "^3.7.10", - "@react-spectrum/label": "^3.16.10", - "@react-spectrum/utils": "^3.12.0", - "@react-stately/radio": "^3.10.9", - "@react-types/radio": "^3.8.5", - "@react-types/shared": "^3.26.0", + "@react-aria/focus": "^3.19.1", + "@react-aria/interactions": "^3.23.0", + "@react-aria/radio": "^3.10.11", + "@react-spectrum/form": "^3.7.11", + "@react-spectrum/label": "^3.16.11", + "@react-spectrum/utils": "^3.12.1", + "@react-stately/radio": "^3.10.10", + "@react-types/radio": "^3.8.6", + "@react-types/shared": "^3.27.0", "@swc/helpers": "^0.5.0" }, "devDependencies": { @@ -52,7 +52,8 @@ }, "peerDependencies": { "@react-spectrum/provider": "^3.0.0", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" }, "publishConfig": { "access": "public" diff --git a/packages/@react-spectrum/s2/chromatic/Tabs.stories.tsx b/packages/@react-spectrum/s2/chromatic/Tabs.stories.tsx index 366f3453d2c..0c69a249419 100644 --- a/packages/@react-spectrum/s2/chromatic/Tabs.stories.tsx +++ b/packages/@react-spectrum/s2/chromatic/Tabs.stories.tsx @@ -16,7 +16,6 @@ import Heart from '../s2wf-icons/S2_Icon_Heart_20_N.svg'; import type {Meta} from '@storybook/react'; import {style} from '../style/spectrum-theme' with { type: 'macro' }; import {Tab, TabList, TabPanel, Tabs} from '../src/Tabs'; -import {Text} from '@react-spectrum/s2'; const meta: Meta = { component: Tabs, @@ -28,72 +27,66 @@ const meta: Meta = { export default meta; -export const Example = { - render: (args: any) => ( - - - Founding of Rome - Monarchy and Republic - Empire - - -
    -

    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum non rutrum augue, a dictum est. Sed ultricies vel orci in blandit. Morbi sed tempor leo. Phasellus et sollicitudin nunc, a volutpat est. In volutpat molestie velit, nec rhoncus felis vulputate porttitor. In efficitur nibh tortor, maximus imperdiet libero sollicitudin sed. Pellentesque dictum, quam id scelerisque rutrum, lorem augue suscipit est, nec ultricies ligula lorem id dui. Cras lacus tortor, fringilla nec ligula quis, semper imperdiet ex.

    -
    -
    - -
    -

    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed ut vulputate justo. Suspendisse potenti. Nunc id fringilla leo, at luctus quam. Maecenas et ipsum nisi. Curabitur in porta purus, a pretium est. Fusce eu urna diam. Sed nunc neque, consectetur ut purus nec, consequat elementum libero. Sed ut diam in quam maximus condimentum at non erat. Vestibulum sagittis rutrum velit, vitae suscipit arcu. Nulla ac feugiat ante, vitae laoreet ligula. Maecenas sed molestie ligula. Nulla sed fringilla ex. Nulla viverra tortor at enim condimentum egestas. Nulla sed tristique sapien. Integer ligula quam, vulputate eget mollis eu, interdum sit amet justo.

    -

    Vivamus dignissim tortor ut sapien congue tristique. Sed ac aliquet mauris. Nulla metus dui, elementum sit amet luctus eu, condimentum id elit. Praesent id nibh sed ligula congue venenatis. Pellentesque urna turpis, eleifend id pellentesque a, auctor nec neque. Vestibulum ipsum mauris, rutrum sit amet magna et, aliquet mollis tellus. Pellentesque nec ultricies nibh, at tempus massa. Phasellus dictum turpis et interdum scelerisque. Aliquam fermentum tincidunt ipsum sit amet suscipit. Fusce non dui sed diam lacinia mattis fermentum eu urna. Cras pretium id nunc in elementum. Mauris laoreet odio vitae laoreet dictum. In non justo nec nunc vehicula posuere non non ligula. Nullam eleifend scelerisque nibh, in sollicitudin tortor ullamcorper vel. Praesent sagittis risus in erat dignissim, non lacinia elit efficitur. Quisque maximus nulla vel luctus pharetra.

    -
    -
    - -
    -

    Alea jacta est.

    -
    -
    -
    - ) -}; +export const Example = (args: any) => ( + + + Founding of Rome + Monarchy and Republic + Empire + + +
    +

    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum non rutrum augue, a dictum est. Sed ultricies vel orci in blandit. Morbi sed tempor leo. Phasellus et sollicitudin nunc, a volutpat est. In volutpat molestie velit, nec rhoncus felis vulputate porttitor. In efficitur nibh tortor, maximus imperdiet libero sollicitudin sed. Pellentesque dictum, quam id scelerisque rutrum, lorem augue suscipit est, nec ultricies ligula lorem id dui. Cras lacus tortor, fringilla nec ligula quis, semper imperdiet ex.

    +
    +
    + +
    +

    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed ut vulputate justo. Suspendisse potenti. Nunc id fringilla leo, at luctus quam. Maecenas et ipsum nisi. Curabitur in porta purus, a pretium est. Fusce eu urna diam. Sed nunc neque, consectetur ut purus nec, consequat elementum libero. Sed ut diam in quam maximus condimentum at non erat. Vestibulum sagittis rutrum velit, vitae suscipit arcu. Nulla ac feugiat ante, vitae laoreet ligula. Maecenas sed molestie ligula. Nulla sed fringilla ex. Nulla viverra tortor at enim condimentum egestas. Nulla sed tristique sapien. Integer ligula quam, vulputate eget mollis eu, interdum sit amet justo.

    +

    Vivamus dignissim tortor ut sapien congue tristique. Sed ac aliquet mauris. Nulla metus dui, elementum sit amet luctus eu, condimentum id elit. Praesent id nibh sed ligula congue venenatis. Pellentesque urna turpis, eleifend id pellentesque a, auctor nec neque. Vestibulum ipsum mauris, rutrum sit amet magna et, aliquet mollis tellus. Pellentesque nec ultricies nibh, at tempus massa. Phasellus dictum turpis et interdum scelerisque. Aliquam fermentum tincidunt ipsum sit amet suscipit. Fusce non dui sed diam lacinia mattis fermentum eu urna. Cras pretium id nunc in elementum. Mauris laoreet odio vitae laoreet dictum. In non justo nec nunc vehicula posuere non non ligula. Nullam eleifend scelerisque nibh, in sollicitudin tortor ullamcorper vel. Praesent sagittis risus in erat dignissim, non lacinia elit efficitur. Quisque maximus nulla vel luctus pharetra.

    +
    +
    + +
    +

    Alea jacta est.

    +
    +
    +
    +); -export const Disabled = { - render: (args: any) => ( - - - Edit - Notifications - Likes - - - Arma virumque cano, Troiae qui primus ab oris. - - - Senatus Populusque Romanus. - - - Alea jacta est. - - - ) -}; +export const Disabled = (args: any) => ( + + + Founding of Rome + Monarchy and Republic + Empire + + + Arma virumque cano, Troiae qui primus ab oris. + + + Senatus Populusque Romanus. + + + Alea jacta est. + + +); -export const Icons = { - render: (args: any) => ( - - - Edit - Notifications - Likes - - - Arma virumque cano, Troiae qui primus ab oris. - - - Senatus Populusque Romanus. - - - Alea jacta est. - - - ) -}; +export const Icons = (args: any) => ( + + + + + + + + Arma virumque cano, Troiae qui primus ab oris. + + + Senatus Populusque Romanus. + + + Alea jacta est. + + +); diff --git a/packages/@react-spectrum/s2/intl/en-US.json b/packages/@react-spectrum/s2/intl/en-US.json index 90828c535bb..38217e1a3cc 100644 --- a/packages/@react-spectrum/s2/intl/en-US.json +++ b/packages/@react-spectrum/s2/intl/en-US.json @@ -25,7 +25,6 @@ "table.sortAscending": "Sort Ascending", "table.sortDescending": "Sort Descending", "table.resizeColumn": "Resize column", - "tabs.selectorLabel": "Tab selector", "tag.showAllButtonLabel": "Show all ({tagCount, number})", "tag.hideButtonLabel": "Show less", "tag.actions": "Actions", diff --git a/packages/@react-spectrum/s2/intl/he-IL.json b/packages/@react-spectrum/s2/intl/he-IL.json index edaeb149912..b433b945340 100644 --- a/packages/@react-spectrum/s2/intl/he-IL.json +++ b/packages/@react-spectrum/s2/intl/he-IL.json @@ -25,7 +25,6 @@ "table.resizeColumn": "שנה את גודל העמודה", "table.sortAscending": "מיין בסדר עולה", "table.sortDescending": "מיין בסדר יורד", - "tabs.selectorLabel": "Tab selector", "tag.actions": "פעולות", "tag.hideButtonLabel": "הצג פחות", "tag.noTags": "ללא", diff --git a/packages/@react-spectrum/s2/package.json b/packages/@react-spectrum/s2/package.json index 14e014f3407..71f65011433 100644 --- a/packages/@react-spectrum/s2/package.json +++ b/packages/@react-spectrum/s2/package.json @@ -1,6 +1,6 @@ { "name": "@react-spectrum/s2", - "version": "0.5.0", + "version": "0.6.0", "description": "Spectrum 2 UI components in React", "license": "Apache-2.0", "repository": { @@ -122,33 +122,34 @@ "devDependencies": { "@adobe/spectrum-tokens": "^13.0.0-beta.53", "@parcel/macros": "^2.13.0", - "@react-aria/test-utils": "1.0.0-alpha.1" + "@react-aria/test-utils": "1.0.0-alpha.3", + "@testing-library/dom": "^10.1.0", + "@testing-library/react": "^15.0.7", + "@testing-library/user-event": "^14.0.0", + "jest": "^29.5.0" }, "dependencies": { - "@react-aria/collections": "3.0.0-alpha.6", - "@react-aria/i18n": "^3.12.4", - "@react-aria/interactions": "^3.22.5", + "@react-aria/collections": "3.0.0-alpha.7", + "@react-aria/i18n": "^3.12.5", + "@react-aria/interactions": "^3.23.0", "@react-aria/live-announcer": "^3.4.1", - "@react-aria/utils": "^3.26.0", - "@react-spectrum/utils": "^3.12.0", - "@react-stately/layout": "^4.1.0", + "@react-aria/utils": "^3.27.0", + "@react-spectrum/utils": "^3.12.1", + "@react-stately/layout": "^4.1.1", "@react-stately/utils": "^3.10.5", - "@react-stately/virtualizer": "^4.2.0", - "@react-types/color": "^3.0.1", - "@react-types/dialog": "^3.5.14", - "@react-types/grid": "^3.2.10", - "@react-types/provider": "^3.8.5", - "@react-types/shared": "^3.26.0", - "@react-types/table": "^3.10.3", - "@react-types/textfield": "^3.10.0", + "@react-stately/virtualizer": "^4.2.1", + "@react-types/color": "^3.0.2", + "@react-types/dialog": "^3.5.15", + "@react-types/grid": "^3.2.11", + "@react-types/provider": "^3.8.6", + "@react-types/shared": "^3.27.0", + "@react-types/table": "^3.10.4", + "@react-types/textfield": "^3.11.0", "csstype": "^3.0.2", - "react-aria": "^3.36.0", - "react-aria-components": "^1.5.0" + "react-aria": "^3.37.0", + "react-aria-components": "^1.6.0" }, "peerDependencies": { - "@testing-library/react": "^15.0.7", - "@testing-library/user-event": "^13.0.0 || ^14.0.0", - "jest": "^29.5.0", "react": "^18.0.0 || ^19.0.0-rc.1", "react-dom": "^18.0.0 || ^19.0.0-rc.1" }, diff --git a/packages/@react-spectrum/s2/src/ActionBar.tsx b/packages/@react-spectrum/s2/src/ActionBar.tsx index 441ed0cc027..c74c0ef7e75 100644 --- a/packages/@react-spectrum/s2/src/ActionBar.tsx +++ b/packages/@react-spectrum/s2/src/ActionBar.tsx @@ -19,24 +19,13 @@ import {DOMRef, DOMRefValue, Key} from '@react-types/shared'; import {FocusScope, useKeyboard} from 'react-aria'; // @ts-ignore import intlMessages from '../intl/*.json'; -import {keyframes} from '../style/style-macro' with {type: 'macro'}; import {style} from '../style' with {type: 'macro'}; import {useControlledState} from '@react-stately/utils'; import {useDOMRef} from '@react-spectrum/utils'; -import {useExitAnimation, useResizeObserver} from '@react-aria/utils'; +import {useEnterAnimation, useExitAnimation, useObjectRef, useResizeObserver} from '@react-aria/utils'; import {useLocalizedStringFormatter} from '@react-aria/i18n'; import {useSpectrumContextProps} from './useSpectrumContextProps'; -const slideIn = keyframes(` - from { transform: translateY(100%); opacity: 0 } - to { transform: translateY(0px); opacity: 1 } -`); - -const slideOut = keyframes(` - from { transform: translateY(0px); opacity: 1 } - to { transform: translateY(100%); opacity: 0 } -`); - const actionBarStyles = style({ borderRadius: 'lg', '--s2-container-bg': { @@ -65,7 +54,7 @@ const actionBarStyles = style({ position: { isInContainer: 'absolute' }, - bottom: 8, + bottom: 0, insetStart: 8, '--insetEnd': { type: 'insetEnd', @@ -77,11 +66,13 @@ const actionBarStyles = style({ }, marginX: 'auto', maxWidth: 960, - animation: { - isInContainer: slideIn, - isExiting: slideOut - }, - animationDuration: 200 + transition: 'transform', + transitionDuration: 200, + translateY: { + default: -8, + isEntering: 'full', + isExiting: 'full' + } }); export interface ActionBarProps extends SlotProps { @@ -158,12 +149,15 @@ const ActionBarInner = forwardRef(function ActionBarInner(props: ActionBarProps } }, [stringFormatter, scrollRef]); + let objectRef = useObjectRef(ref); + let isEntering = useEnterAnimation(objectRef, !!scrollRef); + return (
    - - {typeof props.children === 'string' ? {props.children} : props.children} - {isPending && -
    - (<> + {variant === 'genai' || variant === 'premium' + ? ( + + ) + : null} + + {typeof props.children === 'string' ? {props.children} : props.children} + {isPending && +
    + -
    - } -
    + })({size})} /> +
    + } +
    + )} ); }); diff --git a/packages/@react-spectrum/s2/src/Card.tsx b/packages/@react-spectrum/s2/src/Card.tsx index 48502a6729d..f4ea01def3a 100644 --- a/packages/@react-spectrum/s2/src/Card.tsx +++ b/packages/@react-spectrum/s2/src/Card.tsx @@ -20,7 +20,7 @@ import {ContentContext, FooterContext, TextContext} from './Content'; import {createContext, CSSProperties, forwardRef, ReactNode, useContext} from 'react'; import {DividerContext} from './Divider'; import {DOMProps, DOMRef, DOMRefValue} from '@react-types/shared'; -import {filterDOMProps} from '@react-aria/utils'; +import {filterDOMProps, inertValue} from '@react-aria/utils'; import {focusRing, lightDark, space, style} from '../style' with {type: 'macro'}; import {getAllowedOverrides, StyleProps, UnsafeStyles} from './style-utils' with {type: 'macro'}; import {IllustrationContext} from './Icon'; @@ -421,7 +421,7 @@ export const Card = forwardRef(function Card(props: CardProps, ref: DOMRef diff --git a/packages/@react-spectrum/s2/src/CardView.tsx b/packages/@react-spectrum/s2/src/CardView.tsx index 28745d3ae4f..8d275df67ec 100644 --- a/packages/@react-spectrum/s2/src/CardView.tsx +++ b/packages/@react-spectrum/s2/src/CardView.tsx @@ -62,6 +62,7 @@ export interface CardViewProps extends Omit, 'layout' | 'key onLoadMore?: () => void, /** Spectrum-defined styles, returned by the `style()` macro. */ styles?: StylesPropWithHeight, + /** Provides the ActionBar to render when cards are selected in the CardView. */ renderActionBar?: (selectedKeys: 'all' | Set) => ReactElement } diff --git a/packages/@react-spectrum/s2/src/CloseButton.tsx b/packages/@react-spectrum/s2/src/CloseButton.tsx index f251f506fca..a16e06c9982 100644 --- a/packages/@react-spectrum/s2/src/CloseButton.tsx +++ b/packages/@react-spectrum/s2/src/CloseButton.tsx @@ -65,6 +65,10 @@ const styles = style({ isStaticColor: { default: baseColor('transparent-overlay-800'), isDisabled: 'transparent-overlay-400' + }, + forcedColors: { + default: 'ButtonText', + isDisabled: 'GrayText' } } }, diff --git a/packages/@react-spectrum/s2/src/ComboBox.tsx b/packages/@react-spectrum/s2/src/ComboBox.tsx index dbf48ea20d2..58468d2265f 100644 --- a/packages/@react-spectrum/s2/src/ComboBox.tsx +++ b/packages/@react-spectrum/s2/src/ComboBox.tsx @@ -379,11 +379,12 @@ export function ComboBoxItem(props: ComboBoxItemProps) { export interface ComboBoxSectionProps extends SectionProps {} export function ComboBoxSection(props: ComboBoxSectionProps) { + let {size} = useContext(InternalComboboxContext); return ( <> + className={section({size})}> {props.children} diff --git a/packages/@react-spectrum/s2/src/Content.tsx b/packages/@react-spectrum/s2/src/Content.tsx index 5b0276e4ea6..5f15f593b95 100644 --- a/packages/@react-spectrum/s2/src/Content.tsx +++ b/packages/@react-spectrum/s2/src/Content.tsx @@ -13,6 +13,7 @@ import {ContextValue, Keyboard as KeyboardAria, Header as RACHeader, Heading as RACHeading, TextContext as RACTextContext, SlotProps, Text as TextAria} from 'react-aria-components'; import {createContext, forwardRef, ReactNode, useContext} from 'react'; import {DOMRef, DOMRefValue} from '@react-types/shared'; +import {inertValue} from '@react-aria/utils'; import {StyleString} from '../style/types'; import {UnsafeStyles} from './style-utils'; import {useDOMRef} from '@react-spectrum/utils'; @@ -107,7 +108,7 @@ export const Text = forwardRef(function Text(props: ContentProps, ref: DOMRef) { {...otherProps} ref={domRef} // @ts-ignore - compatibility with React < 19 - inert={isSkeleton ? 'true' : undefined} + inert={inertValue(isSkeleton)} className={UNSAFE_className + styles} style={UNSAFE_style} slot={slot || undefined} diff --git a/packages/@react-spectrum/s2/src/Menu.tsx b/packages/@react-spectrum/s2/src/Menu.tsx index 1013a202c72..2b7b608602e 100644 --- a/packages/@react-spectrum/s2/src/Menu.tsx +++ b/packages/@react-spectrum/s2/src/Menu.tsx @@ -81,22 +81,26 @@ export interface MenuProps extends Omit, 'children' | 'style /** * The contents of the collection. */ - children?: ReactNode | ((item: T) => ReactNode) + children?: ReactNode | ((item: T) => ReactNode), + /** Hides the default link out icons on menu items that open links in a new tab. */ + hideLinkOutIcon?: boolean } export const MenuContext = createContext, DOMRefValue>>(null); +const menuItemGrid = { + size: { + S: [edgeToText(24), 'auto', 'auto', 'minmax(0, 1fr)', 'auto', 'auto', 'auto', edgeToText(24)], + M: [edgeToText(32), 'auto', 'auto', 'minmax(0, 1fr)', 'auto', 'auto', 'auto', edgeToText(32)], + L: [edgeToText(40), 'auto', 'auto', 'minmax(0, 1fr)', 'auto', 'auto', 'auto', edgeToText(40)], + XL: [edgeToText(48), 'auto', 'auto', 'minmax(0, 1fr)', 'auto', 'auto', 'auto', edgeToText(48)] + } +} as const; + export let menu = style({ outlineStyle: 'none', display: 'grid', - gridTemplateColumns: { - size: { - S: [edgeToText(24), 'auto', 'auto', 'minmax(0, 1fr)', 'auto', 'auto', 'auto', edgeToText(24)], - M: [edgeToText(32), 'auto', 'auto', 'minmax(0, 1fr)', 'auto', 'auto', 'auto', edgeToText(32)], - L: [edgeToText(40), 'auto', 'auto', 'minmax(0, 1fr)', 'auto', 'auto', 'auto', edgeToText(40)], - XL: [edgeToText(48), 'auto', 'auto', 'minmax(0, 1fr)', 'auto', 'auto', 'auto', edgeToText(48)] - } - }, + gridTemplateColumns: menuItemGrid, boxSizing: 'border-box', maxHeight: '[inherit]', overflow: { @@ -121,7 +125,7 @@ export let section = style({ '. checkmark icon label value keyboard descriptor .', '. . . description . . . .' ], - gridTemplateColumns: 'subgrid' + gridTemplateColumns: menuItemGrid }); export let sectionHeader = style<{size?: 'S' | 'M' | 'L' | 'XL'}>({ @@ -310,7 +314,12 @@ let descriptor = style({ } }); -let InternalMenuContext = createContext<{size: 'S' | 'M' | 'L' | 'XL', isSubmenu: boolean}>({size: 'M', isSubmenu: false}); +let InternalMenuContext = createContext<{size: 'S' | 'M' | 'L' | 'XL', isSubmenu: boolean, hideLinkOutIcon: boolean}>({ + size: 'M', + isSubmenu: false, + hideLinkOutIcon: false +}); + let InternalMenuTriggerContext = createContext | null>(null); /** @@ -324,7 +333,8 @@ export const Menu = /*#__PURE__*/ (forwardRef as forwardRefType)(function Menu + extends AriaMenuSectionProps {} export function MenuSection(props: MenuSectionProps) { // remember, context doesn't work if it's around Section nor inside + let {size} = useContext(InternalMenuContext); return ( <> + className={section({size})}> {props.children} @@ -454,7 +465,7 @@ export function MenuItem(props: MenuItemProps) { let ref = useRef(null); let isLink = props.href != null; let isLinkOut = isLink && props.target === '_blank'; - let {size} = useContext(InternalMenuContext); + let {size, hideLinkOutIcon} = useContext(InternalMenuContext); let textValue = props.textValue || (typeof props.children === 'string' ? props.children : undefined); let {direction} = useLocale(); return ( @@ -494,13 +505,25 @@ export function MenuItem(props: MenuItemProps) {
    )} {typeof children === 'string' ? {children} : children} - {isLinkOut && } + {isLinkOut && !hideLinkOutIcon && ( +
    + +
    + )} {renderProps.hasSubmenu && (
    extends SectionProps {} export function PickerSection(props: PickerSectionProps) { + let {size} = useContext(InternalPickerContext); return ( <> + className={section({size})}> {props.children} diff --git a/packages/@react-spectrum/s2/src/Popover.tsx b/packages/@react-spectrum/s2/src/Popover.tsx index b0a7f4b71de..5738e0d30b6 100644 --- a/packages/@react-spectrum/s2/src/Popover.tsx +++ b/packages/@react-spectrum/s2/src/Popover.tsx @@ -24,7 +24,6 @@ import {colorScheme, getAllowedOverrides, StyleProps, UnsafeStyles} from './styl import {ColorSchemeContext} from './Provider'; import {DOMRef} from '@react-types/shared'; import {forwardRef, MutableRefObject, useCallback, useContext} from 'react'; -import {keyframes} from '../style/style-macro' with {type: 'macro'}; import {mergeStyles} from '../style/runtime'; import {style} from '../style' with {type: 'macro'}; import {StyleString} from '../style/types' with {type: 'macro'}; @@ -46,52 +45,6 @@ export interface PopoverProps extends UnsafeStyles, Omit any, + /** Provides the ActionBar to display when rows are selected in the TableView. */ renderActionBar?: (selectedKeys: 'all' | Set) => ReactElement } diff --git a/packages/@react-spectrum/s2/src/Tabs.tsx b/packages/@react-spectrum/s2/src/Tabs.tsx index 486ded1fdce..ac8c776c881 100644 --- a/packages/@react-spectrum/s2/src/Tabs.tsx +++ b/packages/@react-spectrum/s2/src/Tabs.tsx @@ -11,35 +11,29 @@ */ import { - TabListProps as AriaTabListProps, - TabPanel as AriaTabPanel, - TabPanelProps as AriaTabPanelProps, - TabProps as AriaTabProps, - TabsProps as AriaTabsProps, - CollectionRenderer, - ContextValue, - Provider, - Tab as RACTab, - TabList as RACTabList, - Tabs as RACTabs, - TabListStateContext, - UNSTABLE_CollectionRendererContext, - UNSTABLE_DefaultCollectionRenderer -} from 'react-aria-components'; + TabListProps as AriaTabListProps, + TabPanel as AriaTabPanel, + TabPanelProps as AriaTabPanelProps, + TabProps as AriaTabProps, + TabsProps as AriaTabsProps, + ContextValue, + Provider, + Tab as RACTab, + TabList as RACTabList, + Tabs as RACTabs, + TabListStateContext, + useSlottedContext + } from 'react-aria-components'; import {centerBaseline} from './CenterBaseline'; -import {Collection, DOMRef, DOMRefValue, FocusableRef, FocusableRefValue, Key, Node, Orientation, RefObject} from '@react-types/shared'; -import {createContext, forwardRef, Fragment, ReactNode, useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react'; -import {focusRing, size, style} from '../style' with {type: 'macro'}; +import {Collection, DOMRef, DOMRefValue, Key, Node, Orientation} from '@react-types/shared'; +import {createContext, forwardRef, ReactNode, useCallback, useContext, useEffect, useRef, useState} from 'react'; +import {focusRing, style} from '../style' with {type: 'macro'}; import {getAllowedOverrides, StyleProps, StylesPropWithHeight, UnsafeStyles} from './style-utils' with {type: 'macro'}; import {IconContext} from './Icon'; -// @ts-ignore -import intlMessages from '../intl/*.json'; -import {Picker, PickerItem} from './TabsPicker'; import {Text, TextContext} from './Content'; -import {useControlledState} from '@react-stately/utils'; import {useDOMRef} from '@react-spectrum/utils'; -import {useEffectEvent, useLayoutEffect, useResizeObserver} from '@react-aria/utils'; -import {useLocale, useLocalizedStringFormatter} from '@react-aria/i18n'; +import {useLayoutEffect} from '@react-aria/utils'; +import {useLocale} from '@react-aria/i18n'; import {useSpectrumContextProps} from './useSpectrumContextProps'; export interface TabsProps extends Omit, UnsafeStyles { @@ -51,19 +45,18 @@ export interface TabsProps extends Omit, StyleProps { /** The content to display in the tab. */ - children: ReactNode + children?: ReactNode } -export interface TabListProps extends Omit, 'style' | 'className'>, StyleProps {} +export interface TabListProps extends Omit, 'children' | 'style' | 'className'>, StyleProps { + /** The content to display in the tablist. */ + children?: ReactNode +} export interface TabPanelProps extends Omit, UnsafeStyles { /** Spectrum-defined styles, returned by the `style()` macro. */ @@ -73,64 +66,82 @@ export interface TabPanelProps extends Omit>>(null); -const InternalTabsContext = createContext void, pickerRef?: FocusableRef}>({onFocus: () => {}}); -const tabs = style({ +const tabPanel = style({ + marginTop: 4, + color: 'gray-800', + flexGrow: 1, + flexBasis: '[0%]', + minHeight: 0, + minWidth: 0 +}, getAllowedOverrides({height: true})); + +export function TabPanel(props: TabPanelProps) { + return ( + + ); +} + +const tab = style({ + ...focusRing(), display: 'flex', - flexShrink: 0, - font: 'ui', - flexDirection: { - orientation: { - horizontal: 'column' + color: { + default: 'neutral-subdued', + isSelected: 'neutral', + isHovered: 'neutral-subdued', + isDisabled: 'disabled', + forcedColors: { + isSelected: 'Highlight', + isDisabled: 'GrayText' + } + }, + borderRadius: 'sm', + gap: 'text-to-visual', + height: { + density: { + compact: 32, + regular: 48 } + }, + alignItems: 'center', + position: 'relative', + cursor: 'default', + flexShrink: 0, + transition: 'default' +}, getAllowedOverrides()); + +const icon = style({ + flexShrink: 0, + '--iconPrimary': { + type: 'fill', + value: 'currentColor' } -}, getAllowedOverrides({height: true})); +}); -/** - * Tabs organize content into multiple sections and allow users to navigate between them. The content under the set of tabs should be related and form a coherent unit. - */ -export const Tabs = forwardRef(function Tabs(props: TabsProps, ref: DOMRef) { - [props, ref] = useSpectrumContextProps(props, ref, TabsContext); - let { - density = 'regular', - isDisabled, - disabledKeys, - orientation = 'horizontal', - isIconOnly = false - } = props; - let domRef = useDOMRef(ref); - let [value, setValue] = useControlledState(props.selectedKey, props.defaultSelectedKey ?? null!, props.onSelectionChange); - let pickerRef = useRef>(null); +export function Tab(props: TabProps) { + let {density} = useSlottedContext(TabsContext) ?? {}; return ( - pickerRef.current?.focus(), - pickerRef - }] - ]}> - - (props.UNSAFE_className || '') + tabs({...renderProps}, props.styles)}> - {props.children} - - - + (props.UNSAFE_className || '') + tab({...renderProps, density}, props.styles)}> + + {typeof props.children === 'string' ? {props.children} : props.children} + + ); -}); +} const tablist = style({ display: 'flex', @@ -140,12 +151,6 @@ const tablist = style({ density: { compact: 24, regular: 32 - }, - isIconOnly: { - density: { - compact: 16, - regular: 24 - } } } } @@ -170,58 +175,63 @@ const tablist = style({ }); export function TabList(props: TabListProps) { - let {density, isDisabled, disabledKeys, orientation, isIconOnly, onFocus} = useContext(InternalTabsContext) ?? {}; - let {showItems} = useContext(CollapseContext) ?? {}; + let {density, isDisabled, disabledKeys, orientation} = useSlottedContext(TabsContext) ?? {}; let state = useContext(TabListStateContext); let [selectedTab, setSelectedTab] = useState(undefined); let tablistRef = useRef(null); useLayoutEffect(() => { - if (tablistRef?.current && showItems) { + if (tablistRef?.current) { let tab: HTMLElement | null = tablistRef.current.querySelector('[role=tab][data-selected=true]'); if (tab != null) { setSelectedTab(tab); } - } else if (tablistRef?.current) { - let picker: HTMLElement | null = tablistRef.current.querySelector('button'); - if (picker != null) { - setSelectedTab(picker); - } - } - }, [tablistRef, state?.selectedItem?.key, showItems]); - - let prevFocused = useRef(false); - useLayoutEffect(() => { - if (!showItems && !prevFocused.current && state?.selectionManager.isFocused) { - onFocus(); } - prevFocused.current = state?.selectionManager.isFocused; - }, [state?.selectionManager.isFocused, state?.selectionManager.focusedKey, showItems]); + }, [tablistRef, state?.selectedItem?.key]); return (
    - {showItems && orientation === 'vertical' && + {orientation === 'vertical' && } tablist({...renderProps, isIconOnly, density})} /> + className={renderProps => tablist({...renderProps, density})} /> {orientation === 'horizontal' && - } + }
    ); } +function isAllTabsDisabled(collection: Collection> | null, disabledKeys: Set) { + let testKey: Key | null = null; + if (collection && collection.size > 0) { + testKey = collection.getFirstKey(); + + let index = 0; + while (testKey && index < collection.size) { + // We have to check if the item in the collection has a key in disabledKeys or has the isDisabled prop set directly on it + if (!disabledKeys.has(testKey) && !collection.getItem(testKey)?.props?.isDisabled) { + return false; + } + + testKey = collection.getKeyAfter(testKey); + index++; + } + return true; + } + return false; +} + interface TabLineProps { disabledKeys: Iterable | undefined, isDisabled: boolean | undefined, selectedTab: HTMLElement | undefined, orientation?: Orientation, - density?: 'compact' | 'regular', - showItems?: boolean + density?: 'compact' | 'regular' } const selectedIndicator = style({ @@ -255,7 +265,7 @@ const selectedIndicator = style({ transitionTimingFunction: 'in-out' }); -function TabLine(props: TabLineProps & {isIconOnly?: boolean}) { +function TabLine(props: TabLineProps) { let { disabledKeys, isDisabled: isTabsDisabled, @@ -266,9 +276,12 @@ function TabLine(props: TabLineProps & {isIconOnly?: boolean}) { let {direction} = useLocale(); let state = useContext(TabListStateContext); - let isDisabled = useMemo(() => { - return isTabsDisabled || isAllTabsDisabled(state?.collection, disabledKeys ? new Set(disabledKeys) : new Set()); - }, [state?.collection, disabledKeys, isTabsDisabled]); + // We want to add disabled styling to the selection indicator only if all the Tabs are disabled + let [isDisabled, setIsDisabled] = useState(false); + useEffect(() => { + let isDisabled = isTabsDisabled || isAllTabsDisabled(state?.collection || null, disabledKeys ? new Set(disabledKeys) : new Set(null)); + setIsDisabled(isDisabled); + }, [state?.collection, disabledKeys, isTabsDisabled, setIsDisabled]); let [style, setStyle] = useState<{transform: string | undefined, width: string | undefined, height: string | undefined}>({ transform: undefined, @@ -301,326 +314,50 @@ function TabLine(props: TabLineProps & {isIconOnly?: boolean}) { useLayoutEffect(() => { onResize(); - }, [onResize, state?.selectedItem?.key, density]); - - let ref = useRef(selectedTab); - // assign ref before the useResizeObserver useEffect runs - useLayoutEffect(() => { - ref.current = selectedTab; - }); - useResizeObserver({ref, onResize}); + }, [onResize, state?.selectedItem?.key, direction, orientation, density]); return (
    ); } -const tab = style({ - ...focusRing(), +const tabs = style({ display: 'flex', - color: { - default: 'neutral-subdued', - isSelected: 'neutral', - isHovered: 'neutral-subdued', - isDisabled: 'disabled', - forcedColors: { - isSelected: 'Highlight', - isDisabled: 'GrayText' - } - }, - borderRadius: 'sm', - gap: 'text-to-visual', - height: { - density: { - compact: 32, - regular: 48 - } - }, - alignItems: 'center', - position: 'relative', - cursor: 'default', - flexShrink: 0, - transition: 'default', - paddingX: { - isIconOnly: size(6) - } -}, getAllowedOverrides()); - -const icon = style({ - display: 'block', flexShrink: 0, - '--iconPrimary': { - type: 'fill', - value: 'currentColor' + fontFamily: 'sans', + fontWeight: 'normal', + flexDirection: { + orientation: { + horizontal: 'column' + } } -}); - -export function Tab(props: TabProps) { - let {density, isIconOnly} = useContext(InternalTabsContext) ?? {}; - - return ( - (props.UNSAFE_className || '') + tab({...renderProps, density, isIconOnly}, props.styles)}> - {({ - // @ts-ignore - isMenu - }) => { - if (isMenu) { - return props.children; - } else { - return ( - - {typeof props.children === 'string' ? {props.children} : props.children} - - ); - } - }} - - ); -} - -const tabPanel = style({ - marginTop: 4, - color: 'gray-800', - flexGrow: 1, - flexBasis: '[0%]', - minHeight: 0, - minWidth: 0 }, getAllowedOverrides({height: true})); -export function TabPanel(props: TabPanelProps) { +/** + * Tabs organize content into multiple sections and allow users to navigate between them. The content under the set of tabs should be related and form a coherent unit. + */ +export const Tabs = forwardRef(function Tabs(props: TabsProps, ref: DOMRef) { + [props, ref] = useSpectrumContextProps(props, ref, TabsContext); + let { + density = 'regular', + isDisabled, + disabledKeys, + orientation = 'horizontal' + } = props; + let domRef = useDOMRef(ref); + return ( - - ); -} - -function isAllTabsDisabled(collection: Collection> | undefined, disabledKeys: Set) { - let testKey: Key | null = null; - if (collection && collection.size > 0) { - testKey = collection.getFirstKey(); - - let index = 0; - while (testKey && index < collection.size) { - // We have to check if the item in the collection has a key in disabledKeys or has the isDisabled prop set directly on it - if (!disabledKeys.has(testKey) && !collection.getItem(testKey)?.props?.isDisabled) { - return false; - } - - testKey = collection.getKeyAfter(testKey); - index++; - } - return true; - } - return false; -} - -let HiddenTabs = function (props: { - listRef: RefObject, - items: Array>, - size?: string, - density?: 'compact' | 'regular' -}) { - let {listRef, items, size, density} = props; - - return ( -
    - {items.map((item) => { - // pull off individual props as an allow list, don't want refs or other props getting through - return ( -
    - {item.props.children({size, density})} -
    - ); - })} -
    + className={renderProps => (props.UNSAFE_className || '') + tabs({...renderProps}, props.styles)}> + + {props.children} + + ); -}; - -let TabsMenu = (props: {items: Array>, onSelectionChange: TabsProps['onSelectionChange']}) => { - let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-spectrum/s2'); - let {items} = props; - let {density, onSelectionChange, selectedKey, isDisabled, disabledKeys, pickerRef, isIconOnly} = useContext(InternalTabsContext); - let state = useContext(TabListStateContext); - let allKeysDisabled = useMemo(() => { - return isAllTabsDisabled(state?.collection, disabledKeys ? new Set(disabledKeys) : new Set()); - }, [state?.collection, disabledKeys]); - - return ( - -
    - - {(item: Node) => { - // need to determine the best way to handle icon only -> icon and text - // good enough to aria-label the picker item? - return ( - - {item.props.children({density, isMenu: true})} - - ); - }} - -
    -
    - ); -}; - -// Context for passing the count for the custom renderer -let CollapseContext = createContext<{ - containerRef: RefObject, - showItems: boolean, - setShowItems:(value: boolean) => void -} | null>(null); - -function CollapsingCollection({children, containerRef}) { - let [showItems, _setShowItems] = useState(true); - let {orientation} = useContext(InternalTabsContext); - let setShowItems = useCallback((value: boolean) => { - if (orientation === 'vertical') { - // if orientation is vertical, we always show the items - _setShowItems(true); - } else { - _setShowItems(value); - } - }, [orientation]); - let ctx = useMemo(() => ({ - containerRef, - showItems: orientation === 'vertical' ? true : showItems, - setShowItems - }), [containerRef, showItems, setShowItems]); - return ( - - - {children} - - - ); -} - -let CollapsingCollectionRenderer: CollectionRenderer = { - CollectionRoot({collection}) { - return useCollectionRender(collection); - }, - CollectionBranch({collection}) { - return useCollectionRender(collection); - } -}; - - -let useCollectionRender = (collection: Collection>) => { - let {containerRef, showItems, setShowItems} = useContext(CollapseContext) ?? {}; - let {density = 'regular', orientation = 'horizontal', onSelectionChange} = useContext(InternalTabsContext); - let {direction} = useLocale(); - - let children = useMemo(() => { - let result: Node[] = []; - for (let key of collection.getKeys()) { - result.push(collection.getItem(key)!); - } - return result; - }, [collection]); - - let listRef = useRef(null); - let updateOverflow = useEffectEvent(() => { - if (orientation === 'vertical' || !listRef.current || !containerRef?.current) { - return; - } - let container = listRef.current; - let containerRect = container.getBoundingClientRect(); - let tabs = container.querySelectorAll('[data-hidden-tab]'); - let lastTab = tabs[tabs.length - 1]; - let lastTabRect = lastTab.getBoundingClientRect(); - if (direction === 'ltr') { - setShowItems?.(lastTabRect.right <= containerRect.right); - } else { - setShowItems?.(lastTabRect.left >= containerRect.left); - } - }); - - useResizeObserver({ref: containerRef, onResize: updateOverflow}); - - useLayoutEffect(() => { - if (collection.size > 0) { - queueMicrotask(updateOverflow); - } - }, [collection.size, updateOverflow]); - - useEffect(() => { - // Recalculate visible tags when fonts are loaded. - document.fonts?.ready.then(() => updateOverflow()); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - return ( - <> - - {showItems ? ( - children.map(node => {node.render?.(node)}) - ) : ( - <> - - - )} - - ); -}; +}); diff --git a/packages/@react-spectrum/s2/src/TabsPicker.tsx b/packages/@react-spectrum/s2/src/TabsPicker.tsx deleted file mode 100644 index c66c8c264e0..00000000000 --- a/packages/@react-spectrum/s2/src/TabsPicker.tsx +++ /dev/null @@ -1,336 +0,0 @@ -/* - * Copyright 2024 Adobe. All rights reserved. - * This file is licensed to you under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. You may obtain a copy - * of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under - * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS - * OF ANY KIND, either express or implied. See the License for the specific language - * governing permissions and limitations under the License. - */ - -import { - PopoverProps as AriaPopoverProps, - Select as AriaSelect, - SelectProps as AriaSelectProps, - Button, - ContextValue, - DEFAULT_SLOT, - ListBox, - ListBoxItem, - ListBoxItemProps, - ListBoxProps, - Provider, - SelectValue -} from 'react-aria-components'; -import {centerBaseline} from './CenterBaseline'; -import { - checkmark, - description, - icon, - label, - menuitem, - sectionHeader, - sectionHeading -} from './Menu'; -import CheckmarkIcon from '../ui-icons/Checkmark'; -import ChevronIcon from '../ui-icons/Chevron'; -import {edgeToText, focusRing, size, style} from '../style' with {type: 'macro'}; -import {fieldInput, StyleProps} from './style-utils' with {type: 'macro'}; -import { - FieldLabel -} from './Field'; -import {FocusableRef, FocusableRefValue, SpectrumLabelableProps} from '@react-types/shared'; -import {forwardRefType} from './types'; -import {HeaderContext, HeadingContext, Text, TextContext} from './Content'; -import {IconContext} from './Icon'; -// @ts-ignore -import intlMessages from '../intl/*.json'; -import {Placement} from 'react-aria'; -import {PopoverBase} from './Popover'; -import {pressScale} from './pressScale'; -import {raw} from '../style/style-macro' with {type: 'macro'}; -import React, {createContext, forwardRef, ReactNode, useContext, useRef} from 'react'; -import {useFocusableRef} from '@react-spectrum/utils'; -import {useFormProps} from './Form'; -import {useLocalizedStringFormatter} from '@react-aria/i18n'; -import {useSpectrumContextProps} from './useSpectrumContextProps'; - - -export interface PickerStyleProps { -} - -export interface PickerProps extends - Omit, 'children' | 'style' | 'className'>, - PickerStyleProps, - StyleProps, - SpectrumLabelableProps, - Pick, 'items'>, - Pick { - /** The contents of the collection. */ - children: ReactNode | ((item: T) => ReactNode), - /** - * Direction the menu will render relative to the Picker. - * - * @default 'bottom' - */ - direction?: 'bottom' | 'top', - /** - * Alignment of the menu relative to the input target. - * - * @default 'start' - */ - align?: 'start' | 'end', - /** Width of the menu. By default, matches width of the trigger. Note that the minimum width of the dropdown is always equal to the trigger's width. */ - menuWidth?: number, - /** Density of the tabs, affects the height of the picker. */ - density: 'compact' | 'regular', - /** - * If the tab picker should only display icon and no text for the button label. - */ - isIconOnly?: boolean -} - -export const PickerContext = createContext>, FocusableRefValue>>(null); - -const inputButton = style({ - ...focusRing(), - ...fieldInput(), - outlineStyle: { - default: 'none', - isFocusVisible: 'solid' - }, - position: 'relative', - font: 'ui', - display: 'flex', - textAlign: 'start', - borderStyle: 'none', - borderRadius: 'sm', - alignItems: 'center', - transition: 'default', - columnGap: 'text-to-visual', - paddingX: 0, - backgroundColor: 'transparent', - color: { - default: 'neutral', - isDisabled: 'disabled' - }, - maxWidth: { - isQuiet: 'max' - }, - disableTapHighlight: true, - height: { - default: 48, - density: { - compact: 32 - } - }, - boxSizing: 'border-box' -}); - -export let menu = style({ - outlineStyle: 'none', - display: 'grid', - gridTemplateColumns: [edgeToText(32), 'auto', 'auto', 'minmax(0, 1fr)', 'auto', 'auto', 'auto', edgeToText(32)], - boxSizing: 'border-box', - maxHeight: '[inherit]', - overflow: 'auto', - padding: 8, - fontFamily: 'sans', - fontSize: 'control' -}); - -const valueStyles = style({ - flexGrow: 0, - truncate: true, - display: 'flex', - alignItems: 'center', - height: 'full' -}); - -const iconStyles = style({ - flexShrink: 0, - rotate: 90, - '--iconPrimary': { - type: 'fill', - value: 'currentColor' - } -}); - -const iconCenterWrapper = style({ - display: 'flex', - gridArea: 'icon', - paddingStart: { - isIconOnly: size(6) - } -}); - -let InsideSelectValueContext = createContext(false); - -function Picker(props: PickerProps, ref: FocusableRef) { - let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-spectrum/s2'); - [props, ref] = useSpectrumContextProps(props, ref, PickerContext); - let domRef = useFocusableRef(ref); - props = useFormProps(props); - let { - direction = 'bottom', - align = 'start', - shouldFlip = true, - children, - items, - placeholder = stringFormatter.format('picker.placeholder'), - density, - isIconOnly, - ...pickerProps - } = props; - let isQuiet = true; - - const menuOffset: number = 6; - const size = 'M'; - - return ( - - {({isOpen}) => ( - <> - - - - - - {children} - - - - - )} - - ); -} - -/** - * Pickers allow users to choose a single option from a collapsible list of options when space is limited. - */ -let _Picker = /*#__PURE__*/ (forwardRef as forwardRefType)(Picker); -export {_Picker as Picker}; - -export interface PickerItemProps extends Omit, StyleProps { - children: ReactNode -} - -export function PickerItem(props: PickerItemProps) { - let ref = useRef(null); - let isLink = props.href != null; - const size = 'M'; - return ( - (props.UNSAFE_className || '') + menuitem({...renderProps, size, isLink}, props.styles)}> - {(renderProps) => { - let {children} = props; - return ( - - - {!isLink && } - {typeof children === 'string' ? {children} : children} - - - ); - }} - - ); -} - -// A Context.Provider that only sets a value if not inside SelectValue. -function DefaultProvider({context, value, children}: {context: React.Context, value: any, children: any}) { - let inSelectValue = useContext(InsideSelectValueContext); - if (inSelectValue) { - return children; - } - - return {children}; -} diff --git a/packages/@react-spectrum/s2/src/TagGroup.tsx b/packages/@react-spectrum/s2/src/TagGroup.tsx index 7f77665ffd8..1da73bd45b7 100644 --- a/packages/@react-spectrum/s2/src/TagGroup.tsx +++ b/packages/@react-spectrum/s2/src/TagGroup.tsx @@ -40,12 +40,12 @@ import {FormContext, useFormProps} from './Form'; import {forwardRefType} from './types'; import {IconContext} from './Icon'; import {ImageContext} from './Image'; +import {inertValue, useEffectEvent, useId, useLayoutEffect, useResizeObserver} from '@react-aria/utils'; // @ts-ignore import intlMessages from '../intl/*.json'; import {pressScale} from './pressScale'; import {Text, TextContext} from './Content'; import {useDOMRef} from '@react-spectrum/utils'; -import {useEffectEvent, useId, useLayoutEffect, useResizeObserver} from '@react-aria/utils'; import {useLocalizedStringFormatter} from '@react-aria/i18n'; import {useSpectrumContextProps} from './useSpectrumContextProps'; @@ -319,7 +319,7 @@ function TagGroupInner({ {maxRows != null && (
    ({ ...colorScheme(), justifyContent: 'center', @@ -82,42 +69,38 @@ const tooltip = style { return (
    - Global unsafe does not apply - @layer UNSAFE_overrides works + UNSAFE_className works
    ); }, diff --git a/packages/@react-spectrum/s2/stories/Tabs.stories.tsx b/packages/@react-spectrum/s2/stories/Tabs.stories.tsx index fa8d585bf5f..688f4cb3842 100644 --- a/packages/@react-spectrum/s2/stories/Tabs.stories.tsx +++ b/packages/@react-spectrum/s2/stories/Tabs.stories.tsx @@ -11,7 +11,6 @@ */ import Bell from '../s2wf-icons/S2_Icon_Bell_20_N.svg'; -import {Collection, Text} from '@react-spectrum/s2'; import Edit from '../s2wf-icons/S2_Icon_Edit_20_N.svg'; import Heart from '../s2wf-icons/S2_Icon_Heart_20_N.svg'; import type {Meta} from '@storybook/react'; @@ -30,103 +29,65 @@ const meta: Meta = { export default meta; export const Example = (args: any) => ( -
    - - - Founding of Rome - Monarchy and Republic - Empire - - -
    -

    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum non rutrum augue, a dictum est. Sed ultricies vel orci in blandit. Morbi sed tempor leo. Phasellus et sollicitudin nunc, a volutpat est. In volutpat molestie velit, nec rhoncus felis vulputate porttitor. In efficitur nibh tortor, maximus imperdiet libero sollicitudin sed. Pellentesque dictum, quam id scelerisque rutrum, lorem augue suscipit est, nec ultricies ligula lorem id dui. Cras lacus tortor, fringilla nec ligula quis, semper imperdiet ex.

    -
    -
    - -
    -

    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed ut vulputate justo. Suspendisse potenti. Nunc id fringilla leo, at luctus quam. Maecenas et ipsum nisi. Curabitur in porta purus, a pretium est. Fusce eu urna diam. Sed nunc neque, consectetur ut purus nec, consequat elementum libero. Sed ut diam in quam maximus condimentum at non erat. Vestibulum sagittis rutrum velit, vitae suscipit arcu. Nulla ac feugiat ante, vitae laoreet ligula. Maecenas sed molestie ligula. Nulla sed fringilla ex. Nulla viverra tortor at enim condimentum egestas. Nulla sed tristique sapien. Integer ligula quam, vulputate eget mollis eu, interdum sit amet justo.

    -

    Vivamus dignissim tortor ut sapien congue tristique. Sed ac aliquet mauris. Nulla metus dui, elementum sit amet luctus eu, condimentum id elit. Praesent id nibh sed ligula congue venenatis. Pellentesque urna turpis, eleifend id pellentesque a, auctor nec neque. Vestibulum ipsum mauris, rutrum sit amet magna et, aliquet mollis tellus. Pellentesque nec ultricies nibh, at tempus massa. Phasellus dictum turpis et interdum scelerisque. Aliquam fermentum tincidunt ipsum sit amet suscipit. Fusce non dui sed diam lacinia mattis fermentum eu urna. Cras pretium id nunc in elementum. Mauris laoreet odio vitae laoreet dictum. In non justo nec nunc vehicula posuere non non ligula. Nullam eleifend scelerisque nibh, in sollicitudin tortor ullamcorper vel. Praesent sagittis risus in erat dignissim, non lacinia elit efficitur. Quisque maximus nulla vel luctus pharetra.

    -
    -
    - -
    -

    Alea jacta est.

    -
    -
    -
    -
    + + + Founding of Rome + Monarchy and Republic + Empire + + +
    +

    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum non rutrum augue, a dictum est. Sed ultricies vel orci in blandit. Morbi sed tempor leo. Phasellus et sollicitudin nunc, a volutpat est. In volutpat molestie velit, nec rhoncus felis vulputate porttitor. In efficitur nibh tortor, maximus imperdiet libero sollicitudin sed. Pellentesque dictum, quam id scelerisque rutrum, lorem augue suscipit est, nec ultricies ligula lorem id dui. Cras lacus tortor, fringilla nec ligula quis, semper imperdiet ex.

    +
    +
    + +
    +

    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed ut vulputate justo. Suspendisse potenti. Nunc id fringilla leo, at luctus quam. Maecenas et ipsum nisi. Curabitur in porta purus, a pretium est. Fusce eu urna diam. Sed nunc neque, consectetur ut purus nec, consequat elementum libero. Sed ut diam in quam maximus condimentum at non erat. Vestibulum sagittis rutrum velit, vitae suscipit arcu. Nulla ac feugiat ante, vitae laoreet ligula. Maecenas sed molestie ligula. Nulla sed fringilla ex. Nulla viverra tortor at enim condimentum egestas. Nulla sed tristique sapien. Integer ligula quam, vulputate eget mollis eu, interdum sit amet justo.

    +

    Vivamus dignissim tortor ut sapien congue tristique. Sed ac aliquet mauris. Nulla metus dui, elementum sit amet luctus eu, condimentum id elit. Praesent id nibh sed ligula congue venenatis. Pellentesque urna turpis, eleifend id pellentesque a, auctor nec neque. Vestibulum ipsum mauris, rutrum sit amet magna et, aliquet mollis tellus. Pellentesque nec ultricies nibh, at tempus massa. Phasellus dictum turpis et interdum scelerisque. Aliquam fermentum tincidunt ipsum sit amet suscipit. Fusce non dui sed diam lacinia mattis fermentum eu urna. Cras pretium id nunc in elementum. Mauris laoreet odio vitae laoreet dictum. In non justo nec nunc vehicula posuere non non ligula. Nullam eleifend scelerisque nibh, in sollicitudin tortor ullamcorper vel. Praesent sagittis risus in erat dignissim, non lacinia elit efficitur. Quisque maximus nulla vel luctus pharetra.

    +
    +
    + +
    +

    Alea jacta est.

    +
    +
    +
    ); export const Disabled = (args: any) => ( -
    - - - Founding of Rome - Monarchy and Republic - Empire - - - Arma virumque cano, Troiae qui primus ab oris. - - - Senatus Populusque Romanus. - - - Alea jacta est. - - -
    + + + Founding of Rome + Monarchy and Republic + Empire + + + Arma virumque cano, Troiae qui primus ab oris. + + + Senatus Populusque Romanus. + + + Alea jacta est. + + ); -const IconsRender = (props) => ( -
    - - - Founding of Rome - Monarchy and Republic - Empire - - - Arma virumque cano, Troiae qui primus ab oris. - - - Senatus Populusque Romanus. - - - Alea jacta est. - - -
    -); - -export const Icons = { - render: (args) => -}; - -interface Item { - id: number, - title: string, - description: string -} -let items: Item[] = [ - {id: 1, title: 'Mouse settings', description: 'Adjust the sensitivity and speed of your mouse.'}, - {id: 2, title: 'Keyboard settings', description: 'Customize the layout and function of your keyboard.'}, - {id: 3, title: 'Gamepad settings', description: 'Configure the buttons and triggers on your gamepad.'} -]; - -export const Dynamic = (args: any) => ( -
    - - - {item => {item.title}} - - - {item => ( - - {item.description} - - )} - - -
    +export const Icons = (args: any) => ( + + + + + + + + Arma virumque cano, Troiae qui primus ab oris. + + + Senatus Populusque Romanus. + + + Alea jacta est. + + ); diff --git a/packages/@react-spectrum/s2/stories/unsafe.css b/packages/@react-spectrum/s2/stories/unsafe.css index c7673082a12..7e17db078cc 100644 --- a/packages/@react-spectrum/s2/stories/unsafe.css +++ b/packages/@react-spectrum/s2/stories/unsafe.css @@ -10,19 +10,7 @@ * governing permissions and limitations under the License. */ -button { - /* This should not apply */ - background: red; -} - -html body .unsafe1 { - /* This should not apply */ - background: red; -} - -@layer UNSAFE_overrides { - .unsafe2 { - /* This one should work */ - background: hotpink; - } +.unsafe2 { + /* This one should work */ + background: hotpink; } diff --git a/packages/@react-spectrum/s2/style/__tests__/style-macro.test.js b/packages/@react-spectrum/s2/style/__tests__/style-macro.test.js index e3541d10c02..3762768c1b6 100644 --- a/packages/@react-spectrum/s2/style/__tests__/style-macro.test.js +++ b/packages/@react-spectrum/s2/style/__tests__/style-macro.test.js @@ -37,9 +37,7 @@ describe('style-macro', () => { }); expect(css).toMatchInlineSnapshot(` - ".\\.:not(#a#b) { all: revert-layer } - - @layer _.a, _.b, _.c, UNSAFE_overrides; + "@layer _.a, _.b, _.c; @layer _.b { .A-13alit4c { @@ -61,7 +59,7 @@ describe('style-macro', () => { " `); - expect(js).toMatchInlineSnapshot('" . A-13alit4c A-13alit4ed"'); + expect(js).toMatchInlineSnapshot('" A-13alit4c A-13alit4ed"'); }); it('should support self references', () => { @@ -72,9 +70,7 @@ describe('style-macro', () => { }); expect(css).toMatchInlineSnapshot(` - ".\\.:not(#a#b) { all: revert-layer } - - @layer _.a, _.b, UNSAFE_overrides; + "@layer _.a, _.b; @layer _.a { .uc { diff --git a/packages/@react-spectrum/s2/style/spectrum-theme.ts b/packages/@react-spectrum/s2/style/spectrum-theme.ts index e80897b4f91..616a3a97754 100644 --- a/packages/@react-spectrum/s2/style/spectrum-theme.ts +++ b/packages/@react-spectrum/s2/style/spectrum-theme.ts @@ -10,7 +10,7 @@ * governing permissions and limitations under the License. */ -import {ArbitraryValue, CSSValue, PropertyValueMap} from './types'; +import {ArbitraryValue, CSSProperties, CSSValue, PropertyValueMap} from './types'; import {autoStaticColor, colorScale, colorToken, fontSizeToken, generateOverlayColorScale, getToken, simpleColorScale, weirdColorToken} from './tokens' with {type: 'macro'}; import {Color, createArbitraryProperty, createColorProperty, createMappedProperty, createRenamedProperty, createSizingProperty, createTheme, parseArbitraryValue} from './style-macro'; import type * as CSS from 'csstype'; @@ -111,8 +111,35 @@ export function colorMix(a: SpectrumColor, b: SpectrumColor, percent: number): ` return `[color-mix(in srgb, ${parseColor(a)}, ${parseColor(b)} ${percent}%)]`; } -export function linearGradient(angle: string, ...tokens: [SpectrumColor, number][]): string { - return `linear-gradient(${angle}, ${tokens.map(([color, stop]) => `${parseColor(color)} ${stop}%`)})`; +interface LinearGradient { + type: 'linear-gradient', + angle: string, + stops: [SpectrumColor, number][] +} + +export function linearGradient(this: MacroContext | void, angle: string, ...tokens: [SpectrumColor, number][]): [LinearGradient] { + // Generate @property rules for each gradient stop color. This allows the gradient to be animated. + let propertyDefinitions: string[] = []; + for (let i = 0; i < tokens.length; i++) { + propertyDefinitions.push(`@property --g${i} { + syntax: ''; + initial-value: #0000; + inherits: false; +}`); + } + + if (this && typeof this.addAsset === 'function') { + this.addAsset({ + type: 'css', + content: propertyDefinitions.join('\n\n') + }); + } + + return [{ + type: 'linear-gradient', + angle, + stops: tokens + }]; } function generateSpacing(px: K): {[P in K[number]]: string} { @@ -320,8 +347,10 @@ let gridTrackSize = (value: GridTrackSize) => { }; const transitionProperty = { - default: 'color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, translate, scale, rotate, filter, backdrop-filter', - colors: 'color, background-color, border-color, text-decoration-color, fill, stroke', + // var(--gp) is generated by the backgroundImage property when setting a gradient. + // It includes a list of all of the custom properties used for each color stop. + default: 'color, background-color, var(--gp), border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, translate, scale, rotate, filter, backdrop-filter', + colors: 'color, background-color, var(--gp), border-color, text-decoration-color, fill, stroke', opacity: 'opacity', shadow: 'box-shadow', transform: 'transform, translate, scale, rotate', @@ -673,7 +702,14 @@ export const style = createTheme({ translate: 'var(--translateX, 0) var(--translateY, 0)' }), translate), rotate: createArbitraryProperty((value: number | `${number}deg` | `${number}rad` | `${number}grad` | `${number}turn`, property) => ({[property]: typeof value === 'number' ? `${value}deg` : value})), - scale: createArbitraryProperty(), + scaleX: createArbitraryProperty(value => ({ + '--scaleX': value, + scale: 'var(--scaleX, 1) var(--scaleY, 1)' + })), + scaleY: createArbitraryProperty(value => ({ + '--scaleY': value, + scale: 'var(--scaleX, 1) var(--scaleY, 1)' + })), transform: createArbitraryProperty(), position: ['absolute', 'fixed', 'relative', 'sticky', 'static'] as const, insetStart: createRenamedProperty('insetInlineStart', inset), @@ -795,7 +831,29 @@ export const style = createTheme({ borderBottomEndRadius: createRenamedProperty('borderEndEndRadius', radius), forcedColorAdjust: ['auto', 'none'] as const, colorScheme: ['light', 'dark', 'light dark'] as const, - backgroundImage: createArbitraryProperty(), + backgroundImage: createArbitraryProperty((value, property) => { + if (typeof value === 'string') { + return {[property]: value}; + } else if (Array.isArray(value) && value[0]?.type === 'linear-gradient') { + let values: CSSProperties = { + [property]: `linear-gradient(${value[0].angle}, ${value[0].stops.map(([, stop], i) => `var(--g${i}) ${stop}%`)})` + }; + + // Create a CSS var for each color stop so the gradient can be transitioned. + // These are registered via @property in the `linearGradient` macro. + let properties: string[] = []; + value[0].stops.forEach(([color], i) => { + properties.push(`--g${i}`); + values[`--g${i}`] = parseColor(color); + }); + + // This is used by transition-property so we automatically transition all of the color stops. + values['--gp'] = properties.join(', '); + return values; + } else { + throw new Error('Unexpected backgroundImage value: ' + JSON.stringify(value)); + } + }), // TODO: do we need separate x and y properties? backgroundPosition: ['bottom', 'center', 'left', 'left bottom', 'left top', 'right', 'right bottom', 'right top', 'top'] as const, backgroundSize: ['auto', 'cover', 'contain'] as const, @@ -934,6 +992,7 @@ export const style = createTheme({ borderStartRadius: ['borderTopStartRadius', 'borderBottomStartRadius'] as const, borderEndRadius: ['borderTopEndRadius', 'borderBottomEndRadius'] as const, translate: ['translateX', 'translateY'] as const, + scale: ['scaleX', 'scaleY'] as const, inset: ['top', 'bottom', 'insetStart', 'insetEnd'] as const, insetX: ['insetStart', 'insetEnd'] as const, insetY: ['top', 'bottom'] as const, diff --git a/packages/@react-spectrum/s2/style/style-macro.ts b/packages/@react-spectrum/s2/style/style-macro.ts index 54f2f7d49f6..4fa4a8d8b16 100644 --- a/packages/@react-spectrum/s2/style/style-macro.ts +++ b/packages/@react-spectrum/s2/style/style-macro.ts @@ -12,10 +12,10 @@ import type {Condition, CSSProperties, CSSValue, CustomValue, PropertyFunction, PropertyValueDefinition, PropertyValueMap, RenderProps, ShorthandProperty, StyleFunction, StyleValue, Theme, ThemeProperties, Value} from './types'; -let defaultArbitraryProperty = (value: T, property: string) => ({[property]: value} as CSSProperties); -export function createArbitraryProperty(fn: (value: T, property: string) => CSSProperties = defaultArbitraryProperty): PropertyFunction { +let defaultArbitraryProperty = (value: T, property: string) => ({[property]: value} as CSSProperties); +export function createArbitraryProperty(fn: (value: T, property: string) => CSSProperties = defaultArbitraryProperty): PropertyFunction { return (value, property) => { - let selector = Array.isArray(value) ? generateArbitraryValueSelector(value.map(v => String(v)).join('')) : generateArbitraryValueSelector(String(value)); + let selector = Array.isArray(value) ? generateArbitraryValueSelector(value.map(v => JSON.stringify(v)).join('')) : generateArbitraryValueSelector(JSON.stringify(value)); return {default: [fn(value, property), selector]}; }; } @@ -222,7 +222,7 @@ export function createTheme(theme: T): StyleFunction(theme: T): StyleFunction(theme: T): StyleFunction(); for (let [property, propertyRules] of rules) { if (isStatic) { @@ -644,6 +644,12 @@ export function raw(this: MacroContext | void, css: string, layer = '_.a') { ${css} } }`; + + // Ensure layer is always declared after the _ layer used by style macro. + if (!layer.startsWith('_.')) { + css = `@layer _, ${layer};\n` + css; + } + if (this && typeof this.addAsset === 'function') { this.addAsset({ type: 'css', diff --git a/packages/@react-spectrum/s2/style/types.ts b/packages/@react-spectrum/s2/style/types.ts index b78196826eb..e66bbb4bab5 100644 --- a/packages/@react-spectrum/s2/style/types.ts +++ b/packages/@react-spectrum/s2/style/types.ts @@ -25,7 +25,7 @@ export type CSSProperties = CSS.Properties & { [k: CustomProperty]: CSSValue }; -export type PropertyFunction = (value: T, property: string) => PropertyValueDefinition<[CSSProperties, string]>; +export type PropertyFunction = (value: T, property: string) => PropertyValueDefinition<[CSSProperties, string]>; export type ShorthandProperty = (value: T) => {[name: string]: Value}; diff --git a/packages/@react-spectrum/s2/test/Menu.test.tsx b/packages/@react-spectrum/s2/test/Menu.test.tsx index 0007251e7e0..32bfae1ee75 100644 --- a/packages/@react-spectrum/s2/test/Menu.test.tsx +++ b/packages/@react-spectrum/s2/test/Menu.test.tsx @@ -118,7 +118,7 @@ AriaMenuTests({ SMS - Twitter + X diff --git a/packages/@react-spectrum/searchfield/package.json b/packages/@react-spectrum/searchfield/package.json index 405b79d2aab..2e335aa1791 100644 --- a/packages/@react-spectrum/searchfield/package.json +++ b/packages/@react-spectrum/searchfield/package.json @@ -1,6 +1,6 @@ { "name": "@react-spectrum/searchfield", - "version": "3.8.11", + "version": "3.8.12", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -36,15 +36,15 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/searchfield": "^3.7.11", - "@react-spectrum/button": "^3.16.9", - "@react-spectrum/form": "^3.7.10", - "@react-spectrum/textfield": "^3.12.7", - "@react-spectrum/utils": "^3.12.0", - "@react-stately/searchfield": "^3.5.8", - "@react-types/searchfield": "^3.5.10", - "@react-types/textfield": "^3.10.0", - "@spectrum-icons/ui": "^3.6.11", + "@react-aria/searchfield": "^3.8.0", + "@react-spectrum/button": "^3.16.10", + "@react-spectrum/form": "^3.7.11", + "@react-spectrum/textfield": "^3.12.8", + "@react-spectrum/utils": "^3.12.1", + "@react-stately/searchfield": "^3.5.9", + "@react-types/searchfield": "^3.5.11", + "@react-types/textfield": "^3.11.0", + "@spectrum-icons/ui": "^3.6.12", "@swc/helpers": "^0.5.0" }, "devDependencies": { @@ -53,7 +53,8 @@ }, "peerDependencies": { "@react-spectrum/provider": "^3.0.0", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" }, "publishConfig": { "access": "public" diff --git a/packages/@react-spectrum/searchwithin/package.json b/packages/@react-spectrum/searchwithin/package.json index bf25f3bbf6a..031e7bebde2 100644 --- a/packages/@react-spectrum/searchwithin/package.json +++ b/packages/@react-spectrum/searchwithin/package.json @@ -53,7 +53,8 @@ }, "peerDependencies": { "@react-spectrum/provider": "^3.0.0-rc.1", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" }, "publishConfig": { "access": "public" diff --git a/packages/@react-spectrum/slider/package.json b/packages/@react-spectrum/slider/package.json index 53241d30640..f5e7a7b9b3f 100644 --- a/packages/@react-spectrum/slider/package.json +++ b/packages/@react-spectrum/slider/package.json @@ -1,6 +1,6 @@ { "name": "@react-spectrum/slider", - "version": "3.7.0", + "version": "3.7.1", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -36,16 +36,16 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/focus": "^3.19.0", - "@react-aria/i18n": "^3.12.4", - "@react-aria/interactions": "^3.22.5", - "@react-aria/slider": "^3.7.14", - "@react-aria/utils": "^3.26.0", - "@react-aria/visually-hidden": "^3.8.18", - "@react-spectrum/utils": "^3.12.0", - "@react-stately/slider": "^3.6.0", - "@react-types/shared": "^3.26.0", - "@react-types/slider": "^3.7.7", + "@react-aria/focus": "^3.19.1", + "@react-aria/i18n": "^3.12.5", + "@react-aria/interactions": "^3.23.0", + "@react-aria/slider": "^3.7.15", + "@react-aria/utils": "^3.27.0", + "@react-aria/visually-hidden": "^3.8.19", + "@react-spectrum/utils": "^3.12.1", + "@react-stately/slider": "^3.6.1", + "@react-types/shared": "^3.27.0", + "@react-types/slider": "^3.7.8", "@swc/helpers": "^0.5.0" }, "devDependencies": { @@ -53,7 +53,8 @@ }, "peerDependencies": { "@react-spectrum/provider": "^3.0.0", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" }, "publishConfig": { "access": "public" diff --git a/packages/@react-spectrum/statuslight/package.json b/packages/@react-spectrum/statuslight/package.json index 7a5148853c1..21b3a63d328 100644 --- a/packages/@react-spectrum/statuslight/package.json +++ b/packages/@react-spectrum/statuslight/package.json @@ -1,6 +1,6 @@ { "name": "@react-spectrum/statuslight", - "version": "3.5.17", + "version": "3.5.18", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -36,10 +36,10 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/utils": "^3.26.0", - "@react-spectrum/utils": "^3.12.0", - "@react-types/shared": "^3.26.0", - "@react-types/statuslight": "^3.3.13", + "@react-aria/utils": "^3.27.0", + "@react-spectrum/utils": "^3.12.1", + "@react-types/shared": "^3.27.0", + "@react-types/statuslight": "^3.3.14", "@swc/helpers": "^0.5.0" }, "devDependencies": { @@ -47,7 +47,8 @@ }, "peerDependencies": { "@react-spectrum/provider": "^3.0.0", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" }, "publishConfig": { "access": "public" diff --git a/packages/@react-spectrum/steplist/package.json b/packages/@react-spectrum/steplist/package.json index 2d25a083852..b86270b8b65 100644 --- a/packages/@react-spectrum/steplist/package.json +++ b/packages/@react-spectrum/steplist/package.json @@ -1,6 +1,6 @@ { "name": "@react-spectrum/steplist", - "version": "3.0.0-alpha.10", + "version": "3.0.0-alpha.11", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -36,17 +36,17 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/focus": "^3.19.0", - "@react-aria/i18n": "^3.12.4", - "@react-aria/interactions": "^3.22.5", - "@react-aria/steplist": "3.0.0-alpha.12", - "@react-aria/utils": "^3.26.0", - "@react-aria/visually-hidden": "^3.8.18", - "@react-spectrum/utils": "^3.12.0", - "@react-stately/collections": "^3.12.0", - "@react-stately/steplist": "3.0.0-alpha.10", - "@react-types/shared": "^3.26.0", - "@spectrum-icons/ui": "^3.6.11", + "@react-aria/focus": "^3.19.1", + "@react-aria/i18n": "^3.12.5", + "@react-aria/interactions": "^3.23.0", + "@react-aria/steplist": "3.0.0-alpha.13", + "@react-aria/utils": "^3.27.0", + "@react-aria/visually-hidden": "^3.8.19", + "@react-spectrum/utils": "^3.12.1", + "@react-stately/collections": "^3.12.1", + "@react-stately/steplist": "3.0.0-alpha.11", + "@react-types/shared": "^3.27.0", + "@spectrum-icons/ui": "^3.6.12", "@swc/helpers": "^0.5.0" }, "devDependencies": { diff --git a/packages/@react-spectrum/switch/package.json b/packages/@react-spectrum/switch/package.json index f11bfd31c51..735cb17064b 100644 --- a/packages/@react-spectrum/switch/package.json +++ b/packages/@react-spectrum/switch/package.json @@ -1,6 +1,6 @@ { "name": "@react-spectrum/switch", - "version": "3.5.10", + "version": "3.5.11", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -36,13 +36,13 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/focus": "^3.19.0", - "@react-aria/interactions": "^3.22.5", - "@react-aria/switch": "^3.6.10", - "@react-spectrum/utils": "^3.12.0", - "@react-stately/toggle": "^3.8.0", - "@react-types/shared": "^3.26.0", - "@react-types/switch": "^3.5.7", + "@react-aria/focus": "^3.19.1", + "@react-aria/interactions": "^3.23.0", + "@react-aria/switch": "^3.6.11", + "@react-spectrum/utils": "^3.12.1", + "@react-stately/toggle": "^3.8.1", + "@react-types/shared": "^3.27.0", + "@react-types/switch": "^3.5.8", "@swc/helpers": "^0.5.0" }, "devDependencies": { diff --git a/packages/@react-spectrum/table/docs/TableView.mdx b/packages/@react-spectrum/table/docs/TableView.mdx index 1ba899b6d23..facd9856abd 100644 --- a/packages/@react-spectrum/table/docs/TableView.mdx +++ b/packages/@react-spectrum/table/docs/TableView.mdx @@ -12,8 +12,9 @@ export default Layout; import docs from 'docs:@react-spectrum/table'; import dndDocs from 'docs:@react-spectrum/dnd'; +import tableUtil from 'docs:@react-aria/test-utils/src/table.ts'; import tableTypes from 'docs:@react-types/table/src/index.d.ts'; -import {HeaderInfo, PropTable, PageDescription, TypeLink, VersionBadge} from '@react-spectrum/docs'; +import {HeaderInfo, PropTable, PageDescription, TypeLink, VersionBadge, ClassAPI} from '@react-spectrum/docs'; import {Keyboard} from '@react-spectrum/text'; import packageData from '@react-spectrum/table/package.json'; import ChevronRight from '@spectrum-icons/workflow/ChevronRight'; @@ -1957,3 +1958,48 @@ behaviors in your test suite. Please also refer to [React Spectrum's test suite](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-spectrum/table/test/Table.test.js) if you find that the above isn't sufficient when resolving issues in your own test cases. + +### Test utils + +`@react-spectrum/test-utils` offers common table interaction utilities which you may find helpful when writing tests. See [here](../react-aria/testing.html#react-aria-test-utils) for more information on how to setup these utilities +in your tests. Below is the full definition of the table tester and a sample of how you could use it in your test suite. + +```ts +// TableView.test.ts +import {render, within} from '@testing-library/react'; +import {theme} from '@react-spectrum/theme-default'; +import {User} from '@react-spectrum/test-utils'; + +let testUtilUser = new User({interactionType: 'mouse', advanceTimer: jest.advanceTimersByTime}); +// ... + +it('TableView can toggle row selection', async function () { + // Render your test component/app and initialize the table tester + let {getByTestId} = render( + + + ... + + + ); + let tableTester = testUtilUser.createTester('Table', {root: getByTestId('test-table')}); + expect(tableTester.selectedRows).toHaveLength(0); + + await tableTester.toggleSelectAll(); + expect(tableTester.selectedRows).toHaveLength(10); + + await tableTester.toggleRowSelection({row: 2}); + expect(tableTester.selectedRows).toHaveLength(9); + let checkbox = within(tableTester.rows[2]).getByRole('checkbox'); + expect(checkbox).not.toBeChecked(); + + await tableTester.toggleSelectAll(); + expect(tableTester.selectedRows).toHaveLength(10); + expect(checkbox).toBeChecked(); + + await tableTester.toggleSelectAll(); + expect(tableTester.selectedRows).toHaveLength(0); +}); +``` + + diff --git a/packages/@react-spectrum/table/package.json b/packages/@react-spectrum/table/package.json index b2f0b99fe25..2988c1f1916 100644 --- a/packages/@react-spectrum/table/package.json +++ b/packages/@react-spectrum/table/package.json @@ -1,6 +1,6 @@ { "name": "@react-spectrum/table", - "version": "3.15.0", + "version": "3.15.1", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -36,31 +36,31 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/button": "^3.11.0", - "@react-aria/focus": "^3.19.0", - "@react-aria/i18n": "^3.12.4", - "@react-aria/interactions": "^3.22.5", - "@react-aria/overlays": "^3.24.0", - "@react-aria/selection": "^3.21.0", - "@react-aria/table": "^3.16.0", - "@react-aria/utils": "^3.26.0", - "@react-aria/virtualizer": "^4.1.0", - "@react-aria/visually-hidden": "^3.8.18", - "@react-spectrum/checkbox": "^3.9.11", - "@react-spectrum/dnd": "^3.5.0", - "@react-spectrum/layout": "^3.6.10", - "@react-spectrum/menu": "^3.21.0", - "@react-spectrum/progress": "^3.7.11", - "@react-spectrum/tooltip": "^3.7.0", - "@react-spectrum/utils": "^3.12.0", + "@react-aria/button": "^3.11.1", + "@react-aria/focus": "^3.19.1", + "@react-aria/i18n": "^3.12.5", + "@react-aria/interactions": "^3.23.0", + "@react-aria/overlays": "^3.25.0", + "@react-aria/selection": "^3.22.0", + "@react-aria/table": "^3.16.1", + "@react-aria/utils": "^3.27.0", + "@react-aria/virtualizer": "^4.1.1", + "@react-aria/visually-hidden": "^3.8.19", + "@react-spectrum/checkbox": "^3.9.12", + "@react-spectrum/dnd": "^3.5.1", + "@react-spectrum/layout": "^3.6.11", + "@react-spectrum/menu": "^3.21.1", + "@react-spectrum/progress": "^3.7.12", + "@react-spectrum/tooltip": "^3.7.1", + "@react-spectrum/utils": "^3.12.1", "@react-stately/flags": "^3.0.5", - "@react-stately/layout": "^4.1.0", - "@react-stately/table": "^3.13.0", - "@react-stately/virtualizer": "^4.2.0", - "@react-types/grid": "^3.2.10", - "@react-types/shared": "^3.26.0", - "@react-types/table": "^3.10.3", - "@spectrum-icons/ui": "^3.6.11", + "@react-stately/layout": "^4.1.1", + "@react-stately/table": "^3.13.1", + "@react-stately/virtualizer": "^4.2.1", + "@react-types/grid": "^3.2.11", + "@react-types/shared": "^3.27.0", + "@react-types/table": "^3.10.4", + "@spectrum-icons/ui": "^3.6.12", "@swc/helpers": "^0.5.0" }, "devDependencies": { diff --git a/packages/@react-spectrum/table/test/Table.test.js b/packages/@react-spectrum/table/test/Table.test.js index 8201349d28d..61818bd704c 100644 --- a/packages/@react-spectrum/table/test/Table.test.js +++ b/packages/@react-spectrum/table/test/Table.test.js @@ -12,7 +12,7 @@ jest.mock('@react-aria/live-announcer'); jest.mock('@react-aria/utils/src/scrollIntoView'); -import {act, fireEvent, installPointerEvent, mockClickDefault, pointerMap, render as renderComponent, within} from '@react-spectrum/test-utils-internal'; +import {act, fireEvent, installPointerEvent, mockClickDefault, pointerMap, render as renderComponent, User, within} from '@react-spectrum/test-utils-internal'; import {ActionButton, Button} from '@react-spectrum/button'; import Add from '@spectrum-icons/workflow/Add'; import {announce} from '@react-aria/live-announcer'; @@ -35,7 +35,6 @@ import * as stories from '../stories/Table.stories'; import {Switch} from '@react-spectrum/switch'; import {TextField} from '@react-spectrum/textfield'; import {theme} from '@react-spectrum/theme-default'; -import {User} from '@react-aria/test-utils'; import userEvent from '@testing-library/user-event'; let { @@ -2851,20 +2850,20 @@ export let tableTests = () => { act(() => jest.runAllTimers()); await user.pointer({target: document.body, keys: '[TouchA]'}); - await tableTester.toggleRowSelection({text: 'Foo 5', needsLongPress: true}); + await tableTester.toggleRowSelection({row: 'Foo 5', needsLongPress: true}); checkSelection(onSelectionChange, ['Foo 5']); expect(onAction).not.toHaveBeenCalled(); onSelectionChange.mockReset(); - await tableTester.toggleRowSelection({text: 'Foo 10', needsLongPress: false}); + await tableTester.toggleRowSelection({row: 'Foo 10', needsLongPress: false}); checkSelection(onSelectionChange, ['Foo 5', 'Foo 10']); // Deselect all to exit selection mode onSelectionChange.mockReset(); - await tableTester.toggleRowSelection({text: 'Foo 10', needsLongPress: false}); + await tableTester.toggleRowSelection({row: 'Foo 10', needsLongPress: false}); checkSelection(onSelectionChange, ['Foo 5']); onSelectionChange.mockReset(); - await tableTester.toggleRowSelection({text: 'Foo 5', needsLongPress: false}); + await tableTester.toggleRowSelection({row: 'Foo 5', needsLongPress: false}); act(() => jest.runAllTimers()); checkSelection(onSelectionChange, []); expect(onAction).not.toHaveBeenCalled(); @@ -2987,11 +2986,11 @@ export let tableTests = () => { tableTester.setInteractionType('touch'); expect(tree.queryByLabelText('Select All')).toBeNull(); - await tableTester.toggleRowSelection({text: 'Baz 5'}); + await tableTester.toggleRowSelection({row: 'Baz 5'}); expect(announce).toHaveBeenLastCalledWith('Foo 5 selected.'); expect(announce).toHaveBeenCalledTimes(1); onSelectionChange.mockReset(); - await tableTester.toggleRowSelection({text: 'Foo 10'}); + await tableTester.toggleRowSelection({row: 'Foo 10'}); expect(announce).toHaveBeenLastCalledWith('Foo 10 selected. 2 items selected.'); expect(announce).toHaveBeenCalledTimes(2); @@ -3017,7 +3016,7 @@ export let tableTests = () => { let tree = renderTable({onSelectionChange, selectionStyle: 'highlight', onAction}); let tableTester = testUtilUser.createTester('Table', {root: tree.getByRole('grid')}); - await tableTester.toggleRowSelection({text: 'Foo 5'}); + await tableTester.toggleRowSelection({row: 'Foo 5'}); expect(announce).toHaveBeenLastCalledWith('Foo 5 selected.'); expect(announce).toHaveBeenCalledTimes(1); checkSelection(onSelectionChange, ['Foo 5']); @@ -3025,7 +3024,7 @@ export let tableTests = () => { announce.mockReset(); onSelectionChange.mockReset(); - await tableTester.triggerRowAction({text: 'Foo 5', needsDoubleClick: true}); + await tableTester.triggerRowAction({row: 'Foo 5', needsDoubleClick: true}); expect(announce).not.toHaveBeenCalled(); expect(onSelectionChange).not.toHaveBeenCalled(); expect(onAction).toHaveBeenCalledTimes(1); @@ -3177,12 +3176,12 @@ export let tableTests = () => { checkSelection(onSelectionChange, ['Foo 5', 'Foo 10']); // Deselect all to exit selection mode - await tableTester.toggleRowSelection({text: 'Foo 10'}); + await tableTester.toggleRowSelection({row: 'Foo 10'}); expect(announce).toHaveBeenLastCalledWith('Foo 10 not selected. 1 item selected.'); expect(announce).toHaveBeenCalledTimes(3); onSelectionChange.mockReset(); - await tableTester.toggleRowSelection({text: 'Foo 5'}); + await tableTester.toggleRowSelection({row: 'Foo 5'}); expect(announce).toHaveBeenLastCalledWith('Foo 5 not selected.'); expect(announce).toHaveBeenCalledTimes(4); @@ -4378,7 +4377,7 @@ export let tableTests = () => { expect(document.getElementById(columnheaders[1].getAttribute('aria-describedby'))).toHaveTextContent('sortable column, ascending'); expect(columnheaders[2]).not.toHaveAttribute('aria-describedby'); - await tableTester.toggleSort({index: 1}); + await tableTester.toggleSort({column: 1}); expect(document.getElementById(columnheaders[1].getAttribute('aria-describedby'))).toHaveTextContent('sortable column, descending'); uaMock.mockRestore(); diff --git a/packages/@react-spectrum/table/test/TestTableUtils.test.js b/packages/@react-spectrum/table/test/TestTableUtils.test.js index 287f02c8d2c..1cd7ad410e0 100644 --- a/packages/@react-spectrum/table/test/TestTableUtils.test.js +++ b/packages/@react-spectrum/table/test/TestTableUtils.test.js @@ -76,11 +76,11 @@ describe('Table ', function () { render(); let tableTester = testUtilRealTimer.createTester('Table', {root: screen.getByTestId('test')}); tableTester.setInteractionType(interactionType); - await tableTester.toggleRowSelection({index: 2}); + await tableTester.toggleRowSelection({row: 2}); expect(onSelectionChange).toHaveBeenCalledTimes(1); expect(new Set(onSelectionChange.mock.calls[0][0])).toEqual(new Set(['Foo 3'])); - await tableTester.toggleRowSelection({text: 'Foo 4'}); + await tableTester.toggleRowSelection({row: 'Foo 4'}); expect(onSelectionChange).toHaveBeenCalledTimes(2); expect(new Set(onSelectionChange.mock.calls[1][0])).toEqual(new Set(['Foo 3', 'Foo 4'])); @@ -88,15 +88,15 @@ describe('Table ', function () { expect(onSelectionChange).toHaveBeenCalledTimes(3); expect((onSelectionChange.mock.calls[2][0])).toEqual('all'); - await tableTester.toggleSort({index: 2}); + await tableTester.toggleSort({column: 2}); expect(onSortChange).toHaveBeenCalledTimes(1); expect(onSortChange).toHaveBeenLastCalledWith({column: 'bar', direction: 'ascending'}); - await tableTester.toggleSort({text: 'Foo'}); + await tableTester.toggleSort({column: 'Foo'}); expect(onSortChange).toHaveBeenCalledTimes(2); expect(onSortChange).toHaveBeenLastCalledWith({column: 'foo', direction: 'ascending'}); - await tableTester.toggleSort({text: 'Foo'}); + await tableTester.toggleSort({column: 'Foo'}); expect(onSortChange).toHaveBeenCalledTimes(3); expect(onSortChange).toHaveBeenLastCalledWith({column: 'foo', direction: 'descending'}); }); @@ -106,23 +106,23 @@ describe('Table ', function () { render(); let tableTester = testUtilRealTimer.createTester('Table', {root: screen.getByTestId('test')}); tableTester.setInteractionType(interactionType); - await tableTester.toggleRowSelection({index: 2}); + await tableTester.toggleRowSelection({row: 2}); expect(onSelectionChange).toHaveBeenCalledTimes(1); expect(new Set(onSelectionChange.mock.calls[0][0])).toEqual(new Set(['Foo 3'])); - await tableTester.toggleRowSelection({text: 'Foo 4'}); + await tableTester.toggleRowSelection({row: 'Foo 4'}); expect(onSelectionChange).toHaveBeenCalledTimes(2); expect(new Set(onSelectionChange.mock.calls[1][0])).toEqual(new Set(['Foo 4'])); - await tableTester.toggleSort({index: 2}); + await tableTester.toggleSort({column: 2}); expect(onSortChange).toHaveBeenCalledTimes(1); expect(onSortChange).toHaveBeenLastCalledWith({column: 'baz', direction: 'ascending'}); - await tableTester.toggleSort({text: 'Foo'}); + await tableTester.toggleSort({column: 'Foo'}); expect(onSortChange).toHaveBeenCalledTimes(2); expect(onSortChange).toHaveBeenLastCalledWith({column: 'foo', direction: 'ascending'}); - await tableTester.toggleSort({text: 'Foo'}); + await tableTester.toggleSort({column: 'Foo'}); expect(onSortChange).toHaveBeenCalledTimes(3); expect(onSortChange).toHaveBeenLastCalledWith({column: 'foo', direction: 'descending'}); }); @@ -148,11 +148,11 @@ describe('Table ', function () { render(); let tableTester = testUtilFakeTimer.createTester('Table', {root: screen.getByTestId('test')}); tableTester.setInteractionType(interactionType); - await tableTester.toggleRowSelection({index: 2}); + await tableTester.toggleRowSelection({row: 2}); expect(onSelectionChange).toHaveBeenCalledTimes(1); expect(new Set(onSelectionChange.mock.calls[0][0])).toEqual(new Set(['Foo 3'])); - await tableTester.toggleRowSelection({text: 'Foo 4'}); + await tableTester.toggleRowSelection({row: 'Foo 4'}); expect(onSelectionChange).toHaveBeenCalledTimes(2); expect(new Set(onSelectionChange.mock.calls[1][0])).toEqual(new Set(['Foo 3', 'Foo 4'])); @@ -160,15 +160,15 @@ describe('Table ', function () { expect(onSelectionChange).toHaveBeenCalledTimes(3); expect((onSelectionChange.mock.calls[2][0])).toEqual('all'); - await tableTester.toggleSort({index: 2}); + await tableTester.toggleSort({column: 2}); expect(onSortChange).toHaveBeenCalledTimes(1); expect(onSortChange).toHaveBeenLastCalledWith({column: 'bar', direction: 'ascending'}); - await tableTester.toggleSort({text: 'Foo'}); + await tableTester.toggleSort({column: 'Foo'}); expect(onSortChange).toHaveBeenCalledTimes(2); expect(onSortChange).toHaveBeenLastCalledWith({column: 'foo', direction: 'ascending'}); - await tableTester.toggleSort({text: 'Foo'}); + await tableTester.toggleSort({column: 'Foo'}); expect(onSortChange).toHaveBeenCalledTimes(3); expect(onSortChange).toHaveBeenLastCalledWith({column: 'foo', direction: 'descending'}); }); @@ -179,23 +179,23 @@ describe('Table ', function () { let tableTester = testUtilFakeTimer.createTester('Table', {root: screen.getByTestId('test')}); tableTester.setInteractionType(interactionType); - await tableTester.toggleRowSelection({index: 2, focusToSelect: true}); + await tableTester.toggleRowSelection({row: 2, focusToSelect: true}); expect(onSelectionChange).toHaveBeenCalledTimes(1); expect(new Set(onSelectionChange.mock.calls[0][0])).toEqual(new Set(['Foo 3'])); - await tableTester.toggleRowSelection({text: 'Foo 4', focusToSelect: true}); + await tableTester.toggleRowSelection({row: 'Foo 4', focusToSelect: true}); expect(onSelectionChange).toHaveBeenCalledTimes(2); expect(new Set(onSelectionChange.mock.calls[1][0])).toEqual(new Set(['Foo 4'])); - await tableTester.toggleSort({index: 2}); + await tableTester.toggleSort({column: 2}); expect(onSortChange).toHaveBeenCalledTimes(1); expect(onSortChange).toHaveBeenLastCalledWith({column: 'baz', direction: 'ascending'}); - await tableTester.toggleSort({text: 'Foo'}); + await tableTester.toggleSort({column: 'Foo'}); expect(onSortChange).toHaveBeenCalledTimes(2); expect(onSortChange).toHaveBeenLastCalledWith({column: 'foo', direction: 'ascending'}); - await tableTester.toggleSort({text: 'Foo'}); + await tableTester.toggleSort({column: 'Foo'}); expect(onSortChange).toHaveBeenCalledTimes(3); expect(onSortChange).toHaveBeenLastCalledWith({column: 'foo', direction: 'descending'}); }); diff --git a/packages/@react-spectrum/tabs/docs/Tabs.mdx b/packages/@react-spectrum/tabs/docs/Tabs.mdx index 0028e535498..85fa97d208c 100644 --- a/packages/@react-spectrum/tabs/docs/Tabs.mdx +++ b/packages/@react-spectrum/tabs/docs/Tabs.mdx @@ -12,8 +12,9 @@ export default Layout; import docs from 'docs:@react-spectrum/tabs'; import utilsDocs from 'docs:@react-aria/utils'; -import {HeaderInfo, PropTable, PageDescription, TypeLink} from '@react-spectrum/docs'; +import {HeaderInfo, PropTable, PageDescription, TypeLink, ClassAPI, VersionBadge} from '@react-spectrum/docs'; import packageData from '@react-spectrum/tabs/package.json'; +import tabsUtils from 'docs:@react-aria/test-utils/src/tabs.ts'; ```jsx import import {ActionGroup} from '@react-spectrum/actiongroup'; @@ -629,3 +630,44 @@ function Example() { ``` + +## Testing + +### Test utils + +Tabs features automatic tab collapse behavior and may need specific mocks to test said behavior. Please also refer to +[React Spectrum's test suite](https://github.com/adobe/react-spectrum/blob/326f48154e301edab425c8198c5c3af72422462b/packages/%40react-spectrum/tabs/test/Tabs.test.js#L58-L62) if you +run into any issues with your tests. + +`@react-spectrum/test-utils` offers common tabs interaction utilities which you may find helpful when writing tests. See [here](../react-aria/testing.html#react-aria-test-utils) for more information on how to setup these utilities +in your tests. Below is the full definition of the tabs tester and a sample of how you could use it in your test suite. + +```ts +// Tabs.test.ts +import {render} from '@testing-library/react'; +import {theme} from '@react-spectrum/theme-default'; +import {User} from '@react-spectrum/test-utils'; + +let testUtilUser = new User({interactionType: 'mouse'}); +// ... + +it('Tabs can change selection via keyboard', async function () { + // Render your test component/app and initialize the listbox tester + let {getByTestId} = render( + + + ... + + + ); + let tabsTester = testUtilUser.createTester('Tabs', {root: getByTestId('test-tabs'), interactionType: 'keyboard'}); + + let tabs = tabsTester.tabs; + expect(tabsTester.selectedTab).toBe(tabs[0]); + + await tabsTester.triggerTab({tab: 1}); + expect(tabsTester.selectedTab).toBe(tabs[1]); +}); +``` + + diff --git a/packages/@react-spectrum/tabs/package.json b/packages/@react-spectrum/tabs/package.json index cf92fd973ed..84cdfa0c1ff 100644 --- a/packages/@react-spectrum/tabs/package.json +++ b/packages/@react-spectrum/tabs/package.json @@ -1,6 +1,6 @@ { "name": "@react-spectrum/tabs", - "version": "3.8.15", + "version": "3.8.16", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -36,20 +36,20 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/focus": "^3.19.0", - "@react-aria/i18n": "^3.12.4", - "@react-aria/interactions": "^3.22.5", - "@react-aria/tabs": "^3.9.8", - "@react-aria/utils": "^3.26.0", - "@react-spectrum/picker": "^3.15.4", - "@react-spectrum/text": "^3.5.10", - "@react-spectrum/utils": "^3.12.0", - "@react-stately/collections": "^3.12.0", - "@react-stately/list": "^3.11.1", - "@react-stately/tabs": "^3.7.0", - "@react-types/select": "^3.9.8", - "@react-types/shared": "^3.26.0", - "@react-types/tabs": "^3.3.11", + "@react-aria/focus": "^3.19.1", + "@react-aria/i18n": "^3.12.5", + "@react-aria/interactions": "^3.23.0", + "@react-aria/tabs": "^3.9.9", + "@react-aria/utils": "^3.27.0", + "@react-spectrum/picker": "^3.15.5", + "@react-spectrum/text": "^3.5.11", + "@react-spectrum/utils": "^3.12.1", + "@react-stately/collections": "^3.12.1", + "@react-stately/list": "^3.11.2", + "@react-stately/tabs": "^3.7.1", + "@react-types/select": "^3.9.9", + "@react-types/shared": "^3.27.0", + "@react-types/tabs": "^3.3.12", "@swc/helpers": "^0.5.0" }, "devDependencies": { diff --git a/packages/@react-spectrum/tabs/test/Tabs.test.js b/packages/@react-spectrum/tabs/test/Tabs.test.js index 7a411b22046..3178a266774 100644 --- a/packages/@react-spectrum/tabs/test/Tabs.test.js +++ b/packages/@react-spectrum/tabs/test/Tabs.test.js @@ -16,6 +16,7 @@ import {Links as LinksExample} from '../stories/Tabs.stories'; import {Provider} from '@react-spectrum/provider'; import React from 'react'; import {theme} from '@react-spectrum/theme-default'; +import {User} from '@react-aria/test-utils'; import userEvent from '@testing-library/user-event'; let defaultItems = [ @@ -25,9 +26,9 @@ let defaultItems = [ ]; function renderComponent(props = {}, itemProps) { - let {items = defaultItems} = props; + let {items = defaultItems, providerProps} = props; return render( - + {item => ( @@ -49,6 +50,7 @@ function renderComponent(props = {}, itemProps) { describe('Tabs', function () { let onSelectionChange = jest.fn(); let user; + let testUtilUser = new User(); beforeAll(function () { user = userEvent.setup({delay: null, pointerMap}); @@ -73,12 +75,13 @@ describe('Tabs', function () { it('renders properly', function () { let container = renderComponent(); - let tablist = container.getByRole('tablist'); - expect(tablist).toBeTruthy(); + let tabsTester = testUtilUser.createTester('Tabs', {root: container.getByRole('tablist')}); + let tablist = tabsTester.tablist; + expect(tablist).toBeTruthy(); expect(tablist).toHaveAttribute('aria-orientation', 'horizontal'); - let tabs = within(tablist).getAllByRole('tab'); + let tabs = tabsTester.tabs; expect(tabs.length).toBe(3); for (let tab of tabs) { @@ -86,12 +89,15 @@ describe('Tabs', function () { expect(tab).toHaveAttribute('aria-selected'); let isSelected = tab.getAttribute('aria-selected') === 'true'; if (isSelected) { + expect(tab).toBe(tabsTester.selectedTab); expect(tab).toHaveAttribute('aria-controls'); let tabpanel = document.getElementById(tab.getAttribute('aria-controls')); expect(tabpanel).toBeTruthy(); expect(tabpanel).toHaveAttribute('aria-labelledby', tab.id); expect(tabpanel).toHaveAttribute('role', 'tabpanel'); expect(tabpanel).toHaveTextContent(defaultItems[0].children); + expect(tabpanel).toBe(tabsTester.activeTabpanel); + expect(tabsTester.tabpanels).toHaveLength(1); } } }); @@ -134,6 +140,33 @@ describe('Tabs', function () { expect(arrowDown.defaultPrevented).toBe(false); }); + it('allows user to change tab item select via arrow keys with horizontal tabs (rtl)', async function () { + let onKeyDown = jest.fn(); + let container = renderComponent({orientation: 'horizontal', providerProps: {locale: 'ar-AE'}}); + let tabsTester = testUtilUser.createTester('Tabs', {root: container.getByRole('tablist'), interactionType: 'keyboard', direction: 'rtl'}); + let tabs = tabsTester.tabs; + window.addEventListener('keydown', onKeyDown); + + expect(tabs[0]).toHaveAttribute('aria-selected', 'true'); + + await tabsTester.triggerTab({tab: 1}); + expect(tabs[0]).not.toHaveAttribute('aria-selected', 'true'); + expect(tabs[1]).toHaveAttribute('aria-selected', 'true'); + // Just to double check that the util is actually pressing the expected arrow key + expect(onKeyDown.mock.calls[0][0].key).toBe('ArrowLeft'); + + await tabsTester.triggerTab({tab: 2}); + expect(tabs[1]).not.toHaveAttribute('aria-selected', 'true'); + expect(tabs[2]).toHaveAttribute('aria-selected', 'true'); + expect(onKeyDown.mock.calls[1][0].key).toBe('ArrowLeft'); + + await tabsTester.triggerTab({tab: 1}); + expect(tabs[2]).not.toHaveAttribute('aria-selected', 'true'); + expect(tabs[1]).toHaveAttribute('aria-selected', 'true'); + expect(onKeyDown.mock.calls[2][0].key).toBe('ArrowRight'); + window.removeEventListener('keydown', onKeyDown); + }); + it('allows user to change tab item select via arrow keys with vertical tabs', function () { let container = renderComponent({orientation: 'vertical'}); let tablist = container.getByRole('tablist'); diff --git a/packages/@react-spectrum/tag/chromatic/TagGroup.stories.tsx b/packages/@react-spectrum/tag/chromatic/TagGroup.stories.tsx index 04977241be8..886495a652b 100644 --- a/packages/@react-spectrum/tag/chromatic/TagGroup.stories.tsx +++ b/packages/@react-spectrum/tag/chromatic/TagGroup.stories.tsx @@ -72,7 +72,7 @@ export const WithIcon: TagGroupStory = { {(item: any) => ( - )} diff --git a/packages/@react-spectrum/tag/package.json b/packages/@react-spectrum/tag/package.json index 1851b5c112b..15045867244 100644 --- a/packages/@react-spectrum/tag/package.json +++ b/packages/@react-spectrum/tag/package.json @@ -1,6 +1,6 @@ { "name": "@react-spectrum/tag", - "version": "3.2.11", + "version": "3.2.12", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -36,20 +36,20 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/focus": "^3.19.0", - "@react-aria/i18n": "^3.12.4", - "@react-aria/interactions": "^3.22.5", - "@react-aria/selection": "^3.21.0", - "@react-aria/tag": "^3.4.8", - "@react-aria/utils": "^3.26.0", - "@react-spectrum/button": "^3.16.9", - "@react-spectrum/form": "^3.7.10", - "@react-spectrum/label": "^3.16.10", - "@react-spectrum/text": "^3.5.10", - "@react-spectrum/utils": "^3.12.0", - "@react-stately/collections": "^3.12.0", - "@react-stately/list": "^3.11.1", - "@react-types/shared": "^3.26.0", + "@react-aria/focus": "^3.19.1", + "@react-aria/i18n": "^3.12.5", + "@react-aria/interactions": "^3.23.0", + "@react-aria/selection": "^3.22.0", + "@react-aria/tag": "^3.4.9", + "@react-aria/utils": "^3.27.0", + "@react-spectrum/button": "^3.16.10", + "@react-spectrum/form": "^3.7.11", + "@react-spectrum/label": "^3.16.11", + "@react-spectrum/text": "^3.5.11", + "@react-spectrum/utils": "^3.12.1", + "@react-stately/collections": "^3.12.1", + "@react-stately/list": "^3.11.2", + "@react-types/shared": "^3.27.0", "@swc/helpers": "^0.5.0" }, "devDependencies": { diff --git a/packages/@react-spectrum/tag/stories/TagGroup.stories.tsx b/packages/@react-spectrum/tag/stories/TagGroup.stories.tsx index bb390cd448b..ff110fc3de5 100644 --- a/packages/@react-spectrum/tag/stories/TagGroup.stories.tsx +++ b/packages/@react-spectrum/tag/stories/TagGroup.stories.tsx @@ -175,7 +175,7 @@ export const WithAvatar: TagGroupStory = { {(item: any) => ( - + {item.key === '1' && } {item.label} )} diff --git a/packages/@react-spectrum/test-utils/package.json b/packages/@react-spectrum/test-utils/package.json index f5becb65d0d..3edb59f902b 100644 --- a/packages/@react-spectrum/test-utils/package.json +++ b/packages/@react-spectrum/test-utils/package.json @@ -1,6 +1,6 @@ { "name": "@react-spectrum/test-utils", - "version": "1.0.0-alpha.3", + "version": "1.0.0-alpha.4", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -24,7 +24,7 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/test-utils": "1.0.0-alpha.3", + "@react-aria/test-utils": "1.0.0-alpha.4", "@swc/helpers": "^0.5.0" }, "peerDependencies": { diff --git a/packages/@react-spectrum/text/package.json b/packages/@react-spectrum/text/package.json index 34c487eb668..58436853460 100644 --- a/packages/@react-spectrum/text/package.json +++ b/packages/@react-spectrum/text/package.json @@ -1,6 +1,6 @@ { "name": "@react-spectrum/text", - "version": "3.5.10", + "version": "3.5.11", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -36,12 +36,12 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/utils": "^3.26.0", - "@react-spectrum/utils": "^3.12.0", - "@react-types/shared": "^3.26.0", - "@react-types/text": "^3.3.13", + "@react-aria/utils": "^3.27.0", + "@react-spectrum/utils": "^3.12.1", + "@react-types/shared": "^3.27.0", + "@react-types/text": "^3.3.14", "@swc/helpers": "^0.5.0", - "react-aria-components": "^1.5.0" + "react-aria-components": "^1.6.0" }, "devDependencies": { "@adobe/spectrum-css-temp": "3.0.0-alpha.1", diff --git a/packages/@react-spectrum/textfield/package.json b/packages/@react-spectrum/textfield/package.json index 24a3cc888ef..4074b6b4ad4 100644 --- a/packages/@react-spectrum/textfield/package.json +++ b/packages/@react-spectrum/textfield/package.json @@ -1,6 +1,6 @@ { "name": "@react-spectrum/textfield", - "version": "3.12.7", + "version": "3.12.8", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -36,17 +36,17 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/focus": "^3.19.0", - "@react-aria/interactions": "^3.22.5", - "@react-aria/textfield": "^3.15.0", - "@react-aria/utils": "^3.26.0", - "@react-spectrum/form": "^3.7.10", - "@react-spectrum/label": "^3.16.10", - "@react-spectrum/utils": "^3.12.0", + "@react-aria/focus": "^3.19.1", + "@react-aria/interactions": "^3.23.0", + "@react-aria/textfield": "^3.16.0", + "@react-aria/utils": "^3.27.0", + "@react-spectrum/form": "^3.7.11", + "@react-spectrum/label": "^3.16.11", + "@react-spectrum/utils": "^3.12.1", "@react-stately/utils": "^3.10.5", - "@react-types/shared": "^3.26.0", - "@react-types/textfield": "^3.10.0", - "@spectrum-icons/ui": "^3.6.11", + "@react-types/shared": "^3.27.0", + "@react-types/textfield": "^3.11.0", + "@spectrum-icons/ui": "^3.6.12", "@swc/helpers": "^0.5.0" }, "devDependencies": { @@ -55,7 +55,8 @@ }, "peerDependencies": { "@react-spectrum/provider": "^3.0.0", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" }, "publishConfig": { "access": "public" diff --git a/packages/@react-spectrum/theme-dark/package.json b/packages/@react-spectrum/theme-dark/package.json index 7255c02c646..59150030669 100644 --- a/packages/@react-spectrum/theme-dark/package.json +++ b/packages/@react-spectrum/theme-dark/package.json @@ -1,6 +1,6 @@ { "name": "@react-spectrum/theme-dark", - "version": "3.5.14", + "version": "3.5.15", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -36,7 +36,7 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-types/provider": "^3.8.5", + "@react-types/provider": "^3.8.6", "@swc/helpers": "^0.5.0" }, "devDependencies": { diff --git a/packages/@react-spectrum/theme-default/package.json b/packages/@react-spectrum/theme-default/package.json index 422434b8c08..92dddd1b447 100644 --- a/packages/@react-spectrum/theme-default/package.json +++ b/packages/@react-spectrum/theme-default/package.json @@ -1,6 +1,6 @@ { "name": "@react-spectrum/theme-default", - "version": "3.5.14", + "version": "3.5.15", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -36,7 +36,7 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-types/provider": "^3.8.5", + "@react-types/provider": "^3.8.6", "@swc/helpers": "^0.5.0" }, "devDependencies": { diff --git a/packages/@react-spectrum/theme-express/package.json b/packages/@react-spectrum/theme-express/package.json index 250d5c2916c..91cf8aac276 100644 --- a/packages/@react-spectrum/theme-express/package.json +++ b/packages/@react-spectrum/theme-express/package.json @@ -1,6 +1,6 @@ { "name": "@react-spectrum/theme-express", - "version": "3.0.0-alpha.16", + "version": "3.0.0-alpha.17", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -36,8 +36,8 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-spectrum/theme-default": "^3.5.14", - "@react-types/provider": "^3.8.5", + "@react-spectrum/theme-default": "^3.5.15", + "@react-types/provider": "^3.8.6", "@swc/helpers": "^0.5.0" }, "devDependencies": { diff --git a/packages/@react-spectrum/theme-light/package.json b/packages/@react-spectrum/theme-light/package.json index c8e42f63654..2ba43a51e8e 100644 --- a/packages/@react-spectrum/theme-light/package.json +++ b/packages/@react-spectrum/theme-light/package.json @@ -1,6 +1,6 @@ { "name": "@react-spectrum/theme-light", - "version": "3.4.14", + "version": "3.4.15", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -36,7 +36,7 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-types/provider": "^3.8.5", + "@react-types/provider": "^3.8.6", "@swc/helpers": "^0.5.0" }, "devDependencies": { diff --git a/packages/@react-spectrum/toast/docs/Toast.mdx b/packages/@react-spectrum/toast/docs/Toast.mdx index 213a6e43fc0..917a7556d46 100644 --- a/packages/@react-spectrum/toast/docs/Toast.mdx +++ b/packages/@react-spectrum/toast/docs/Toast.mdx @@ -165,6 +165,14 @@ function Example() { } ``` +## Placement + +By default, toasts are displayed at the bottom center of the screen. This can be changed by setting the `placement` prop on the `ToastContainer` to `'top'`, `'top end'`, `'bottom'`, or `'bottom end'`. + +```tsx example render=false hidden + +``` + ## API ### ToastQueue diff --git a/packages/@react-spectrum/toast/package.json b/packages/@react-spectrum/toast/package.json index 3a781d16c31..b615639b041 100644 --- a/packages/@react-spectrum/toast/package.json +++ b/packages/@react-spectrum/toast/package.json @@ -1,6 +1,6 @@ { "name": "@react-spectrum/toast", - "version": "3.0.0-beta.17", + "version": "3.0.0-beta.18", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -36,16 +36,16 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/focus": "^3.19.0", - "@react-aria/i18n": "^3.12.4", - "@react-aria/overlays": "^3.24.0", - "@react-aria/toast": "3.0.0-beta.18", - "@react-aria/utils": "^3.26.0", - "@react-spectrum/button": "^3.16.9", - "@react-spectrum/utils": "^3.12.0", + "@react-aria/focus": "^3.19.1", + "@react-aria/i18n": "^3.12.5", + "@react-aria/overlays": "^3.25.0", + "@react-aria/toast": "3.0.0-beta.19", + "@react-aria/utils": "^3.27.0", + "@react-spectrum/button": "^3.16.10", + "@react-spectrum/utils": "^3.12.1", "@react-stately/toast": "3.0.0-beta.7", - "@react-types/shared": "^3.26.0", - "@spectrum-icons/ui": "^3.6.11", + "@react-types/shared": "^3.27.0", + "@spectrum-icons/ui": "^3.6.12", "@swc/helpers": "^0.5.0", "use-sync-external-store": "^1.2.0" }, diff --git a/packages/@react-spectrum/toast/src/ToastContainer.tsx b/packages/@react-spectrum/toast/src/ToastContainer.tsx index e5af9d47814..720adbd409c 100644 --- a/packages/@react-spectrum/toast/src/ToastContainer.tsx +++ b/packages/@react-spectrum/toast/src/ToastContainer.tsx @@ -21,7 +21,11 @@ import {Toaster} from './Toaster'; import {ToastOptions, ToastQueue, useToastQueue} from '@react-stately/toast'; import {useSyncExternalStore} from 'use-sync-external-store/shim/index.js'; -export interface SpectrumToastContainerProps extends AriaToastRegionProps {} +export type ToastPlacement = 'top' | 'top end' | 'bottom' | 'bottom end'; + +export interface SpectrumToastContainerProps extends AriaToastRegionProps { + placement?: ToastPlacement +} export interface SpectrumToastOptions extends Omit, DOMProps { /** A label for the action button within the toast. */ diff --git a/packages/@react-spectrum/toast/src/Toaster.tsx b/packages/@react-spectrum/toast/src/Toaster.tsx index 8be59a78644..356fc97fc4c 100644 --- a/packages/@react-spectrum/toast/src/Toaster.tsx +++ b/packages/@react-spectrum/toast/src/Toaster.tsx @@ -15,15 +15,17 @@ import {classNames} from '@react-spectrum/utils'; import {FocusScope, useFocusRing} from '@react-aria/focus'; import {mergeProps} from '@react-aria/utils'; import {Provider} from '@react-spectrum/provider'; -import React, {createContext, ReactElement, ReactNode, useRef} from 'react'; +import React, {createContext, ReactElement, ReactNode, useMemo, useRef} from 'react'; import ReactDOM from 'react-dom'; import toastContainerStyles from './toastContainer.css'; +import type {ToastPlacement} from './ToastContainer'; import {ToastState} from '@react-stately/toast'; import {useUNSTABLE_PortalContext} from '@react-aria/overlays'; interface ToastContainerProps extends AriaToastRegionProps { children: ReactNode, - state: ToastState + state: ToastState, + placement?: ToastPlacement } export const ToasterContext = createContext(false); @@ -39,6 +41,11 @@ export function Toaster(props: ToastContainerProps): ReactElement { let {focusProps, isFocusVisible} = useFocusRing(); let {getContainer} = useUNSTABLE_PortalContext(); + let [position, placement] = useMemo(() => { + let [pos = 'bottom', place = 'center'] = props.placement?.split(' ') || []; + return [pos, place]; + }, [props.placement]); + let contents = ( @@ -46,8 +53,8 @@ export function Toaster(props: ToastContainerProps): ReactElement {
    ( + (story, {parameters, args}) => ( <> - {!parameters.disableToastContainer && } + {!parameters.disableToastContainer && } {story()} ) ], args: { shouldCloseOnAction: false, - timeout: null + timeout: null, + placement: undefined }, argTypes: { timeout: { control: 'radio', options: [null, 5000] + }, + placement: { + control: 'select', + options: [undefined, 'top', 'top end', 'bottom', 'bottom end'] } } }; diff --git a/packages/@react-spectrum/tooltip/package.json b/packages/@react-spectrum/tooltip/package.json index 8cf157a4352..88815322715 100644 --- a/packages/@react-spectrum/tooltip/package.json +++ b/packages/@react-spectrum/tooltip/package.json @@ -1,6 +1,6 @@ { "name": "@react-spectrum/tooltip", - "version": "3.7.0", + "version": "3.7.1", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -36,17 +36,17 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/focus": "^3.19.0", - "@react-aria/overlays": "^3.24.0", - "@react-aria/tooltip": "^3.7.10", - "@react-aria/utils": "^3.26.0", - "@react-spectrum/overlays": "^5.7.0", - "@react-spectrum/utils": "^3.12.0", - "@react-stately/tooltip": "^3.5.0", - "@react-types/overlays": "^3.8.11", - "@react-types/shared": "^3.26.0", - "@react-types/tooltip": "^3.4.13", - "@spectrum-icons/ui": "^3.6.11", + "@react-aria/focus": "^3.19.1", + "@react-aria/overlays": "^3.25.0", + "@react-aria/tooltip": "^3.7.11", + "@react-aria/utils": "^3.27.0", + "@react-spectrum/overlays": "^5.7.1", + "@react-spectrum/utils": "^3.12.1", + "@react-stately/tooltip": "^3.5.1", + "@react-types/overlays": "^3.8.12", + "@react-types/shared": "^3.27.0", + "@react-types/tooltip": "^3.4.14", + "@spectrum-icons/ui": "^3.6.12", "@swc/helpers": "^0.5.0" }, "devDependencies": { diff --git a/packages/@react-spectrum/tree/docs/TreeView.mdx b/packages/@react-spectrum/tree/docs/TreeView.mdx index 06334ca4dc8..9e1fd3a0d75 100644 --- a/packages/@react-spectrum/tree/docs/TreeView.mdx +++ b/packages/@react-spectrum/tree/docs/TreeView.mdx @@ -11,7 +11,8 @@ import {Layout} from '@react-spectrum/docs'; export default Layout; import docs from 'docs:@react-spectrum/tree'; -import {HeaderInfo, PropTable, PageDescription, TypeLink, VersionBadge} from '@react-spectrum/docs'; +import treeUtils from 'docs:@react-aria/test-utils/src/tree.ts'; +import {HeaderInfo, PropTable, PageDescription, TypeLink, VersionBadge, ClassAPI} from '@react-spectrum/docs'; import {Keyboard} from '@react-spectrum/text'; import packageData from '@react-spectrum/tree/package.json'; import ChevronRight from '@spectrum-icons/workflow/ChevronRight'; @@ -219,7 +220,7 @@ function ControlledSelection() { aria-label="Example tree with controlled selection" defaultExpandedKeys={['projects', 'project-2']} /*- begin highlight -*/ - selectionMode="multiple" + selectionMode="multiple" selectedKeys={selectedKeys} onSelectionChange={setSelectedKeys} /*- end highlight -*/ @@ -454,5 +455,46 @@ behaviors in your test suite. [Long press](./testing.html#simulating-user-long-press) -Please also refer to [React Spectrum's test suite](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-spectrum/tree/test/TreeView.test.js) if you find that the above +Please also refer to [React Spectrum's test suite](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-spectrum/tree/test/TreeView.test.tsx) if you find that the above isn't sufficient when resolving issues in your own test cases. + +### Test utils + +`@react-spectrum/test-utils` offers common tree interaction utilities which you may find helpful when writing tests. See [here](../react-aria/testing.html#react-aria-test-utils) for more information on how to setup these utilities +in your tests. Below is the full definition of the tree tester and a sample of how you could use it in your test suite. + +```ts +// Tree.test.ts +import {render, within} from '@testing-library/react'; +import {theme} from '@react-spectrum/theme-default'; +import {User} from '@react-spectrum/test-utils'; + +let testUtilUser = new User({interactionType: 'mouse'}); +// ... + +it('TreeView can select a row via keyboard', async function () { + // Render your test component/app and initialize the Tree tester + let {getByTestId} = render( + + + ... + + + ); + let treeTester = testUtilUser.createTester('Tree', {root: getByTestId('test-tree'), interactionType: 'keyboard'}); + + await treeTester.toggleRowSelection({row: 0}); + expect(treeTester.selectedRows).toHaveLength(1); + expect(within(treeTester.rows[0]).getByRole('checkbox')).toBeChecked(); + + await treeTester.toggleRowSelection({row: 1}); + expect(treeTester.selectedRows).toHaveLength(2); + expect(within(treeTester.rows[1]).getByRole('checkbox')).toBeChecked(); + + await treeTester.toggleRowSelection({row: 0}); + expect(treeTester.selectedRows).toHaveLength(1); + expect(within(treeTester.rows[0]).getByRole('checkbox')).not.toBeChecked(); +}); +``` + + diff --git a/packages/@react-spectrum/tree/package.json b/packages/@react-spectrum/tree/package.json index 547b8b20010..b9af5527929 100644 --- a/packages/@react-spectrum/tree/package.json +++ b/packages/@react-spectrum/tree/package.json @@ -1,6 +1,6 @@ { "name": "@react-spectrum/tree", - "version": "3.0.0-beta.2", + "version": "3.0.0-beta.3", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -36,17 +36,17 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/button": "^3.11.0", - "@react-aria/i18n": "^3.12.4", - "@react-aria/tree": "3.0.0-beta.2", - "@react-aria/utils": "^3.26.0", - "@react-spectrum/checkbox": "^3.9.11", - "@react-spectrum/text": "^3.5.10", - "@react-spectrum/utils": "^3.12.0", - "@react-types/shared": "^3.26.0", - "@spectrum-icons/ui": "^3.6.11", + "@react-aria/button": "^3.11.1", + "@react-aria/i18n": "^3.12.5", + "@react-aria/tree": "3.0.0-beta.3", + "@react-aria/utils": "^3.27.0", + "@react-spectrum/checkbox": "^3.9.12", + "@react-spectrum/text": "^3.5.11", + "@react-spectrum/utils": "^3.12.1", + "@react-types/shared": "^3.27.0", + "@spectrum-icons/ui": "^3.6.12", "@swc/helpers": "^0.5.0", - "react-aria-components": "^1.5.0" + "react-aria-components": "^1.6.0" }, "peerDependencies": { "@react-spectrum/provider": "^3.0.0", diff --git a/packages/@react-spectrum/tree/test/TreeView.test.tsx b/packages/@react-spectrum/tree/test/TreeView.test.tsx index 5130a8b40f4..be7bc2bd8a9 100644 --- a/packages/@react-spectrum/tree/test/TreeView.test.tsx +++ b/packages/@react-spectrum/tree/test/TreeView.test.tsx @@ -22,6 +22,7 @@ import {Provider} from '@react-spectrum/provider'; import React from 'react'; import {theme} from '@react-spectrum/theme-default'; import {TreeView, TreeViewItem} from '../'; +import {User} from '@react-aria/test-utils'; import userEvent from '@testing-library/user-event'; let onSelectionChange = jest.fn(); @@ -170,6 +171,7 @@ let DynamicTree = ({treeProps = {}, rowProps = {}}) => ( describe('Tree', () => { let user; + let testUtilUser = new User(); beforeAll(() => { user = userEvent.setup({delay: null, pointerMap}); @@ -320,8 +322,9 @@ describe('Tree', () => { }); it('should support dynamic trees', () => { - let {getAllByRole} = render(); - let rows = getAllByRole('row'); + let {getByRole} = render(); + let treeTester = testUtilUser.createTester('Tree', {user, root: getByRole('treegrid')}); + let rows = treeTester.rows; expect(rows).toHaveLength(20); // Check the rough structure to make sure dynamic rows are rendering as expected (just checks the expandable rows and their attributes) @@ -385,11 +388,12 @@ describe('Tree', () => { }); it('should not render checkboxes for selection with selectionStyle=highlight', async () => { - let {getByRole, getAllByRole} = render(); - let tree = getByRole('treegrid'); - expect(tree).toHaveAttribute('aria-multiselectable', 'true'); + let {getByRole} = render(); + let treeTester = testUtilUser.createTester('Tree', {user, root: getByRole('treegrid')}); + expect(treeTester.tree).toHaveAttribute('aria-multiselectable', 'true'); + let rows = treeTester.rows; - for (let row of getAllByRole('row')) { + for (let row of treeTester.rows) { let checkbox = within(row).queryByRole('checkbox'); expect(checkbox).toBeNull(); expect(row).toHaveAttribute('aria-selected', 'false'); @@ -397,21 +401,25 @@ describe('Tree', () => { expect(row).toHaveAttribute('data-selection-mode', 'multiple'); } - let row2 = getAllByRole('row')[2]; - await user.click(row2); + let row2 = rows[2]; + await treeTester.toggleRowSelection({row: 'Projects-1'}); expect(row2).toHaveAttribute('aria-selected', 'true'); expect(row2).toHaveAttribute('data-selected', 'true'); expect(onSelectionChange).toHaveBeenCalledTimes(1); expect(new Set(onSelectionChange.mock.calls[0][0])).toEqual(new Set(['Projects-1'])); + expect(treeTester.selectedRows).toHaveLength(1); + expect(treeTester.selectedRows[0]).toBe(row2); - let row1 = getAllByRole('row')[1]; - await user.click(row1); + let row1 = rows[1]; + await treeTester.toggleRowSelection({row: row1}); expect(row1).toHaveAttribute('aria-selected', 'true'); expect(row1).toHaveAttribute('data-selected', 'true'); expect(row2).toHaveAttribute('aria-selected', 'false'); expect(row2).not.toHaveAttribute('data-selected'); expect(onSelectionChange).toHaveBeenCalledTimes(2); expect(new Set(onSelectionChange.mock.calls[1][0])).toEqual(new Set(['Projects'])); + expect(treeTester.selectedRows).toHaveLength(1); + expect(treeTester.selectedRows[0]).toBe(row1); }); it('should render a chevron for an expandable row marked with hasChildRows', () => { @@ -585,28 +593,29 @@ describe('Tree', () => { }); it('should support actions on rows', async () => { - let {getAllByRole} = render(); + let {getByRole} = render(); + let treeTester = testUtilUser.createTester('Tree', {user, root: getByRole('treegrid')}); - let row = getAllByRole('row')[0]; - await user.click(row); + let rows = treeTester.rows; + await treeTester.triggerRowAction({row: rows[0]}); expect(onAction).toHaveBeenCalledTimes(1); expect(onAction).toHaveBeenLastCalledWith('Photos'); expect(onSelectionChange).toHaveBeenCalledTimes(0); // Due to disabledBehavior being set to 'all' this expandable row has its action disabled - let disabledRow = getAllByRole('row')[1]; + let disabledRow = rows[1]; expect(disabledRow).toHaveAttribute('data-disabled', 'true'); - await user.click(disabledRow); + await treeTester.triggerRowAction({row: disabledRow}); expect(onAction).toHaveBeenCalledTimes(1); expect(onSelectionChange).toHaveBeenCalledTimes(0); - let expandableRow = getAllByRole('row')[2]; - await user.click(expandableRow); + let expandableRow = rows[2]; + await treeTester.triggerRowAction({row: expandableRow}); expect(onAction).toHaveBeenCalledTimes(2); expect(onAction).toHaveBeenLastCalledWith('Projects-1'); expect(onSelectionChange).toHaveBeenCalledTimes(0); - await user.keyboard('{Enter}'); + await treeTester.triggerRowAction({row: expandableRow, interactionType: 'keyboard'}); expect(onAction).toHaveBeenCalledTimes(3); expect(onAction).toHaveBeenLastCalledWith('Projects-1'); expect(onSelectionChange).toHaveBeenCalledTimes(0); @@ -827,8 +836,9 @@ describe('Tree', () => { }; it('should expand/collapse a row when clicking/using Enter on the row itself and there arent any other primary actions', async () => { - let {getAllByRole} = render(); - let rows = getAllByRole('row'); + let {getByRole} = render(); + let treeTester = testUtilUser.createTester('Tree', {user, root: getByRole('treegrid')}); + let rows = treeTester.rows; expect(rows).toHaveLength(20); await user.tab(); @@ -842,7 +852,7 @@ describe('Tree', () => { expect(onExpandedChange).toHaveBeenCalledTimes(0); // Check we can open/close a top level row - await trigger(rows[0], 'Enter'); + await treeTester.toggleRowExpansion({row: rows[0], interactionType: type as 'mouse' | 'keyboard'}); expect(document.activeElement).toBe(rows[0]); expect(rows[0]).toHaveAttribute('aria-expanded', 'false'); expect(rows[0]).not.toHaveAttribute('data-expanded'); @@ -853,10 +863,10 @@ describe('Tree', () => { expect(onExpandedChange).toHaveBeenCalledTimes(1); // Note that the children of the parent row will still be in the "expanded" array expect(new Set(onExpandedChange.mock.calls[0][0])).toEqual(new Set(['Project-2', 'Project-5', 'Reports', 'Reports-1', 'Reports-1A', 'Reports-1AB'])); - rows = getAllByRole('row'); + rows = treeTester.rows; expect(rows).toHaveLength(9); - await trigger(rows[0], 'Enter'); + await treeTester.toggleRowExpansion({row: rows[0], interactionType: type as 'mouse' | 'keyboard'}); expect(document.activeElement).toBe(rows[0]); expect(rows[0]).toHaveAttribute('aria-expanded', 'true'); expect(rows[0]).toHaveAttribute('data-expanded', 'true'); @@ -866,7 +876,7 @@ describe('Tree', () => { expect(rows[0]).toHaveAttribute('data-has-child-rows', 'true'); expect(onExpandedChange).toHaveBeenCalledTimes(2); expect(new Set(onExpandedChange.mock.calls[1][0])).toEqual(new Set(['Projects', 'Project-2', 'Project-5', 'Reports', 'Reports-1', 'Reports-1A', 'Reports-1AB'])); - rows = getAllByRole('row'); + rows = treeTester.rows; expect(rows).toHaveLength(20); await user.keyboard('{ArrowDown}'); @@ -880,7 +890,7 @@ describe('Tree', () => { expect(rows[2]).toHaveAttribute('data-has-child-rows', 'true'); // Check we can close a nested row and it doesn't affect the parent - await trigger(rows[2], 'ArrowLeft'); + await treeTester.toggleRowExpansion({row: rows[2], interactionType: type as 'mouse' | 'keyboard'}); expect(document.activeElement).toBe(rows[2]); expect(rows[2]).toHaveAttribute('aria-expanded', 'false'); expect(rows[2]).not.toHaveAttribute('data-expanded'); @@ -896,25 +906,25 @@ describe('Tree', () => { expect(rows[0]).toHaveAttribute('data-has-child-rows', 'true'); expect(onExpandedChange).toHaveBeenCalledTimes(3); expect(new Set(onExpandedChange.mock.calls[2][0])).toEqual(new Set(['Projects', 'Project-5', 'Reports', 'Reports-1', 'Reports-1A', 'Reports-1AB'])); - rows = getAllByRole('row'); + rows = treeTester.rows; expect(rows).toHaveLength(17); // Check behavior of onExpandedChange when a nested row is already closed and the parent is collapsed await user.keyboard('{ArrowUp}'); await user.keyboard('{ArrowUp}'); - await trigger(rows[0], 'ArrowLeft'); + await treeTester.toggleRowExpansion({row: rows[0], interactionType: type as 'mouse' | 'keyboard'}); expect(document.activeElement).toBe(rows[0]); expect(onExpandedChange).toHaveBeenCalledTimes(4); expect(new Set(onExpandedChange.mock.calls[3][0])).toEqual(new Set(['Project-5', 'Reports', 'Reports-1', 'Reports-1A', 'Reports-1AB'])); - rows = getAllByRole('row'); + rows = treeTester.rows; expect(rows).toHaveLength(9); // Check that the nested collapsed row is still closed when the parent is reexpanded - await trigger(rows[0], 'ArrowRight'); + await treeTester.toggleRowExpansion({row: rows[0], interactionType: type as 'mouse' | 'keyboard'}); expect(document.activeElement).toBe(rows[0]); expect(onExpandedChange).toHaveBeenCalledTimes(5); expect(new Set(onExpandedChange.mock.calls[4][0])).toEqual(new Set(['Projects', 'Project-5', 'Reports', 'Reports-1', 'Reports-1A', 'Reports-1AB'])); - rows = getAllByRole('row'); + rows = treeTester.rows; expect(rows).toHaveLength(17); }); @@ -1138,7 +1148,7 @@ describe('Tree', () => { ); } - let {getAllByRole, getByRole} = render( + let {getByRole} = render( { ); - let tree = getByRole('treegrid'); + let treeTester = testUtilUser.createTester('Tree', {user, root: getByRole('treegrid')}); + let tree = treeTester.tree; expect(tree).toHaveAttribute('data-empty', 'true'); expect(tree).not.toHaveAttribute('data-focused'); expect(tree).not.toHaveAttribute('data-focus-visible'); - let row = getAllByRole('row')[0]; + let row = treeTester.rows[0]; expect(row).toHaveAttribute('aria-level', '1'); expect(row).toHaveAttribute('aria-posinset', '1'); expect(row).toHaveAttribute('aria-setsize', '1'); - let gridCell = within(row).getByRole('gridcell'); + let gridCell = treeTester.cells({element: row})[0]; expect(gridCell).toHaveTextContent('No resultsNo results found.'); await user.tab(); diff --git a/packages/@react-spectrum/utils/package.json b/packages/@react-spectrum/utils/package.json index d71cd754c00..66ad7444696 100644 --- a/packages/@react-spectrum/utils/package.json +++ b/packages/@react-spectrum/utils/package.json @@ -1,6 +1,6 @@ { "name": "@react-spectrum/utils", - "version": "3.12.0", + "version": "3.12.1", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -24,15 +24,16 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/i18n": "^3.12.4", + "@react-aria/i18n": "^3.12.5", "@react-aria/ssr": "^3.9.7", - "@react-aria/utils": "^3.26.0", - "@react-types/shared": "^3.26.0", + "@react-aria/utils": "^3.27.0", + "@react-types/shared": "^3.27.0", "@swc/helpers": "^0.5.0", "clsx": "^2.0.0" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" }, "publishConfig": { "access": "public" diff --git a/packages/@react-spectrum/utils/src/Slots.tsx b/packages/@react-spectrum/utils/src/Slots.tsx index e910ccd7048..d6aa6204ccf 100644 --- a/packages/@react-spectrum/utils/src/Slots.tsx +++ b/packages/@react-spectrum/utils/src/Slots.tsx @@ -36,7 +36,7 @@ export function cssModuleToSlots(cssModule) { export function SlotProvider(props) { const emptyObj = useMemo(() => ({}), []); - // eslint-disable-next-line react-hooks/exhaustive-deps + let parentSlots = useContext(SlotContext) || emptyObj; let {slots = emptyObj, children} = props; diff --git a/packages/@react-spectrum/view/package.json b/packages/@react-spectrum/view/package.json index 18a2d0afe2e..888af55a024 100644 --- a/packages/@react-spectrum/view/package.json +++ b/packages/@react-spectrum/view/package.json @@ -1,6 +1,6 @@ { "name": "@react-spectrum/view", - "version": "3.6.14", + "version": "3.6.15", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -36,10 +36,10 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/utils": "^3.26.0", - "@react-spectrum/utils": "^3.12.0", - "@react-types/shared": "^3.26.0", - "@react-types/view": "^3.4.13", + "@react-aria/utils": "^3.27.0", + "@react-spectrum/utils": "^3.12.1", + "@react-types/shared": "^3.27.0", + "@react-types/view": "^3.4.14", "@swc/helpers": "^0.5.0" }, "devDependencies": { @@ -47,7 +47,8 @@ }, "peerDependencies": { "@react-spectrum/provider": "^3.0.0", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" }, "publishConfig": { "access": "public" diff --git a/packages/@react-spectrum/well/package.json b/packages/@react-spectrum/well/package.json index 72ce9c4c6dc..2b40c40ea7a 100644 --- a/packages/@react-spectrum/well/package.json +++ b/packages/@react-spectrum/well/package.json @@ -1,6 +1,6 @@ { "name": "@react-spectrum/well", - "version": "3.4.18", + "version": "3.4.19", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -36,17 +36,18 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/utils": "^3.26.0", - "@react-spectrum/utils": "^3.12.0", - "@react-types/shared": "^3.26.0", - "@react-types/well": "^3.3.13", + "@react-aria/utils": "^3.27.0", + "@react-spectrum/utils": "^3.12.1", + "@react-types/shared": "^3.27.0", + "@react-types/well": "^3.3.14", "@swc/helpers": "^0.5.0" }, "devDependencies": { "@adobe/spectrum-css-temp": "3.0.0-alpha.1" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" }, "publishConfig": { "access": "public" diff --git a/packages/@react-stately/autocomplete/package.json b/packages/@react-stately/autocomplete/package.json index c8b6e31070f..bc5757931be 100644 --- a/packages/@react-stately/autocomplete/package.json +++ b/packages/@react-stately/autocomplete/package.json @@ -1,6 +1,6 @@ { "name": "@react-stately/autocomplete", - "version": "3.0.0-alpha.1", + "version": "3.0.0-alpha.0", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", diff --git a/packages/@react-stately/autocomplete/src/index.ts b/packages/@react-stately/autocomplete/src/index.ts index c95279ae2fb..4e067fcf24f 100644 --- a/packages/@react-stately/autocomplete/src/index.ts +++ b/packages/@react-stately/autocomplete/src/index.ts @@ -10,6 +10,6 @@ * governing permissions and limitations under the License. */ -export {useAutocompleteState} from './useAutocompleteState'; +export {UNSTABLE_useAutocompleteState} from './useAutocompleteState'; export type {AutocompleteProps, AutocompleteStateOptions, AutocompleteState} from './useAutocompleteState'; diff --git a/packages/@react-stately/autocomplete/src/useAutocompleteState.ts b/packages/@react-stately/autocomplete/src/useAutocompleteState.ts index 230682867d7..bb27d86db66 100644 --- a/packages/@react-stately/autocomplete/src/useAutocompleteState.ts +++ b/packages/@react-stately/autocomplete/src/useAutocompleteState.ts @@ -1,5 +1,5 @@ /* - * Copyright 2020 Adobe. All rights reserved. + * Copyright 2024 Adobe. All rights reserved. * This file is licensed to you under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. You may obtain a copy * of the License at http://www.apache.org/licenses/LICENSE-2.0 @@ -41,7 +41,7 @@ export interface AutocompleteStateOptions extends Omit(props: Calenda maxValue, selectionAlignment, isDateUnavailable, - pageBehavior = 'visible' + pageBehavior = 'visible', + firstDayOfWeek } = props; let calendar = useMemo(() => createCalendar(resolvedOptions.calendar), [createCalendar, resolvedOptions.calendar]); @@ -326,15 +327,14 @@ export function useCalendarState(props: Calenda return isSameDay(next, endDate) || this.isInvalid(next); }, getDatesInWeek(weekIndex, from = startDate) { - // let date = startOfWeek(from, locale); let date = from.add({weeks: weekIndex}); let dates: (CalendarDate | null)[] = []; - date = startOfWeek(date, locale); - + date = startOfWeek(date, locale, firstDayOfWeek); + // startOfWeek will clamp dates within the calendar system's valid range, which may // start in the middle of a week. In this case, add null placeholders. - let dayOfWeek = getDayOfWeek(date, locale); + let dayOfWeek = getDayOfWeek(date, locale, firstDayOfWeek); for (let i = 0; i < dayOfWeek; i++) { dates.push(null); } diff --git a/packages/@react-stately/checkbox/package.json b/packages/@react-stately/checkbox/package.json index 6ece3639a3b..87a511ded10 100644 --- a/packages/@react-stately/checkbox/package.json +++ b/packages/@react-stately/checkbox/package.json @@ -1,6 +1,6 @@ { "name": "@react-stately/checkbox", - "version": "3.6.10", + "version": "3.6.11", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -22,10 +22,10 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-stately/form": "^3.1.0", + "@react-stately/form": "^3.1.1", "@react-stately/utils": "^3.10.5", - "@react-types/checkbox": "^3.9.0", - "@react-types/shared": "^3.26.0", + "@react-types/checkbox": "^3.9.1", + "@react-types/shared": "^3.27.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { diff --git a/packages/@react-stately/collections/package.json b/packages/@react-stately/collections/package.json index 997a112b7bf..14c4e38d690 100644 --- a/packages/@react-stately/collections/package.json +++ b/packages/@react-stately/collections/package.json @@ -1,6 +1,6 @@ { "name": "@react-stately/collections", - "version": "3.12.0", + "version": "3.12.1", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -22,7 +22,7 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-types/shared": "^3.26.0", + "@react-types/shared": "^3.27.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { diff --git a/packages/@react-stately/color/package.json b/packages/@react-stately/color/package.json index 77baee5c306..7ec483c9852 100644 --- a/packages/@react-stately/color/package.json +++ b/packages/@react-stately/color/package.json @@ -1,6 +1,6 @@ { "name": "@react-stately/color", - "version": "3.8.1", + "version": "3.8.2", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -24,13 +24,12 @@ "dependencies": { "@internationalized/number": "^3.6.0", "@internationalized/string": "^3.2.5", - "@react-aria/i18n": "^3.12.4", - "@react-stately/form": "^3.1.0", - "@react-stately/numberfield": "^3.9.8", - "@react-stately/slider": "^3.6.0", + "@react-stately/form": "^3.1.1", + "@react-stately/numberfield": "^3.9.9", + "@react-stately/slider": "^3.6.1", "@react-stately/utils": "^3.10.5", - "@react-types/color": "^3.0.1", - "@react-types/shared": "^3.26.0", + "@react-types/color": "^3.0.2", + "@react-types/shared": "^3.27.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { diff --git a/packages/@react-stately/combobox/package.json b/packages/@react-stately/combobox/package.json index 8ffa498ed54..912e32c983d 100644 --- a/packages/@react-stately/combobox/package.json +++ b/packages/@react-stately/combobox/package.json @@ -1,6 +1,6 @@ { "name": "@react-stately/combobox", - "version": "3.10.1", + "version": "3.10.2", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -22,14 +22,14 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-stately/collections": "^3.12.0", - "@react-stately/form": "^3.1.0", - "@react-stately/list": "^3.11.1", - "@react-stately/overlays": "^3.6.12", - "@react-stately/select": "^3.6.9", + "@react-stately/collections": "^3.12.1", + "@react-stately/form": "^3.1.1", + "@react-stately/list": "^3.11.2", + "@react-stately/overlays": "^3.6.13", + "@react-stately/select": "^3.6.10", "@react-stately/utils": "^3.10.5", - "@react-types/combobox": "^3.13.1", - "@react-types/shared": "^3.26.0", + "@react-types/combobox": "^3.13.2", + "@react-types/shared": "^3.27.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { diff --git a/packages/@react-stately/data/package.json b/packages/@react-stately/data/package.json index 93d73d2ead2..f9b7febe965 100644 --- a/packages/@react-stately/data/package.json +++ b/packages/@react-stately/data/package.json @@ -1,6 +1,6 @@ { "name": "@react-stately/data", - "version": "3.12.0", + "version": "3.12.1", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -22,7 +22,7 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-types/shared": "^3.26.0", + "@react-types/shared": "^3.27.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { diff --git a/packages/@react-stately/datepicker/package.json b/packages/@react-stately/datepicker/package.json index 484fde822ea..c112f43b7e9 100644 --- a/packages/@react-stately/datepicker/package.json +++ b/packages/@react-stately/datepicker/package.json @@ -1,6 +1,6 @@ { "name": "@react-stately/datepicker", - "version": "3.11.0", + "version": "3.12.0", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -22,13 +22,13 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@internationalized/date": "^3.6.0", + "@internationalized/date": "^3.7.0", "@internationalized/string": "^3.2.5", - "@react-stately/form": "^3.1.0", - "@react-stately/overlays": "^3.6.12", + "@react-stately/form": "^3.1.1", + "@react-stately/overlays": "^3.6.13", "@react-stately/utils": "^3.10.5", - "@react-types/datepicker": "^3.9.0", - "@react-types/shared": "^3.26.0", + "@react-types/datepicker": "^3.10.0", + "@react-types/shared": "^3.27.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { diff --git a/packages/@react-stately/disclosure/package.json b/packages/@react-stately/disclosure/package.json index 0a463cbe1b1..7071c0734fa 100644 --- a/packages/@react-stately/disclosure/package.json +++ b/packages/@react-stately/disclosure/package.json @@ -1,6 +1,6 @@ { "name": "@react-stately/disclosure", - "version": "3.0.0", + "version": "3.0.1", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -23,7 +23,7 @@ }, "dependencies": { "@react-stately/utils": "^3.10.5", - "@react-types/shared": "^3.26.0", + "@react-types/shared": "^3.27.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { diff --git a/packages/@react-stately/dnd/package.json b/packages/@react-stately/dnd/package.json index 459bcab7b1e..1f076abe53f 100644 --- a/packages/@react-stately/dnd/package.json +++ b/packages/@react-stately/dnd/package.json @@ -1,6 +1,6 @@ { "name": "@react-stately/dnd", - "version": "3.5.0", + "version": "3.5.1", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -22,8 +22,8 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-stately/selection": "^3.18.0", - "@react-types/shared": "^3.26.0", + "@react-stately/selection": "^3.19.0", + "@react-types/shared": "^3.27.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { diff --git a/packages/@react-stately/form/package.json b/packages/@react-stately/form/package.json index 3d745e4a749..658da7c5cef 100644 --- a/packages/@react-stately/form/package.json +++ b/packages/@react-stately/form/package.json @@ -1,6 +1,6 @@ { "name": "@react-stately/form", - "version": "3.1.0", + "version": "3.1.1", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -22,7 +22,7 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-types/shared": "^3.26.0", + "@react-types/shared": "^3.27.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { diff --git a/packages/@react-stately/grid/package.json b/packages/@react-stately/grid/package.json index 2adbfe2be56..df1c4d1340c 100644 --- a/packages/@react-stately/grid/package.json +++ b/packages/@react-stately/grid/package.json @@ -1,6 +1,6 @@ { "name": "@react-stately/grid", - "version": "3.10.0", + "version": "3.10.1", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -22,10 +22,10 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-stately/collections": "^3.12.0", - "@react-stately/selection": "^3.18.0", - "@react-types/grid": "^3.2.10", - "@react-types/shared": "^3.26.0", + "@react-stately/collections": "^3.12.1", + "@react-stately/selection": "^3.19.0", + "@react-types/grid": "^3.2.11", + "@react-types/shared": "^3.27.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { diff --git a/packages/@react-stately/layout/package.json b/packages/@react-stately/layout/package.json index 76b9cdd4fd2..30f1ade1672 100644 --- a/packages/@react-stately/layout/package.json +++ b/packages/@react-stately/layout/package.json @@ -1,6 +1,6 @@ { "name": "@react-stately/layout", - "version": "4.1.0", + "version": "4.1.1", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -22,12 +22,12 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-stately/collections": "^3.12.0", - "@react-stately/table": "^3.13.0", - "@react-stately/virtualizer": "^4.2.0", - "@react-types/grid": "^3.2.10", - "@react-types/shared": "^3.26.0", - "@react-types/table": "^3.10.3", + "@react-stately/collections": "^3.12.1", + "@react-stately/table": "^3.13.1", + "@react-stately/virtualizer": "^4.2.1", + "@react-types/grid": "^3.2.11", + "@react-types/shared": "^3.27.0", + "@react-types/table": "^3.10.4", "@swc/helpers": "^0.5.0" }, "peerDependencies": { diff --git a/packages/@react-stately/list/package.json b/packages/@react-stately/list/package.json index 1dc5d906424..e807e5807db 100644 --- a/packages/@react-stately/list/package.json +++ b/packages/@react-stately/list/package.json @@ -1,6 +1,6 @@ { "name": "@react-stately/list", - "version": "3.11.1", + "version": "3.11.2", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -22,10 +22,10 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-stately/collections": "^3.12.0", - "@react-stately/selection": "^3.18.0", + "@react-stately/collections": "^3.12.1", + "@react-stately/selection": "^3.19.0", "@react-stately/utils": "^3.10.5", - "@react-types/shared": "^3.26.0", + "@react-types/shared": "^3.27.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { diff --git a/packages/@react-stately/menu/package.json b/packages/@react-stately/menu/package.json index ec173acb626..3d304855c87 100644 --- a/packages/@react-stately/menu/package.json +++ b/packages/@react-stately/menu/package.json @@ -1,6 +1,6 @@ { "name": "@react-stately/menu", - "version": "3.9.0", + "version": "3.9.1", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -22,9 +22,9 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-stately/overlays": "^3.6.12", - "@react-types/menu": "^3.9.13", - "@react-types/shared": "^3.26.0", + "@react-stately/overlays": "^3.6.13", + "@react-types/menu": "^3.9.14", + "@react-types/shared": "^3.27.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { diff --git a/packages/@react-stately/numberfield/package.json b/packages/@react-stately/numberfield/package.json index 79fb7a6b484..4d57b435dd8 100644 --- a/packages/@react-stately/numberfield/package.json +++ b/packages/@react-stately/numberfield/package.json @@ -1,6 +1,6 @@ { "name": "@react-stately/numberfield", - "version": "3.9.8", + "version": "3.9.9", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -23,9 +23,9 @@ }, "dependencies": { "@internationalized/number": "^3.6.0", - "@react-stately/form": "^3.1.0", + "@react-stately/form": "^3.1.1", "@react-stately/utils": "^3.10.5", - "@react-types/numberfield": "^3.8.7", + "@react-types/numberfield": "^3.8.8", "@swc/helpers": "^0.5.0" }, "peerDependencies": { diff --git a/packages/@react-stately/overlays/package.json b/packages/@react-stately/overlays/package.json index 82353fec877..5f71578fdac 100644 --- a/packages/@react-stately/overlays/package.json +++ b/packages/@react-stately/overlays/package.json @@ -1,6 +1,6 @@ { "name": "@react-stately/overlays", - "version": "3.6.12", + "version": "3.6.13", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -23,7 +23,7 @@ }, "dependencies": { "@react-stately/utils": "^3.10.5", - "@react-types/overlays": "^3.8.11", + "@react-types/overlays": "^3.8.12", "@swc/helpers": "^0.5.0" }, "peerDependencies": { diff --git a/packages/@react-stately/radio/package.json b/packages/@react-stately/radio/package.json index ed5fdf95cc8..f716e180e76 100644 --- a/packages/@react-stately/radio/package.json +++ b/packages/@react-stately/radio/package.json @@ -1,6 +1,6 @@ { "name": "@react-stately/radio", - "version": "3.10.9", + "version": "3.10.10", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -22,10 +22,10 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-stately/form": "^3.1.0", + "@react-stately/form": "^3.1.1", "@react-stately/utils": "^3.10.5", - "@react-types/radio": "^3.8.5", - "@react-types/shared": "^3.26.0", + "@react-types/radio": "^3.8.6", + "@react-types/shared": "^3.27.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { diff --git a/packages/@react-stately/searchfield/package.json b/packages/@react-stately/searchfield/package.json index f684ee40be0..648c7d69904 100644 --- a/packages/@react-stately/searchfield/package.json +++ b/packages/@react-stately/searchfield/package.json @@ -1,6 +1,6 @@ { "name": "@react-stately/searchfield", - "version": "3.5.8", + "version": "3.5.9", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -23,7 +23,7 @@ }, "dependencies": { "@react-stately/utils": "^3.10.5", - "@react-types/searchfield": "^3.5.10", + "@react-types/searchfield": "^3.5.11", "@swc/helpers": "^0.5.0" }, "peerDependencies": { diff --git a/packages/@react-stately/select/package.json b/packages/@react-stately/select/package.json index 0ed5275209c..48066b12f74 100644 --- a/packages/@react-stately/select/package.json +++ b/packages/@react-stately/select/package.json @@ -1,6 +1,6 @@ { "name": "@react-stately/select", - "version": "3.6.9", + "version": "3.6.10", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -22,11 +22,11 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-stately/form": "^3.1.0", - "@react-stately/list": "^3.11.1", - "@react-stately/overlays": "^3.6.12", - "@react-types/select": "^3.9.8", - "@react-types/shared": "^3.26.0", + "@react-stately/form": "^3.1.1", + "@react-stately/list": "^3.11.2", + "@react-stately/overlays": "^3.6.13", + "@react-types/select": "^3.9.9", + "@react-types/shared": "^3.27.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { diff --git a/packages/@react-stately/selection/package.json b/packages/@react-stately/selection/package.json index e5c014a3893..0fd68b19f5b 100644 --- a/packages/@react-stately/selection/package.json +++ b/packages/@react-stately/selection/package.json @@ -1,6 +1,6 @@ { "name": "@react-stately/selection", - "version": "3.18.0", + "version": "3.19.0", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -22,9 +22,9 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-stately/collections": "^3.12.0", + "@react-stately/collections": "^3.12.1", "@react-stately/utils": "^3.10.5", - "@react-types/shared": "^3.26.0", + "@react-types/shared": "^3.27.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { diff --git a/packages/@react-stately/selection/src/types.ts b/packages/@react-stately/selection/src/types.ts index 30dbee9c0b1..8eafe03307b 100644 --- a/packages/@react-stately/selection/src/types.ts +++ b/packages/@react-stately/selection/src/types.ts @@ -10,7 +10,7 @@ * governing permissions and limitations under the License. */ -import {DisabledBehavior, FocusStrategy, Key, LongPressEvent, PressEvent, Selection, SelectionBehavior, SelectionMode} from '@react-types/shared'; +import {Collection, DisabledBehavior, FocusStrategy, Key, LongPressEvent, Node, PressEvent, Selection, SelectionBehavior, SelectionMode} from '@react-types/shared'; export interface FocusState { @@ -107,5 +107,7 @@ export interface MultipleSelectionManager extends FocusState { /** Returns whether the given key is a hyperlink. */ isLink(key: Key): boolean, /** Returns the props for the given item. */ - getItemProps(key: Key): any + getItemProps(key: Key): any, + /** The collection of nodes that the selection manager handles. */ + collection: Collection> } diff --git a/packages/@react-stately/slider/package.json b/packages/@react-stately/slider/package.json index 8b3997df69a..8df5f8e4e94 100644 --- a/packages/@react-stately/slider/package.json +++ b/packages/@react-stately/slider/package.json @@ -1,6 +1,6 @@ { "name": "@react-stately/slider", - "version": "3.6.0", + "version": "3.6.1", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -23,8 +23,8 @@ }, "dependencies": { "@react-stately/utils": "^3.10.5", - "@react-types/shared": "^3.26.0", - "@react-types/slider": "^3.7.7", + "@react-types/shared": "^3.27.0", + "@react-types/slider": "^3.7.8", "@swc/helpers": "^0.5.0" }, "peerDependencies": { diff --git a/packages/@react-stately/steplist/package.json b/packages/@react-stately/steplist/package.json index 4ce1614e595..f22d08605dd 100644 --- a/packages/@react-stately/steplist/package.json +++ b/packages/@react-stately/steplist/package.json @@ -1,6 +1,6 @@ { "name": "@react-stately/steplist", - "version": "3.0.0-alpha.10", + "version": "3.0.0-alpha.11", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -22,9 +22,9 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-stately/list": "^3.11.1", + "@react-stately/list": "^3.11.2", "@react-stately/utils": "^3.10.5", - "@react-types/shared": "^3.26.0", + "@react-types/shared": "^3.27.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { diff --git a/packages/@react-stately/table/package.json b/packages/@react-stately/table/package.json index 7bd86202449..0d63ab248d7 100644 --- a/packages/@react-stately/table/package.json +++ b/packages/@react-stately/table/package.json @@ -1,6 +1,6 @@ { "name": "@react-stately/table", - "version": "3.13.0", + "version": "3.13.1", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -22,14 +22,14 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-stately/collections": "^3.12.0", + "@react-stately/collections": "^3.12.1", "@react-stately/flags": "^3.0.5", - "@react-stately/grid": "^3.10.0", - "@react-stately/selection": "^3.18.0", + "@react-stately/grid": "^3.10.1", + "@react-stately/selection": "^3.19.0", "@react-stately/utils": "^3.10.5", - "@react-types/grid": "^3.2.10", - "@react-types/shared": "^3.26.0", - "@react-types/table": "^3.10.3", + "@react-types/grid": "^3.2.11", + "@react-types/shared": "^3.27.0", + "@react-types/table": "^3.10.4", "@swc/helpers": "^0.5.0" }, "peerDependencies": { diff --git a/packages/@react-stately/tabs/package.json b/packages/@react-stately/tabs/package.json index b91e4a3d9bd..99fca2604e1 100644 --- a/packages/@react-stately/tabs/package.json +++ b/packages/@react-stately/tabs/package.json @@ -1,6 +1,6 @@ { "name": "@react-stately/tabs", - "version": "3.7.0", + "version": "3.7.1", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -22,9 +22,9 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-stately/list": "^3.11.1", - "@react-types/shared": "^3.26.0", - "@react-types/tabs": "^3.3.11", + "@react-stately/list": "^3.11.2", + "@react-types/shared": "^3.27.0", + "@react-types/tabs": "^3.3.12", "@swc/helpers": "^0.5.0" }, "peerDependencies": { diff --git a/packages/@react-stately/toggle/package.json b/packages/@react-stately/toggle/package.json index 3186644f0de..00e71258ec5 100644 --- a/packages/@react-stately/toggle/package.json +++ b/packages/@react-stately/toggle/package.json @@ -1,6 +1,6 @@ { "name": "@react-stately/toggle", - "version": "3.8.0", + "version": "3.8.1", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -23,8 +23,8 @@ }, "dependencies": { "@react-stately/utils": "^3.10.5", - "@react-types/checkbox": "^3.9.0", - "@react-types/shared": "^3.26.0", + "@react-types/checkbox": "^3.9.1", + "@react-types/shared": "^3.27.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { diff --git a/packages/@react-stately/tooltip/package.json b/packages/@react-stately/tooltip/package.json index 7d3d48a7e0b..71bafc183a0 100644 --- a/packages/@react-stately/tooltip/package.json +++ b/packages/@react-stately/tooltip/package.json @@ -1,6 +1,6 @@ { "name": "@react-stately/tooltip", - "version": "3.5.0", + "version": "3.5.1", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -22,8 +22,8 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-stately/overlays": "^3.6.12", - "@react-types/tooltip": "^3.4.13", + "@react-stately/overlays": "^3.6.13", + "@react-types/tooltip": "^3.4.14", "@swc/helpers": "^0.5.0" }, "peerDependencies": { diff --git a/packages/@react-stately/tree/package.json b/packages/@react-stately/tree/package.json index 08147613f3f..23b5ea24a2f 100644 --- a/packages/@react-stately/tree/package.json +++ b/packages/@react-stately/tree/package.json @@ -1,6 +1,6 @@ { "name": "@react-stately/tree", - "version": "3.8.6", + "version": "3.8.7", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -22,10 +22,10 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-stately/collections": "^3.12.0", - "@react-stately/selection": "^3.18.0", + "@react-stately/collections": "^3.12.1", + "@react-stately/selection": "^3.19.0", "@react-stately/utils": "^3.10.5", - "@react-types/shared": "^3.26.0", + "@react-types/shared": "^3.27.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { diff --git a/packages/@react-stately/virtualizer/package.json b/packages/@react-stately/virtualizer/package.json index 035af9fae7f..d835bd5a7b0 100644 --- a/packages/@react-stately/virtualizer/package.json +++ b/packages/@react-stately/virtualizer/package.json @@ -1,6 +1,6 @@ { "name": "@react-stately/virtualizer", - "version": "4.2.0", + "version": "4.2.1", "description": "Spectrum UI components in React", "license": "Apache-2.0", "main": "dist/main.js", @@ -22,12 +22,13 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-aria/utils": "^3.26.0", - "@react-types/shared": "^3.26.0", + "@react-aria/utils": "^3.27.0", + "@react-types/shared": "^3.27.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" }, "publishConfig": { "access": "public" diff --git a/packages/@react-types/accordion/package.json b/packages/@react-types/accordion/package.json index 3f86c0a400b..5c9c1001b23 100644 --- a/packages/@react-types/accordion/package.json +++ b/packages/@react-types/accordion/package.json @@ -1,6 +1,6 @@ { "name": "@react-types/accordion", - "version": "3.0.0-alpha.25", + "version": "3.0.0-alpha.26", "description": "Spectrum UI components in React", "license": "Apache-2.0", "types": "src/index.d.ts", @@ -9,7 +9,7 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-types/shared": "^3.26.0" + "@react-types/shared": "^3.27.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" diff --git a/packages/@react-types/actionbar/package.json b/packages/@react-types/actionbar/package.json index 81b2ef3f91e..0d55b482c20 100644 --- a/packages/@react-types/actionbar/package.json +++ b/packages/@react-types/actionbar/package.json @@ -1,6 +1,6 @@ { "name": "@react-types/actionbar", - "version": "3.1.11", + "version": "3.1.12", "description": "Spectrum UI components in React", "license": "Apache-2.0", "types": "src/index.d.ts", @@ -9,7 +9,7 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-types/shared": "^3.26.0" + "@react-types/shared": "^3.27.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" diff --git a/packages/@react-types/actiongroup/package.json b/packages/@react-types/actiongroup/package.json index ca848c0b40e..78da60815a9 100644 --- a/packages/@react-types/actiongroup/package.json +++ b/packages/@react-types/actiongroup/package.json @@ -1,6 +1,6 @@ { "name": "@react-types/actiongroup", - "version": "3.4.13", + "version": "3.4.14", "description": "Spectrum UI components in React", "license": "Apache-2.0", "types": "src/index.d.ts", @@ -9,7 +9,7 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-types/shared": "^3.26.0" + "@react-types/shared": "^3.27.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" diff --git a/packages/@react-types/autocomplete/package.json b/packages/@react-types/autocomplete/package.json index 53a0c5d1ddf..d8041a3563d 100644 --- a/packages/@react-types/autocomplete/package.json +++ b/packages/@react-types/autocomplete/package.json @@ -1,6 +1,6 @@ { "name": "@react-types/autocomplete", - "version": "3.0.0-alpha.27", + "version": "3.0.0-alpha.28", "description": "Spectrum UI components in React", "license": "Apache-2.0", "types": "src/index.d.ts", @@ -9,9 +9,9 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-types/combobox": "^3.13.1", - "@react-types/searchfield": "^3.5.10", - "@react-types/shared": "^3.26.0" + "@react-types/combobox": "^3.13.2", + "@react-types/searchfield": "^3.5.11", + "@react-types/shared": "^3.27.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" diff --git a/packages/@react-types/avatar/package.json b/packages/@react-types/avatar/package.json index 4914c53a9c4..8ba3700c0ab 100644 --- a/packages/@react-types/avatar/package.json +++ b/packages/@react-types/avatar/package.json @@ -1,6 +1,6 @@ { "name": "@react-types/avatar", - "version": "3.0.11", + "version": "3.0.12", "description": "Spectrum UI components in React", "license": "Apache-2.0", "types": "src/index.d.ts", @@ -9,7 +9,7 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-types/shared": "^3.26.0" + "@react-types/shared": "^3.27.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" diff --git a/packages/@react-types/badge/package.json b/packages/@react-types/badge/package.json index 8873670026b..f89c3c53acc 100644 --- a/packages/@react-types/badge/package.json +++ b/packages/@react-types/badge/package.json @@ -1,6 +1,6 @@ { "name": "@react-types/badge", - "version": "3.1.13", + "version": "3.1.14", "description": "Spectrum UI components in React", "license": "Apache-2.0", "types": "src/index.d.ts", @@ -9,7 +9,7 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-types/shared": "^3.26.0" + "@react-types/shared": "^3.27.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" diff --git a/packages/@react-types/breadcrumbs/package.json b/packages/@react-types/breadcrumbs/package.json index 4122f2e447d..72e6421df27 100644 --- a/packages/@react-types/breadcrumbs/package.json +++ b/packages/@react-types/breadcrumbs/package.json @@ -1,6 +1,6 @@ { "name": "@react-types/breadcrumbs", - "version": "3.7.9", + "version": "3.7.10", "description": "Spectrum UI components in React", "license": "Apache-2.0", "types": "src/index.d.ts", @@ -9,8 +9,8 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-types/link": "^3.5.9", - "@react-types/shared": "^3.26.0" + "@react-types/link": "^3.5.10", + "@react-types/shared": "^3.27.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" diff --git a/packages/@react-types/button/package.json b/packages/@react-types/button/package.json index 5e0b5ab2443..9ddd9eb6635 100644 --- a/packages/@react-types/button/package.json +++ b/packages/@react-types/button/package.json @@ -1,6 +1,6 @@ { "name": "@react-types/button", - "version": "3.10.1", + "version": "3.10.2", "description": "Spectrum UI components in React", "license": "Apache-2.0", "types": "src/index.d.ts", @@ -9,7 +9,7 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-types/shared": "^3.26.0" + "@react-types/shared": "^3.27.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" diff --git a/packages/@react-types/buttongroup/package.json b/packages/@react-types/buttongroup/package.json index 279ba848a3c..283c90a4954 100644 --- a/packages/@react-types/buttongroup/package.json +++ b/packages/@react-types/buttongroup/package.json @@ -1,6 +1,6 @@ { "name": "@react-types/buttongroup", - "version": "3.3.13", + "version": "3.3.14", "description": "Spectrum UI components in React", "license": "Apache-2.0", "types": "src/index.d.ts", @@ -9,7 +9,7 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-types/shared": "^3.26.0" + "@react-types/shared": "^3.27.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" diff --git a/packages/@react-types/calendar/package.json b/packages/@react-types/calendar/package.json index 1b38fd14eeb..6f0d3101cea 100644 --- a/packages/@react-types/calendar/package.json +++ b/packages/@react-types/calendar/package.json @@ -1,6 +1,6 @@ { "name": "@react-types/calendar", - "version": "3.5.0", + "version": "3.6.0", "description": "Spectrum UI components in React", "license": "Apache-2.0", "types": "src/index.d.ts", @@ -9,8 +9,8 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@internationalized/date": "^3.6.0", - "@react-types/shared": "^3.26.0" + "@internationalized/date": "^3.7.0", + "@react-types/shared": "^3.27.0" }, "publishConfig": { "access": "public" diff --git a/packages/@react-types/calendar/src/index.d.ts b/packages/@react-types/calendar/src/index.d.ts index 0fc6d643559..29ec9945a13 100644 --- a/packages/@react-types/calendar/src/index.d.ts +++ b/packages/@react-types/calendar/src/index.d.ts @@ -62,7 +62,11 @@ export interface CalendarPropsBase { * Controls the behavior of paging. Pagination either works by advancing the visible page by visibleDuration (default) or one unit of visibleDuration. * @default visible */ - pageBehavior?: PageBehavior + pageBehavior?: PageBehavior, + /** + * The day that starts the week. + */ + firstDayOfWeek?: 'sun' | 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat' } export type DateRange = RangeValue | null; diff --git a/packages/@react-types/card/package.json b/packages/@react-types/card/package.json index 440668e64ff..8fb616719b3 100644 --- a/packages/@react-types/card/package.json +++ b/packages/@react-types/card/package.json @@ -1,6 +1,6 @@ { "name": "@react-types/card", - "version": "3.0.0-alpha.31", + "version": "3.0.0-alpha.32", "description": "Spectrum UI components in React", "license": "Apache-2.0", "types": "src/index.d.ts", @@ -9,9 +9,9 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-stately/virtualizer": "^4.2.0", - "@react-types/provider": "^3.8.5", - "@react-types/shared": "^3.26.0" + "@react-stately/virtualizer": "^4.2.1", + "@react-types/provider": "^3.8.6", + "@react-types/shared": "^3.27.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" diff --git a/packages/@react-types/checkbox/package.json b/packages/@react-types/checkbox/package.json index d0ccf57d7e6..e21ad00464c 100644 --- a/packages/@react-types/checkbox/package.json +++ b/packages/@react-types/checkbox/package.json @@ -1,6 +1,6 @@ { "name": "@react-types/checkbox", - "version": "3.9.0", + "version": "3.9.1", "description": "Spectrum UI components in React", "license": "Apache-2.0", "types": "src/index.d.ts", @@ -9,7 +9,7 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-types/shared": "^3.26.0" + "@react-types/shared": "^3.27.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" diff --git a/packages/@react-types/color/package.json b/packages/@react-types/color/package.json index 6ab92db4511..36badabc86c 100644 --- a/packages/@react-types/color/package.json +++ b/packages/@react-types/color/package.json @@ -1,6 +1,6 @@ { "name": "@react-types/color", - "version": "3.0.1", + "version": "3.0.2", "description": "Spectrum UI components in React", "license": "Apache-2.0", "types": "src/index.d.ts", @@ -9,8 +9,8 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@react-types/shared": "^3.26.0", - "@react-types/slider": "^3.7.7" + "@react-types/shared": "^3.27.0", + "@react-types/slider": "^3.7.8" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" diff --git a/packages/@react-types/color/src/index.d.ts b/packages/@react-types/color/src/index.d.ts index 9112e35b81d..7350c51497d 100644 --- a/packages/@react-types/color/src/index.d.ts +++ b/packages/@react-types/color/src/index.d.ts @@ -118,14 +118,14 @@ export interface ColorFieldProps extends Omit, onChange?: (color: Color | null) => void } -export interface AriaColorFieldProps extends ColorFieldProps, AriaLabelingProps, FocusableDOMProps, Omit, AriaValidationProps { +export interface AriaColorFieldProps extends ColorFieldProps, AriaLabelingProps, FocusableDOMProps, Omit, AriaValidationProps { /** Enables or disables changing the value with scroll. */ isWheelDisabled?: boolean } export interface SpectrumColorFieldProps extends SpectrumTextInputBase, Omit, SpectrumFieldValidation, SpectrumLabelableProps, StyleProps { /** - * The color channel that this field edits. If not provided, + * The color channel that this field edits. If not provided, * the color is edited as a hex value. */ channel?: ColorChannel, @@ -160,7 +160,7 @@ export interface SpectrumColorWheelProps extends AriaColorWheelProps, Omit
    ))}