diff --git a/packages/@react-aria/test-utils/src/index.ts b/packages/@react-aria/test-utils/src/index.ts index b5b7da34492..7ad297d0245 100644 --- a/packages/@react-aria/test-utils/src/index.ts +++ b/packages/@react-aria/test-utils/src/index.ts @@ -14,5 +14,16 @@ export {triggerLongPress} from './events'; export {installMouseEvent, installPointerEvent} from './testSetup'; export {pointerMap} from './userEventMaps'; export {User} from './user'; +export type {CheckboxGroupTester} from './checkboxgroup'; +export type {ComboBoxTester} from './combobox'; +export type {DialogTester} from './dialog'; +export type {GridListTester} from './gridlist'; +export type {ListBoxTester} from './listbox'; +export type {MenuTester} from './menu'; +export type {RadioGroupTester} from './radiogroup'; +export type {SelectTester} from './select'; +export type {TableTester} from './table'; +export type {TabsTester} from './tabs'; +export type {TreeTester} from './tree'; export type {UserOpts} from './types'; diff --git a/packages/@react-spectrum/checkbox/docs/CheckboxGroup.mdx b/packages/@react-spectrum/checkbox/docs/CheckboxGroup.mdx index 67dd8eba6b7..91e27b8197e 100644 --- a/packages/@react-spectrum/checkbox/docs/CheckboxGroup.mdx +++ b/packages/@react-spectrum/checkbox/docs/CheckboxGroup.mdx @@ -11,8 +11,9 @@ import {Layout} from '@react-spectrum/docs'; export default Layout; import docs from 'docs:@react-spectrum/checkbox'; +import checkboxgroupUtil from 'docs:@react-aria/test-utils/src/checkboxgroup.ts'; import packageData from '@react-spectrum/checkbox/package.json'; -import {HeaderInfo, PropTable, PageDescription} from '@react-spectrum/docs'; +import {HeaderInfo, PropTable, PageDescription, VersionBadge, ClassAPI} from '@react-spectrum/docs'; ```jsx import import {Checkbox, CheckboxGroup} from '@react-spectrum/checkbox'; @@ -332,3 +333,43 @@ See the [MDN docs](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/ Basketball ``` + +## Testing + +### Test utils + +`@react-spectrum/test-utils` offers common checkbox group interaction utilities which you may find helpful when writing tests. See [here](./testing.html#react-spectrum-test-utils) for more information on how to setup these utilities +in your tests. Below is the full definition of the checkbox group tester and a sample of how you could use it in your test suite. + +```ts +// CheckboxGroup.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', advanceTimer: jest.advanceTimersByTime}); +// ... + +it('CheckboxGroup can select multiple checkboxes', async function () { + // Render your test component/app and initialize the checkbox group tester + let {getByTestId} = render( + + + ... + + + ); + let checkboxGroupTester = testUtilUser.createTester('CheckboxGroup', {root: getByTestId('test-checkboxgroup')}); + expect(checkboxGroupTester.selectedCheckboxes).toHaveLength(0); + + await checkboxGroupTester.toggleCheckbox({checkbox: 0}); + expect(checkboxGroupTester.checkboxes[0]).toBeChecked(); + expect(checkboxGroupTester.selectedCheckboxes).toHaveLength(1); + + await checkboxGroupTester.toggleCheckbox({checkbox: 4}); + expect(checkboxGroupTester.checkboxes[4]).toBeChecked(); + expect(checkboxGroupTester.selectedCheckboxes).toHaveLength(2); +}); +``` + + diff --git a/packages/@react-spectrum/combobox/docs/ComboBox.mdx b/packages/@react-spectrum/combobox/docs/ComboBox.mdx index 81ccd82d6a4..ff582b53d73 100644 --- a/packages/@react-spectrum/combobox/docs/ComboBox.mdx +++ b/packages/@react-spectrum/combobox/docs/ComboBox.mdx @@ -996,7 +996,7 @@ 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 +`@react-spectrum/test-utils` offers common combobox interaction utilities which you may find helpful when writing tests. See [here](./testing.html#react-spectrum-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 diff --git a/packages/@react-spectrum/dialog/docs/Dialog.mdx b/packages/@react-spectrum/dialog/docs/Dialog.mdx index 6bfd237b82e..3353600dec6 100644 --- a/packages/@react-spectrum/dialog/docs/Dialog.mdx +++ b/packages/@react-spectrum/dialog/docs/Dialog.mdx @@ -12,7 +12,8 @@ export default Layout; import DialogAnatomy from './images/DialogAnatomy.svg'; import docs from 'docs:@react-spectrum/dialog'; -import {Image, HeaderInfo, PropTable, PageDescription} from '@react-spectrum/docs'; +import dialogUtil from 'docs:@react-aria/test-utils/src/dialog.ts'; +import {Image, HeaderInfo, PropTable, PageDescription, VersionBadge, ClassAPI} from '@react-spectrum/docs'; import packageData from '@react-spectrum/dialog/package.json'; import styles from '@react-spectrum/docs/src/docs.css'; @@ -398,3 +399,43 @@ respectively for container sizing considerations. Modal sizes on mobile devices )} ``` + +## Testing + +### Test utils + +`@react-spectrum/test-utils` offers common dialog interaction utilities which you may find helpful when writing tests. See [here](./testing.html#react-spectrum-test-utils) for more information on how to setup these utilities +in your tests. Below is the full definition of the dialog tester and a sample of how you could use it in your test suite. + +```ts +// Dialog.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', advanceTimer: jest.advanceTimersByTime}); +// ... + +it('Dialog can be opened and closed', async function () { + // Render your test component/app and initialize the dialog tester + let {getByTestId, getByRole} = render( + + + Trigger + + ... + + + + ); + let button = getByRole('button'); + let dialogTester = testUtilUser.createTester('Dialog', {root: button, overlayType: 'modal'}); + await dialogTester.open(); + let dialog = dialogTester.dialog; + expect(dialog).toBeVisible(); + await dialogTester.close(); + expect(dialog).not.toBeInTheDocument(); +}); +``` + + diff --git a/packages/@react-spectrum/list/docs/ListView.mdx b/packages/@react-spectrum/list/docs/ListView.mdx index ae791b43f22..2c64058f70e 100644 --- a/packages/@react-spectrum/list/docs/ListView.mdx +++ b/packages/@react-spectrum/list/docs/ListView.mdx @@ -1195,7 +1195,7 @@ 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 +`@react-spectrum/test-utils` offers common gridlist interaction utilities which you may find helpful when writing tests. See [here](./testing.html#react-spectrum-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 diff --git a/packages/@react-spectrum/listbox/docs/ListBox.mdx b/packages/@react-spectrum/listbox/docs/ListBox.mdx index 336386f4572..2ef9cf0a17b 100644 --- a/packages/@react-spectrum/listbox/docs/ListBox.mdx +++ b/packages/@react-spectrum/listbox/docs/ListBox.mdx @@ -413,7 +413,7 @@ 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 +`@react-spectrum/test-utils` offers common listbox interaction utilities which you may find helpful when writing tests. See [here](./testing.html#react-spectrum-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 diff --git a/packages/@react-spectrum/menu/docs/MenuTrigger.mdx b/packages/@react-spectrum/menu/docs/MenuTrigger.mdx index 251bddaec2f..b3b72eb44c8 100644 --- a/packages/@react-spectrum/menu/docs/MenuTrigger.mdx +++ b/packages/@react-spectrum/menu/docs/MenuTrigger.mdx @@ -260,7 +260,7 @@ 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 +`@react-spectrum/test-utils` offers common menu interaction utilities which you may find helpful when writing tests. See [here](./testing.html#react-spectrum-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 diff --git a/packages/@react-spectrum/picker/docs/Picker.mdx b/packages/@react-spectrum/picker/docs/Picker.mdx index 6c327e3166b..d1eb7f56fc5 100644 --- a/packages/@react-spectrum/picker/docs/Picker.mdx +++ b/packages/@react-spectrum/picker/docs/Picker.mdx @@ -592,7 +592,7 @@ 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 +`@react-spectrum/test-utils` offers common select interaction utilities which you may find helpful when writing tests. See [here](./testing.html#react-spectrum-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 diff --git a/packages/@react-spectrum/radio/docs/RadioGroup.mdx b/packages/@react-spectrum/radio/docs/RadioGroup.mdx index 050672f1517..e82b7c5d428 100644 --- a/packages/@react-spectrum/radio/docs/RadioGroup.mdx +++ b/packages/@react-spectrum/radio/docs/RadioGroup.mdx @@ -11,8 +11,9 @@ import {Layout} from '@react-spectrum/docs'; export default Layout; import docs from 'docs:@react-spectrum/radio'; +import radiogroupUtil from 'docs:@react-aria/test-utils/src/radiogroup.ts'; import packageData from '@react-spectrum/radio/package.json'; -import {HeaderInfo, PropTable, PageDescription} from '@react-spectrum/docs'; +import {HeaderInfo, PropTable, PageDescription, VersionBadge, ClassAPI} from '@react-spectrum/docs'; ```jsx import import {Radio, RadioGroup} from '@react-spectrum/radio'; @@ -306,3 +307,43 @@ See the [MDN docs](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/ Dragon ``` + +## Testing + +### Test utils + +`@react-spectrum/test-utils` offers common radio group interaction utilities which you may find helpful when writing tests. See [here](./testing.html#react-spectrum-test-utils) for more information on how to setup these utilities +in your tests. Below is the full definition of the radio group tester and a sample of how you could use it in your test suite. + +```ts +// RadioGroup.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', advanceTimer: jest.advanceTimersByTime}); +// ... + +it('RadioGroup can switch the selected radio', async function () { + // Render your test component/app and initialize the radiogroup tester + let {getByRole} = render( + + + ... + + + ); + + let radioGroupTester = testUtilUser.createTester('RadioGroup', {root: getByRole('radiogroup')}); + let radios = radioGroupTester.radios; + expect(radioGroupTester.selectedRadio).toBeFalsy(); + + await radioGroupTester.triggerRadio({radio: radios[0]}); + expect(radioGroupTester.selectedRadio).toBe(radios[0]); + + await radioGroupTester.triggerRadio({radio: radios[1]}); + expect(radioGroupTester.selectedRadio).toBe(radios[1]); +}); +``` + + diff --git a/packages/@react-spectrum/s2/src/Accordion.tsx b/packages/@react-spectrum/s2/src/Accordion.tsx index 554d6e5d482..4df49d6066c 100644 --- a/packages/@react-spectrum/s2/src/Accordion.tsx +++ b/packages/@react-spectrum/s2/src/Accordion.tsx @@ -55,7 +55,7 @@ const accordion = style({ export const AccordionContext = createContext, DOMRefValue>>(null); /** - * An accordion is a container for multiple disclosures. + * An accordion is a container for multiple accordion items. */ export const Accordion = forwardRef(function Accordion(props: AccordionProps, ref: DOMRef) { [props, ref] = useSpectrumContextProps(props, ref, AccordionContext); 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 765df9d23da..6d8eba4888c 100644 --- a/packages/@react-spectrum/s2/style/__tests__/style-macro.test.js +++ b/packages/@react-spectrum/s2/style/__tests__/style-macro.test.js @@ -173,6 +173,23 @@ describe('style-macro', () => { expect(js({}, overrides)).toMatchInlineSnapshot('" Tm12 Qm12 Sm12 Rm12"'); }); + it('should support allowed overrides for fontSize', () => { + let {js} = testStyle( + { + fontSize: 'heading-3xl' + }, + ['fontSize'] + ); + + let {js: overrides} = testStyle({ + fontSize: 'ui-xs' + }); + + expect(js()).toMatchInlineSnapshot('" -_6BNtrc-woabcc12 vx12"'); + expect(overrides).toMatchInlineSnapshot('" -_6BNtrc-a12 vx12"'); + expect(js({}, overrides)).toMatchInlineSnapshot('" -_6BNtrc-a12 vx12"'); + }); + it("should support allowed overrides for values that aren't defined", () => { let {js} = testStyle( { diff --git a/packages/@react-spectrum/s2/style/spectrum-theme.ts b/packages/@react-spectrum/s2/style/spectrum-theme.ts index e38a203845d..c1da1a04974 100644 --- a/packages/@react-spectrum/s2/style/spectrum-theme.ts +++ b/packages/@react-spectrum/s2/style/spectrum-theme.ts @@ -757,7 +757,7 @@ export const style = createTheme({ }, code: 'source-code-pro, "Source Code Pro", Monaco, monospace' }, - fontSize: new ExpandedProperty(['fontSize', 'lineHeight'], (value) => { + fontSize: new ExpandedProperty(['--fs', 'fontSize'], (value) => { if (typeof value === 'number') { return { '--fs': `pow(1.125, ${value})`, diff --git a/packages/@react-spectrum/table/docs/TableView.mdx b/packages/@react-spectrum/table/docs/TableView.mdx index ae4e9909584..1740998ce5e 100644 --- a/packages/@react-spectrum/table/docs/TableView.mdx +++ b/packages/@react-spectrum/table/docs/TableView.mdx @@ -1964,7 +1964,7 @@ 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 +`@react-spectrum/test-utils` offers common table interaction utilities which you may find helpful when writing tests. See [here](./testing.html#react-spectrum-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 diff --git a/packages/@react-spectrum/tabs/docs/Tabs.mdx b/packages/@react-spectrum/tabs/docs/Tabs.mdx index 3c81dd2e12e..74eb54adbf2 100644 --- a/packages/@react-spectrum/tabs/docs/Tabs.mdx +++ b/packages/@react-spectrum/tabs/docs/Tabs.mdx @@ -639,7 +639,7 @@ Tabs features automatic tab collapse behavior and may need specific mocks to tes [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 +`@react-spectrum/test-utils` offers common tabs interaction utilities which you may find helpful when writing tests. See [here](./testing.html#react-spectrum-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 diff --git a/packages/@react-spectrum/tree/docs/TreeView.mdx b/packages/@react-spectrum/tree/docs/TreeView.mdx index 4dc29a886d7..7d2915fe3ea 100644 --- a/packages/@react-spectrum/tree/docs/TreeView.mdx +++ b/packages/@react-spectrum/tree/docs/TreeView.mdx @@ -533,7 +533,7 @@ 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 +`@react-spectrum/test-utils` offers common tree interaction utilities which you may find helpful when writing tests. See [here](./testing.html#react-spectrum-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 diff --git a/packages/dev/docs/pages/react-aria/components.mdx b/packages/dev/docs/pages/react-aria/components.mdx index 53cfbd2f43a..7ecc13f704f 100644 --- a/packages/dev/docs/pages/react-aria/components.mdx +++ b/packages/dev/docs/pages/react-aria/components.mdx @@ -167,7 +167,7 @@ order: 5 + description="A tree displays hierarchical data with selection and collapsing."> diff --git a/packages/dev/docs/pages/react-aria/testing.mdx b/packages/dev/docs/pages/react-aria/testing.mdx index aab35a898ec..c649d45ea4d 100644 --- a/packages/dev/docs/pages/react-aria/testing.mdx +++ b/packages/dev/docs/pages/react-aria/testing.mdx @@ -201,14 +201,20 @@ See below for the full definition of the `User` object. Below is a list of the ARIA patterns testers currently supported by `createTester`. See the accompanying component testing docs pages for a sample of how to use the testers in your test suite. +- [CheckboxGroup](CheckboxGroup.html#test-utils) + - [ComboBox](ComboBox.html#test-utils) +- [Dialog](Dialog.html#test-utils) + - [GridList](GridList.html#test-utils) - [ListBox](ListBox.html#test-utils) - [Menu](Menu.html#test-utils) +- [RadioGroup](RadioGroup.html#test-utils) + - [Select](Select.html#test-utils) - [Table](Table.html#test-utils) diff --git a/packages/dev/docs/pages/react-spectrum/index.mdx b/packages/dev/docs/pages/react-spectrum/index.mdx index dfea46df848..be329e21923 100644 --- a/packages/dev/docs/pages/react-spectrum/index.mdx +++ b/packages/dev/docs/pages/react-spectrum/index.mdx @@ -231,7 +231,7 @@ A React implementation of Spectrum, Adobe’s design system. + description="A tree view displays hierarchical data with selection and collapsing."> diff --git a/packages/dev/docs/pages/react-spectrum/testing.mdx b/packages/dev/docs/pages/react-spectrum/testing.mdx index 99c179e3d61..90dd1e8050d 100644 --- a/packages/dev/docs/pages/react-spectrum/testing.mdx +++ b/packages/dev/docs/pages/react-spectrum/testing.mdx @@ -395,14 +395,20 @@ See below for the full definition of the `User` object. Below is a list of the ARIA patterns testers currently supported by `createTester`. See the accompanying component testing docs pages for a sample of how to use the testers in your test suite. +- [CheckboxGroup](CheckboxGroup.html#test-utils) + - [ComboBox](ComboBox.html#test-utils) +- [Dialog](Dialog.html#test-utils) + - [ListView](ListView.html#test-utils) - [ListBox](ListBox.html#test-utils) - [MenuTrigger](MenuTrigger.html#test-utils) +- [RadioGroup](RadioGroup.html#test-utils) + - [Picker](Picker.html#test-utils) - [TableView](TableView.html#test-utils) diff --git a/packages/dev/s2-docs/pages/index.mdx b/packages/dev/s2-docs/pages/index.mdx index 145df571ae6..4ea5026be6f 100644 --- a/packages/dev/s2-docs/pages/index.mdx +++ b/packages/dev/s2-docs/pages/index.mdx @@ -4,6 +4,7 @@ export default Layout; export const section = 'Getting started'; export const hideNav = true; +export const description = 'Adobe\'s collection of libraries and tools for building adaptive, accessible, and robust user experiences.'; export const hideFromSearch = true; diff --git a/packages/dev/s2-docs/pages/internationalized/date/Calendar.mdx b/packages/dev/s2-docs/pages/internationalized/date/Calendar.mdx index 6597d1a181b..affc07a8ab8 100644 --- a/packages/dev/s2-docs/pages/internationalized/date/Calendar.mdx +++ b/packages/dev/s2-docs/pages/internationalized/date/Calendar.mdx @@ -15,6 +15,7 @@ import {StaticTable} from '../../../src/StaticTable'; export const section = 'Date and Time'; export const group = 'Internationalized'; +export const description = 'Calendar systems for international date calculations.'; # Calendar diff --git a/packages/dev/s2-docs/pages/internationalized/date/CalendarDate.mdx b/packages/dev/s2-docs/pages/internationalized/date/CalendarDate.mdx index 2044c291940..bc96be3a93f 100644 --- a/packages/dev/s2-docs/pages/internationalized/date/CalendarDate.mdx +++ b/packages/dev/s2-docs/pages/internationalized/date/CalendarDate.mdx @@ -14,6 +14,7 @@ import docs from 'docs:@internationalized/date'; export const section = 'Date and Time'; export const group = 'Internationalized'; +export const description = 'A date without time in a specific calendar system.'; # CalendarDate diff --git a/packages/dev/s2-docs/pages/internationalized/date/CalendarDateTime.mdx b/packages/dev/s2-docs/pages/internationalized/date/CalendarDateTime.mdx index f694a78ad1a..df49547bb43 100644 --- a/packages/dev/s2-docs/pages/internationalized/date/CalendarDateTime.mdx +++ b/packages/dev/s2-docs/pages/internationalized/date/CalendarDateTime.mdx @@ -14,6 +14,7 @@ import docs from 'docs:@internationalized/date'; export const section = 'Date and Time'; export const group = 'Internationalized'; +export const description = 'A date and time without a time zone.'; # CalendarDateTime diff --git a/packages/dev/s2-docs/pages/internationalized/date/DateFormatter.mdx b/packages/dev/s2-docs/pages/internationalized/date/DateFormatter.mdx index b790fbac2ba..0d46760795b 100644 --- a/packages/dev/s2-docs/pages/internationalized/date/DateFormatter.mdx +++ b/packages/dev/s2-docs/pages/internationalized/date/DateFormatter.mdx @@ -14,6 +14,7 @@ import docs from 'docs:@internationalized/date'; export const section = 'Date and Time'; export const group = 'Internationalized'; +export const description = 'Provides locale-aware date formatting with browser bug fixes.'; # DateFormatter diff --git a/packages/dev/s2-docs/pages/internationalized/date/Time.mdx b/packages/dev/s2-docs/pages/internationalized/date/Time.mdx index fb0edb08c36..0e4c9e904eb 100644 --- a/packages/dev/s2-docs/pages/internationalized/date/Time.mdx +++ b/packages/dev/s2-docs/pages/internationalized/date/Time.mdx @@ -14,6 +14,7 @@ import docs from 'docs:@internationalized/date'; export const section = 'Date and Time'; export const group = 'Internationalized'; +export const description = 'A clock time without any date components.'; # Time diff --git a/packages/dev/s2-docs/pages/internationalized/date/ZonedDateTime.mdx b/packages/dev/s2-docs/pages/internationalized/date/ZonedDateTime.mdx index 81711c63f3f..d90ee33354c 100644 --- a/packages/dev/s2-docs/pages/internationalized/date/ZonedDateTime.mdx +++ b/packages/dev/s2-docs/pages/internationalized/date/ZonedDateTime.mdx @@ -14,6 +14,7 @@ import docs from 'docs:@internationalized/date'; export const section = 'Date and Time'; export const group = 'Internationalized'; +export const description = 'An exact date and time in a specific time zone.'; # ZonedDateTime diff --git a/packages/dev/s2-docs/pages/internationalized/number/NumberFormatter.mdx b/packages/dev/s2-docs/pages/internationalized/number/NumberFormatter.mdx index 2e5d27dd06d..48d92d922bb 100644 --- a/packages/dev/s2-docs/pages/internationalized/number/NumberFormatter.mdx +++ b/packages/dev/s2-docs/pages/internationalized/number/NumberFormatter.mdx @@ -14,6 +14,7 @@ import docs from 'docs:@internationalized/number'; export const section = 'Numbers'; export const group = 'Internationalized'; +export const description = 'Provides locale-aware number formatting with polyfills.'; # NumberFormatter diff --git a/packages/dev/s2-docs/pages/internationalized/number/NumberParser.mdx b/packages/dev/s2-docs/pages/internationalized/number/NumberParser.mdx index 8c94bd66684..c19f36e3d86 100644 --- a/packages/dev/s2-docs/pages/internationalized/number/NumberParser.mdx +++ b/packages/dev/s2-docs/pages/internationalized/number/NumberParser.mdx @@ -14,6 +14,7 @@ import docs from 'docs:@internationalized/number'; export const section = 'Numbers'; export const group = 'Internationalized'; +export const description = 'Validates and parses numbers from user input with locale support.'; # NumberParser diff --git a/packages/dev/s2-docs/pages/react-aria/Autocomplete.mdx b/packages/dev/s2-docs/pages/react-aria/Autocomplete.mdx index 73176705766..7c4dfe74a78 100644 --- a/packages/dev/s2-docs/pages/react-aria/Autocomplete.mdx +++ b/packages/dev/s2-docs/pages/react-aria/Autocomplete.mdx @@ -6,6 +6,8 @@ import vanillaDocs from 'docs:vanilla-starter/CommandPalette'; import '../../tailwind/tailwind.css'; export const tags = ['combobox', 'typeahead', 'input']; +export const relatedPages = [{'title': 'useAutocomplete', 'url': 'https://react-spectrum.adobe.com/react-aria/useAutocomplete.html'}]; +export const description = 'Allows users to search or filter a list of suggestions.'; # Autocomplete diff --git a/packages/dev/s2-docs/pages/react-aria/Breadcrumbs.mdx b/packages/dev/s2-docs/pages/react-aria/Breadcrumbs.mdx index 28d18faa40e..8f46e6623da 100644 --- a/packages/dev/s2-docs/pages/react-aria/Breadcrumbs.mdx +++ b/packages/dev/s2-docs/pages/react-aria/Breadcrumbs.mdx @@ -8,6 +8,7 @@ import {InlineAlert, Heading, Content} from '@react-spectrum/s2'; export const tags = ['navigation']; export const relatedPages = [{'title': 'useBreadcrumbs', 'url': 'https://react-spectrum.adobe.com/react-aria/useBreadcrumbs.html'}]; +export const description = 'Displays a hierarchy of links to the current page or resource in an application.'; # Breadcrumbs diff --git a/packages/dev/s2-docs/pages/react-aria/Button.mdx b/packages/dev/s2-docs/pages/react-aria/Button.mdx index be228def8ca..afdbb7bd056 100644 --- a/packages/dev/s2-docs/pages/react-aria/Button.mdx +++ b/packages/dev/s2-docs/pages/react-aria/Button.mdx @@ -13,6 +13,7 @@ import typesDocs from 'docs:@react-types/shared/src/events.d.ts'; export const tags = ['btn']; export const relatedPages = [{'title': 'useButton', 'url': 'https://react-spectrum.adobe.com/react-aria/useButton.html'}]; +export const description = 'Allows a user to perform an action, with mouse, touch, and keyboard interactions.'; # Button diff --git a/packages/dev/s2-docs/pages/react-aria/Calendar.mdx b/packages/dev/s2-docs/pages/react-aria/Calendar.mdx index 1205c731d5b..a11002e93fb 100644 --- a/packages/dev/s2-docs/pages/react-aria/Calendar.mdx +++ b/packages/dev/s2-docs/pages/react-aria/Calendar.mdx @@ -10,6 +10,7 @@ import Anatomy from '@react-aria/calendar/docs/calendar-anatomy.svg'; export const tags = ['date']; export const relatedPages = [{'title': 'useCalendar', 'url': 'https://react-spectrum.adobe.com/react-aria/useCalendar.html'}]; +export const description = 'Displays one or more date grids and allows users to select a single date.'; # Calendar diff --git a/packages/dev/s2-docs/pages/react-aria/Checkbox.mdx b/packages/dev/s2-docs/pages/react-aria/Checkbox.mdx index 8a95dae82bd..65c9f9d3404 100644 --- a/packages/dev/s2-docs/pages/react-aria/Checkbox.mdx +++ b/packages/dev/s2-docs/pages/react-aria/Checkbox.mdx @@ -9,6 +9,7 @@ import Anatomy from '@react-aria/checkbox/docs/checkbox-anatomy.svg'; export const tags = ['input']; export const relatedPages = [{'title': 'useCheckbox', 'url': 'https://react-spectrum.adobe.com/react-aria/useCheckbox.html'}]; +export const description = 'Allows a user to select multiple items from a list of individual items, or to mark one individual item as selected.'; # Checkbox diff --git a/packages/dev/s2-docs/pages/react-aria/CheckboxGroup.mdx b/packages/dev/s2-docs/pages/react-aria/CheckboxGroup.mdx index df4721fee24..c420ff1f40b 100644 --- a/packages/dev/s2-docs/pages/react-aria/CheckboxGroup.mdx +++ b/packages/dev/s2-docs/pages/react-aria/CheckboxGroup.mdx @@ -5,9 +5,14 @@ import docs from 'docs:react-aria-components'; import vanillaDocs from 'docs:vanilla-starter/CheckboxGroup'; import '../../tailwind/tailwind.css'; import Anatomy from '@react-aria/checkbox/docs/checkboxgroup-anatomy.svg'; +import {InlineAlert, Heading, Content} from '@react-spectrum/s2' export const tags = ['input']; -export const relatedPages = [{'title': 'useCheckboxGroup', 'url': 'https://react-spectrum.adobe.com/react-aria/useCheckboxGroup.html'}]; +export const relatedPages = [ + {title: 'useCheckboxGroup', url: 'https://react-spectrum.adobe.com/react-aria/useCheckboxGroup.html'}, + {title: 'Testing', url: './CheckboxGroup/testing.html'} +]; +export const description = 'Allows a user to select multiple items from a list of options.'; # CheckboxGroup diff --git a/packages/dev/s2-docs/pages/react-aria/CheckboxGroup/testing.mdx b/packages/dev/s2-docs/pages/react-aria/CheckboxGroup/testing.mdx new file mode 100644 index 00000000000..2841f7a2984 --- /dev/null +++ b/packages/dev/s2-docs/pages/react-aria/CheckboxGroup/testing.mdx @@ -0,0 +1,71 @@ +import {Layout} from '../../../src/Layout'; +export default Layout; + +import {InlineAlert, Heading, Content} from '@react-spectrum/s2'; +import testUtilDocs from 'docs:@react-aria/test-utils'; +import {InstallCommand} from '../../../src/InstallCommand'; +import {PatternTestingFAQ} from '../../../src/PatternTestingFAQ'; + +export const isSubpage = true; +export const tags = ['testing', 'checkboxgroup', 'test-utils']; +export const description = 'Testing CheckboxGroup with React Aria test utils'; + +# Testing CheckboxGroup + +## Test utils + +`@react-aria/test-utils` offers common checkbox group interaction testing utilities. Install it with your preferred package manager. + + + + + Requirements + Please note that this library uses [@testing-library/react@16](https://www.npmjs.com/package/@testing-library/react) and [@testing-library/user-event@14](https://www.npmjs.com/package/@testing-library/user-event). This means that you need to be on React 18+ in order for these utilities to work. + + +Initialize a `User` object at the top of your test file, and use it to create an `CheckboxGroup` pattern tester in your test cases. The tester has methods that you can call within your test to query for specific subcomponents or simulate common interactions. + +```ts +// CheckboxGroup.test.ts +import {render} from '@testing-library/react'; +import {User} from '@react-aria/test-utils'; + +let testUtilUser = new User({ + interactionType: 'mouse', + advanceTimer: jest.advanceTimersByTime +}); +// ... + +it('CheckboxGroup can select multiple checkboxes', async function () { + // Render your test component/app and initialize the checkbox group tester + let {getByTestId} = render( + + ... + + ); + let checkboxGroupTester = testUtilUser.createTester('CheckboxGroup', {root: getByTestId('test-checkboxgroup')}); + expect(checkboxGroupTester.selectedCheckboxes).toHaveLength(0); + + await checkboxGroupTester.toggleCheckbox({checkbox: 0}); + expect(checkboxGroupTester.checkboxes[0]).toBeChecked(); + expect(checkboxGroupTester.selectedCheckboxes).toHaveLength(1); + + await checkboxGroupTester.toggleCheckbox({checkbox: 4}); + expect(checkboxGroupTester.checkboxes[4]).toBeChecked(); + expect(checkboxGroupTester.selectedCheckboxes).toHaveLength(2); +}); +``` + +## API + +### User + + + +### CheckboxGroupTester + + + +## Testing FAQ + + diff --git a/packages/dev/s2-docs/pages/react-aria/ColorArea.mdx b/packages/dev/s2-docs/pages/react-aria/ColorArea.mdx index 84045da46b0..92912569a63 100644 --- a/packages/dev/s2-docs/pages/react-aria/ColorArea.mdx +++ b/packages/dev/s2-docs/pages/react-aria/ColorArea.mdx @@ -9,6 +9,7 @@ import Anatomy from '@react-aria/color/docs/ColorAreaAnatomy.svg'; export const tags = []; export const relatedPages = [{'title': 'useColorArea', 'url': 'https://react-spectrum.adobe.com/react-aria/useColorArea.html'}]; +export const description = 'Allows users to adjust two channels of an RGB, HSL or HSB color value against a two-dimensional gradient background.'; # ColorArea diff --git a/packages/dev/s2-docs/pages/react-aria/ColorField.mdx b/packages/dev/s2-docs/pages/react-aria/ColorField.mdx index ff8079cbdc4..36a76d68ef9 100644 --- a/packages/dev/s2-docs/pages/react-aria/ColorField.mdx +++ b/packages/dev/s2-docs/pages/react-aria/ColorField.mdx @@ -10,6 +10,7 @@ import Anatomy from '@react-aria/color/docs/ColorFieldAnatomy.svg'; export const tags = ['input']; export const relatedPages = [{'title': 'useColorField', 'url': 'https://react-spectrum.adobe.com/react-aria/useColorField.html'}]; +export const description = 'Allows users to edit a hex color or individual color channel value.'; # ColorField diff --git a/packages/dev/s2-docs/pages/react-aria/ColorPicker.mdx b/packages/dev/s2-docs/pages/react-aria/ColorPicker.mdx index 5697e70a219..cc66d651fe4 100644 --- a/packages/dev/s2-docs/pages/react-aria/ColorPicker.mdx +++ b/packages/dev/s2-docs/pages/react-aria/ColorPicker.mdx @@ -8,7 +8,7 @@ import {ColorPicker as TailwindColorPicker} from 'tailwind-starter/ColorPicker'; import '../../tailwind/tailwind.css'; export const tags = ['input']; -export const relatedPages = [{'title': 'useColorPicker', 'url': 'https://react-spectrum.adobe.com/react-aria/useColorPicker.html'}]; +export const description = 'Synchronizes a color value between multiple React Aria color components.'; # ColorPicker diff --git a/packages/dev/s2-docs/pages/react-aria/ColorSlider.mdx b/packages/dev/s2-docs/pages/react-aria/ColorSlider.mdx index 5f46bb4afa7..c8cc971e9af 100644 --- a/packages/dev/s2-docs/pages/react-aria/ColorSlider.mdx +++ b/packages/dev/s2-docs/pages/react-aria/ColorSlider.mdx @@ -10,6 +10,7 @@ import Anatomy from '@react-aria/color/docs/ColorSliderAnatomy.svg'; export const tags = ['input']; export const relatedPages = [{'title': 'useColorSlider', 'url': 'https://react-spectrum.adobe.com/react-aria/useColorSlider.html'}]; +export const description = 'Allows users to adjust an individual channel of a color value.'; # ColorSlider diff --git a/packages/dev/s2-docs/pages/react-aria/ColorSwatch.mdx b/packages/dev/s2-docs/pages/react-aria/ColorSwatch.mdx index 9ece1c24401..4c52304538f 100644 --- a/packages/dev/s2-docs/pages/react-aria/ColorSwatch.mdx +++ b/packages/dev/s2-docs/pages/react-aria/ColorSwatch.mdx @@ -9,6 +9,7 @@ import '../../tailwind/tailwind.css'; export const tags = []; export const relatedPages = [{'title': 'useColorSwatch', 'url': 'https://react-spectrum.adobe.com/react-aria/useColorSwatch.html'}]; +export const description = 'Displays a preview of a selected color.'; # ColorSwatch diff --git a/packages/dev/s2-docs/pages/react-aria/ColorSwatchPicker.mdx b/packages/dev/s2-docs/pages/react-aria/ColorSwatchPicker.mdx index 6507f370aa9..827b6453b43 100644 --- a/packages/dev/s2-docs/pages/react-aria/ColorSwatchPicker.mdx +++ b/packages/dev/s2-docs/pages/react-aria/ColorSwatchPicker.mdx @@ -7,6 +7,7 @@ import '../../tailwind/tailwind.css'; import {InlineAlert, Heading, Content} from '@react-spectrum/s2' export const tags = ['input']; +export const description = 'Displays a list of color swatches and allows a user to select one of them.'; # ColorSwatchPicker diff --git a/packages/dev/s2-docs/pages/react-aria/ColorWheel.mdx b/packages/dev/s2-docs/pages/react-aria/ColorWheel.mdx index f64cb81dd6e..14d06088625 100644 --- a/packages/dev/s2-docs/pages/react-aria/ColorWheel.mdx +++ b/packages/dev/s2-docs/pages/react-aria/ColorWheel.mdx @@ -9,6 +9,7 @@ import Anatomy from '@react-aria/color/docs/ColorWheelAnatomy.svg'; export const tags = ['input']; export const relatedPages = [{'title': 'useColorWheel', 'url': 'https://react-spectrum.adobe.com/react-aria/useColorWheel.html'}]; +export const description = 'Allows users to adjust the hue of an HSL or HSB color value on a circular track.'; # ColorWheel diff --git a/packages/dev/s2-docs/pages/react-aria/ComboBox.mdx b/packages/dev/s2-docs/pages/react-aria/ComboBox.mdx index 1e5fcb0357d..78318381d78 100644 --- a/packages/dev/s2-docs/pages/react-aria/ComboBox.mdx +++ b/packages/dev/s2-docs/pages/react-aria/ComboBox.mdx @@ -6,9 +6,14 @@ import {ComboBox as VanillaComboBox, ComboBoxItem} from 'vanilla-starter/ComboBo import vanillaDocs from 'docs:vanilla-starter/ComboBox'; import '../../tailwind/tailwind.css'; import Anatomy from 'react-aria-components/docs/ComboBoxAnatomy.svg'; +import {InlineAlert, Heading, Content} from '@react-spectrum/s2' export const tags = ['autocomplete', 'search', 'typeahead', 'input']; -export const relatedPages = [{'title': 'useComboBox', 'url': 'https://react-spectrum.adobe.com/react-aria/useComboBox.html'}]; +export const relatedPages = [ + {title: 'useComboBox', url: 'https://react-spectrum.adobe.com/react-aria/useComboBox.html'}, + {title: 'Testing', url: './ComboBox/testing.html'} +]; +export const description = 'Combines a text input with a listbox, allowing users to filter a list of options to items matching a query.'; # ComboBox diff --git a/packages/dev/s2-docs/pages/react-aria/ComboBox/testing.mdx b/packages/dev/s2-docs/pages/react-aria/ComboBox/testing.mdx new file mode 100644 index 00000000000..ef1a3389f35 --- /dev/null +++ b/packages/dev/s2-docs/pages/react-aria/ComboBox/testing.mdx @@ -0,0 +1,69 @@ +import {Layout} from '../../../src/Layout'; +export default Layout; + +import {InlineAlert, Heading, Content} from '@react-spectrum/s2'; +import testUtilDocs from 'docs:@react-aria/test-utils'; +import {InstallCommand} from '../../../src/InstallCommand'; +import {PatternTestingFAQ} from '../../../src/PatternTestingFAQ'; + +export const isSubpage = true; +export const tags = ['testing', 'combobox', 'test-utils']; +export const description = 'Testing ComboBox with React Aria test utils'; + +# Testing ComboBox + +## Test utils + +`@react-aria/test-utils` offers common combobox interaction testing utilities. Install it with your preferred package manager. + + + + + Requirements + Please note that this library uses [@testing-library/react@16](https://www.npmjs.com/package/@testing-library/react) and [@testing-library/user-event@14](https://www.npmjs.com/package/@testing-library/user-event). This means that you need to be on React 18+ in order for these utilities to work. + + +Initialize a `User` object at the top of your test file, and use it to create a `ComboBox` pattern tester in your test cases. The tester has methods that you can call within your test to query for specific subcomponents or simulate common interactions. + +```ts +// Combobox.test.ts +import {render} from '@testing-library/react'; +import {User} from '@react-aria/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(); +}); +``` + +## API + +### User + + + +### ComboBoxTester + + + +## Testing FAQ + + diff --git a/packages/dev/s2-docs/pages/react-aria/DateField.mdx b/packages/dev/s2-docs/pages/react-aria/DateField.mdx index 9a096bf5c72..04bfad06bec 100644 --- a/packages/dev/s2-docs/pages/react-aria/DateField.mdx +++ b/packages/dev/s2-docs/pages/react-aria/DateField.mdx @@ -10,6 +10,7 @@ import Anatomy from '@react-aria/datepicker/docs/datefield-anatomy.svg'; export const tags = ['calendar', 'input']; export const relatedPages = [{'title': 'useDateField', 'url': 'https://react-spectrum.adobe.com/react-aria/useDateField.html'}]; +export const description = 'Allows users to enter and edit date and time values using a keyboard.'; # DateField diff --git a/packages/dev/s2-docs/pages/react-aria/DatePicker.mdx b/packages/dev/s2-docs/pages/react-aria/DatePicker.mdx index 253d526dc72..fb4caa77e8d 100644 --- a/packages/dev/s2-docs/pages/react-aria/DatePicker.mdx +++ b/packages/dev/s2-docs/pages/react-aria/DatePicker.mdx @@ -10,6 +10,7 @@ import Anatomy from '@react-aria/datepicker/docs/datepicker-anatomy.svg'; export const tags = ['calendar', 'input']; export const relatedPages = [{'title': 'useDatePicker', 'url': 'https://react-spectrum.adobe.com/react-aria/useDatePicker.html'}]; +export const description = 'Combines a DateField and a Calendar popover to allow users to enter or select a date and time value.'; # DatePicker @@ -31,7 +32,7 @@ export const relatedPages = [{'title': 'useDatePicker', 'url': 'https://react-sp props={['label', 'granularity', 'isDisabled']} initialProps={{label: 'Date'}} type="tailwind" - files={["starters/tailwind/src/DatePicker.tsx", "starters/tailwind/src/DateField.tsx", "starters/tailwind/src/index.css"]} /> + files={["starters/tailwind/src/DatePicker.tsx"]} /> ## Value diff --git a/packages/dev/s2-docs/pages/react-aria/DateRangePicker.mdx b/packages/dev/s2-docs/pages/react-aria/DateRangePicker.mdx index b3532440797..a4265cbd67b 100644 --- a/packages/dev/s2-docs/pages/react-aria/DateRangePicker.mdx +++ b/packages/dev/s2-docs/pages/react-aria/DateRangePicker.mdx @@ -10,6 +10,7 @@ import Anatomy from '@react-aria/datepicker/docs/daterangepicker-anatomy.svg'; export const tags = ['calendar', 'input']; export const relatedPages = [{'title': 'useDateRangePicker', 'url': 'https://react-spectrum.adobe.com/react-aria/useDateRangePicker.html'}]; +export const description = 'Combines two DateFields and a RangeCalendar popover to allow users to enter or select a date range.'; # DateRangePicker @@ -31,7 +32,7 @@ export const relatedPages = [{'title': 'useDateRangePicker', 'url': 'https://rea props={['label', 'granularity', 'isDisabled']} initialProps={{label: 'Date range'}} type="tailwind" - files={["starters/tailwind/src/DateRangePicker.tsx", "starters/tailwind/src/DateField.tsx", "starters/tailwind/src/index.css"]} /> + files={["starters/tailwind/src/DateRangePicker.tsx"]} /> ## Value diff --git a/packages/dev/s2-docs/pages/react-aria/Disclosure.mdx b/packages/dev/s2-docs/pages/react-aria/Disclosure.mdx index 45bdbe7084f..5eac4b68d01 100644 --- a/packages/dev/s2-docs/pages/react-aria/Disclosure.mdx +++ b/packages/dev/s2-docs/pages/react-aria/Disclosure.mdx @@ -8,6 +8,7 @@ import Anatomy from 'react-aria-components/docs/DisclosureAnatomy.svg'; export const tags = ['accordion', 'collapsible', 'expandable', 'details']; export const relatedPages = [{'title': 'useDisclosure', 'url': 'https://react-spectrum.adobe.com/react-aria/useDisclosure.html'}]; +export const description = 'A collapsible section of content composed of a header with a heading and trigger button, and a panel that contains the content.'; # Disclosure diff --git a/packages/dev/s2-docs/pages/react-aria/DisclosureGroup.mdx b/packages/dev/s2-docs/pages/react-aria/DisclosureGroup.mdx index 15017daedf3..f826ba279b0 100644 --- a/packages/dev/s2-docs/pages/react-aria/DisclosureGroup.mdx +++ b/packages/dev/s2-docs/pages/react-aria/DisclosureGroup.mdx @@ -7,7 +7,7 @@ import '../../tailwind/tailwind.css'; import Anatomy from 'react-aria-components/docs/DisclosureGroupAnatomy.svg'; export const tags = ['accordion', 'collapsible', 'expandable', 'details']; -export const relatedPages = [{'title': 'useDisclosureGroup', 'url': 'https://react-spectrum.adobe.com/react-aria/useDisclosureGroup.html'}]; +export const description = 'A grouping of related disclosures, sometimes called an accordion.'; # DisclosureGroup diff --git a/packages/dev/s2-docs/pages/react-aria/DropZone.mdx b/packages/dev/s2-docs/pages/react-aria/DropZone.mdx index aacc3bfb5b4..4db278e8941 100644 --- a/packages/dev/s2-docs/pages/react-aria/DropZone.mdx +++ b/packages/dev/s2-docs/pages/react-aria/DropZone.mdx @@ -6,6 +6,7 @@ import '../../tailwind/tailwind.css'; export const tags = ['file', 'drag', 'dnd', 'upload']; export const relatedPages = [{'title': 'useDrop', 'url': 'https://react-spectrum.adobe.com/react-aria/useDrop.html'}]; +export const description = 'An area into which one or multiple objects can be dragged and dropped.'; # DropZone diff --git a/packages/dev/s2-docs/pages/react-aria/FileTrigger.mdx b/packages/dev/s2-docs/pages/react-aria/FileTrigger.mdx index 65faff1d806..513bc78f0ac 100644 --- a/packages/dev/s2-docs/pages/react-aria/FileTrigger.mdx +++ b/packages/dev/s2-docs/pages/react-aria/FileTrigger.mdx @@ -6,6 +6,7 @@ import '../../tailwind/tailwind.css'; import {InlineAlert, Heading, Content} from '@react-spectrum/s2' export const tags = ['upload', 'input']; +export const description = 'Allows a user to access the file system with any pressable React Aria or React Spectrum component.'; # FileTrigger diff --git a/packages/dev/s2-docs/pages/react-aria/FocusRing.mdx b/packages/dev/s2-docs/pages/react-aria/FocusRing.mdx index e357743a85a..33c59885622 100644 --- a/packages/dev/s2-docs/pages/react-aria/FocusRing.mdx +++ b/packages/dev/s2-docs/pages/react-aria/FocusRing.mdx @@ -14,6 +14,7 @@ import {FunctionAPI} from '../../src/FunctionAPI'; import docs from 'docs:@react-aria/focus'; export const section = 'Interactions'; +export const description = 'A utility component that applies a visual focus indicator to its child element when focused via keyboard.'; # FocusRing diff --git a/packages/dev/s2-docs/pages/react-aria/FocusScope.mdx b/packages/dev/s2-docs/pages/react-aria/FocusScope.mdx index e687fffb72d..ef34e800876 100644 --- a/packages/dev/s2-docs/pages/react-aria/FocusScope.mdx +++ b/packages/dev/s2-docs/pages/react-aria/FocusScope.mdx @@ -14,6 +14,7 @@ import {FunctionAPI} from '../../src/FunctionAPI'; import docs from 'docs:@react-aria/focus'; export const section = 'Interactions'; +export const description = 'Manages focus within a group of elements, supporting features like focus containment and restoration.'; # FocusScope diff --git a/packages/dev/s2-docs/pages/react-aria/Form.mdx b/packages/dev/s2-docs/pages/react-aria/Form.mdx index 7f7098c97c5..d5c55e7b498 100644 --- a/packages/dev/s2-docs/pages/react-aria/Form.mdx +++ b/packages/dev/s2-docs/pages/react-aria/Form.mdx @@ -5,6 +5,7 @@ import docs from 'docs:react-aria-components'; import '../../tailwind/tailwind.css'; export const tags = ['input', 'field']; +export const description = 'A group of inputs that allows users to submit data to a server.'; # Form diff --git a/packages/dev/s2-docs/pages/react-aria/GridList.mdx b/packages/dev/s2-docs/pages/react-aria/GridList.mdx index ea3fde197c2..106012170b4 100644 --- a/packages/dev/s2-docs/pages/react-aria/GridList.mdx +++ b/packages/dev/s2-docs/pages/react-aria/GridList.mdx @@ -6,9 +6,14 @@ import {GridList as VanillaGridList, GridListItem} from 'vanilla-starter/GridLis import vanillaDocs from 'docs:vanilla-starter/GridList'; import '../../tailwind/tailwind.css'; import Anatomy from 'react-aria-components/docs/GridListAnatomy.svg'; +import {InlineAlert, Heading, Content} from '@react-spectrum/s2' export const tags = ['list view']; -export const relatedPages = [{'title': 'useGridList', 'url': 'https://react-spectrum.adobe.com/react-aria/useGridList.html'}]; +export const relatedPages = [ + {title: 'useGridList', url: 'https://react-spectrum.adobe.com/react-aria/useGridList.html'}, + {title: 'Testing', url: './GridList/testing.html'} +]; +export const description = 'Displays a list of interactive items, with support for keyboard navigation, selection, and actions.'; # GridList diff --git a/packages/dev/s2-docs/pages/react-aria/GridList/testing.mdx b/packages/dev/s2-docs/pages/react-aria/GridList/testing.mdx new file mode 100644 index 00000000000..373275ceac4 --- /dev/null +++ b/packages/dev/s2-docs/pages/react-aria/GridList/testing.mdx @@ -0,0 +1,80 @@ +import {Layout} from '../../../src/Layout'; +export default Layout; + +import {InlineAlert, Heading, Content} from '@react-spectrum/s2'; +import testUtilDocs from 'docs:@react-aria/test-utils'; +import {InstallCommand} from '../../../src/InstallCommand'; +import {PatternTestingFAQ} from '../../../src/PatternTestingFAQ'; + +export const isSubpage = true; +export const tags = ['testing', 'gridlist', 'test-utils']; +export const description = 'Testing GridList with React Aria test utils'; + +# Testing GridList + +## General setup + +GridList supports long press interactions on its items in certain configurations. See the following sections on how to handle these behaviors in your tests. + +* [Timers](../testing.html#timers) +* [Long press](../testing.html#simulating-user-long-press) + +## Test utils + +`@react-aria/test-utils` offers common gridlist interaction testing utilities. Install it with your preferred package manager. + + + + + Requirements + Please note that this library uses [@testing-library/react@16](https://www.npmjs.com/package/@testing-library/react) and [@testing-library/user-event@14](https://www.npmjs.com/package/@testing-library/user-event). This means that you need to be on React 18+ in order for these utilities to work. + + +Initialize a `User` object at the top of your test file, and use it to create a `GridList` pattern tester in your test cases. The tester has methods that you can call within your test to query for specific subcomponents or simulate common interactions. + +```ts +// GridList.test.ts +import {render, within} from '@testing-library/react'; +import {User} from '@react-aria/test-utils'; + +let testUtilUser = new User({ + interactionType: 'mouse' +}); +// ... + +it('GridList 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); +}); +``` + +## API + +### User + + + +### GridListTester + + + +## Testing FAQ + + diff --git a/packages/dev/s2-docs/pages/react-aria/Group.mdx b/packages/dev/s2-docs/pages/react-aria/Group.mdx index 1cac683c7bd..79abe6a79c4 100644 --- a/packages/dev/s2-docs/pages/react-aria/Group.mdx +++ b/packages/dev/s2-docs/pages/react-aria/Group.mdx @@ -6,6 +6,7 @@ import vanillaDocs from 'docs:vanilla-starter/InputGroup'; import '../../tailwind/tailwind.css'; export const tags = []; +export const description = 'Represents a set of related UI controls, and supports interactive states for styling.'; # Group diff --git a/packages/dev/s2-docs/pages/react-aria/I18nProvider.mdx b/packages/dev/s2-docs/pages/react-aria/I18nProvider.mdx index b0c5a8f9d0b..2b00b4bfe1f 100644 --- a/packages/dev/s2-docs/pages/react-aria/I18nProvider.mdx +++ b/packages/dev/s2-docs/pages/react-aria/I18nProvider.mdx @@ -13,7 +13,7 @@ import {FunctionAPI} from '../../src/FunctionAPI'; import docs from 'docs:@react-aria/i18n'; export const section = 'Utilities'; -export const description = 'Implementing collections in React Aria'; +export const description = 'Override the default browser locale with an application-defined locale for all child components.'; # I18nProvider diff --git a/packages/dev/s2-docs/pages/react-aria/Link.mdx b/packages/dev/s2-docs/pages/react-aria/Link.mdx index a8592ce551c..ebc143d991b 100644 --- a/packages/dev/s2-docs/pages/react-aria/Link.mdx +++ b/packages/dev/s2-docs/pages/react-aria/Link.mdx @@ -9,6 +9,7 @@ import '../../tailwind/tailwind.css'; export const tags = ['anchor', 'hyperlink', 'href']; export const relatedPages = [{'title': 'useLink', 'url': 'https://react-spectrum.adobe.com/react-aria/useLink.html'}]; +export const description = 'Allows a user to navigate to another page or resource within a web page or application.'; # Link diff --git a/packages/dev/s2-docs/pages/react-aria/ListBox.mdx b/packages/dev/s2-docs/pages/react-aria/ListBox.mdx index f34c4e25ce3..9de1c8dd81c 100644 --- a/packages/dev/s2-docs/pages/react-aria/ListBox.mdx +++ b/packages/dev/s2-docs/pages/react-aria/ListBox.mdx @@ -9,7 +9,11 @@ import Anatomy from 'react-aria-components/docs/ListBoxAnatomy.svg'; import {InlineAlert, Heading, Content} from '@react-spectrum/s2' export const tags = ['options']; -export const relatedPages = [{'title': 'useListBox', 'url': 'https://react-spectrum.adobe.com/react-aria/useListBox.html'}]; +export const relatedPages = [ + {title: 'useListBox', url: 'https://react-spectrum.adobe.com/react-aria/useListBox.html'}, + {title: 'Testing', url: './ListBox/testing.html'} +]; +export const description = 'Displays a list of options and allows a user to select one or more of them.'; # ListBox diff --git a/packages/dev/s2-docs/pages/react-aria/ListBox/testing.mdx b/packages/dev/s2-docs/pages/react-aria/ListBox/testing.mdx new file mode 100644 index 00000000000..71cb74220f9 --- /dev/null +++ b/packages/dev/s2-docs/pages/react-aria/ListBox/testing.mdx @@ -0,0 +1,71 @@ +import {Layout} from '../../../src/Layout'; +export default Layout; + +import {InlineAlert, Heading, Content} from '@react-spectrum/s2'; +import testUtilDocs from 'docs:@react-aria/test-utils'; +import {InstallCommand} from '../../../src/InstallCommand'; +import {PatternTestingFAQ} from '../../../src/PatternTestingFAQ'; + +export const isSubpage = true; +export const tags = ['testing', 'listbox', 'test-utils']; +export const description = 'Testing ListBox with React Aria test utils'; + +# Testing ListBox + +## General setup + +ListBox supports long press interactions on its options in certain configurations. See the following sections on how to handle these behaviors in your tests. + +* [Timers](../testing.html#timers) +* [Long press](../testing.html#simulating-user-long-press) + +## Test utils + +`@react-aria/test-utils` offers common listbox interaction testing utilities. Install it with your preferred package manager. + + + + + Requirements + Please note that this library uses [@testing-library/react@16](https://www.npmjs.com/package/@testing-library/react) and [@testing-library/user-event@14](https://www.npmjs.com/package/@testing-library/user-event). This means that you need to be on React 18+ in order for these utilities to work. + + +Initialize a `User` object at the top of your test file, and use it to create a `ListBox` pattern tester in your test cases. The tester has methods that you can call within your test to query for specific subcomponents or simulate common interactions. + +```ts +// ListBox.test.ts +import {render} from '@testing-library/react'; +import {User} from '@react-aria/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'); +}); +``` + +## API + +### User + + + +### ListBoxTester + + + +## Testing FAQ + + diff --git a/packages/dev/s2-docs/pages/react-aria/Menu.mdx b/packages/dev/s2-docs/pages/react-aria/Menu.mdx index 22d2222a549..1bb490753ae 100644 --- a/packages/dev/s2-docs/pages/react-aria/Menu.mdx +++ b/packages/dev/s2-docs/pages/react-aria/Menu.mdx @@ -7,7 +7,11 @@ import Anatomy from 'react-aria-components/docs/MenuAnatomy.svg'; import {InlineAlert, Heading, Content} from '@react-spectrum/s2' export const tags = ['dropdown']; -export const relatedPages = [{'title': 'useMenu', 'url': 'https://react-spectrum.adobe.com/react-aria/useMenu.html'}]; +export const relatedPages = [ + {title: 'useMenu', url: 'https://react-spectrum.adobe.com/react-aria/useMenu.html'}, + {title: 'Testing', url: './Menu/testing.html'} +]; +export const description = 'Displays a list of actions or options that a user can choose.'; # Menu diff --git a/packages/dev/s2-docs/pages/react-aria/Menu/testing.mdx b/packages/dev/s2-docs/pages/react-aria/Menu/testing.mdx new file mode 100644 index 00000000000..6b8a641bcd6 --- /dev/null +++ b/packages/dev/s2-docs/pages/react-aria/Menu/testing.mdx @@ -0,0 +1,81 @@ +import {Layout} from '../../../src/Layout'; +export default Layout; + +import {InlineAlert, Heading, Content} from '@react-spectrum/s2'; +import testUtilDocs from 'docs:@react-aria/test-utils'; +import {InstallCommand} from '../../../src/InstallCommand'; +import {PatternTestingFAQ} from '../../../src/PatternTestingFAQ'; + +export const isSubpage = true; +export const tags = ['testing', 'menu', 'test-utils']; +export const description = 'Testing Menu with React Aria test utils'; + +# Testing Menu + +## General setup + +Menu supports long press interactions in certain configurations. See the following sections on how to handle these behaviors in your tests. + +* [Timers](../testing.html#timers) +* [Long press](../testing.html#simulating-user-long-press) + +## Test utils + +`@react-aria/test-utils` offers common menu interaction testing utilities. Install it with your preferred package manager. + + + + + Requirements + Please note that this library uses [@testing-library/react@16](https://www.npmjs.com/package/@testing-library/react) and [@testing-library/user-event@14](https://www.npmjs.com/package/@testing-library/user-event). This means that you need to be on React 18+ in order for these utilities to work. + + +Initialize a `User` object at the top of your test file, and use it to create a `Menu` pattern tester in your test cases. The tester has methods that you can call within your test to query for specific subcomponents or simulate common interactions. + +```ts +// Menu.test.ts +import {render} from '@testing-library/react'; +import {User} from '@react-aria/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(); +}); +``` + +## API + +### User + + + +### MenuTester + + + +## Testing FAQ + + diff --git a/packages/dev/s2-docs/pages/react-aria/Meter.mdx b/packages/dev/s2-docs/pages/react-aria/Meter.mdx index 249980dbe37..a9bd4dcebb2 100644 --- a/packages/dev/s2-docs/pages/react-aria/Meter.mdx +++ b/packages/dev/s2-docs/pages/react-aria/Meter.mdx @@ -10,6 +10,7 @@ import Anatomy from '@react-aria/meter/docs/anatomy.svg'; export const tags = ['gauge', 'progress', 'level']; export const relatedPages = [{'title': 'useMeter', 'url': 'https://react-spectrum.adobe.com/react-aria/useMeter.html'}]; +export const description = 'Represents a quantity within a known range, or a fractional value.'; # Meter diff --git a/packages/dev/s2-docs/pages/react-aria/Modal.mdx b/packages/dev/s2-docs/pages/react-aria/Modal.mdx index b0e5dc0b981..cae2e534a9d 100644 --- a/packages/dev/s2-docs/pages/react-aria/Modal.mdx +++ b/packages/dev/s2-docs/pages/react-aria/Modal.mdx @@ -9,6 +9,7 @@ import {InlineAlert, Heading, Content} from '@react-spectrum/s2' export const tags = ['dialog', 'popup', 'overlay']; export const relatedPages = [{'title': 'useModalOverlay', 'url': 'https://react-spectrum.adobe.com/react-aria/useModalOverlay.html'}]; +export const description = 'An overlay element which blocks interaction with elements outside it.'; # Modal @@ -47,7 +48,7 @@ export const relatedPages = [{'title': 'useModalOverlay', 'url': 'https://react- } ``` - ```tsx render docs={vanillaDocs.exports.Modal} links={vanillaDocs.links} props={['isDismissable', 'isKeyboardDismissDisabled']} type="tailwind" files={["starters/docs/src/Modal.tsx"]} + ```tsx render docs={vanillaDocs.exports.Modal} links={vanillaDocs.links} props={['isDismissable', 'isKeyboardDismissDisabled']} type="tailwind" files={["starters/tailwind/src/Modal.tsx"]} "use client"; import {DialogTrigger, Heading} from 'react-aria-components'; import {Modal} from 'tailwind-starter/Modal'; diff --git a/packages/dev/s2-docs/pages/react-aria/NumberField.mdx b/packages/dev/s2-docs/pages/react-aria/NumberField.mdx index 3331df35bcf..fc4d57f0792 100644 --- a/packages/dev/s2-docs/pages/react-aria/NumberField.mdx +++ b/packages/dev/s2-docs/pages/react-aria/NumberField.mdx @@ -10,6 +10,7 @@ import Anatomy from '@react-aria/numberfield/docs/anatomy.svg'; export const tags = ['input']; export const relatedPages = [{'title': 'useNumberField', 'url': 'https://react-spectrum.adobe.com/react-aria/useNumberField.html'}]; +export const description = 'Allows a user to enter a number, and increment or decrement the value using stepper buttons.'; # NumberField diff --git a/packages/dev/s2-docs/pages/react-aria/Popover.mdx b/packages/dev/s2-docs/pages/react-aria/Popover.mdx index 17bc2912a0a..a0a754f9522 100644 --- a/packages/dev/s2-docs/pages/react-aria/Popover.mdx +++ b/packages/dev/s2-docs/pages/react-aria/Popover.mdx @@ -9,6 +9,7 @@ import {InlineAlert, Heading, Content} from '@react-spectrum/s2' export const tags = ['popup', 'overlay']; export const relatedPages = [{'title': 'usePopover', 'url': 'https://react-spectrum.adobe.com/react-aria/usePopover.html'}]; +export const description = 'An overlay element positioned relative to a trigger.'; # Popover @@ -43,7 +44,7 @@ export const relatedPages = [{'title': 'usePopover', 'url': 'https://react-spect } ``` - ```tsx render docs={vanillaDocs.exports.Popover} links={vanillaDocs.links} props={['placement', 'offset', 'crossOffset', 'shouldFlip']} type="tailwind" files={["starters/docs/src/Popover.tsx"]} + ```tsx render docs={vanillaDocs.exports.Popover} links={vanillaDocs.links} props={['placement', 'offset', 'crossOffset', 'shouldFlip']} type="tailwind" files={["starters/tailwind/src/Popover.tsx"]} "use client"; import {DialogTrigger} from 'react-aria-components'; import {Popover} from 'tailwind-starter/Popover'; diff --git a/packages/dev/s2-docs/pages/react-aria/PortalProvider.mdx b/packages/dev/s2-docs/pages/react-aria/PortalProvider.mdx index d6d45cb6115..027f9c4a608 100644 --- a/packages/dev/s2-docs/pages/react-aria/PortalProvider.mdx +++ b/packages/dev/s2-docs/pages/react-aria/PortalProvider.mdx @@ -13,6 +13,7 @@ import docs from 'docs:@react-aria/overlays'; import {FunctionAPI} from '../../src/FunctionAPI'; export const section = 'Utilities'; +export const description = 'Allows specifying the container element where overlays like modals and popovers are rendered.'; # PortalProvider diff --git a/packages/dev/s2-docs/pages/react-aria/ProgressBar.mdx b/packages/dev/s2-docs/pages/react-aria/ProgressBar.mdx index f6f8756b30d..03ebdbee71e 100644 --- a/packages/dev/s2-docs/pages/react-aria/ProgressBar.mdx +++ b/packages/dev/s2-docs/pages/react-aria/ProgressBar.mdx @@ -12,6 +12,7 @@ import Anatomy from '@react-aria/progress/docs/anatomy.svg'; export const tags = ['loading', 'progress']; export const relatedPages = [{'title': 'useProgressBar', 'url': 'https://react-spectrum.adobe.com/react-aria/useProgressBar.html'}]; +export const description = 'Shows either determinate or indeterminate progress of an operation over time.'; # ProgressBar diff --git a/packages/dev/s2-docs/pages/react-aria/RadioGroup.mdx b/packages/dev/s2-docs/pages/react-aria/RadioGroup.mdx index 7483ae74df8..7d0efc978e1 100644 --- a/packages/dev/s2-docs/pages/react-aria/RadioGroup.mdx +++ b/packages/dev/s2-docs/pages/react-aria/RadioGroup.mdx @@ -5,9 +5,14 @@ import docs from 'docs:react-aria-components'; import vanillaDocs from 'docs:vanilla-starter/RadioGroup'; import '../../tailwind/tailwind.css'; import Anatomy from '@react-aria/radio/docs/anatomy.svg'; +import {InlineAlert, Heading, Content} from '@react-spectrum/s2' export const tags = ['input']; -export const relatedPages = [{'title': 'useRadioGroup', 'url': 'https://react-spectrum.adobe.com/react-aria/useRadioGroup.html'}]; +export const relatedPages = [ + {title: 'useRadioGroup', url: 'https://react-spectrum.adobe.com/react-aria/useRadioGroup.html'}, + {title: 'Testing', url: './RadioGroup/testing.html'} +]; +export const description = 'Allows a user to select a single item from a list of mutually exclusive options.'; # RadioGroup diff --git a/packages/dev/s2-docs/pages/react-aria/RadioGroup/testing.mdx b/packages/dev/s2-docs/pages/react-aria/RadioGroup/testing.mdx new file mode 100644 index 00000000000..cc9413467c3 --- /dev/null +++ b/packages/dev/s2-docs/pages/react-aria/RadioGroup/testing.mdx @@ -0,0 +1,71 @@ +import {Layout} from '../../../src/Layout'; +export default Layout; + +import {InlineAlert, Heading, Content} from '@react-spectrum/s2'; +import testUtilDocs from 'docs:@react-aria/test-utils'; +import {InstallCommand} from '../../../src/InstallCommand'; +import {PatternTestingFAQ} from '../../../src/PatternTestingFAQ'; + +export const isSubpage = true; +export const tags = ['testing', 'radiogroup', 'test-utils']; +export const description = 'Testing RadioGroup with React Aria test utils'; + +# Testing RadioGroup + +## Test utils + +`@react-aria/test-utils` offers common radio group interaction testing utilities. Install it with your preferred package manager. + + + + + Requirements + Please note that this library uses [@testing-library/react@16](https://www.npmjs.com/package/@testing-library/react) and [@testing-library/user-event@14](https://www.npmjs.com/package/@testing-library/user-event). This means that you need to be on React 18+ in order for these utilities to work. + + +Initialize a `User` object at the top of your test file, and use it to create a `RadioGroup` pattern tester in your test cases. The tester has methods that you can call within your test to query for specific subcomponents or simulate common interactions. + +```ts +// RadioGroup.test.ts +import {render} from '@testing-library/react'; +import {User} from '@react-aria/test-utils'; + +let testUtilUser = new User({ + interactionType: 'mouse', + advanceTimer: jest.advanceTimersByTime +}); +// ... + +it('RadioGroup can switch the selected radio', async function () { + // Render your test component/app and initialize the radiogroup tester + let {getByRole} = render( + + ... + + ); + + let radioGroupTester = testUtilUser.createTester('RadioGroup', {root: getByRole('radiogroup')}); + let radios = radioGroupTester.radios; + expect(radioGroupTester.selectedRadio).toBeFalsy(); + + await radioGroupTester.triggerRadio({radio: radios[0]}); + expect(radioGroupTester.selectedRadio).toBe(radios[0]); + + await radioGroupTester.triggerRadio({radio: radios[1]}); + expect(radioGroupTester.selectedRadio).toBe(radios[1]); +}); +``` + +## API + +### User + + + +### RadioGroupTester + + + +## Testing FAQ + + diff --git a/packages/dev/s2-docs/pages/react-aria/RangeCalendar.mdx b/packages/dev/s2-docs/pages/react-aria/RangeCalendar.mdx index 0967abe328d..b90feb7ecc0 100644 --- a/packages/dev/s2-docs/pages/react-aria/RangeCalendar.mdx +++ b/packages/dev/s2-docs/pages/react-aria/RangeCalendar.mdx @@ -10,6 +10,7 @@ import Anatomy from '@react-aria/calendar/docs/rangecalendar-anatomy.svg'; export const tags = ['calendar']; export const relatedPages = [{'title': 'useRangeCalendar', 'url': 'https://react-spectrum.adobe.com/react-aria/useRangeCalendar.html'}]; +export const description = 'Displays one or more date grids and allows users to select a contiguous range of dates.'; # RangeCalendar diff --git a/packages/dev/s2-docs/pages/react-aria/SSRProvider.mdx b/packages/dev/s2-docs/pages/react-aria/SSRProvider.mdx index 11d26466884..3a04beed616 100644 --- a/packages/dev/s2-docs/pages/react-aria/SSRProvider.mdx +++ b/packages/dev/s2-docs/pages/react-aria/SSRProvider.mdx @@ -12,7 +12,7 @@ export default Layout; import docs from 'docs:@react-aria/ssr'; export const section = 'Utilities'; -export const description = 'Implementing collections in React Aria'; +export const description = 'Ensure consistent auto-generated IDs between server and client in React 16 and 17.'; # SSRProvider diff --git a/packages/dev/s2-docs/pages/react-aria/SearchField.mdx b/packages/dev/s2-docs/pages/react-aria/SearchField.mdx index ed5f120338e..56286cb6cb0 100644 --- a/packages/dev/s2-docs/pages/react-aria/SearchField.mdx +++ b/packages/dev/s2-docs/pages/react-aria/SearchField.mdx @@ -10,6 +10,7 @@ import Anatomy from '@react-aria/searchfield/docs/anatomy.svg'; export const tags = ['input']; export const relatedPages = [{'title': 'useSearchField', 'url': 'https://react-spectrum.adobe.com/react-aria/useSearchField.html'}]; +export const description = 'Allows a user to enter and clear a search query.'; # SearchField diff --git a/packages/dev/s2-docs/pages/react-aria/Select.mdx b/packages/dev/s2-docs/pages/react-aria/Select.mdx index 6d184c3fd1d..b272e993b7f 100644 --- a/packages/dev/s2-docs/pages/react-aria/Select.mdx +++ b/packages/dev/s2-docs/pages/react-aria/Select.mdx @@ -5,9 +5,14 @@ import docs from 'docs:react-aria-components'; import vanillaDocs from 'docs:vanilla-starter/Select'; import '../../tailwind/tailwind.css'; import Anatomy from 'react-aria-components/docs/SelectAnatomy.svg'; +import {InlineAlert, Heading, Content} from '@react-spectrum/s2' export const tags = ['picker', 'dropdown', 'menu', 'input']; -export const relatedPages = [{'title': 'useSelect', 'url': 'https://react-spectrum.adobe.com/react-aria/useSelect.html'}]; +export const relatedPages = [ + {title: 'useSelect', url: 'https://react-spectrum.adobe.com/react-aria/useSelect.html'}, + {title: 'Testing', url: './Select/testing.html'} +]; +export const description = 'Displays a collapsible list of options and allows a user to select one of them.'; # Select diff --git a/packages/dev/s2-docs/pages/react-aria/Select/testing.mdx b/packages/dev/s2-docs/pages/react-aria/Select/testing.mdx new file mode 100644 index 00000000000..7a442362ad5 --- /dev/null +++ b/packages/dev/s2-docs/pages/react-aria/Select/testing.mdx @@ -0,0 +1,66 @@ +import {Layout} from '../../../src/Layout'; +export default Layout; + +import {InlineAlert, Heading, Content} from '@react-spectrum/s2'; +import testUtilDocs from 'docs:@react-aria/test-utils'; +import {InstallCommand} from '../../../src/InstallCommand'; +import {PatternTestingFAQ} from '../../../src/PatternTestingFAQ'; + +export const isSubpage = true; +export const tags = ['testing', 'select', 'test-utils']; +export const description = 'Testing Select with React Aria test utils'; + +# Testing Select + +## Test utils + +`@react-aria/test-utils` offers common select interaction testing utilities. Install it with your preferred package manager. + + + + + Requirements + Please note that this library uses [@testing-library/react@16](https://www.npmjs.com/package/@testing-library/react) and [@testing-library/user-event@14](https://www.npmjs.com/package/@testing-library/user-event). This means that you need to be on React 18+ in order for these utilities to work. + + +Initialize a `User` object at the top of your test file, and use it to create a `Select` pattern tester in your test cases. The tester has methods that you can call within your test to query for specific subcomponents or simulate common interactions. + +```ts +// Select.test.ts +import {render} from '@testing-library/react'; +import {User} from '@react-aria/test-utils'; + +let testUtilUser = new User({ + interactionType: 'mouse' +}); +// ... + +it('Select 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 an item'); + + await selectTester.selectOption({option: 'Cat'}); + expect(trigger).toHaveTextContent('Cat'); +}); +``` + +## API + +### User + + + +### SelectTester + + + +## Testing FAQ + + diff --git a/packages/dev/s2-docs/pages/react-aria/Slider.mdx b/packages/dev/s2-docs/pages/react-aria/Slider.mdx index 3548cf73e05..0be73dcdb90 100644 --- a/packages/dev/s2-docs/pages/react-aria/Slider.mdx +++ b/packages/dev/s2-docs/pages/react-aria/Slider.mdx @@ -10,6 +10,7 @@ import Anatomy from '@react-aria/slider/docs/anatomy.svg'; export const tags = ['range input', 'track', 'scrubber']; export const relatedPages = [{'title': 'useSlider', 'url': 'https://react-spectrum.adobe.com/react-aria/useSlider.html'}]; +export const description = 'Allows a user to select one or more values within a range.'; # Slider diff --git a/packages/dev/s2-docs/pages/react-aria/Switch.mdx b/packages/dev/s2-docs/pages/react-aria/Switch.mdx index fb452c06c5e..f5b16d51f6a 100644 --- a/packages/dev/s2-docs/pages/react-aria/Switch.mdx +++ b/packages/dev/s2-docs/pages/react-aria/Switch.mdx @@ -9,6 +9,7 @@ import Anatomy from '@react-aria/switch/docs/anatomy.svg'; export const tags = ['toggle', 'input']; export const relatedPages = [{'title': 'useSwitch', 'url': 'https://react-spectrum.adobe.com/react-aria/useSwitch.html'}]; +export const description = 'Allows a user to turn a setting on or off.'; # Switch diff --git a/packages/dev/s2-docs/pages/react-aria/Table.mdx b/packages/dev/s2-docs/pages/react-aria/Table.mdx index 6c3f601859d..2edbe5339c5 100644 --- a/packages/dev/s2-docs/pages/react-aria/Table.mdx +++ b/packages/dev/s2-docs/pages/react-aria/Table.mdx @@ -1,3 +1,4 @@ +import {InstallCommand} from '../../src/InstallCommand'; import {Layout} from '../../src/Layout'; export default Layout; @@ -5,10 +6,14 @@ import docs from 'docs:react-aria-components'; import vanillaDocs from 'docs:vanilla-starter/Table'; import '../../tailwind/tailwind.css'; import Anatomy from 'react-aria-components/docs/TableAnatomy.svg'; -import {InlineAlert, Heading, Content} from '@react-spectrum/s2' +import {InlineAlert, Heading, Content} from '@react-spectrum/s2'; export const tags = ['data', 'grid']; -export const relatedPages = [{'title': 'useTable', 'url': 'https://react-spectrum.adobe.com/react-aria/useTable.html'}]; +export const relatedPages = [ + {title: 'useTable', url: 'https://react-spectrum.adobe.com/react-aria/useTable.html'}, + {title: 'Testing', url: './Table/testing.html'} +]; +export const description = 'Displays data in rows and columns and enables a user to navigate its contents via directional navigation keys.'; # Table diff --git a/packages/dev/s2-docs/pages/react-aria/Table/testing.mdx b/packages/dev/s2-docs/pages/react-aria/Table/testing.mdx new file mode 100644 index 00000000000..ec5d5f1f201 --- /dev/null +++ b/packages/dev/s2-docs/pages/react-aria/Table/testing.mdx @@ -0,0 +1,85 @@ +import {Layout} from '../../../src/Layout'; +export default Layout; + +import {InlineAlert, Heading, Content} from '@react-spectrum/s2'; +import testUtilDocs from 'docs:@react-aria/test-utils'; +import {InstallCommand} from '../../../src/InstallCommand'; +import {PatternTestingFAQ} from '../../../src/PatternTestingFAQ'; + +export const isSubpage = true; +export const tags = ['testing', 'table', 'test-utils']; +export const description = 'Testing Table with React Aria test utils'; + +# Testing Table + +## General setup + +Table supports long press interactions on its rows in certain configurations. See the following sections on how to handle these behaviors in your tests. + +* [Timers](../testing.html#timers) +* [Long press](../testing.html#simulating-user-long-press) + +## Test utils + +`@react-aria/test-utils` offers common table interaction testing utilities. Install it with your preferred package manager. + + + + + Requirements + Please note that this library uses [@testing-library/react@16](https://www.npmjs.com/package/@testing-library/react) and [@testing-library/user-event@14](https://www.npmjs.com/package/@testing-library/user-event). This means that you need to be on React 18+ in order for these utilities to work. + + +Initialize a `User` object at the top of your test file, and use it to create a `Table` pattern tester in your test cases. The tester has methods that you can call within your test to query for specific subcomponents or simulate common interactions. + +```ts +// Table.test.ts +import {render, within} from '@testing-library/react'; +import {User} from '@react-aria/test-utils'; + +let testUtilUser = new User({ + interactionType: 'mouse', + advanceTimer: jest.advanceTimersByTime +}); +// ... + +it('Table 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); +}); +``` + +## API + +### User + + + +### TableTester + + + +## Testing FAQ + + diff --git a/packages/dev/s2-docs/pages/react-aria/Tabs.mdx b/packages/dev/s2-docs/pages/react-aria/Tabs.mdx index becb5cf3c5d..689e6fddd4a 100644 --- a/packages/dev/s2-docs/pages/react-aria/Tabs.mdx +++ b/packages/dev/s2-docs/pages/react-aria/Tabs.mdx @@ -6,9 +6,14 @@ import {Tabs as VanillaTabs, TabsItem} from 'vanilla-starter/Tabs'; import vanillaDocs from 'docs:vanilla-starter/Tabs'; import '../../tailwind/tailwind.css'; import Anatomy from '@react-aria/tabs/docs/anatomy.svg'; +import {InlineAlert, Heading, Content} from '@react-spectrum/s2' export const tags = ['navigation']; -export const relatedPages = [{'title': 'useTabs', 'url': 'https://react-spectrum.adobe.com/react-aria/useTabs.html'}]; +export const relatedPages = [ + {title: 'useTabs', url: 'https://react-spectrum.adobe.com/react-aria/useTabs.html'}, + {title: 'Testing', url: './Tabs/testing.html'} +]; +export const description = 'Organizes content into multiple sections and allows users to navigate between them.'; # Tabs diff --git a/packages/dev/s2-docs/pages/react-aria/Tabs/testing.mdx b/packages/dev/s2-docs/pages/react-aria/Tabs/testing.mdx new file mode 100644 index 00000000000..a1aaad1a796 --- /dev/null +++ b/packages/dev/s2-docs/pages/react-aria/Tabs/testing.mdx @@ -0,0 +1,67 @@ +import {Layout} from '../../../src/Layout'; +export default Layout; + +import {InlineAlert, Heading, Content} from '@react-spectrum/s2'; +import testUtilDocs from 'docs:@react-aria/test-utils'; +import {InstallCommand} from '../../../src/InstallCommand'; +import {PatternTestingFAQ} from '../../../src/PatternTestingFAQ'; + +export const isSubpage = true; +export const tags = ['testing', 'tabs', 'test-utils']; +export const description = 'Testing Tabs with React Aria test utils'; + +# Testing Tabs + +## Test utils + +`@react-aria/test-utils` offers common tabs interaction testing utilities. Install it with your preferred package manager. + + + + + Requirements + Please note that this library uses [@testing-library/react@16](https://www.npmjs.com/package/@testing-library/react) and [@testing-library/user-event@14](https://www.npmjs.com/package/@testing-library/user-event). This means that you need to be on React 18+ in order for these utilities to work. + + +Initialize a `User` object at the top of your test file, and use it to create a `Tabs` pattern tester in your test cases. The tester has methods that you can call within your test to query for specific subcomponents or simulate common interactions. + +```ts +// Tabs.test.ts +import {render} from '@testing-library/react'; +import {User} from '@react-aria/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]); +}); +``` + +## API + +### User + + + +### TabsTester + + + +## Testing FAQ + + diff --git a/packages/dev/s2-docs/pages/react-aria/TagGroup.mdx b/packages/dev/s2-docs/pages/react-aria/TagGroup.mdx index 472218b461d..af5f8c4b09b 100644 --- a/packages/dev/s2-docs/pages/react-aria/TagGroup.mdx +++ b/packages/dev/s2-docs/pages/react-aria/TagGroup.mdx @@ -9,6 +9,7 @@ import Anatomy from '@react-aria/tag/docs/anatomy.svg'; export const tags = ['chips', 'pills']; export const relatedPages = [{'title': 'useTagGroup', 'url': 'https://react-spectrum.adobe.com/react-aria/useTagGroup.html'}]; +export const description = 'A focusable list of labels, categories, keywords, filters, or other items.'; # TagGroup diff --git a/packages/dev/s2-docs/pages/react-aria/TextField.mdx b/packages/dev/s2-docs/pages/react-aria/TextField.mdx index bd2629eb852..2a263fa4b11 100644 --- a/packages/dev/s2-docs/pages/react-aria/TextField.mdx +++ b/packages/dev/s2-docs/pages/react-aria/TextField.mdx @@ -10,6 +10,7 @@ import Anatomy from '@react-aria/textfield/docs/anatomy.svg'; export const tags = ['input']; export const relatedPages = [{'title': 'useTextField', 'url': 'https://react-spectrum.adobe.com/react-aria/useTextField.html'}]; +export const description = 'Allows a user to enter a plain text value with a keyboard.'; # TextField diff --git a/packages/dev/s2-docs/pages/react-aria/TimeField.mdx b/packages/dev/s2-docs/pages/react-aria/TimeField.mdx index 5aa02dd2c3e..d15774c68cc 100644 --- a/packages/dev/s2-docs/pages/react-aria/TimeField.mdx +++ b/packages/dev/s2-docs/pages/react-aria/TimeField.mdx @@ -10,6 +10,7 @@ import Anatomy from '@react-aria/datepicker/docs/timefield-anatomy.svg'; export const tags = ['date', 'input']; export const relatedPages = [{'title': 'useTimeField', 'url': 'https://react-spectrum.adobe.com/react-aria/useTimeField.html'}]; +export const description = 'Allows users to enter and edit time values using a keyboard.'; # TimeField diff --git a/packages/dev/s2-docs/pages/react-aria/Toast.mdx b/packages/dev/s2-docs/pages/react-aria/Toast.mdx index 0b39ee77e56..ec14013517e 100644 --- a/packages/dev/s2-docs/pages/react-aria/Toast.mdx +++ b/packages/dev/s2-docs/pages/react-aria/Toast.mdx @@ -9,6 +9,8 @@ import {InlineAlert, Heading, Content} from '@react-spectrum/s2'; export const tags = ['notifications']; export const version = 'alpha'; +export const relatedPages = [{'title': 'useToast', 'url': 'https://react-spectrum.adobe.com/react-aria/useToast.html'}]; +export const description = 'Displays brief, temporary notifications of actions, errors, or other events in an application.'; # Toast diff --git a/packages/dev/s2-docs/pages/react-aria/ToggleButton.mdx b/packages/dev/s2-docs/pages/react-aria/ToggleButton.mdx index cf5048a5a03..1a0f5f091d0 100644 --- a/packages/dev/s2-docs/pages/react-aria/ToggleButton.mdx +++ b/packages/dev/s2-docs/pages/react-aria/ToggleButton.mdx @@ -9,6 +9,7 @@ import '../../tailwind/tailwind.css'; export const tags = ['button', 'btn']; export const relatedPages = [{'title': 'useToggleButton', 'url': 'https://react-spectrum.adobe.com/react-aria/useToggleButton.html'}]; +export const description = 'Allows a user to toggle a selection on or off.'; # ToggleButton diff --git a/packages/dev/s2-docs/pages/react-aria/ToggleButtonGroup.mdx b/packages/dev/s2-docs/pages/react-aria/ToggleButtonGroup.mdx index a3115e6e4ae..c7a64e89452 100644 --- a/packages/dev/s2-docs/pages/react-aria/ToggleButtonGroup.mdx +++ b/packages/dev/s2-docs/pages/react-aria/ToggleButtonGroup.mdx @@ -7,6 +7,7 @@ import Anatomy from '@react-aria/button/docs/ToggleButtonGroupAnatomy.svg'; export const tags = ['toggle', 'btn']; export const relatedPages = [{'title': 'useToggleButtonGroup', 'url': 'https://react-spectrum.adobe.com/react-aria/useToggleButtonGroup.html'}]; +export const description = 'Allows a user to toggle multiple options, with single or multiple selection.'; # ToggleButtonGroup diff --git a/packages/dev/s2-docs/pages/react-aria/Toolbar.mdx b/packages/dev/s2-docs/pages/react-aria/Toolbar.mdx index 96ea1c026e0..282c1a2636d 100644 --- a/packages/dev/s2-docs/pages/react-aria/Toolbar.mdx +++ b/packages/dev/s2-docs/pages/react-aria/Toolbar.mdx @@ -7,6 +7,7 @@ import Anatomy from '@react-aria/toolbar/docs/toolbar-anatomy.svg'; export const tags = ['group']; export const relatedPages = [{'title': 'useToolbar', 'url': 'https://react-spectrum.adobe.com/react-aria/useToolbar.html'}]; +export const description = 'A container for a set of interactive controls, such as buttons, dropdown menus, or checkboxes.'; # Toolbar @@ -56,7 +57,7 @@ export const relatedPages = [{'title': 'useToolbar', 'url': 'https://react-spect ``` - ```tsx render docs={docs.exports.Toolbar} links={docs.links} props={['orientation']} initialProps={{'aria-label': 'Text formatting'}} type="tailwind" files={["starters/docs/src/Toolbar.tsx"]} + ```tsx render docs={docs.exports.Toolbar} links={docs.links} props={['orientation']} initialProps={{'aria-label': 'Text formatting'}} type="tailwind" files={["starters/tailwind/src/Toolbar.tsx"]} "use client"; import {Toolbar} from 'tailwind-starter/Toolbar'; import {ToggleButtonGroup} from 'tailwind-starter/ToggleButtonGroup'; diff --git a/packages/dev/s2-docs/pages/react-aria/Tooltip.mdx b/packages/dev/s2-docs/pages/react-aria/Tooltip.mdx index da719e7d468..c5f8534d342 100644 --- a/packages/dev/s2-docs/pages/react-aria/Tooltip.mdx +++ b/packages/dev/s2-docs/pages/react-aria/Tooltip.mdx @@ -9,6 +9,7 @@ import {InlineAlert, Heading, Content} from '@react-spectrum/s2' export const tags = ['hint', 'popup', 'info']; export const relatedPages = [{'title': 'useTooltipTrigger', 'url': 'https://react-spectrum.adobe.com/react-aria/useTooltipTrigger.html'}]; +export const description = 'Displays a description of an element on hover or focus.'; # Tooltip @@ -38,7 +39,7 @@ export const relatedPages = [{'title': 'useTooltipTrigger', 'url': 'https://reac } ``` - ```tsx render docs={vanillaDocs.exports.Tooltip} links={vanillaDocs.links} props={['placement', 'offset', 'crossOffset', 'shouldFlip']} type="tailwind" files={["starters/docs/src/Tooltip.tsx"]} + ```tsx render docs={vanillaDocs.exports.Tooltip} links={vanillaDocs.links} props={['placement', 'offset', 'crossOffset', 'shouldFlip']} type="tailwind" files={["starters/tailwind/src/Tooltip.tsx"]} "use client"; import {TooltipTrigger} from 'react-aria-components'; import {Tooltip} from 'tailwind-starter/Tooltip'; diff --git a/packages/dev/s2-docs/pages/react-aria/Tree.mdx b/packages/dev/s2-docs/pages/react-aria/Tree.mdx index e36c40a4b7b..e318dc15ce8 100644 --- a/packages/dev/s2-docs/pages/react-aria/Tree.mdx +++ b/packages/dev/s2-docs/pages/react-aria/Tree.mdx @@ -6,8 +6,13 @@ import {Tree as VanillaTree, TreeItem} from 'vanilla-starter/Tree'; import vanillaDocs from 'docs:vanilla-starter/Tree'; import '../../tailwind/tailwind.css'; import Anatomy from 'react-aria-components/docs/TreeAnatomy.svg'; +import {InlineAlert, Heading, Content} from '@react-spectrum/s2' export const tags = ['data', 'tree', 'nested', 'hierarchy']; +export const relatedPages = [ + {title: 'Testing', url: './Tree/testing.html'} +]; +export const description = 'Provides users with a way to navigate nested hierarchical information.'; # Tree diff --git a/packages/dev/s2-docs/pages/react-aria/Tree/testing.mdx b/packages/dev/s2-docs/pages/react-aria/Tree/testing.mdx new file mode 100644 index 00000000000..f7f66d4fed7 --- /dev/null +++ b/packages/dev/s2-docs/pages/react-aria/Tree/testing.mdx @@ -0,0 +1,83 @@ +import {Layout} from '../../../src/Layout'; +export default Layout; + +import {InlineAlert, Heading, Content} from '@react-spectrum/s2'; +import testUtilDocs from 'docs:@react-aria/test-utils'; +import {InstallCommand} from '../../../src/InstallCommand'; +import {PatternTestingFAQ} from '../../../src/PatternTestingFAQ'; + +export const isSubpage = true; +export const tags = ['testing', 'tree', 'test-utils']; +export const description = 'Testing Tree with React Aria test utils'; + +# Testing Tree + +## General setup + +Tree supports long press interactions on its rows in certain configurations. See the following sections on how to handle these behaviors in your tests. + +* [Timers](../testing.html#timers) +* [Long press](../testing.html#simulating-user-long-press) + +## Test utils + +`@react-aria/test-utils` offers common tree interaction testing utilities. Install it with your preferred package manager. + + + + + Requirements + Please note that this library uses [@testing-library/react@16](https://www.npmjs.com/package/@testing-library/react) and [@testing-library/user-event@14](https://www.npmjs.com/package/@testing-library/user-event). This means that you need to be on React 18+ in order for these utilities to work. + + +Initialize a `User` object at the top of your test file, and use it to create a `Tree` pattern tester in your test cases. The tester has methods that you can call within your test to query for specific subcomponents or simulate common interactions. + +```ts +// Tree.test.ts +import {render, within} from '@testing-library/react'; +import {User} from '@react-aria/test-utils'; + +let testUtilUser = new User({ + interactionType: 'mouse' +}); +// ... + +it('Tree can select and expand a item 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(); + + await treeTester.toggleRowExpansion({index: 0}); + expect(treeTester.rows[0]).toHaveAttribute('aria-expanded', 'true'); +}); +``` + +## API + +### User + + + +### TreeTester + + + +## Testing FAQ + + diff --git a/packages/dev/s2-docs/pages/react-aria/Virtualizer.mdx b/packages/dev/s2-docs/pages/react-aria/Virtualizer.mdx index bcc24aa04b2..40b0c6b3a87 100644 --- a/packages/dev/s2-docs/pages/react-aria/Virtualizer.mdx +++ b/packages/dev/s2-docs/pages/react-aria/Virtualizer.mdx @@ -5,6 +5,7 @@ import docs from 'docs:react-aria-components'; import {GroupedPropTable} from '../../src/PropTable'; export const tags = ['windowing', 'list', 'grid', 'infinite']; +export const description = 'Renders a scrollable collection of data using customizable layouts.'; # Virtualizer diff --git a/packages/dev/s2-docs/pages/react-aria/VisuallyHidden.mdx b/packages/dev/s2-docs/pages/react-aria/VisuallyHidden.mdx index 1c59d28d698..23fad7226f7 100644 --- a/packages/dev/s2-docs/pages/react-aria/VisuallyHidden.mdx +++ b/packages/dev/s2-docs/pages/react-aria/VisuallyHidden.mdx @@ -13,6 +13,7 @@ import docs from 'docs:@react-aria/visually-hidden'; import {FunctionAPI} from '../../src/FunctionAPI'; export const section = 'Utilities'; +export const description = 'Hides its children visually but keeps content accessible to screen readers.'; # VisuallyHidden diff --git a/packages/dev/s2-docs/pages/react-aria/blog/index.mdx b/packages/dev/s2-docs/pages/react-aria/blog/index.mdx index dd6e3fcb049..b1ef5236044 100644 --- a/packages/dev/s2-docs/pages/react-aria/blog/index.mdx +++ b/packages/dev/s2-docs/pages/react-aria/blog/index.mdx @@ -16,6 +16,7 @@ export const hideNav = true; export const section = 'Blog'; export const title = 'Blog'; export const hideFromSearch = true; +export const description = 'Blog posts from the React Aria team.'; export const tags = ['blog', 'articles', 'posts']; export const isPostList = true; diff --git a/packages/dev/s2-docs/pages/react-aria/examples/index.mdx b/packages/dev/s2-docs/pages/react-aria/examples/index.mdx index 202a3363822..83c6c91651b 100644 --- a/packages/dev/s2-docs/pages/react-aria/examples/index.mdx +++ b/packages/dev/s2-docs/pages/react-aria/examples/index.mdx @@ -5,6 +5,7 @@ export const section = 'Overview'; export const hideFromSearch = true; export const title = 'Examples'; export const isPostList = true; +export const description = 'Examples built with React Aria.'; # Examples diff --git a/packages/dev/s2-docs/pages/react-aria/index.mdx b/packages/dev/s2-docs/pages/react-aria/index.mdx index 92a588f780e..37f5fb65afd 100644 --- a/packages/dev/s2-docs/pages/react-aria/index.mdx +++ b/packages/dev/s2-docs/pages/react-aria/index.mdx @@ -25,6 +25,7 @@ import SearchMenuWrapper from '../../src/SearchMenuWrapper'; export const section = 'Overview'; export const title = 'Home'; export const hideFromSearch = true; +export const description = 'Accessible, high quality UI components and hooks for building design systems.'; diff --git a/packages/dev/s2-docs/pages/react-aria/mergeProps.mdx b/packages/dev/s2-docs/pages/react-aria/mergeProps.mdx index 5bd8bf6b915..ccb7909b895 100644 --- a/packages/dev/s2-docs/pages/react-aria/mergeProps.mdx +++ b/packages/dev/s2-docs/pages/react-aria/mergeProps.mdx @@ -13,6 +13,7 @@ import docs from 'docs:@react-aria/utils'; import {FunctionAPI} from '../../src/FunctionAPI'; export const section = 'Utilities'; +export const description = 'Combines multiple prop objects together, merging event handlers, classNames, styles, and other properties.'; # mergeProps diff --git a/packages/dev/s2-docs/pages/react-aria/releases/index.mdx b/packages/dev/s2-docs/pages/react-aria/releases/index.mdx index f10e1b48439..7a2d9c2b43f 100644 --- a/packages/dev/s2-docs/pages/react-aria/releases/index.mdx +++ b/packages/dev/s2-docs/pages/react-aria/releases/index.mdx @@ -17,6 +17,7 @@ export const tags = ['changelog', 'versions', 'updates']; export const title = 'Releases'; export const hideFromSearch = true; export const isPostList = true; +export const description = 'Release notes for React Aria.'; # Releases diff --git a/packages/dev/s2-docs/pages/react-aria/testing.mdx b/packages/dev/s2-docs/pages/react-aria/testing.mdx new file mode 100644 index 00000000000..b38a9f1fe8f --- /dev/null +++ b/packages/dev/s2-docs/pages/react-aria/testing.mdx @@ -0,0 +1,226 @@ +import {VersionBadge} from '../../src/VersionBadge'; +import {InstallCommand} from '../../src/InstallCommand'; +import {Layout} from '../../src/Layout'; +export default Layout; + +import testUtilDocs from 'docs:@react-aria/test-utils'; +import {InlineAlert, Heading, Content} from '@react-spectrum/s2' + +export const section = 'Guides'; +export const description = 'Writing tests for apps built with React Aria'; + +# Testing + +This page describes how to test an application built with React Aria. It documents the available testing utilities available for each aria pattern and how they can be used to simulate common user interactions. + +## Testing semantics + +The recommended way to query for React Aria Components and their internals is by semantics. React Aria +Components implement [ARIA patterns](https://www.w3.org/TR/wai-aria-practices-1.2/). ARIA is a W3C standard +that specifies the semantics for many UI components. Unlike the DOM structure of the component, these semantics are much less likely to change over time, +making them ideal to query for. + +The main attribute to look for when querying is the [role](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques). +This attribute represents the type of element a DOM node represents, e.g. a button, list option, or tab. + +### React Testing Library + +[React Testing Library](https://testing-library.com/docs/react-testing-library/intro) is useful because it +enforces that you write tests using semantics instead of implementation details. We use React Testing Library +to test React Aria itself, and it's quite easy to [query](https://testing-library.com/docs/dom-testing-library/api-queries) +elements by role, text, label, etc. + +```tsx +import {render} from '@testing-library/react'; + +let tree = render(); +let option = tree.getByRole('button'); +``` + +## Test ids + +Querying by semantics covers many scenarios, but what if you have many buttons on a page or its text changes due to translations based on locale? +In these cases, you may need a way to identify specific elements in tests, and that's where test ids come in. + +React Aria Components pass all [data attributes](https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes) +through to their underlying DOM nodes, which allows you to use an attribute like `data-testid` to identify +a particular instance of a component. + +```tsx +import {render} from '@testing-library/react'; +import {Input, Label, TextField} from 'react-aria-components'; + +function LoginForm() { + return ( + <> + + + + + + + + + + ); +} + +let tree = render(); +let username = tree.getByTestId('username'); +let password = tree.getByTestId('password'); +``` + +## Triggering events + +React Aria Components rely on many different browser events to support different devices and platforms, so it's important to simulate +these correctly in your tests. For example, a click is really a `mousemove` and `mouseover` the target, followed +by `mousedown`, `focus`, and `mouseup` events, and finally a `click` event. + +The best way to handle this is with the [user-event](https://github.com/testing-library/user-event) library. +This lets you trigger high level interactions like a user would, and the library handles firing all of the individual +events that make up that interaction. + +```tsx +import {render} from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +let tree = render(); + +// Click on the username field to focus it, and enter the value. +userEvent.click(tree.getByLabelText('Username')); +userEvent.type(document.activeElement, 'devon'); + +// Tab to the password field, and enter the value. +userEvent.tab(); +userEvent.type(document.activeElement, 'Pas$w0rd'); + +// Tab to the submit button and click it. +userEvent.tab(); +userEvent.click(document.activeElement); +``` + +## Test setup + +### Timers + +When using fake timers, you may need to advance timers after various interactions, e.g. after selection. In Jest, use `jest.runAllTimers()`. You should also run all timers after each test completes. +See [Jest's timer docs](https://jestjs.io/docs/timer-mocks) or the equivalent docs of your test framework for more information. + +```tsx +afterEach(() => { + act(() => jest.runAllTimers()); +}); +``` + +Consider adding a `act(() => jest.runAllTimers());` after your simulated user interaction if you run into a test failure that looks like the following: + +``` +TestingLibraryElementError: Unable to find an accessible element with the role "listbox" +``` + +If you are using real timers instead, you can await a particular state of your app to be reached. If you are using React Testing Library, you can perform a `waitFor` query +to wait for a dialog to appear: + +```tsx +await waitFor(() => { + expect(getByRole('dialog')).toBeInTheDocument(); +}); +``` + +### Simulating long press + +To simulate a long press event in components like Menu, mock PointerEvent globally and use the function from `@react-aria/test-utils`. + +```tsx +import {installPointerEvent, triggerLongPress} from '@react-aria/test-utils'; +installPointerEvent(); + +// In test case +let button = getByRole('button'); +triggerLongPress(button); +``` + +### Simulating move event + +Components like ColorArea, ColorSlider, ColorWheel, and Slider each feature a draggable handle that a user can interact with to change the component's value. To simulate a drag event, mock MouseEvent and use `fireEvent` from `@testing-library/react` +to simulate these drag/move events in your tests. Additionally, the track dimensions for the draggable handle should be mocked so that the move operation calculations can be properly computed. + +```tsx +import {fireEvent} from '@testing-library/react'; +import {installMouseEvent} from '@react-aria/test-utils'; +installMouseEvent(); + +beforeAll(() => { + jest.spyOn(window.HTMLElement.prototype, 'getBoundingClientRect').mockImplementation(() => ({top: 0, left: 0, width: 100, height: 10})); +}) + +// In test case +let sliderThumb = getByRole('slider').parentElement; + +// With fireEvent, move thumb from 0 to 50 +fireEvent.mouseDown(thumb, {clientX: 0, pageX: 0}); +fireEvent.mouseMove(thumb, {pageX: 50}); +fireEvent.mouseUp(thumb, {pageX: 50}); +``` + +## React Aria test utils + + +[@react-aria/test-utils](https://www.npmjs.com/package/@react-aria/test-utils) is a set of testing utilities that aims to make writing unit tests easier for consumers of React Aria +or for users who have built their own components following the respective ARIA pattern specification. + +### Installation + + + + + Requirements + Please note that this library uses [@testing-library/react@16](https://www.npmjs.com/package/@testing-library/react) and [@testing-library/user-event@14](https://www.npmjs.com/package/@testing-library/user-event). This means that you need to be on React 18+ in order for these utilities to work. + + + +### Setup + +Initialize a `User` object at the top of your test file, and use it to create an ARIA pattern tester in your test cases. The tester has methods that you can call within your test to query for specific subcomponents or simulate common interactions. See [below](#patterns) for what patterns are currently supported. + +```ts +// YourTest.test.ts +import {screen} from '@testing-library/react'; +import {User} from '@react-aria/test-utils'; + +// Provide whatever method of advancing timers you use in your test, this example assumes Jest with fake timers. +// 'interactionType' specifies what mode of interaction should be simulated by the tester +// 'advanceTimer' is used by the tester to advance the timers in the tests for specific interactions (e.g. long press) +let testUtilUser = new User({interactionType: 'mouse', advanceTimer: jest.advanceTimersByTime}); +// ... + +it('my test case', async function () { + // Render your test component/app + render(); + // Initialize the table tester via providing the 'Table' pattern name and the root element of said table + let table = testUtilUser.createTester('Table', {root: screen.getByTestId('test_table')}); + + // ... +}); +``` + +### User API + + + + +### Patterns + +Below is a list of the ARIA patterns testers currently supported by `createTester`. See the accompanying component testing docs pages for a sample of how to use +the testers in your test suite. + +- [CheckboxGroup](./CheckboxGroup/testing.html) +- [ComboBox](./ComboBox/testing.html) +- [GridList](./GridList/testing.html) +- [ListBox](./ListBox/testing.html) +- [Menu](./Menu/testing.html) +- [RadioGroup](./RadioGroup/testing.html) +- [Select](./Select/testing.html) +- [Table](./Table/testing.html) +- [Tabs](./Tabs/testing.html) +- [Tree](./Tree/testing.html) diff --git a/packages/dev/s2-docs/pages/react-aria/useClipboard.mdx b/packages/dev/s2-docs/pages/react-aria/useClipboard.mdx index e60693d2d9a..4bfddecdf88 100644 --- a/packages/dev/s2-docs/pages/react-aria/useClipboard.mdx +++ b/packages/dev/s2-docs/pages/react-aria/useClipboard.mdx @@ -15,6 +15,7 @@ import docs from 'docs:@react-aria/dnd'; import sharedDocs from 'docs:@react-types/shared/src/dnd.d.ts'; export const section = 'Interactions'; +export const description = 'Handles interactions with the clipboard, including copy and paste operations.'; # useClipboard diff --git a/packages/dev/s2-docs/pages/react-aria/useCollator.mdx b/packages/dev/s2-docs/pages/react-aria/useCollator.mdx index eae2aa7f72f..128f7f72898 100644 --- a/packages/dev/s2-docs/pages/react-aria/useCollator.mdx +++ b/packages/dev/s2-docs/pages/react-aria/useCollator.mdx @@ -13,7 +13,7 @@ import {FunctionAPI} from '../../src/FunctionAPI'; import docs from 'docs:@react-aria/i18n'; export const section = 'Utilities'; -export const description = 'Implementing collections in React Aria'; +export const description = 'Wraps Intl.Collator and compares and sorts strings according to the current locale.'; # useCollator diff --git a/packages/dev/s2-docs/pages/react-aria/useDateFormatter.mdx b/packages/dev/s2-docs/pages/react-aria/useDateFormatter.mdx index 2200b1df9be..f8335a739dc 100644 --- a/packages/dev/s2-docs/pages/react-aria/useDateFormatter.mdx +++ b/packages/dev/s2-docs/pages/react-aria/useDateFormatter.mdx @@ -14,7 +14,7 @@ import {InterfaceType} from '../../src/types'; import docs from 'docs:@react-aria/i18n'; export const section = 'Utilities'; -export const description = 'Implementing collections in React Aria'; +export const description = 'Wraps Intl.DateTimeFormat and formats dates for the current locale.'; # useDateFormatter diff --git a/packages/dev/s2-docs/pages/react-aria/useDrag.mdx b/packages/dev/s2-docs/pages/react-aria/useDrag.mdx index f23fce97bb4..ab28cda2797 100644 --- a/packages/dev/s2-docs/pages/react-aria/useDrag.mdx +++ b/packages/dev/s2-docs/pages/react-aria/useDrag.mdx @@ -16,6 +16,7 @@ import docs from 'docs:@react-aria/dnd'; import sharedDocs from 'docs:@react-types/shared/src/dnd.d.ts'; export const section = 'Interactions'; +export const description = 'Handles drag interactions for an element, providing accessibility and cross-browser support.'; # useDrag diff --git a/packages/dev/s2-docs/pages/react-aria/useDrop.mdx b/packages/dev/s2-docs/pages/react-aria/useDrop.mdx index 3c8d7eca42e..3a7b936cb24 100644 --- a/packages/dev/s2-docs/pages/react-aria/useDrop.mdx +++ b/packages/dev/s2-docs/pages/react-aria/useDrop.mdx @@ -16,6 +16,7 @@ import sharedDocs from 'docs:@react-types/shared/src/dnd.d.ts'; import typesDocs from 'docs:@react-types/shared/src/events.d.ts'; export const section = 'Interactions'; +export const description = 'Handles drop interactions for a target element, providing accessibility and cross-browser support.'; # useDrop diff --git a/packages/dev/s2-docs/pages/react-aria/useField.mdx b/packages/dev/s2-docs/pages/react-aria/useField.mdx index 5d337f9d0b0..236e7a63788 100644 --- a/packages/dev/s2-docs/pages/react-aria/useField.mdx +++ b/packages/dev/s2-docs/pages/react-aria/useField.mdx @@ -14,6 +14,7 @@ import {FunctionAPI} from '../../src/FunctionAPI'; import {InterfaceType} from '../../src/types'; export const section = 'Utilities'; +export const description = 'Provides accessibility and labeling support for form field components.'; # useField diff --git a/packages/dev/s2-docs/pages/react-aria/useFilter.mdx b/packages/dev/s2-docs/pages/react-aria/useFilter.mdx index 8cbcb7121d0..167ca7a72ac 100644 --- a/packages/dev/s2-docs/pages/react-aria/useFilter.mdx +++ b/packages/dev/s2-docs/pages/react-aria/useFilter.mdx @@ -13,7 +13,7 @@ import {FunctionAPI} from '../../src/FunctionAPI'; import docs from 'docs:@react-aria/i18n'; export const section = 'Utilities'; -export const description = 'Implementing collections in React Aria'; +export const description = 'Provides locale-sensitive string matching for filtering and searching.'; # useFilter diff --git a/packages/dev/s2-docs/pages/react-aria/useFocus.mdx b/packages/dev/s2-docs/pages/react-aria/useFocus.mdx index db4b707e80c..481a84271b0 100644 --- a/packages/dev/s2-docs/pages/react-aria/useFocus.mdx +++ b/packages/dev/s2-docs/pages/react-aria/useFocus.mdx @@ -13,6 +13,7 @@ import {FunctionAPI} from '../../src/FunctionAPI'; import docs from 'docs:@react-aria/interactions'; import {InterfaceType} from '../../src/types'; export const section = 'Interactions'; +export const description = 'Handles focus events for the target element only, not descendants.'; # useFocus diff --git a/packages/dev/s2-docs/pages/react-aria/useFocusRing.mdx b/packages/dev/s2-docs/pages/react-aria/useFocusRing.mdx index d3c3cae7a4b..7fba23c08d3 100644 --- a/packages/dev/s2-docs/pages/react-aria/useFocusRing.mdx +++ b/packages/dev/s2-docs/pages/react-aria/useFocusRing.mdx @@ -14,6 +14,7 @@ import {FunctionAPI} from '../../src/FunctionAPI'; import docs from 'docs:@react-aria/focus'; export const section = 'Interactions'; +export const description = 'Determines whether a focus ring should be displayed for keyboard focus.'; # useFocusRing diff --git a/packages/dev/s2-docs/pages/react-aria/useFocusVisible.mdx b/packages/dev/s2-docs/pages/react-aria/useFocusVisible.mdx index 53b02e28e41..f70608fcfc3 100644 --- a/packages/dev/s2-docs/pages/react-aria/useFocusVisible.mdx +++ b/packages/dev/s2-docs/pages/react-aria/useFocusVisible.mdx @@ -14,6 +14,7 @@ import {InterfaceType} from '../../src/types'; import {FunctionAPI} from '../../src/FunctionAPI'; import docs from 'docs:@react-aria/interactions'; export const section = 'Interactions'; +export const description = 'Determines whether keyboard focus should be visible based on interaction mode.'; # useFocusVisible diff --git a/packages/dev/s2-docs/pages/react-aria/useFocusWithin.mdx b/packages/dev/s2-docs/pages/react-aria/useFocusWithin.mdx index aa2aa2b4ae6..e798626f53a 100644 --- a/packages/dev/s2-docs/pages/react-aria/useFocusWithin.mdx +++ b/packages/dev/s2-docs/pages/react-aria/useFocusWithin.mdx @@ -13,6 +13,7 @@ import {InterfaceType} from '../../src/types'; import {FunctionAPI} from '../../src/FunctionAPI'; import docs from 'docs:@react-aria/interactions'; export const section = 'Interactions'; +export const description = 'Handles focus interactions for an element and its descendants.'; # useFocusWithin diff --git a/packages/dev/s2-docs/pages/react-aria/useHover.mdx b/packages/dev/s2-docs/pages/react-aria/useHover.mdx index b834c730e12..54be1e4eb45 100644 --- a/packages/dev/s2-docs/pages/react-aria/useHover.mdx +++ b/packages/dev/s2-docs/pages/react-aria/useHover.mdx @@ -14,6 +14,7 @@ import {FunctionAPI} from '../../src/FunctionAPI'; import docs from 'docs:@react-aria/interactions'; import typesDocs from 'docs:@react-types/shared/src/events.d.ts'; export const section = 'Interactions'; +export const description = 'Handles hover interactions with proper support for touch devices.'; # useHover diff --git a/packages/dev/s2-docs/pages/react-aria/useId.mdx b/packages/dev/s2-docs/pages/react-aria/useId.mdx index c68111d397b..8d37f686f0f 100644 --- a/packages/dev/s2-docs/pages/react-aria/useId.mdx +++ b/packages/dev/s2-docs/pages/react-aria/useId.mdx @@ -13,6 +13,7 @@ import docs from 'docs:@react-aria/utils'; import {FunctionAPI} from '../../src/FunctionAPI'; export const section = 'Utilities'; +export const description = 'Generates unique element ids with SSR support.'; # useId diff --git a/packages/dev/s2-docs/pages/react-aria/useIsSSR.mdx b/packages/dev/s2-docs/pages/react-aria/useIsSSR.mdx index 0a0c6cdcc81..8bffb01881f 100644 --- a/packages/dev/s2-docs/pages/react-aria/useIsSSR.mdx +++ b/packages/dev/s2-docs/pages/react-aria/useIsSSR.mdx @@ -13,6 +13,7 @@ import docs from 'docs:@react-aria/ssr'; import {FunctionAPI} from '../../src/FunctionAPI'; export const section = 'Utilities'; +export const description = 'Returns whether the app is currently in server-side rendering or hydration.'; # useIsSSR diff --git a/packages/dev/s2-docs/pages/react-aria/useKeyboard.mdx b/packages/dev/s2-docs/pages/react-aria/useKeyboard.mdx index d969cd86d47..625c1c0e0e2 100644 --- a/packages/dev/s2-docs/pages/react-aria/useKeyboard.mdx +++ b/packages/dev/s2-docs/pages/react-aria/useKeyboard.mdx @@ -13,6 +13,7 @@ import {InterfaceType} from '../../src/types'; import {FunctionAPI} from '../../src/FunctionAPI'; import docs from 'docs:@react-aria/interactions'; export const section = 'Interactions'; +export const description = 'Handles keyboard interactions with improved event propagation behavior.'; # useKeyboard diff --git a/packages/dev/s2-docs/pages/react-aria/useLabel.mdx b/packages/dev/s2-docs/pages/react-aria/useLabel.mdx index f912caff4b7..1e345bb8e52 100644 --- a/packages/dev/s2-docs/pages/react-aria/useLabel.mdx +++ b/packages/dev/s2-docs/pages/react-aria/useLabel.mdx @@ -14,6 +14,7 @@ import {FunctionAPI} from '../../src/FunctionAPI'; import {InterfaceType} from '../../src/types'; export const section = 'Utilities'; +export const description = 'Associates a label with a form field for accessibility.'; # useLabel diff --git a/packages/dev/s2-docs/pages/react-aria/useLandmark.mdx b/packages/dev/s2-docs/pages/react-aria/useLandmark.mdx index 74714b61f1d..c8c78745f2b 100644 --- a/packages/dev/s2-docs/pages/react-aria/useLandmark.mdx +++ b/packages/dev/s2-docs/pages/react-aria/useLandmark.mdx @@ -13,6 +13,7 @@ import {InterfaceType} from '../../src/types'; import {FunctionAPI} from '../../src/FunctionAPI'; import docs from 'docs:@react-aria/landmark'; export const section = 'Interactions'; +export const description = 'Enables keyboard navigation between page landmarks.'; # useLandmark diff --git a/packages/dev/s2-docs/pages/react-aria/useLocale.mdx b/packages/dev/s2-docs/pages/react-aria/useLocale.mdx index 5acf845fd6d..ec59bd6cf6c 100644 --- a/packages/dev/s2-docs/pages/react-aria/useLocale.mdx +++ b/packages/dev/s2-docs/pages/react-aria/useLocale.mdx @@ -13,7 +13,7 @@ import {FunctionAPI} from '../../src/FunctionAPI'; import docs from 'docs:@react-aria/i18n'; export const section = 'Utilities'; -export const description = 'Implementing collections in React Aria'; +export const description = 'Provides access to the current locale and layout direction.'; # useLocale diff --git a/packages/dev/s2-docs/pages/react-aria/useLongPress.mdx b/packages/dev/s2-docs/pages/react-aria/useLongPress.mdx index f1aecc1e016..560827c67f7 100644 --- a/packages/dev/s2-docs/pages/react-aria/useLongPress.mdx +++ b/packages/dev/s2-docs/pages/react-aria/useLongPress.mdx @@ -14,6 +14,7 @@ import docs from 'docs:@react-aria/interactions'; import typesDocs from 'docs:@react-types/shared/src/events.d.ts'; import {InterfaceType} from '../../src/types'; export const section = 'Interactions'; +export const description = 'Handles long press interactions across mouse and touch devices.'; # useLongPress diff --git a/packages/dev/s2-docs/pages/react-aria/useMove.mdx b/packages/dev/s2-docs/pages/react-aria/useMove.mdx index 3891c093099..b8b1db60d4a 100644 --- a/packages/dev/s2-docs/pages/react-aria/useMove.mdx +++ b/packages/dev/s2-docs/pages/react-aria/useMove.mdx @@ -14,6 +14,7 @@ import docs from 'docs:@react-aria/interactions'; import typesDocs from 'docs:@react-types/shared/src/events.d.ts'; import {InterfaceType} from '../../src/types'; export const section = 'Interactions'; +export const description = 'Handles move interactions for pointer and keyboard, useful for sliders and drag operations.'; # useMove diff --git a/packages/dev/s2-docs/pages/react-aria/useNumberFormatter.mdx b/packages/dev/s2-docs/pages/react-aria/useNumberFormatter.mdx index a094baf1ab3..8a6a78028dc 100644 --- a/packages/dev/s2-docs/pages/react-aria/useNumberFormatter.mdx +++ b/packages/dev/s2-docs/pages/react-aria/useNumberFormatter.mdx @@ -15,7 +15,7 @@ import docs from 'docs:@react-aria/i18n'; import typeDocs from 'docs:@internationalized/number'; export const section = 'Utilities'; -export const description = 'Implementing collections in React Aria'; +export const description = 'Wraps Intl.NumberFormat and formats numbers for the current locale.'; # useNumberFormatter diff --git a/packages/dev/s2-docs/pages/react-aria/useObjectRef.mdx b/packages/dev/s2-docs/pages/react-aria/useObjectRef.mdx index 295b57c23e3..895bdb83666 100644 --- a/packages/dev/s2-docs/pages/react-aria/useObjectRef.mdx +++ b/packages/dev/s2-docs/pages/react-aria/useObjectRef.mdx @@ -13,6 +13,7 @@ import docs from 'docs:@react-aria/utils'; import {FunctionAPI} from '../../src/FunctionAPI'; export const section = 'Utilities'; +export const description = 'Converts a callback ref or object ref to an object ref.'; # useObjectRef diff --git a/packages/dev/s2-docs/pages/react-aria/usePress.mdx b/packages/dev/s2-docs/pages/react-aria/usePress.mdx index 7e118b18af9..3ba871e4cf2 100644 --- a/packages/dev/s2-docs/pages/react-aria/usePress.mdx +++ b/packages/dev/s2-docs/pages/react-aria/usePress.mdx @@ -14,6 +14,7 @@ import docs from 'docs:@react-aria/interactions'; import typesDocs from 'docs:@react-types/shared/src/events.d.ts'; import {InterfaceType} from '../../src/types'; export const section = 'Interactions'; +export const description = 'Handles press interactions across mouse, touch, and keyboard.'; # usePress diff --git a/packages/dev/s2-docs/pages/s2/Accordion.mdx b/packages/dev/s2-docs/pages/s2/Accordion.mdx index 305acb95dd3..5af1318f734 100644 --- a/packages/dev/s2-docs/pages/s2/Accordion.mdx +++ b/packages/dev/s2-docs/pages/s2/Accordion.mdx @@ -4,6 +4,7 @@ export default Layout; import docs from 'docs:@react-spectrum/s2'; export const tags = ['collapsible', 'expandable', 'disclosure']; +export const description = 'A grouping of related disclosures.'; # Accordion diff --git a/packages/dev/s2-docs/pages/s2/ActionBar.mdx b/packages/dev/s2-docs/pages/s2/ActionBar.mdx index 6793685fca7..52161cc8ad1 100644 --- a/packages/dev/s2-docs/pages/s2/ActionBar.mdx +++ b/packages/dev/s2-docs/pages/s2/ActionBar.mdx @@ -4,6 +4,7 @@ export default Layout; import docs from 'docs:@react-spectrum/s2'; export const tags = ['toolbar']; +export const description = 'Used when a user needs to perform actions on one or more items at the same time.'; # ActionBar diff --git a/packages/dev/s2-docs/pages/s2/ActionButton.mdx b/packages/dev/s2-docs/pages/s2/ActionButton.mdx index 67d396871f7..93b904f174f 100644 --- a/packages/dev/s2-docs/pages/s2/ActionButton.mdx +++ b/packages/dev/s2-docs/pages/s2/ActionButton.mdx @@ -5,6 +5,7 @@ import {ActionButton} from '@react-spectrum/s2'; import docs from 'docs:@react-spectrum/s2'; export const tags = []; +export const description = 'Allows a user to perform an action.'; # ActionButton diff --git a/packages/dev/s2-docs/pages/s2/ActionButtonGroup.mdx b/packages/dev/s2-docs/pages/s2/ActionButtonGroup.mdx index 8ff16cf5942..d0fd10694ad 100644 --- a/packages/dev/s2-docs/pages/s2/ActionButtonGroup.mdx +++ b/packages/dev/s2-docs/pages/s2/ActionButtonGroup.mdx @@ -4,6 +4,7 @@ export default Layout; import docs from 'docs:@react-spectrum/s2'; export const tags = ['group']; +export const description = 'A grouping of related ActionButtons.'; # ActionButtonGroup diff --git a/packages/dev/s2-docs/pages/s2/ActionMenu.mdx b/packages/dev/s2-docs/pages/s2/ActionMenu.mdx index 3a90c41eeef..5e5683571f5 100644 --- a/packages/dev/s2-docs/pages/s2/ActionMenu.mdx +++ b/packages/dev/s2-docs/pages/s2/ActionMenu.mdx @@ -4,6 +4,7 @@ export default Layout; import docs from 'docs:@react-spectrum/s2'; export const tags = ['dropdown']; +export const description = 'Combines an ActionButton with a Menu for simple "more actions" use cases.'; # ActionMenu diff --git a/packages/dev/s2-docs/pages/s2/Avatar.mdx b/packages/dev/s2-docs/pages/s2/Avatar.mdx index 39e9a138afe..175164c3a86 100644 --- a/packages/dev/s2-docs/pages/s2/Avatar.mdx +++ b/packages/dev/s2-docs/pages/s2/Avatar.mdx @@ -5,6 +5,7 @@ import {Avatar} from '@react-spectrum/s2'; import docs from 'docs:@react-spectrum/s2'; export const tags = ['profile', 'user']; +export const description = 'A thumbnail representation of an entity, such as a user or an organization.'; # Avatar diff --git a/packages/dev/s2-docs/pages/s2/AvatarGroup.mdx b/packages/dev/s2-docs/pages/s2/AvatarGroup.mdx index abe911f1e0c..137493ff837 100644 --- a/packages/dev/s2-docs/pages/s2/AvatarGroup.mdx +++ b/packages/dev/s2-docs/pages/s2/AvatarGroup.mdx @@ -4,6 +4,7 @@ export default Layout; import docs from 'docs:@react-spectrum/s2'; export const tags = ['profile', 'user']; +export const description = 'A grouping of avatars that are related to each other.'; # AvatarGroup diff --git a/packages/dev/s2-docs/pages/s2/Badge.mdx b/packages/dev/s2-docs/pages/s2/Badge.mdx index 63b0efc7b00..67d3b30eb01 100644 --- a/packages/dev/s2-docs/pages/s2/Badge.mdx +++ b/packages/dev/s2-docs/pages/s2/Badge.mdx @@ -5,6 +5,7 @@ import {Badge, Text} from '@react-spectrum/s2'; import docs from 'docs:@react-spectrum/s2'; export const tags = ['label', 'tag', 'chip']; +export const description = 'Displays color-categorized metadata for an object.'; # Badge diff --git a/packages/dev/s2-docs/pages/s2/Breadcrumbs.mdx b/packages/dev/s2-docs/pages/s2/Breadcrumbs.mdx index c8480681a01..f175194fc0a 100644 --- a/packages/dev/s2-docs/pages/s2/Breadcrumbs.mdx +++ b/packages/dev/s2-docs/pages/s2/Breadcrumbs.mdx @@ -5,6 +5,7 @@ import {InlineAlert, Heading, Content} from '@react-spectrum/s2'; import docs from 'docs:@react-spectrum/s2'; export const tags = ['navigation']; +export const description = 'Display a hierarchy of links to the current page or resource.'; # Breadcrumbs diff --git a/packages/dev/s2-docs/pages/s2/Button.mdx b/packages/dev/s2-docs/pages/s2/Button.mdx index e6640c6946c..f1ace12b36c 100644 --- a/packages/dev/s2-docs/pages/s2/Button.mdx +++ b/packages/dev/s2-docs/pages/s2/Button.mdx @@ -5,6 +5,7 @@ import {Button} from '@react-spectrum/s2'; import docs from 'docs:@react-spectrum/s2'; export const tags = ['btn']; +export const description = 'Allows a user to perform an action or to navigate to another page.'; # Button diff --git a/packages/dev/s2-docs/pages/s2/ButtonGroup.mdx b/packages/dev/s2-docs/pages/s2/ButtonGroup.mdx index 3a37dacfd52..b7b641f10ca 100644 --- a/packages/dev/s2-docs/pages/s2/ButtonGroup.mdx +++ b/packages/dev/s2-docs/pages/s2/ButtonGroup.mdx @@ -5,6 +5,7 @@ import {ButtonGroup, Button} from '@react-spectrum/s2'; import docs from 'docs:@react-spectrum/s2'; export const tags = []; +export const description = 'A grouping of buttons whose actions are related to each other.'; # ButtonGroup diff --git a/packages/dev/s2-docs/pages/s2/Calendar.mdx b/packages/dev/s2-docs/pages/s2/Calendar.mdx index d06db8c2ffb..f34898958d8 100644 --- a/packages/dev/s2-docs/pages/s2/Calendar.mdx +++ b/packages/dev/s2-docs/pages/s2/Calendar.mdx @@ -5,6 +5,7 @@ import {Calendar} from '@react-spectrum/s2'; import docs from 'docs:@react-spectrum/s2'; export const tags = ['date']; +export const description = 'Allows a user to select a single date from a date grid.'; # Calendar diff --git a/packages/dev/s2-docs/pages/s2/Card.mdx b/packages/dev/s2-docs/pages/s2/Card.mdx index b663040d504..c260593edf9 100644 --- a/packages/dev/s2-docs/pages/s2/Card.mdx +++ b/packages/dev/s2-docs/pages/s2/Card.mdx @@ -4,6 +4,7 @@ export default Layout; import docs from 'docs:@react-spectrum/s2'; export const tags = ['box']; +export const description = 'Summarizes an object that a user can select or navigate to.'; # Card diff --git a/packages/dev/s2-docs/pages/s2/CardView.mdx b/packages/dev/s2-docs/pages/s2/CardView.mdx index f641e28b8f9..8b1ac96edf6 100644 --- a/packages/dev/s2-docs/pages/s2/CardView.mdx +++ b/packages/dev/s2-docs/pages/s2/CardView.mdx @@ -4,6 +4,7 @@ export default Layout; import docs from 'docs:@react-spectrum/s2'; export const tags = ['grid', 'gallery']; +export const description = 'Displays a group of related objects, with support for selection and bulk actions.'; # CardView diff --git a/packages/dev/s2-docs/pages/s2/Checkbox.mdx b/packages/dev/s2-docs/pages/s2/Checkbox.mdx index fb56f3f0bf6..8a10aa4c827 100644 --- a/packages/dev/s2-docs/pages/s2/Checkbox.mdx +++ b/packages/dev/s2-docs/pages/s2/Checkbox.mdx @@ -5,6 +5,7 @@ import {Checkbox} from '@react-spectrum/s2'; import docs from 'docs:@react-spectrum/s2'; export const tags = []; +export const description = 'Allows a user to select an individual option.'; # Checkbox diff --git a/packages/dev/s2-docs/pages/s2/CheckboxGroup.mdx b/packages/dev/s2-docs/pages/s2/CheckboxGroup.mdx index 7ecc1ad5288..1b2ccd1d7a7 100644 --- a/packages/dev/s2-docs/pages/s2/CheckboxGroup.mdx +++ b/packages/dev/s2-docs/pages/s2/CheckboxGroup.mdx @@ -1,10 +1,14 @@ import {Layout} from '../../src/Layout'; export default Layout; -import {CheckboxGroup} from '@react-spectrum/s2'; +import {CheckboxGroup, InlineAlert, Heading, Content} from '@react-spectrum/s2'; import docs from 'docs:@react-spectrum/s2'; export const tags = []; +export const relatedPages = [ + {title: 'Testing', url: './CheckboxGroup/testing.html'} +]; +export const description = 'Allows a user to select one or more items in a list of options.'; # CheckboxGroup diff --git a/packages/dev/s2-docs/pages/s2/CheckboxGroup/testing.mdx b/packages/dev/s2-docs/pages/s2/CheckboxGroup/testing.mdx new file mode 100644 index 00000000000..25bbb96a132 --- /dev/null +++ b/packages/dev/s2-docs/pages/s2/CheckboxGroup/testing.mdx @@ -0,0 +1,71 @@ +import {Layout} from '../../../src/Layout'; +export default Layout; + +import {InlineAlert, Heading, Content} from '@react-spectrum/s2'; +import testUtilDocs from 'docs:@react-spectrum/test-utils'; +import {InstallCommand} from '../../../src/InstallCommand'; +import {PatternTestingFAQ} from '../../../src/PatternTestingFAQ'; + +export const isSubpage = true; +export const tags = ['testing', 'checkboxgroup', 'test-utils']; +export const description = 'Testing CheckboxGroup with React Spectrum test utils'; + +# Testing CheckboxGroup + +## Test utils + +`@react-spectrum/test-utils` offers common checkbox group interaction testing utilities. Install it with your preferred package manager. + + + + + Requirements + Please note that this library uses [@testing-library/react@16](https://www.npmjs.com/package/@testing-library/react) and [@testing-library/user-event@14](https://www.npmjs.com/package/@testing-library/user-event). This means that you need to be on React 18+ in order for these utilities to work. + + +Initialize a `User` object at the top of your test file, and use it to create a `CheckboxGroup` pattern tester in your test cases. The tester has methods that you can call within your test to query for specific subcomponents or simulate common interactions. + +```ts +// CheckboxGroup.test.ts +import {render} from '@testing-library/react'; +import {User} from '@react-spectrum/test-utils'; + +let testUtilUser = new User({ + interactionType: 'mouse', + advanceTimer: jest.advanceTimersByTime +}); +// ... + +it('CheckboxGroup can select multiple checkboxes', async function () { + // Render your test component/app and initialize the checkbox group tester + let {getByTestId} = render( + + ... + + ); + let checkboxGroupTester = testUtilUser.createTester('CheckboxGroup', {root: getByTestId('test-checkboxgroup')}); + expect(checkboxGroupTester.selectedCheckboxes).toHaveLength(0); + + await checkboxGroupTester.toggleCheckbox({checkbox: 0}); + expect(checkboxGroupTester.checkboxes[0]).toBeChecked(); + expect(checkboxGroupTester.selectedCheckboxes).toHaveLength(1); + + await checkboxGroupTester.toggleCheckbox({checkbox: 4}); + expect(checkboxGroupTester.checkboxes[4]).toBeChecked(); + expect(checkboxGroupTester.selectedCheckboxes).toHaveLength(2); +}); +``` + +## API + +### User + + + +### CheckboxGroupTester + + + +## Testing FAQ + + diff --git a/packages/dev/s2-docs/pages/s2/ColorArea.mdx b/packages/dev/s2-docs/pages/s2/ColorArea.mdx index c2f54f2a509..5e68b992f6f 100644 --- a/packages/dev/s2-docs/pages/s2/ColorArea.mdx +++ b/packages/dev/s2-docs/pages/s2/ColorArea.mdx @@ -6,6 +6,7 @@ import docs from 'docs:@react-spectrum/s2'; import racDocs from 'docs:react-aria-components'; export const tags = []; +export const description = 'Allows users to adjust two channels of a color value.'; # ColorArea diff --git a/packages/dev/s2-docs/pages/s2/ColorField.mdx b/packages/dev/s2-docs/pages/s2/ColorField.mdx index a876d7194cc..0258fd7fb8b 100644 --- a/packages/dev/s2-docs/pages/s2/ColorField.mdx +++ b/packages/dev/s2-docs/pages/s2/ColorField.mdx @@ -5,6 +5,7 @@ import {ColorField} from '@react-spectrum/s2'; import docs from 'docs:@react-spectrum/s2'; export const tags = []; +export const description = 'Allows users to edit a hex color or individual color channel value.'; # ColorField diff --git a/packages/dev/s2-docs/pages/s2/ColorSlider.mdx b/packages/dev/s2-docs/pages/s2/ColorSlider.mdx index ee7b72c0e1c..486f6914ce4 100644 --- a/packages/dev/s2-docs/pages/s2/ColorSlider.mdx +++ b/packages/dev/s2-docs/pages/s2/ColorSlider.mdx @@ -5,6 +5,7 @@ import {ColorSlider} from '@react-spectrum/s2'; import docs from 'docs:@react-spectrum/s2'; export const tags = []; +export const description = 'Allows users to adjust an individual channel of a color value.'; # ColorSlider diff --git a/packages/dev/s2-docs/pages/s2/ColorSwatch.mdx b/packages/dev/s2-docs/pages/s2/ColorSwatch.mdx index 8b99e7d45ae..6883615ff28 100644 --- a/packages/dev/s2-docs/pages/s2/ColorSwatch.mdx +++ b/packages/dev/s2-docs/pages/s2/ColorSwatch.mdx @@ -5,6 +5,7 @@ import {ColorSwatch} from '@react-spectrum/s2'; import docs from 'docs:@react-spectrum/s2'; export const tags = []; +export const description = 'Displays a preview of a selected color.'; # ColorSwatch diff --git a/packages/dev/s2-docs/pages/s2/ColorSwatchPicker.mdx b/packages/dev/s2-docs/pages/s2/ColorSwatchPicker.mdx index fbae353e8aa..8ac84a99ec7 100644 --- a/packages/dev/s2-docs/pages/s2/ColorSwatchPicker.mdx +++ b/packages/dev/s2-docs/pages/s2/ColorSwatchPicker.mdx @@ -5,6 +5,7 @@ import {InlineAlert, Heading, Content} from '@react-spectrum/s2' import docs from 'docs:@react-spectrum/s2'; export const tags = []; +export const description = 'Displays a list of color swatches and allows a user to select one of them.'; # ColorSwatchPicker diff --git a/packages/dev/s2-docs/pages/s2/ColorWheel.mdx b/packages/dev/s2-docs/pages/s2/ColorWheel.mdx index fd06582c1cd..78ea21d77e8 100644 --- a/packages/dev/s2-docs/pages/s2/ColorWheel.mdx +++ b/packages/dev/s2-docs/pages/s2/ColorWheel.mdx @@ -5,6 +5,7 @@ import {ColorWheel} from '@react-spectrum/s2'; import docs from 'docs:@react-spectrum/s2'; export const tags = []; +export const description = 'Allows users to adjust the hue of a color value on a circular track.'; # ColorWheel diff --git a/packages/dev/s2-docs/pages/s2/ComboBox.mdx b/packages/dev/s2-docs/pages/s2/ComboBox.mdx index bec99ac1f2e..ab6cfabed07 100644 --- a/packages/dev/s2-docs/pages/s2/ComboBox.mdx +++ b/packages/dev/s2-docs/pages/s2/ComboBox.mdx @@ -1,10 +1,14 @@ import {Layout} from '../../src/Layout'; export default Layout; -import {InlineAlert, Heading, Content} from '@react-spectrum/s2' +import {InlineAlert, Heading, Content} from '@react-spectrum/s2'; import docs from 'docs:@react-spectrum/s2'; export const tags = ['autocomplete', 'search', 'typeahead']; +export const relatedPages = [ + {title: 'Testing', url: './ComboBox/testing.html'} +]; +export const description = 'Combines a text input with a listbox, and allows a user to filter a list of options.'; # ComboBox diff --git a/packages/dev/s2-docs/pages/s2/ComboBox/testing.mdx b/packages/dev/s2-docs/pages/s2/ComboBox/testing.mdx new file mode 100644 index 00000000000..d98e1fb530c --- /dev/null +++ b/packages/dev/s2-docs/pages/s2/ComboBox/testing.mdx @@ -0,0 +1,69 @@ +import {Layout} from '../../../src/Layout'; +export default Layout; + +import {InlineAlert, Heading, Content} from '@react-spectrum/s2'; +import testUtilDocs from 'docs:@react-spectrum/test-utils'; +import {InstallCommand} from '../../../src/InstallCommand'; +import {PatternTestingFAQ} from '../../../src/PatternTestingFAQ'; + +export const isSubpage = true; +export const tags = ['testing', 'combobox', 'test-utils']; +export const description = 'Testing ComboBox with React Spectrum test utils'; + +# Testing ComboBox + +## Test utils + +`@react-spectrum/test-utils` offers common combobox interaction testing utilities. Install it with your preferred package manager. + + + + + Requirements + Please note that this library uses [@testing-library/react@16](https://www.npmjs.com/package/@testing-library/react) and [@testing-library/user-event@14](https://www.npmjs.com/package/@testing-library/user-event). This means that you need to be on React 18+ in order for these utilities to work. + + +Initialize a `User` object at the top of your test file, and use it to create a `ComboBox` pattern tester in your test cases. The tester has methods that you can call within your test to query for specific subcomponents or simulate common interactions. + +```ts +// Combobox.test.ts +import {render} from '@testing-library/react'; +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(); +}); +``` + +## API + +### User + + + +### ComboBoxTester + + + +## Testing FAQ + + diff --git a/packages/dev/s2-docs/pages/s2/ContextualHelp.mdx b/packages/dev/s2-docs/pages/s2/ContextualHelp.mdx index 2f76b45567d..27b57980390 100644 --- a/packages/dev/s2-docs/pages/s2/ContextualHelp.mdx +++ b/packages/dev/s2-docs/pages/s2/ContextualHelp.mdx @@ -5,6 +5,7 @@ import {ContextualHelp, Heading, Content, Footer} from '@react-spectrum/s2'; import docs from 'docs:@react-spectrum/s2'; export const tags = ['help']; +export const description = 'Shows extra information about an adjacent component.'; # ContextualHelp diff --git a/packages/dev/s2-docs/pages/s2/DateField.mdx b/packages/dev/s2-docs/pages/s2/DateField.mdx index f20afb4c5b0..6ea11a47f77 100644 --- a/packages/dev/s2-docs/pages/s2/DateField.mdx +++ b/packages/dev/s2-docs/pages/s2/DateField.mdx @@ -5,6 +5,7 @@ import {DateField} from '@react-spectrum/s2'; import docs from 'docs:@react-spectrum/s2'; export const tags = ['calendar']; +export const description = 'Allows a user to enter and edit date values using a keyboard.'; # DateField diff --git a/packages/dev/s2-docs/pages/s2/DatePicker.mdx b/packages/dev/s2-docs/pages/s2/DatePicker.mdx index f0c83851e4f..dce61f145d2 100644 --- a/packages/dev/s2-docs/pages/s2/DatePicker.mdx +++ b/packages/dev/s2-docs/pages/s2/DatePicker.mdx @@ -5,6 +5,7 @@ import {DatePicker} from '@react-spectrum/s2'; import docs from 'docs:@react-spectrum/s2'; export const tags = ['calendar']; +export const description = 'Combines a DateField and a Calendar popover.'; # DatePicker diff --git a/packages/dev/s2-docs/pages/s2/DateRangePicker.mdx b/packages/dev/s2-docs/pages/s2/DateRangePicker.mdx index 7a8b8792244..38665aa6c77 100644 --- a/packages/dev/s2-docs/pages/s2/DateRangePicker.mdx +++ b/packages/dev/s2-docs/pages/s2/DateRangePicker.mdx @@ -5,6 +5,7 @@ import {DateRangePicker} from '@react-spectrum/s2'; import docs from 'docs:@react-spectrum/s2'; export const tags = ['calendar']; +export const description = 'Combines two DateFields and a RangeCalendar popover.'; # DateRangePicker diff --git a/packages/dev/s2-docs/pages/s2/Dialog.mdx b/packages/dev/s2-docs/pages/s2/Dialog.mdx index dfc3e176a13..d785787c318 100644 --- a/packages/dev/s2-docs/pages/s2/Dialog.mdx +++ b/packages/dev/s2-docs/pages/s2/Dialog.mdx @@ -1,10 +1,14 @@ import {Layout} from '../../src/Layout'; export default Layout; -import {Dialog, FullscreenDialog, CustomDialog, DialogTrigger, DialogContainer, Button, ButtonGroup, Heading, Header, Content, Footer, Image, TextField, Checkbox, CloseButton, ActionButton, IllustratedMessage} from '@react-spectrum/s2'; +import {Dialog, FullscreenDialog, CustomDialog, DialogTrigger, DialogContainer, Button, ButtonGroup, Heading, Header, Content, Footer, Image, TextField, Checkbox, CloseButton, ActionButton, IllustratedMessage, InlineAlert} from '@react-spectrum/s2'; import docs from 'docs:@react-spectrum/s2'; export const tags = ['modal', 'popup', 'overlay']; +export const relatedPages = [ + {title: 'Testing', url: './Dialog/testing.html'} +]; +export const description = 'An overlay shown above other content in an application.'; # Dialog @@ -200,7 +204,7 @@ function DialogContainerExample() { Delete Item Share Item - + {/*- begin highlight -*/} setDialogType(null)}> {/*- end highlight -*/} diff --git a/packages/dev/s2-docs/pages/s2/Dialog/testing.mdx b/packages/dev/s2-docs/pages/s2/Dialog/testing.mdx new file mode 100644 index 00000000000..76dfdf32eed --- /dev/null +++ b/packages/dev/s2-docs/pages/s2/Dialog/testing.mdx @@ -0,0 +1,71 @@ +import {Layout} from '../../../src/Layout'; +export default Layout; + +import {InlineAlert, Heading, Content} from '@react-spectrum/s2'; +import testUtilDocs from 'docs:@react-spectrum/test-utils'; +import {InstallCommand} from '../../../src/InstallCommand'; +import {PatternTestingFAQ} from '../../../src/PatternTestingFAQ'; + +export const isSubpage = true; +export const tags = ['testing', 'dialog', 'test-utils']; +export const description = 'Testing Dialog with React Spectrum test utils'; + +# Testing Dialog + +## Test utils + +`@react-spectrum/test-utils` offers common dialog interaction testing utilities. Install it with your preferred package manager. + + + + + Requirements + Please note that this library uses [@testing-library/react@16](https://www.npmjs.com/package/@testing-library/react) and [@testing-library/user-event@14](https://www.npmjs.com/package/@testing-library/user-event). This means that you need to be on React 18+ in order for these utilities to work. + + +Initialize a `User` object at the top of your test file, and use it to create a `Dialog` pattern tester in your test cases. The tester has methods that you can call within your test to query for specific subcomponents or simulate common interactions. + +```ts +// Dialog.test.ts +import {render} from '@testing-library/react'; +import {User} from '@react-spectrum/test-utils'; + +let testUtilUser = new User({ + interactionType: 'mouse', + advanceTimer: jest.advanceTimersByTime +}); +// ... + +it('Dialog can be opened and closed', async function () { + // Render your test component/app and initialize the dialog tester + let {getByTestId, getByRole} = render( + + Trigger + + ... + + + ); + let button = getByRole('button'); + let dialogTester = testUtilUser.createTester('Dialog', {root: button, overlayType: 'modal'}); + await dialogTester.open(); + let dialog = dialogTester.dialog; + expect(dialog).toBeVisible(); + await dialogTester.close(); + expect(dialog).not.toBeInTheDocument(); +}); +``` + +## API + +### User + + + +### DialogTester + + + +## Testing FAQ + + diff --git a/packages/dev/s2-docs/pages/s2/Disclosure.mdx b/packages/dev/s2-docs/pages/s2/Disclosure.mdx index 7aa0cf0b2cf..4cbad968113 100644 --- a/packages/dev/s2-docs/pages/s2/Disclosure.mdx +++ b/packages/dev/s2-docs/pages/s2/Disclosure.mdx @@ -5,6 +5,7 @@ import {Disclosure, DisclosureTitle, DisclosurePanel, DisclosureHeader, ActionBu import docs from 'docs:@react-spectrum/s2'; export const tags = ['accordion', 'collapsible', 'expandable']; +export const description = 'A collapsible section of content.'; # Disclosure diff --git a/packages/dev/s2-docs/pages/s2/Divider.mdx b/packages/dev/s2-docs/pages/s2/Divider.mdx index 02dd3c132ce..d6314ba8cea 100644 --- a/packages/dev/s2-docs/pages/s2/Divider.mdx +++ b/packages/dev/s2-docs/pages/s2/Divider.mdx @@ -5,6 +5,7 @@ import {Divider} from '@react-spectrum/s2'; import docs from 'docs:@react-spectrum/s2'; export const tags = ['separator', 'hr', 'horizontal rule', 'line']; +export const description = 'Brings clarity to a layout by grouping and dividing content in close proximity.'; # Divider diff --git a/packages/dev/s2-docs/pages/s2/DropZone.mdx b/packages/dev/s2-docs/pages/s2/DropZone.mdx index c6b40508207..4e668184b04 100644 --- a/packages/dev/s2-docs/pages/s2/DropZone.mdx +++ b/packages/dev/s2-docs/pages/s2/DropZone.mdx @@ -4,6 +4,7 @@ export default Layout; import docs from 'docs:@react-spectrum/s2'; export const tags = ['file', 'drag', 'upload']; +export const description = 'An area into which one or multiple objects can be dragged and dropped.'; # DropZone diff --git a/packages/dev/s2-docs/pages/s2/Form.mdx b/packages/dev/s2-docs/pages/s2/Form.mdx index d8c64df8097..19ecbe45d14 100644 --- a/packages/dev/s2-docs/pages/s2/Form.mdx +++ b/packages/dev/s2-docs/pages/s2/Form.mdx @@ -5,6 +5,7 @@ import {Form, TextField, Checkbox, Button, RadioGroup, Radio, InlineAlert, Headi import docs from 'docs:@react-spectrum/s2'; export const tags = ['input', 'field']; +export const description = 'Provides layout and alignment for a grouping of fields.'; # Form diff --git a/packages/dev/s2-docs/pages/s2/IllustratedMessage.mdx b/packages/dev/s2-docs/pages/s2/IllustratedMessage.mdx index 565cf212191..6a385421cb2 100644 --- a/packages/dev/s2-docs/pages/s2/IllustratedMessage.mdx +++ b/packages/dev/s2-docs/pages/s2/IllustratedMessage.mdx @@ -5,6 +5,7 @@ import {IllustratedMessage, Heading, Content, Button} from '@react-spectrum/s2'; import docs from 'docs:@react-spectrum/s2'; export const tags = ['empty', 'placeholder', 'error']; +export const description = 'Displays an illustration and a message, usually for an empty state or an error page.'; # IllustratedMessage diff --git a/packages/dev/s2-docs/pages/s2/Illustrations.mdx b/packages/dev/s2-docs/pages/s2/Illustrations.mdx index 7ca25b39d6b..52f502bd93c 100644 --- a/packages/dev/s2-docs/pages/s2/Illustrations.mdx +++ b/packages/dev/s2-docs/pages/s2/Illustrations.mdx @@ -8,6 +8,7 @@ export default Layout; import docs from 'docs:@react-spectrum/s2'; export const tags = ['svg', 'gradient', 'linear', 'symbol']; +export const description = 'A collection of illustrations that can be imported from @react-spectrum/s2/illustrations.'; # Illustrations diff --git a/packages/dev/s2-docs/pages/s2/Image.mdx b/packages/dev/s2-docs/pages/s2/Image.mdx index cee1784e3d8..6d9200dd9e3 100644 --- a/packages/dev/s2-docs/pages/s2/Image.mdx +++ b/packages/dev/s2-docs/pages/s2/Image.mdx @@ -5,6 +5,7 @@ import {Image} from '@react-spectrum/s2'; import docs from 'docs:@react-spectrum/s2'; export const tags = ['img', 'picture', 'photo']; +export const description = 'Displays an image with support for skeleton loading and custom error states.'; # Image diff --git a/packages/dev/s2-docs/pages/s2/InlineAlert.mdx b/packages/dev/s2-docs/pages/s2/InlineAlert.mdx index 73c74d94060..6567542668e 100644 --- a/packages/dev/s2-docs/pages/s2/InlineAlert.mdx +++ b/packages/dev/s2-docs/pages/s2/InlineAlert.mdx @@ -5,6 +5,7 @@ import {InlineAlert} from '@react-spectrum/s2'; import docs from 'docs:@react-spectrum/s2'; export const tags = ['alert', 'notification', 'banner', 'message']; +export const description = 'Displays a non-modal message associated with objects in a view.'; # InlineAlert diff --git a/packages/dev/s2-docs/pages/s2/Link.mdx b/packages/dev/s2-docs/pages/s2/Link.mdx index 844832dc262..fde2ccc3a92 100644 --- a/packages/dev/s2-docs/pages/s2/Link.mdx +++ b/packages/dev/s2-docs/pages/s2/Link.mdx @@ -5,6 +5,7 @@ import {Link, Text} from '@react-spectrum/s2'; import docs from 'docs:@react-spectrum/s2'; export const tags = ['anchor', 'hyperlink', 'href']; +export const description = 'Allows a user to navigate to another page.'; # Link diff --git a/packages/dev/s2-docs/pages/s2/LinkButton.mdx b/packages/dev/s2-docs/pages/s2/LinkButton.mdx index 2c1853cc3ac..3c8b2f641f2 100644 --- a/packages/dev/s2-docs/pages/s2/LinkButton.mdx +++ b/packages/dev/s2-docs/pages/s2/LinkButton.mdx @@ -5,6 +5,7 @@ import {LinkButton, Text} from '@react-spectrum/s2'; import docs from 'docs:@react-spectrum/s2'; export const tags = ['button']; +export const description = 'Combines the functionality of a link with the appearance of a button.'; # LinkButton diff --git a/packages/dev/s2-docs/pages/s2/Menu.mdx b/packages/dev/s2-docs/pages/s2/Menu.mdx index b2f2e26c278..bbde6d3a089 100644 --- a/packages/dev/s2-docs/pages/s2/Menu.mdx +++ b/packages/dev/s2-docs/pages/s2/Menu.mdx @@ -5,6 +5,10 @@ import {InlineAlert, Heading, Content} from '@react-spectrum/s2' import docs from 'docs:@react-spectrum/s2'; export const tags = ['dropdown']; +export const relatedPages = [ + {title: 'Testing', url: './Menu/testing.html'} +]; +export const description = 'Displays a list of actions or options that a user can choose.'; # Menu diff --git a/packages/dev/s2-docs/pages/s2/Menu/testing.mdx b/packages/dev/s2-docs/pages/s2/Menu/testing.mdx new file mode 100644 index 00000000000..e5f27109e05 --- /dev/null +++ b/packages/dev/s2-docs/pages/s2/Menu/testing.mdx @@ -0,0 +1,81 @@ +import {Layout} from '../../../src/Layout'; +export default Layout; + +import {InlineAlert, Heading, Content} from '@react-spectrum/s2'; +import testUtilDocs from 'docs:@react-spectrum/test-utils'; +import {InstallCommand} from '../../../src/InstallCommand'; +import {PatternTestingFAQ} from '../../../src/PatternTestingFAQ'; + +export const isSubpage = true; +export const tags = ['testing', 'menu', 'test-utils']; +export const description = 'Testing Menu with React Spectrum test utils'; + +# Testing Menu + +## General setup + +Menu supports long press interactions in certain configurations. See the following sections on how to handle these behaviors in your tests. + +* [Timers](../testing.html#timers) +* [Long press](../testing.html#simulating-user-long-press) + +## Test utils + +`@react-spectrum/test-utils` offers common menu interaction testing utilities. Install it with your preferred package manager. + + + + + Requirements + Please note that this library uses [@testing-library/react@16](https://www.npmjs.com/package/@testing-library/react) and [@testing-library/user-event@14](https://www.npmjs.com/package/@testing-library/user-event). This means that you need to be on React 18+ in order for these utilities to work. + + +Initialize a `User` object at the top of your test file, and use it to create a `Menu` pattern tester in your test cases. The tester has methods that you can call within your test to query for specific subcomponents or simulate common interactions. + +```ts +// Menu.test.ts +import {render} from '@testing-library/react'; +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(); +}); +``` + +## API + +### User + + + +### MenuTester + + + +## Testing FAQ + + diff --git a/packages/dev/s2-docs/pages/s2/Meter.mdx b/packages/dev/s2-docs/pages/s2/Meter.mdx index 60c35b2c4d1..4b55fcc6820 100644 --- a/packages/dev/s2-docs/pages/s2/Meter.mdx +++ b/packages/dev/s2-docs/pages/s2/Meter.mdx @@ -5,6 +5,7 @@ import {Meter} from '@react-spectrum/s2'; import docs from 'docs:@react-spectrum/s2'; export const tags = ['gauge', 'progress', 'level']; +export const description = 'Represents a quantity within a known range, or a fractional value.'; # Meter diff --git a/packages/dev/s2-docs/pages/s2/NumberField.mdx b/packages/dev/s2-docs/pages/s2/NumberField.mdx index 0b2d777374c..44c3a868d3e 100644 --- a/packages/dev/s2-docs/pages/s2/NumberField.mdx +++ b/packages/dev/s2-docs/pages/s2/NumberField.mdx @@ -5,6 +5,7 @@ import {NumberField} from '@react-spectrum/s2'; import docs from 'docs:@react-spectrum/s2'; export const tags = ['input']; +export const description = 'Allows a user to enter, increment, or decrement a numeric value.'; # NumberField diff --git a/packages/dev/s2-docs/pages/s2/Picker.mdx b/packages/dev/s2-docs/pages/s2/Picker.mdx index 3b075330519..f68551b5ef8 100644 --- a/packages/dev/s2-docs/pages/s2/Picker.mdx +++ b/packages/dev/s2-docs/pages/s2/Picker.mdx @@ -1,10 +1,14 @@ import {Layout} from '../../src/Layout'; export default Layout; -import {InlineAlert, Heading, Content} from '@react-spectrum/s2' +import {InlineAlert, Heading, Content} from '@react-spectrum/s2'; import docs from 'docs:@react-spectrum/s2'; export const tags = ['select', 'dropdown']; +export const relatedPages = [ + {title: 'Testing', url: './Picker/testing.html'} +]; +export const description = 'Displays a collapsible list of options, and allows a user to select one of them.'; # Picker diff --git a/packages/dev/s2-docs/pages/s2/Picker/testing.mdx b/packages/dev/s2-docs/pages/s2/Picker/testing.mdx new file mode 100644 index 00000000000..dc950758076 --- /dev/null +++ b/packages/dev/s2-docs/pages/s2/Picker/testing.mdx @@ -0,0 +1,66 @@ +import {Layout} from '../../../src/Layout'; +export default Layout; + +import {InlineAlert, Heading, Content} from '@react-spectrum/s2'; +import testUtilDocs from 'docs:@react-spectrum/test-utils'; +import {InstallCommand} from '../../../src/InstallCommand'; +import {PatternTestingFAQ} from '../../../src/PatternTestingFAQ'; + +export const isSubpage = true; +export const tags = ['testing', 'picker', 'test-utils']; +export const description = 'Testing Picker with React Spectrum test utils'; + +# Testing Picker + +## Test utils + +`@react-spectrum/test-utils` offers common picker interaction testing utilities. Install it with your preferred package manager. + + + + + Requirements + Please note that this library uses [@testing-library/react@16](https://www.npmjs.com/package/@testing-library/react) and [@testing-library/user-event@14](https://www.npmjs.com/package/@testing-library/user-event). This means that you need to be on React 18+ in order for these utilities to work. + + +Initialize a `User` object at the top of your test file, and use it to create a `Picker` pattern tester in your test cases. The tester has methods that you can call within your test to query for specific subcomponents or simulate common interactions. + +```ts +// Picker.test.ts +import {render} from '@testing-library/react'; +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 an item'); + + await selectTester.selectOption({option: 'Cat'}); + expect(trigger).toHaveTextContent('Cat'); +}); +``` + +## API + +### User + + + +### SelectTester + + + +## Testing FAQ + + diff --git a/packages/dev/s2-docs/pages/s2/Popover.mdx b/packages/dev/s2-docs/pages/s2/Popover.mdx index e0bf9ee4944..d142cb5a2ad 100644 --- a/packages/dev/s2-docs/pages/s2/Popover.mdx +++ b/packages/dev/s2-docs/pages/s2/Popover.mdx @@ -4,6 +4,7 @@ export default Layout; import docs from 'docs:@react-spectrum/s2'; export const tags = ['popup', 'overlay']; +export const description = 'Displays interactive content in context with a trigger element.'; # Popover diff --git a/packages/dev/s2-docs/pages/s2/ProgressBar.mdx b/packages/dev/s2-docs/pages/s2/ProgressBar.mdx index 6175ec944e0..615d05facb9 100644 --- a/packages/dev/s2-docs/pages/s2/ProgressBar.mdx +++ b/packages/dev/s2-docs/pages/s2/ProgressBar.mdx @@ -5,6 +5,7 @@ import {ProgressBar} from '@react-spectrum/s2'; import docs from 'docs:@react-spectrum/s2'; export const tags = ['loading', 'progress']; +export const description = 'Shows progress of an operation over time with a linear representation.'; # ProgressBar diff --git a/packages/dev/s2-docs/pages/s2/ProgressCircle.mdx b/packages/dev/s2-docs/pages/s2/ProgressCircle.mdx index 026c69f62ea..f02bbae35f1 100644 --- a/packages/dev/s2-docs/pages/s2/ProgressCircle.mdx +++ b/packages/dev/s2-docs/pages/s2/ProgressCircle.mdx @@ -5,6 +5,7 @@ import {ProgressCircle} from '@react-spectrum/s2'; import docs from 'docs:@react-spectrum/s2'; export const tags = ['spinner', 'circular', 'loading']; +export const description = 'Shows progress of an operation over time with a circular representation.'; # ProgressCircle diff --git a/packages/dev/s2-docs/pages/s2/Provider.mdx b/packages/dev/s2-docs/pages/s2/Provider.mdx index ecf2a093b6e..36cd2fc6843 100644 --- a/packages/dev/s2-docs/pages/s2/Provider.mdx +++ b/packages/dev/s2-docs/pages/s2/Provider.mdx @@ -7,6 +7,8 @@ import {Disclosure, DisclosureTitle, DisclosurePanel} from '@react-spectrum/s2'; export const section = 'Components'; import docs from 'docs:@react-spectrum/s2'; +export const description = 'The container for all React Spectrum components.'; + # Provider {docs.exports.Provider.description} diff --git a/packages/dev/s2-docs/pages/s2/RadioGroup.mdx b/packages/dev/s2-docs/pages/s2/RadioGroup.mdx index 30ae82b98eb..cff76d42d75 100644 --- a/packages/dev/s2-docs/pages/s2/RadioGroup.mdx +++ b/packages/dev/s2-docs/pages/s2/RadioGroup.mdx @@ -1,10 +1,14 @@ import {Layout} from '../../src/Layout'; export default Layout; -import {RadioGroup, Radio} from '@react-spectrum/s2'; +import {RadioGroup, Radio, InlineAlert, Heading, Content} from '@react-spectrum/s2'; import docs from 'docs:@react-spectrum/s2'; export const tags = ['input']; +export const relatedPages = [ + {title: 'Testing', url: './RadioGroup/testing.html'} +]; +export const description = 'Allows a user to select a single item from a list of options.'; # RadioGroup diff --git a/packages/dev/s2-docs/pages/s2/RadioGroup/testing.mdx b/packages/dev/s2-docs/pages/s2/RadioGroup/testing.mdx new file mode 100644 index 00000000000..34b9a565d16 --- /dev/null +++ b/packages/dev/s2-docs/pages/s2/RadioGroup/testing.mdx @@ -0,0 +1,71 @@ +import {Layout} from '../../../src/Layout'; +export default Layout; + +import {InlineAlert, Heading, Content} from '@react-spectrum/s2'; +import testUtilDocs from 'docs:@react-spectrum/test-utils'; +import {InstallCommand} from '../../../src/InstallCommand'; +import {PatternTestingFAQ} from '../../../src/PatternTestingFAQ'; + +export const isSubpage = true; +export const tags = ['testing', 'radiogroup', 'test-utils']; +export const description = 'Testing RadioGroup with React Spectrum test utils'; + +# Testing RadioGroup + +## Test utils + +`@react-spectrum/test-utils` offers common radio group interaction testing utilities. Install it with your preferred package manager. + + + + + Requirements + Please note that this library uses [@testing-library/react@16](https://www.npmjs.com/package/@testing-library/react) and [@testing-library/user-event@14](https://www.npmjs.com/package/@testing-library/user-event). This means that you need to be on React 18+ in order for these utilities to work. + + +Initialize a `User` object at the top of your test file, and use it to create a `RadioGroup` pattern tester in your test cases. The tester has methods that you can call within your test to query for specific subcomponents or simulate common interactions. + +```ts +// RadioGroup.test.ts +import {render} from '@testing-library/react'; +import {User} from '@react-spectrum/test-utils'; + +let testUtilUser = new User({ + interactionType: 'mouse', + advanceTimer: jest.advanceTimersByTime +}); +// ... + +it('RadioGroup can switch the selected radio', async function () { + // Render your test component/app and initialize the radiogroup tester + let {getByRole} = render( + + ... + + ); + + let radioGroupTester = testUtilUser.createTester('RadioGroup', {root: getByRole('radiogroup')}); + let radios = radioGroupTester.radios; + expect(radioGroupTester.selectedRadio).toBeFalsy(); + + await radioGroupTester.triggerRadio({radio: radios[0]}); + expect(radioGroupTester.selectedRadio).toBe(radios[0]); + + await radioGroupTester.triggerRadio({radio: radios[1]}); + expect(radioGroupTester.selectedRadio).toBe(radios[1]); +}); +``` + +## API + +### User + + + +### RadioGroupTester + + + +## Testing FAQ + + diff --git a/packages/dev/s2-docs/pages/s2/RangeCalendar.mdx b/packages/dev/s2-docs/pages/s2/RangeCalendar.mdx index a9df14504c0..c3df01d926f 100644 --- a/packages/dev/s2-docs/pages/s2/RangeCalendar.mdx +++ b/packages/dev/s2-docs/pages/s2/RangeCalendar.mdx @@ -5,6 +5,7 @@ import {RangeCalendar} from '@react-spectrum/s2'; import docs from 'docs:@react-spectrum/s2'; export const tags = ['calendar']; +export const description = 'Allows a user to select a contiguous range of dates.'; # RangeCalendar diff --git a/packages/dev/s2-docs/pages/s2/RangeSlider.mdx b/packages/dev/s2-docs/pages/s2/RangeSlider.mdx index d236c22eb3b..4122a5dba18 100644 --- a/packages/dev/s2-docs/pages/s2/RangeSlider.mdx +++ b/packages/dev/s2-docs/pages/s2/RangeSlider.mdx @@ -5,6 +5,7 @@ import {RangeSlider} from '@react-spectrum/s2'; import docs from 'docs:@react-spectrum/s2'; export const tags = ['slider']; +export const description = 'Allows a user to select a range of values within a larger range.'; # RangeSlider diff --git a/packages/dev/s2-docs/pages/s2/SearchField.mdx b/packages/dev/s2-docs/pages/s2/SearchField.mdx index 93edb280ce1..247cf7efd45 100644 --- a/packages/dev/s2-docs/pages/s2/SearchField.mdx +++ b/packages/dev/s2-docs/pages/s2/SearchField.mdx @@ -5,6 +5,7 @@ import {SearchField} from '@react-spectrum/s2'; import docs from 'docs:@react-spectrum/s2'; export const tags = ['input']; +export const description = 'Allows a user to enter and clear a search query.'; # SearchField diff --git a/packages/dev/s2-docs/pages/s2/SegmentedControl.mdx b/packages/dev/s2-docs/pages/s2/SegmentedControl.mdx index c38da282644..f2d9fbdae53 100644 --- a/packages/dev/s2-docs/pages/s2/SegmentedControl.mdx +++ b/packages/dev/s2-docs/pages/s2/SegmentedControl.mdx @@ -5,6 +5,7 @@ import {SegmentedControl, SegmentedControlItem, Text} from '@react-spectrum/s2'; import docs from 'docs:@react-spectrum/s2'; export const tags = ['toggle group', 'tabs']; +export const description = 'A mutually exclusive group of buttons used for view switching.'; # SegmentedControl diff --git a/packages/dev/s2-docs/pages/s2/SelectBoxGroup.mdx b/packages/dev/s2-docs/pages/s2/SelectBoxGroup.mdx index 49dd45914c4..42493a86a5c 100644 --- a/packages/dev/s2-docs/pages/s2/SelectBoxGroup.mdx +++ b/packages/dev/s2-docs/pages/s2/SelectBoxGroup.mdx @@ -6,6 +6,7 @@ import docs from 'docs:@react-spectrum/s2'; export const tags = ['box']; export const version = 'alpha'; +export const description = 'Allows users to select one or more options from a list.'; # SelectBoxGroup diff --git a/packages/dev/s2-docs/pages/s2/Skeleton.mdx b/packages/dev/s2-docs/pages/s2/Skeleton.mdx index e82018cb1af..48c974388a8 100644 --- a/packages/dev/s2-docs/pages/s2/Skeleton.mdx +++ b/packages/dev/s2-docs/pages/s2/Skeleton.mdx @@ -4,6 +4,7 @@ export default Layout; import docs from 'docs:@react-spectrum/s2'; export const tags = ['loading', 'placeholder', 'shimmer', 'ghost']; +export const description = 'Wraps around content to render it as a placeholder.'; # Skeleton diff --git a/packages/dev/s2-docs/pages/s2/Slider.mdx b/packages/dev/s2-docs/pages/s2/Slider.mdx index 60fee9a47c2..c7b67b3bdce 100644 --- a/packages/dev/s2-docs/pages/s2/Slider.mdx +++ b/packages/dev/s2-docs/pages/s2/Slider.mdx @@ -5,6 +5,7 @@ import {Slider} from '@react-spectrum/s2'; import docs from 'docs:@react-spectrum/s2'; export const tags = ['range input', 'track', 'scrubber']; +export const description = 'Allows a user to select a numeric value within a range.'; # Slider diff --git a/packages/dev/s2-docs/pages/s2/StatusLight.mdx b/packages/dev/s2-docs/pages/s2/StatusLight.mdx index 4accd00bd9a..3b93f6ea269 100644 --- a/packages/dev/s2-docs/pages/s2/StatusLight.mdx +++ b/packages/dev/s2-docs/pages/s2/StatusLight.mdx @@ -5,6 +5,7 @@ import {StatusLight} from '@react-spectrum/s2'; import docs from 'docs:@react-spectrum/s2'; export const tags = ['indicator', 'dot', 'badge']; +export const description = 'Displays the status or category of an entity.'; # StatusLight diff --git a/packages/dev/s2-docs/pages/s2/Switch.mdx b/packages/dev/s2-docs/pages/s2/Switch.mdx index c6335487386..7832c828c15 100644 --- a/packages/dev/s2-docs/pages/s2/Switch.mdx +++ b/packages/dev/s2-docs/pages/s2/Switch.mdx @@ -5,6 +5,7 @@ import {Switch} from '@react-spectrum/s2'; import docs from 'docs:@react-spectrum/s2'; export const tags = ['toggle', 'input']; +export const description = 'Allows a user to turn a setting on or off.'; # Switch diff --git a/packages/dev/s2-docs/pages/s2/TableView.mdx b/packages/dev/s2-docs/pages/s2/TableView.mdx index 876475a578c..0cce1a7274f 100644 --- a/packages/dev/s2-docs/pages/s2/TableView.mdx +++ b/packages/dev/s2-docs/pages/s2/TableView.mdx @@ -2,9 +2,13 @@ import {Layout} from '../../src/Layout'; export default Layout; import docs from 'docs:@react-spectrum/s2'; -import {InlineAlert, Heading, Content} from '@react-spectrum/s2' +import {InlineAlert, Heading, Content} from '@react-spectrum/s2'; export const tags = ['table', 'data', 'grid']; +export const relatedPages = [ + {title: 'Testing', url: './TableView/testing.html'} +]; +export const description = 'Displays data in rows and columns, with row selection and sorting.'; # TableView diff --git a/packages/dev/s2-docs/pages/s2/TableView/testing.mdx b/packages/dev/s2-docs/pages/s2/TableView/testing.mdx new file mode 100644 index 00000000000..a446ca764db --- /dev/null +++ b/packages/dev/s2-docs/pages/s2/TableView/testing.mdx @@ -0,0 +1,85 @@ +import {Layout} from '../../../src/Layout'; +export default Layout; + +import {InlineAlert, Heading, Content} from '@react-spectrum/s2'; +import testUtilDocs from 'docs:@react-spectrum/test-utils'; +import {InstallCommand} from '../../../src/InstallCommand'; +import {PatternTestingFAQ} from '../../../src/PatternTestingFAQ'; + +export const isSubpage = true; +export const tags = ['testing', 'tableview', 'test-utils']; +export const description = 'Testing TableView with React Spectrum test utils'; + +# Testing TableView + +## General setup + +TableView supports long press interactions on its rows in certain configurations. See the following sections on how to handle these behaviors in your tests. + +* [Timers](../testing.html#timers) +* [Long press](../testing.html#simulating-user-long-press) + +## Test utils + +`@react-spectrum/test-utils` offers common table interaction testing utilities. Install it with your preferred package manager. + + + + + Requirements + Please note that this library uses [@testing-library/react@16](https://www.npmjs.com/package/@testing-library/react) and [@testing-library/user-event@14](https://www.npmjs.com/package/@testing-library/user-event). This means that you need to be on React 18+ in order for these utilities to work. + + +Initialize a `User` object at the top of your test file, and use it to create a `TableView` pattern tester in your test cases. The tester has methods that you can call within your test to query for specific subcomponents or simulate common interactions. + +```ts +// Table.test.ts +import {render, within} from '@testing-library/react'; +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); +}); +``` + +## API + +### User + + + +### TableTester + + + +## Testing FAQ + + diff --git a/packages/dev/s2-docs/pages/s2/Tabs.mdx b/packages/dev/s2-docs/pages/s2/Tabs.mdx index 3bd7a1013e6..cec7f9a7680 100644 --- a/packages/dev/s2-docs/pages/s2/Tabs.mdx +++ b/packages/dev/s2-docs/pages/s2/Tabs.mdx @@ -1,8 +1,13 @@ import {Layout} from '../../src/Layout'; export default Layout; import docs from 'docs:@react-spectrum/s2'; +import {InlineAlert, Heading, Content} from '@react-spectrum/s2'; export const tags = ['navigation']; +export const relatedPages = [ + {title: 'Testing', url: './Tabs/testing.html'} +]; +export const description = 'Organize content into multiple sections, and allow a user to view one at a time.'; # Tabs @@ -161,7 +166,7 @@ import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; width: 320, padding: 16, borderWidth: 1, - borderStyle: 'solid', + borderStyle: 'solid', borderColor: 'gray-300', borderRadius: 'default', overflow: 'hidden', @@ -275,4 +280,4 @@ function Example() { ### TabPanel - \ No newline at end of file + diff --git a/packages/dev/s2-docs/pages/s2/Tabs/testing.mdx b/packages/dev/s2-docs/pages/s2/Tabs/testing.mdx new file mode 100644 index 00000000000..8aa53e1dcc7 --- /dev/null +++ b/packages/dev/s2-docs/pages/s2/Tabs/testing.mdx @@ -0,0 +1,67 @@ +import {Layout} from '../../../src/Layout'; +export default Layout; + +import {InlineAlert, Heading, Content} from '@react-spectrum/s2'; +import testUtilDocs from 'docs:@react-spectrum/test-utils'; +import {InstallCommand} from '../../../src/InstallCommand'; +import {PatternTestingFAQ} from '../../../src/PatternTestingFAQ'; + +export const isSubpage = true; +export const tags = ['testing', 'tabs', 'test-utils']; +export const description = 'Testing Tabs with React Spectrum test utils'; + +# Testing Tabs + +## Test utils + +`@react-spectrum/test-utils` offers common tabs interaction testing utilities. Install it with your preferred package manager. + + + + + Requirements + Please note that this library uses [@testing-library/react@16](https://www.npmjs.com/package/@testing-library/react) and [@testing-library/user-event@14](https://www.npmjs.com/package/@testing-library/user-event). This means that you need to be on React 18+ in order for these utilities to work. + + +Initialize a `User` object at the top of your test file, and use it to create a `Tabs` pattern tester in your test cases. The tester has methods that you can call within your test to query for specific subcomponents or simulate common interactions. + +```ts +// Tabs.test.ts +import {render} from '@testing-library/react'; +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]); +}); +``` + +## API + +### User + + + +### TabsTester + + + +## Testing FAQ + + diff --git a/packages/dev/s2-docs/pages/s2/TagGroup.mdx b/packages/dev/s2-docs/pages/s2/TagGroup.mdx index 1226f2a49d1..a8ed2a7c272 100644 --- a/packages/dev/s2-docs/pages/s2/TagGroup.mdx +++ b/packages/dev/s2-docs/pages/s2/TagGroup.mdx @@ -4,6 +4,7 @@ export default Layout; import docs from 'docs:@react-spectrum/s2'; export const tags = ['chips', 'pills']; +export const description = 'Displays a list of items, with support for keyboard navigation and removal.'; # TagGroup diff --git a/packages/dev/s2-docs/pages/s2/TextArea.mdx b/packages/dev/s2-docs/pages/s2/TextArea.mdx index 02e71c1522d..a80e5683b57 100644 --- a/packages/dev/s2-docs/pages/s2/TextArea.mdx +++ b/packages/dev/s2-docs/pages/s2/TextArea.mdx @@ -5,6 +5,7 @@ import {TextArea} from '@react-spectrum/s2'; import docs from 'docs:@react-spectrum/s2'; export const tags = ['multiline', 'input']; +export const description = 'Allows a user to enter a multi-line text value with a keyboard.'; # TextArea diff --git a/packages/dev/s2-docs/pages/s2/TextField.mdx b/packages/dev/s2-docs/pages/s2/TextField.mdx index 35fcb4217cc..57b48983963 100644 --- a/packages/dev/s2-docs/pages/s2/TextField.mdx +++ b/packages/dev/s2-docs/pages/s2/TextField.mdx @@ -5,6 +5,7 @@ import {TextField} from '@react-spectrum/s2'; import docs from 'docs:@react-spectrum/s2'; export const tags = ['input']; +export const description = 'Allows a user to enter a plain text value with a keyboard.'; # TextField diff --git a/packages/dev/s2-docs/pages/s2/TimeField.mdx b/packages/dev/s2-docs/pages/s2/TimeField.mdx index 1f86da27d77..83813395249 100644 --- a/packages/dev/s2-docs/pages/s2/TimeField.mdx +++ b/packages/dev/s2-docs/pages/s2/TimeField.mdx @@ -5,6 +5,7 @@ import {TimeField} from '@react-spectrum/s2'; import docs from 'docs:@react-spectrum/s2'; export const tags = ['date', 'input']; +export const description = 'Allows a user to enter and edit time values using a keyboard.'; # TimeField diff --git a/packages/dev/s2-docs/pages/s2/Toast.mdx b/packages/dev/s2-docs/pages/s2/Toast.mdx index c240cbc4750..203d5610b9a 100644 --- a/packages/dev/s2-docs/pages/s2/Toast.mdx +++ b/packages/dev/s2-docs/pages/s2/Toast.mdx @@ -8,6 +8,7 @@ import {VersionBadge} from '../../src/VersionBadge'; export const tags = ['snackbar', 'notification', 'alert']; export const version = 'alpha'; +export const description = 'Displays a temporary notification of an action, error, or other event.'; # Toast diff --git a/packages/dev/s2-docs/pages/s2/ToggleButton.mdx b/packages/dev/s2-docs/pages/s2/ToggleButton.mdx index 0363494bbd6..f1b59a43e87 100644 --- a/packages/dev/s2-docs/pages/s2/ToggleButton.mdx +++ b/packages/dev/s2-docs/pages/s2/ToggleButton.mdx @@ -5,6 +5,7 @@ import {ToggleButton, Text} from '@react-spectrum/s2'; import docs from 'docs:@react-spectrum/s2'; export const tags = ['button', 'btn']; +export const description = 'Allows a user to toggle between two states.'; # ToggleButton diff --git a/packages/dev/s2-docs/pages/s2/ToggleButtonGroup.mdx b/packages/dev/s2-docs/pages/s2/ToggleButtonGroup.mdx index dbec0acb6ed..d274e8393d3 100644 --- a/packages/dev/s2-docs/pages/s2/ToggleButtonGroup.mdx +++ b/packages/dev/s2-docs/pages/s2/ToggleButtonGroup.mdx @@ -5,6 +5,7 @@ import {ToggleButtonGroup, ToggleButton, Text} from '@react-spectrum/s2'; import docs from 'docs:@react-spectrum/s2'; export const tags = ['toggle', 'btn']; +export const description = 'Allows a user to toggle multiple options, with single or multiple selection.'; # ToggleButtonGroup diff --git a/packages/dev/s2-docs/pages/s2/Tooltip.mdx b/packages/dev/s2-docs/pages/s2/Tooltip.mdx index 914cda306a5..a2447def2b5 100644 --- a/packages/dev/s2-docs/pages/s2/Tooltip.mdx +++ b/packages/dev/s2-docs/pages/s2/Tooltip.mdx @@ -5,6 +5,7 @@ import {InlineAlert, Heading, Content} from '@react-spectrum/s2' import docs from 'docs:@react-spectrum/s2'; export const tags = ['hint', 'popup', 'info']; +export const description = 'Displays a description of an element on hover or focus.'; # Tooltip diff --git a/packages/dev/s2-docs/pages/s2/TreeView.mdx b/packages/dev/s2-docs/pages/s2/TreeView.mdx index 3eb5e10c2b5..8947c8b7b9e 100644 --- a/packages/dev/s2-docs/pages/s2/TreeView.mdx +++ b/packages/dev/s2-docs/pages/s2/TreeView.mdx @@ -1,10 +1,14 @@ import {Layout} from '../../src/Layout'; export default Layout; -import {TreeView, TreeViewItem, TreeViewItemContent, Collection, Text, ActionMenu, MenuItem} from '@react-spectrum/s2'; +import {TreeView, TreeViewItem, TreeViewItemContent, Collection, Text, ActionMenu, MenuItem, InlineAlert, Heading, Content} from '@react-spectrum/s2'; import docs from 'docs:@react-spectrum/s2'; export const tags = ['hierarchy', 'data', 'nested']; +export const relatedPages = [ + {title: 'Testing', url: './TreeView/testing.html'} +]; +export const description = 'Displays hierarchical data with selection and collapsing.'; # TreeView diff --git a/packages/dev/s2-docs/pages/s2/TreeView/testing.mdx b/packages/dev/s2-docs/pages/s2/TreeView/testing.mdx new file mode 100644 index 00000000000..5da61c942b4 --- /dev/null +++ b/packages/dev/s2-docs/pages/s2/TreeView/testing.mdx @@ -0,0 +1,83 @@ +import {Layout} from '../../../src/Layout'; +export default Layout; + +import {InlineAlert, Heading, Content} from '@react-spectrum/s2'; +import testUtilDocs from 'docs:@react-spectrum/test-utils'; +import {InstallCommand} from '../../../src/InstallCommand'; +import {PatternTestingFAQ} from '../../../src/PatternTestingFAQ'; + +export const isSubpage = true; +export const tags = ['testing', 'treeview', 'test-utils']; +export const description = 'Testing TreeView with React Spectrum test utils'; + +# Testing TreeView + +## General setup + +TreeView supports long press interactions on its rows in certain configurations. See the following sections on how to handle these behaviors in your tests. + +* [Timers](../testing.html#timers) +* [Long press](../testing.html#simulating-user-long-press) + +## Test utils + +`@react-spectrum/test-utils` offers common tree interaction testing utilities. Install it with your preferred package manager. + + + + + Requirements + Please note that this library uses [@testing-library/react@16](https://www.npmjs.com/package/@testing-library/react) and [@testing-library/user-event@14](https://www.npmjs.com/package/@testing-library/user-event). This means that you need to be on React 18+ in order for these utilities to work. + + +Initialize a `User` object at the top of your test file, and use it to create a `TreeView` pattern tester in your test cases. The tester has methods that you can call within your test to query for specific subcomponents or simulate common interactions. + +```ts +// Tree.test.ts +import {render, within} from '@testing-library/react'; +import {User} from '@react-spectrum/test-utils'; + +let testUtilUser = new User({ + interactionType: 'mouse' +}); +// ... + +it('TreeView can select a item 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(); + + await treeTester.toggleRowExpansion({index: 0}); + expect(treeTester.rows[0]).toHaveAttribute('aria-expanded', 'true'); +}); +``` + +## API + +### User + + + +### TreeTester + + + +## Testing FAQ + + diff --git a/packages/dev/s2-docs/pages/s2/icons.mdx b/packages/dev/s2-docs/pages/s2/icons.mdx index b0728970116..0b90bf84a08 100644 --- a/packages/dev/s2-docs/pages/s2/icons.mdx +++ b/packages/dev/s2-docs/pages/s2/icons.mdx @@ -10,6 +10,7 @@ export default Layout; import docs from 'docs:@react-spectrum/s2'; export const tags = ['svg', 'workflow', 'symbol']; +export const description = 'A collection of workflow icons that can be imported from @react-spectrum/s2/icons.'; # Icons diff --git a/packages/dev/s2-docs/pages/s2/index.mdx b/packages/dev/s2-docs/pages/s2/index.mdx index 3d78809d26b..9d22d32ef54 100644 --- a/packages/dev/s2-docs/pages/s2/index.mdx +++ b/packages/dev/s2-docs/pages/s2/index.mdx @@ -6,6 +6,7 @@ import {ComponentCardView} from '../../src/ComponentCardView'; export const section = 'Overview'; export const title = 'Home'; +export const description = 'A React implementation of Spectrum, Adobe\'s design system.'; export const omitFromNav = true; export const hideFromSearch = true; @@ -16,75 +17,75 @@ export const hideFromSearch = true; [Getting started](getting-started.html) • [GitHub](https://github.com/adobe/react-spectrum) diff --git a/packages/dev/s2-docs/pages/s2/releases/index.mdx b/packages/dev/s2-docs/pages/s2/releases/index.mdx index 873bffb49a0..63bf7862e5e 100644 --- a/packages/dev/s2-docs/pages/s2/releases/index.mdx +++ b/packages/dev/s2-docs/pages/s2/releases/index.mdx @@ -13,6 +13,7 @@ export default Layout; export const hideNav = true; export const section = 'Releases'; +export const description = 'Release notes for React Spectrum.'; export const tags = ['changelog', 'versions', 'updates']; export const title = 'Releases'; export const hideFromSearch = true; diff --git a/packages/dev/s2-docs/pages/s2/style-macro.mdx b/packages/dev/s2-docs/pages/s2/style-macro.mdx index 4d312d9a2ca..8592c288d17 100644 --- a/packages/dev/s2-docs/pages/s2/style-macro.mdx +++ b/packages/dev/s2-docs/pages/s2/style-macro.mdx @@ -1,7 +1,5 @@ import {Layout} from '../../src/Layout'; import {InlineAlert, Heading, Content, Link} from '@react-spectrum/s2'; -import {S2Colors} from '../../src/S2Colors'; -import {S2Typography} from '../../src/S2Typography'; import {StyleMacroProperties} from '../../src/types'; import {getPropertyDefinitions, getShorthandDefinitions} from '../../src/styleProperties'; export default Layout; @@ -17,23 +15,11 @@ The `style` macro supports a constrained set of values per property that conform ## Colors All Spectrum 2 color tokens are available across color properties (e.g., `backgroundColor`, `color`, `borderColor`). -`baseColors` consists of the semantic and global colors listed below. - - ## Dimensions -Spacing props like `margin` and `padding` accept values on a **4px grid**. These are specified in `px` and get converted to `rem`. In addition to numbers, these named options are available: - -- `edge-to-text` – default spacing between the edge of a control and its text. Relative to control height. -- `pill` – default spacing between the edge of a pill-shaped control and its text. Relative to control height. -- `text-to-control` – default spacing between text and a control (e.g., label and input). Scales with font size. -- `text-to-visual` – default spacing between text and a visual element (e.g., icon). Scales with font size. - -Size props like `width` and `height` accept arbitrary pixel values. Values are converted to `rem` and multiplied by 1.25x on touch devices to increase hit targets. - ## Text @@ -51,11 +37,6 @@ Note that `font` should be applied on a per element basis rather than globally s ``` -Type scales include: UI, Body, Heading, Title, Detail, and Code. Each scale has a default and additional t-shirt sizes (e.g., `ui-sm`, `heading-2xl`, `code-xl`). - - - - diff --git a/packages/dev/s2-docs/pages/s2/testing.mdx b/packages/dev/s2-docs/pages/s2/testing.mdx new file mode 100644 index 00000000000..940e6ba7e40 --- /dev/null +++ b/packages/dev/s2-docs/pages/s2/testing.mdx @@ -0,0 +1,219 @@ +import {VersionBadge} from '../../src/VersionBadge'; +import {InstallCommand} from '../../src/InstallCommand'; +import {Layout} from '../../src/Layout'; +export default Layout; + +import testUtilDocs from 'docs:@react-aria/test-utils'; +import {InlineAlert, Heading, Content} from '@react-spectrum/s2' + +export const section = 'Guides'; +export const description = 'Writing tests for apps built with React Spectrum'; + +# Testing + +This page describes how to test an application built with React Spectrum. It documents the available testing utilities available for each aria pattern and how they can be used to simulate common user interactions. + +## Testing semantics + +The recommended way to query for React Spectrum components and their internals is by semantics. React Spectrum +Components implement [ARIA patterns](https://www.w3.org/TR/wai-aria-practices-1.2/). ARIA is a W3C standard +that specifies the semantics for many UI components. Unlike the class names and DOM structure of the component, these +semantics are much less likely to change over time, making them ideal to query for. + +The main attribute to look for when querying is the [role](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques). +This attribute represents the type of element a DOM node represents, e.g. a button, list option, or tab. + +### React Testing Library + +[React Testing Library](https://testing-library.com/docs/react-testing-library/intro) is useful because it +enforces that you write tests using semantics instead of implementation details. We use React Testing Library +to test React Spectrum itself, and it's quite easy to [query](https://testing-library.com/docs/dom-testing-library/api-queries) +elements by role, text, label, etc. + +```tsx +import {render} from '@testing-library/react'; + +let tree = render(); +let option = tree.getByRole('button'); +``` + +## Test ids + +Querying by semantics covers many scenarios, but what if you have many buttons on a page or its text changes due to translations based on locale? +In these cases, you may need a way to identify specific elements in tests, and that's where test ids come in. + +React Spectrum components pass all [data attributes](https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes) +through to their underlying DOM nodes, which allows you to use an attribute like `data-testid` to identify +a particular instance of a component. + +```tsx +import {render} from '@testing-library/react'; +import {TextField} from '@react-spectrum/s2'; + +function LoginForm() { + return ( + <> + + + + ); +} + +let tree = render(); +let username = tree.getByTestId('username'); +let password = tree.getByTestId('password'); +``` + +## Triggering events + +React Spectrum components rely on many different browser events to support different devices and platforms, so it's important to simulate +these correctly in your tests. For example, a click is really a `mousemove` and `mouseover` the target, followed +by `mousedown`, `focus`, and `mouseup` events, and finally a `click` event. + +The best way to handle this is with the [user-event](https://github.com/testing-library/user-event) library. +This lets you trigger high level interactions like a user would, and the library handles firing all of the individual +events that make up that interaction. + +```tsx +import {render} from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +let tree = render(); + +// Click on the username field to focus it, and enter the value. +userEvent.click(tree.getByLabelText('Username')); +userEvent.type(document.activeElement, 'devon'); + +// Tab to the password field, and enter the value. +userEvent.tab(); +userEvent.type(document.activeElement, 'Pas$w0rd'); + +// Tab to the submit button and click it. +userEvent.tab(); +userEvent.click(document.activeElement); +``` + +## Test setup + +### Timers + +When using fake timers, you may need to advance timers after various interactions, e.g. after selection. In Jest, use `jest.runAllTimers()`. You should also run all timers after each test completes. +See [Jest's timer docs](https://jestjs.io/docs/timer-mocks) or the equivalent docs of your test framework for more information. + +```tsx +afterEach(() => { + act(() => jest.runAllTimers()); +}); +``` + +Consider adding a `act(() => jest.runAllTimers());` after your simulated user interaction if you run into a test failure that looks like the following: + +``` +TestingLibraryElementError: Unable to find an accessible element with the role "listbox" +``` + +If you are using real timers instead, you can await a particular state of your app to be reached. If you are using React Testing Library, you can perform a `waitFor` query +to wait for a dialog to appear: + +```tsx +await waitFor(() => { + expect(getByRole('dialog')).toBeInTheDocument(); +}); +``` + +### Simulating long press + +To simulate a long press event in components like Menu, mock PointerEvent globally and use the function from `@react-spectrum/test-utils`. + +```tsx +import {installPointerEvent, triggerLongPress} from '@react-spectrum/test-utils'; +installPointerEvent(); + +// In test case +let button = getByRole('button'); +triggerLongPress(button); +``` + +### Simulating move event + +Components like ColorArea, ColorSlider, ColorWheel, and Slider each feature a draggable handle that a user can interact with to change the component's value. To simulate a drag event, mock MouseEvent and use `fireEvent` from `@testing-library/react` +to simulate these drag/move events in your tests. Additionally, the track dimensions for the draggable handle should be mocked so that the move operation calculations can be properly computed. + +```tsx +import {fireEvent} from '@testing-library/react'; +import {installMouseEvent} from '@react-spectrum/test-utils'; +installMouseEvent(); + +beforeAll(() => { + jest.spyOn(window.HTMLElement.prototype, 'getBoundingClientRect').mockImplementation(() => ({top: 0, left: 0, width: 100, height: 10})); +}) + +// In test case +let sliderThumb = getByRole('slider').parentElement; + +// With fireEvent, move thumb from 0 to 50 +fireEvent.mouseDown(thumb, {clientX: 0, pageX: 0}); +fireEvent.mouseMove(thumb, {pageX: 50}); +fireEvent.mouseUp(thumb, {pageX: 50}); +``` + + +## React Spectrum test utils + + +In addition to the test utilities mentioned above, [@react-spectrum/test-utils](https://www.npmjs.com/package/@react-spectrum/test-utils) re-exports the same test utils available in `@react-aria/test-utils`, including +the ARIA pattern testers. These testers are set of testing utilities that aims to make writing unit tests easier for consumers of React Spectrum. + +### Installation + + + + + Requirements + Please note that this library uses [@testing-library/react@16](https://www.npmjs.com/package/@testing-library/react) and [@testing-library/user-event@14](https://www.npmjs.com/package/@testing-library/user-event). This means that you need to be on React 18+ in order for these utilities to work. + + + +### Setup + +Initialize a `User` object at the top of your test file, and use it to create an ARIA pattern tester in your test cases. The tester has methods that you can call within your test to query for specific subcomponents or simulate common interactions. See [below](#patterns) for what patterns are currently supported. + +```ts +// YourTest.test.ts +import {screen} from '@testing-library/react'; +import {User} from '@react-spectrum/test-utils'; + +// Provide whatever method of advancing timers you use in your test, this example assumes Jest with fake timers. +// 'interactionType' specifies what mode of interaction should be simulated by the tester +// 'advanceTimer' is used by the tester to advance the timers in the tests for specific interactions (e.g. long press) +let testUtilUser = new User({interactionType: 'mouse', advanceTimer: jest.advanceTimersByTime}); +// ... + +it('my test case', async function () { + // Render your test component/app + render(); + // Initialize the table tester via providing the 'Table' pattern name and the root element of said table + let table = testUtilUser.createTester('Table', {root: screen.getByTestId('test_table')}); + + // ... +}); +``` + +### User API + + + + +### Patterns + +Below is a list of the ARIA patterns testers currently supported by createTester. See the accompanying component testing docs pages for a sample of how to use the testers in your test suite. + +- [CheckboxGroup](./CheckboxGroup/testing.html) +- [ComboBox](./ComboBox/testing.html) +- [Dialog](./Dialog/testing.html) +- [Menu](./Menu/testing.html) +- [Picker](./Picker/testing.html) +- [RadioGroup](./RadioGroup/testing.html) +- [TableView](./TableView/testing.html) +- [Tabs](./Tabs/testing.html) +- [TreeView](./TreeView/testing.html) diff --git a/packages/dev/s2-docs/scripts/generateMarkdownDocs.mjs b/packages/dev/s2-docs/scripts/generateMarkdownDocs.mjs index 3279cdddfdc..423ffe0df3f 100644 --- a/packages/dev/s2-docs/scripts/generateMarkdownDocs.mjs +++ b/packages/dev/s2-docs/scripts/generateMarkdownDocs.mjs @@ -44,6 +44,34 @@ function cleanTypeText(t) { return cleaned; } +/** + * Transform relative URLs to use .md extension instead of .html or no extension. + * Preserves query params and hash fragments. + */ +function transformRelativeUrl(href) { + if (!href || href.startsWith('http://') || href.startsWith('https://') || href.startsWith('mailto:') || href.startsWith('#')) { + return href; + } + + // Split href into path and query/hash parts + const match = href.match(/^([^?#]*)(\?[^#]*)?(#.*)?$/); + if (!match) { + return href; + } + + let [, pathPart, queryPart = '', hashPart = ''] = match; + + if (pathPart.endsWith('.html')) { + // Replace .html with .md + pathPart = pathPart.slice(0, -5) + '.md'; + } else if (pathPart && !pathPart.match(/\.[a-zA-Z0-9]+$/)) { + // Add .md to paths without an extension + pathPart = pathPart + '.md'; + } + + return pathPart + queryPart + hashPart; +} + function getIconNames() { if (iconNamesCache) { return iconNamesCache; @@ -1149,6 +1177,9 @@ function remarkDocsComponentsToMarkdown() { const childrenText = extractText(node.children); const linkText = ariaLabel || childrenText || href; + // Transform relative links to use .md extension + href = transformRelativeUrl(href); + if (href) { const linkNode = { type: 'link', @@ -1548,6 +1579,11 @@ function remarkDocsComponentsToMarkdown() { .join('\n'); }); + // Transform relative links to use .md extension. + visit(tree, 'link', (node) => { + node.url = transformRelativeUrl(node.url); + }); + // Append "Related Types" section if we collected any. if (relatedTypes.size > 0) { const newNodes = [ diff --git a/packages/dev/s2-docs/scripts/generateOGImages.mjs b/packages/dev/s2-docs/scripts/generateOGImages.mjs index aac8b044589..b24ab2f3dd8 100644 --- a/packages/dev/s2-docs/scripts/generateOGImages.mjs +++ b/packages/dev/s2-docs/scripts/generateOGImages.mjs @@ -251,6 +251,7 @@ for (let file of files) { alignItems: 'center', width: '100%', height: '100%', + padding: '60px', backgroundColor: '#ffffff', fontFamily: 'adobe-clean', color: '#000000' @@ -261,19 +262,32 @@ for (let file of files) { style: { display: 'flex', alignItems: 'center', - gap: 44 + gap: 44, + maxWidth: '100%' }, children: [ // Library logo - getLibraryLogo(subtitle), + { + type: 'div', + props: { + style: { + display: 'flex', + flexShrink: 0 + }, + children: getLibraryLogo(subtitle) + } + }, // Library name { type: 'div', props: { style: { + display: 'flex', fontSize: 84, fontWeight: 700, - lineHeight: 1.1 + lineHeight: 1.1, + flexShrink: 1, + minWidth: 0 }, children: subtitle } @@ -294,6 +308,7 @@ for (let file of files) { alignItems: 'center', width: '100%', height: '100%', + padding: '60px', backgroundColor: '#ffffff', fontFamily: 'adobe-clean', color: '#000000' @@ -304,11 +319,21 @@ for (let file of files) { style: { display: 'flex', alignItems: 'center', - gap: 44 + gap: 44, + maxWidth: '100%' }, children: [ // Library logo - getLibraryLogo(subtitle), + { + type: 'div', + props: { + style: { + display: 'flex', + flexShrink: 0 + }, + children: getLibraryLogo(subtitle) + } + }, // Text content { type: 'div', @@ -316,13 +341,16 @@ for (let file of files) { style: { display: 'flex', flexDirection: 'column', - gap: 0 + gap: 0, + flexShrink: 1, + minWidth: 0 }, children: [ { type: 'div', props: { style: { + display: 'flex', fontSize: 84, fontWeight: 700, lineHeight: 1.1 @@ -334,6 +362,7 @@ for (let file of files) { type: 'div', props: { style: { + display: 'flex', fontSize: 56, fontWeight: 400, color: '#464646' diff --git a/packages/dev/s2-docs/src/CodeBlock.tsx b/packages/dev/s2-docs/src/CodeBlock.tsx index 918f4d32a95..8cbd4871d53 100644 --- a/packages/dev/s2-docs/src/CodeBlock.tsx +++ b/packages/dev/s2-docs/src/CodeBlock.tsx @@ -117,7 +117,7 @@ export function CodeBlock({render, children, dir, files, expanded, hidden, ...pr component={render} align={props.align} />
- {files ? + {files ? (); for (let [, specifier] of contents.matchAll(/import (?:.|\n)*?['"](.+?)['"]/g)) { specifier = specifier.replace(/(vanilla-starter|tailwind-starter)\//g, (m, s) => 'starters/' + (s === 'vanilla-starter' ? 'docs' : 'tailwind') + '/src/'); - + if (specifier.startsWith('url:')) { urls[specifier] = resolveUrl(specifier.slice(4), file); continue; } - + if (!/^(\.|starters)/.test(specifier)) { let dep = specifier.startsWith('@') ? specifier.split('/').slice(0, 2).join('/') : specifier.split('/')[0]; npmDeps[dep] ??= '^' + getPackageVersion(dep); diff --git a/packages/dev/s2-docs/src/Layout.tsx b/packages/dev/s2-docs/src/Layout.tsx index 6960c8ab4f7..c05376f8bc7 100644 --- a/packages/dev/s2-docs/src/Layout.tsx +++ b/packages/dev/s2-docs/src/Layout.tsx @@ -194,9 +194,27 @@ export function Layout(props: PageProps & {children: ReactElement}) { let ogImage = getOgImageUrl(currentPage); let title = getTitle(currentPage); let description = getDescription(currentPage); - let parentPage = pages.find(p => { - return p.url === currentPage.url.replace(/\/[^/]+\.html$/, '/index.html'); - }); + let parentUrl; + let parentPage; + if (isSubpage) { + let pathParts = currentPage.url.split('/'); + let fileName = pathParts.pop(); + + if (fileName === 'testing.html') { + // for testing pages like /CheckboxGroup/testing.html, parent is /CheckboxGroup.html + let parentDir = pathParts.pop(); + parentUrl = `../${parentDir}.html`; + + let parentPageUrl = pathParts.join('/') + `/${parentDir}.html`; + parentPage = pages.find(p => p.url === parentPageUrl); + } else { + // for release subpages like releases/2024-01-15.html, parent is just the same but with the end replaced with index.html + parentUrl = './index.html'; + let parentIndexUrl = pathParts.join('/') + '/index.html'; + parentPage = pages.find(p => p.url === parentIndexUrl); + } + } + let isPostList = currentPage.exports?.isPostList; let Content = isPostList ? PostListContainer : Article; return ( @@ -305,7 +323,7 @@ export function Layout(props: PageProps & {children: ReactElement}) { })}> - + {React.cloneElement(children, { components: components(isLongForm), pages @@ -346,10 +364,11 @@ interface ArticleProps { parentPage?: Page, children: ReactNode, isLongForm?: boolean, - isWide?: boolean + isWide?: boolean, + parentHref?: string } -function Article({page, parentPage, children, isLongForm, isWide}: ArticleProps) { +function Article({page, parentPage, children, isLongForm, isWide, parentHref}: ArticleProps) { let section = page.exports?.section; return (
{page.exports?.version && } {page.exports?.isSubpage - ? + ? : page.tableOfContents?.[0].level === 1 &&

{page.tableOfContents?.[0].title}

}
- {parentPage?.exports?.title} + {parentPage?.exports?.title ?? parentPage?.tableOfContents?.[0]?.title ?? parentPage?.name}

{currentPage.tableOfContents?.[0].title}

@@ -409,6 +429,10 @@ function SubpageHeader({currentPage, parentPage, isLongForm}: SubpageHeaderProps ); } +function isExternalUrl(url: string): boolean { + return url.startsWith('http://') || url.startsWith('https://'); +} + function MobileRelatedPages({pages}: {pages: Array<{title: string, url: string}>}) { return (
})}>
    - {pages.map((page, i) => ( -
  • - - {page.title} - -
  • - ))} + {pages.map((page, i) => { + let isExternal = isExternalUrl(page.url); + return ( +
  • + + {page.title} + +
  • + ); + })}
); diff --git a/packages/dev/s2-docs/src/MobileSearchMenu.tsx b/packages/dev/s2-docs/src/MobileSearchMenu.tsx index b75a023acd1..10496d2ced9 100644 --- a/packages/dev/s2-docs/src/MobileSearchMenu.tsx +++ b/packages/dev/s2-docs/src/MobileSearchMenu.tsx @@ -1,30 +1,21 @@ 'use client'; -import {Autocomplete, Key, OverlayTriggerStateContext, Provider, Dialog as RACDialog, DialogProps as RACDialogProps, Tab as RACTab, TabList as RACTabList, TabPanel as RACTabPanel, TabPanelProps as RACTabPanelProps, TabProps as RACTabProps, Tabs as RACTabs, SelectionIndicator, TabRenderProps} from 'react-aria-components'; +import {Autocomplete, OverlayTriggerStateContext, Provider, Dialog as RACDialog, DialogProps as RACDialogProps, Tab as RACTab, TabList as RACTabList, TabPanel as RACTabPanel, TabPanelProps as RACTabPanelProps, TabProps as RACTabProps, Tabs as RACTabs, SelectionIndicator, TabRenderProps} from 'react-aria-components'; import {baseColor, focusRing, style} from '@react-spectrum/s2/style' with {type: 'macro'}; import {CloseButton, SearchField, TextContext} from '@react-spectrum/s2'; -import {ComponentCardItem, ComponentCardView} from './ComponentCardView'; +import {ComponentCardView} from './ComponentCardView'; import { - filterAndSortSearchItems, - getOrderedLibraries, - getPageTitle, getResourceTags, - getSearchSection, + LazyIconSearchView, SearchEmptyState, - type Section, - sortItemsForDisplay, - useFilteredIcons, - useSearchTagSelection, - useSectionTagsForDisplay + useSearchMenuState } from './searchUtils'; -import {getLibraryFromPage} from './library'; import {IconSearchSkeleton, useIconFilter} from './IconSearchView'; // @ts-ignore import {type Library} from './constants'; import {Page} from '@parcel/rsc'; -import React, {lazy, ReactNode, Suspense, useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react'; +import React, {ReactNode, Suspense, useContext, useEffect, useRef, useState} from 'react'; import {SearchTagGroups} from './SearchTagGroups'; -import {stripMarkdown} from './SearchMenu'; import {useId} from '@react-aria/utils'; @@ -141,8 +132,6 @@ const mobileTabPanel = style({ outlineStyle: 'none' }); -const IconSearchView = lazy(() => import('./IconSearchView').then(({IconSearchView}) => ({default: IconSearchView}))); - const stickySearchContainer = style({ width: 'full', display: 'flex', @@ -228,24 +217,28 @@ const MobileCustomDialog = function MobileCustomDialog(props: MobileDialogProps) function MobileNav({pages, currentPage, initialTag}: {pages: Page[], currentPage: Page, initialTag?: string}) { let overlayTriggerState = useContext(OverlayTriggerStateContext); let [searchFocused, setSearchFocused] = useState(false); - let [searchValue, setSearchValue] = useState(''); let scrollContainerRef = useRef(null); - let [selectedLibrary, setSelectedLibrary] = useState(getLibraryFromPage(currentPage)); - - let getSectionsForLibrary = useCallback((libraryId: string) => { - let sectionsMap = new Map(); - let filteredPages = pages.filter(page => getLibraryFromPage(page) === libraryId && !page.exports?.hideFromSearch); - for (let page of filteredPages) { - let section = getSearchSection(page); - let sectionPages = sectionsMap.get(section) ?? []; - sectionPages.push(page); - sectionsMap.set(section, sectionPages); - } - return sectionsMap; - }, [pages]); - + const iconFilter = useIconFilter(); - let libraries = useMemo(() => getOrderedLibraries(currentPage), [currentPage]); + const { + selectedLibrary, + setSelectedLibrary, + orderedLibraries: libraries, + searchValue, + setSearchValue, + sectionTagsForDisplay: sectionTags, + selectedTagId: selectedSection, + setSelectedTagId: setSelectedSection, + handleTagSelectionChange, + filteredIcons, + isIconsSelected, + selectedItems, + getPlaceholderText + } = useSearchMenuState({ + pages, + currentPage, + initialTag + }); let handleSearchFocus = () => { setSearchFocused(true); @@ -264,147 +257,8 @@ function MobileNav({pages, currentPage, initialTag}: {pages: Page[], currentPage } }; - let filterPages = (pages: Page[], searchValue: string) => { - return filterAndSortSearchItems(pages, searchValue, { - getName: (page: Page) => getPageTitle(page), - getTags: (page: Page) => page.exports?.tags || [], - getDate: (page: Page) => page.exports?.date, - shouldUseDateSort: (page: Page) => { - const section = getSearchSection(page); - return section === 'Blog' || section === 'Releases'; - } - }); - }; - - let getSectionContent = (sectionName: string, libraryId: string, searchValue: string = ''): ComponentCardItem[] => { - let librarySections = getSectionsForLibrary(libraryId); - let pages = librarySections.get(sectionName) ?? []; - - let filteredPages = filterPages(pages, searchValue); - - let items = filteredPages.map(page => ({ - id: page.url.replace(/^\//, ''), - name: getPageTitle(page), - href: page.url, - description: stripMarkdown(page.exports?.description), - date: page.exports?.date - })); - - return sortItemsForDisplay(items, searchValue); - }; - - let getAllContent = (libraryId: string, searchValue: string = ''): ComponentCardItem[] => { - let librarySections = getSectionsForLibrary(libraryId); - let allPages = Array.from(librarySections.values()).flat(); - let filteredPages = filterPages(allPages, searchValue); - - let items = filteredPages.map(page => ({ - id: page.url.replace(/^\//, ''), - name: getPageTitle(page), - href: page.url, - description: stripMarkdown(page.exports?.description), - date: page.exports?.date - })); - - return sortItemsForDisplay(items, searchValue); - }; - - let getItemsForSelection = (section: string | undefined, libraryId: string, searchValue: string = ''): ComponentCardItem[] => { - if (!section) { - return []; - } - let items: ComponentCardItem[] = []; - if (section === 'all') { - items = getAllContent(libraryId, searchValue); - } else { - // Check if this is a resource tag (e.g., icons) - const libraryResourceTags = getResourceTags(libraryId as Library); - const libraryResourceTagIds = libraryResourceTags.map(t => t.id); - if (libraryResourceTagIds.includes(section)) { - // Resources are handled separately, return empty for now - return []; - } - // Convert lowercase ID back to section name for getSectionContent - const librarySections = getSectionNamesForLibrary(libraryId); - const sectionName = librarySections.find(s => s.toLowerCase() === section) || section; - items = getSectionContent(sectionName, libraryId, searchValue); - } - - items = sortItemsForDisplay(items, searchValue); - - return items; - }; - - let getSectionNamesForLibrary = (libraryId: string) => { - let librarySections = getSectionsForLibrary(libraryId); - let sectionArray = [...librarySections.keys()]; - - // Show 'Components' first - sectionArray.sort((a, b) => { - if (a === 'Components') { - return -1; - } - if (b === 'Components') { - return 1; - } - return a.localeCompare(b); - }); - - return sectionArray; - }; - - let currentLibrarySections = getSectionNamesForLibrary(selectedLibrary); - - const sectionsForDisplay: Section[] = useMemo(() => { - return currentLibrarySections.map(name => ({ - id: name.toLowerCase(), - name, - children: [] - })); - }, [currentLibrarySections]); - - const initialSelectedSection = useMemo(() => { - const section = getSearchSection(currentPage); - const firstSection = currentLibrarySections[0]?.toLowerCase() || 'components'; - return initialTag || (section ? section.toLowerCase() : firstSection); - }, [initialTag, currentPage, currentLibrarySections]); - - const resourceTags = useMemo(() => getResourceTags(selectedLibrary), [selectedLibrary]); - - const [selectedSection, setSelectedSection] = useSearchTagSelection( - searchValue, - sectionsForDisplay.map(s => ({id: s.id, name: s.name})), - resourceTags, - initialSelectedSection - ); - - const sectionTags = useSectionTagsForDisplay( - sectionsForDisplay, - searchValue, - selectedSection, - resourceTags.map(t => t.id) - ); - - const filteredIcons = useFilteredIcons(searchValue); - const iconFilter = useIconFilter(); - - let handleSectionSelectionChange = useCallback((keys: Iterable) => { - const firstKey = Array.from(keys)[0] as string; - if (firstKey) { - setSelectedSection(firstKey); - } - }, [setSelectedSection]); - - let handleResourceSelectionChange = useCallback((keys: Iterable) => { - const firstKey = Array.from(keys)[0] as string; - if (firstKey) { - setSelectedSection(firstKey); - } - }, [setSelectedSection]); - useEffect(() => { if (scrollContainerRef.current) { - // Ensure newly selected section starts at the top of the vertical scroll area scrollContainerRef.current.scrollTo({top: 0, behavior: 'auto'}); } }, [selectedSection, selectedLibrary, searchValue]); @@ -420,10 +274,7 @@ function MobileNav({pages, currentPage, initialTag}: {pages: Page[], currentPage let newLib = key as Library; setSelectedLibrary(newLib); if (!searchFocused) { - let nextSections = getSectionNamesForLibrary(newLib); - if (nextSections.length > 0) { - setSelectedSection(nextSections[0].toLowerCase()); - } + setSelectedSection('components'); } }}>
@@ -439,15 +290,12 @@ function MobileNav({pages, currentPage, initialTag}: {pages: Page[], currentPage
{libraries.map(library => { - const isIconsSelected = selectedSection === 'icons' && library.id === 'react-spectrum'; const libraryResourceTags = getResourceTags(library.id); - const selectedResourceTag = libraryResourceTags.find(tag => tag.id === selectedSection); - const placeholderText = selectedResourceTag - ? `Search ${selectedResourceTag.name}` - : `Search ${library.label}`; + const placeholderText = getPlaceholderText(library.label); + const showIcons = isIconsSelected && library.id === 'react-spectrum'; return ( - +
- {isIconsSelected ? ( + {showIcons ? ( }> - @@ -483,7 +331,7 @@ function MobileNav({pages, currentPage, initialTag}: {pages: Page[], currentPage setSearchValue(''); overlayTriggerState?.close(); }} - items={getItemsForSelection(selectedSection, library.id, searchValue)} + items={library.id === selectedLibrary ? selectedItems : []} ariaLabel="Pages" size="S" renderEmptyState={() => } /> diff --git a/packages/dev/s2-docs/src/Nav.tsx b/packages/dev/s2-docs/src/Nav.tsx index 13493bf4091..1d3d38ff1de 100644 --- a/packages/dev/s2-docs/src/Nav.tsx +++ b/packages/dev/s2-docs/src/Nav.tsx @@ -5,6 +5,7 @@ import {focusRing, size, space, style} from '@react-spectrum/s2/style' with {typ import {getLibraryFromPage} from './library'; import {getPageFromPathname, getSnapshot, subscribe} from './NavigationSuspense'; import {Link} from 'react-aria-components'; +import LinkOutIcon from '../../../@react-spectrum/s2/ui-icons/LinkOut'; import type {Page, PageProps} from '@parcel/rsc'; import React, {createContext, useContext, useEffect, useRef, useState, useSyncExternalStore} from 'react'; @@ -36,7 +37,7 @@ export function Nav({pages, currentPage}: PageProps) { let section = page.exports?.section ?? 'Components'; let group = page.exports?.group ?? undefined; - if (section === '') { + if (section === '' || page.exports?.isSubpage) { continue; } @@ -46,7 +47,7 @@ export function Nav({pages, currentPage}: PageProps) { if (value instanceof Map) { groupMap = value; } else { - groupMap = new Map(); + groupMap = new Map(); } let groupPages = groupMap.get(section) ?? []; groupPages.push(page); @@ -82,7 +83,7 @@ export function Nav({pages, currentPage}: PageProps) { if (b[0] === 'Guides') { return -1; } - + return a[0].localeCompare(b[0]); }); @@ -271,12 +272,14 @@ export function SideNavItem(props) { export function SideNavLink(props) { let linkRef = useRef(null); let selected = useContext(SideNavContext); - let {...linkProps} = props; + let {isExternal, ...linkProps} = props; return ( {props.children} + {isExternal && ( + + )} )} ); diff --git a/packages/dev/s2-docs/src/OptimisticToc.tsx b/packages/dev/s2-docs/src/OptimisticToc.tsx index 812611a1f95..c1c9c9b090b 100644 --- a/packages/dev/s2-docs/src/OptimisticToc.tsx +++ b/packages/dev/s2-docs/src/OptimisticToc.tsx @@ -64,6 +64,10 @@ export function OptimisticToc({currentPage, pages}: {currentPage: Page, pages: P ); } +function isExternalUrl(url: string): boolean { + return url.startsWith('http://') || url.startsWith('https://'); +} + function RelatedPages({pages}: {pages: Array<{title: string, url: string}>}) { return (
@@ -72,7 +76,7 @@ function RelatedPages({pages}: {pages: Array<{title: string, url: string}>}) { {pages.map((page, i) => ( - {page.title} + {page.title} ))} diff --git a/packages/dev/s2-docs/src/PatternTestingFAQ.tsx b/packages/dev/s2-docs/src/PatternTestingFAQ.tsx new file mode 100644 index 00000000000..6ef7b742f81 --- /dev/null +++ b/packages/dev/s2-docs/src/PatternTestingFAQ.tsx @@ -0,0 +1,24 @@ +import {Disclosure, DisclosurePanel, DisclosureTitle} from '@react-spectrum/s2'; +import React from 'react'; + +export function PatternTestingFAQ({patternName}: {patternName: string}) { + return ( + <> + + When using the test utils, what if a certain interaction errors or doesn't seem to result in the expected state? + + In cases like this, first double check your test setup and make sure that your test is rendering your {patternName} in its expected + state before the test util interaction call. If everything looks correct, you can always fall back to simulating interactions manually, + and using the test util to query your {patternName}'s state post interaction. + + + + The tester doesn't offer a specific interaction flow, what should I do? + + Whenever the {patternName} tester queries its elements or triggers a user flow, it does so against the current state of the {patternName}. Therefore the {patternName} tester can be used alongside + whatever simulated user flow you add. + + + + ); +} diff --git a/packages/dev/s2-docs/src/S2Colors.tsx b/packages/dev/s2-docs/src/S2Colors.tsx index 946aa8eba13..e4dd8f5e457 100644 --- a/packages/dev/s2-docs/src/S2Colors.tsx +++ b/packages/dev/s2-docs/src/S2Colors.tsx @@ -1,135 +1,150 @@ +'use client'; + import {colorSwatch, getColorScale} from './color.macro' with {type: 'macro'}; import {Disclosure, DisclosurePanel, DisclosureTitle} from '@react-spectrum/s2'; import React from 'react'; import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; -export function S2Colors() { +export function BackgroundColorsDisclosure() { + return ( + + Background colors + +

The backgroundColor property supports the following values, in addition to the semantic and global colors. These colors are specifically chosen to be used as backgrounds, so prefer them over global colors where possible.

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ ); +} + +export function TextColorsDisclosure() { + return ( + + Text colors + +

The color property supports the following values, in addition to the semantic and global colors. These colors are specifically chosen to be used as text colors, so prefer them over global colors where possible.

+
+ + + + + + + + + + +
+
+
+ ); +} + +export function SemanticColorsDisclosure() { + return ( + + Semantic colors + +

The following values are available across all color properties. Prefer to use semantic colors over global colors when they represent a specific meaning.

+
+ + + + + +
+
+
+ ); +} + +export function GlobalColorsDisclosure() { return ( - <> - - Background colors - -

The backgroundColor property supports the following values, in addition to the semantic and global colors shown below. These colors are specifically chosen to be used as backgrounds, so prefer them over global colors where possible.

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
- - Text colors - -

The color property supports the following values, in addition to the semantic and global colors shown below. These colors are specifically chosen to be used as text colors, so prefer them over global colors where possible.

-
- - - - - - - - - - -
-
-
- - Semantic colors - -

The following values are available across all color properties. Prefer to use semantic colors over global colors when they represent a specific meaning.

-
- - - - - -
-
-
- - Global colors - -

The following values are available across all color properties.

-
- - - - - - - - - - - - - - - - - - - -
-
-
- + + Global colors + +

The following values are available across all color properties.

+
+ + + + + + + + + + + + + + + + + + + +
+
+
); } diff --git a/packages/dev/s2-docs/src/S2Typography.tsx b/packages/dev/s2-docs/src/S2Typography.tsx index b490cda231b..eeff8cafa67 100644 --- a/packages/dev/s2-docs/src/S2Typography.tsx +++ b/packages/dev/s2-docs/src/S2Typography.tsx @@ -1,3 +1,5 @@ +'use client'; + import React from 'react'; import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; diff --git a/packages/dev/s2-docs/src/SearchMenu.tsx b/packages/dev/s2-docs/src/SearchMenu.tsx index 3572af7856b..4530271d21f 100644 --- a/packages/dev/s2-docs/src/SearchMenu.tsx +++ b/packages/dev/s2-docs/src/SearchMenu.tsx @@ -5,35 +5,21 @@ import {Autocomplete, Dialog, Key, OverlayTriggerStateContext, Provider} from 'r import Close from '@react-spectrum/s2/icons/Close'; import {ComponentCardView} from './ComponentCardView'; import { - type ComponentItem, - createSearchOptions, - filterAndSortSearchItems, - getOrderedLibraries, - getPageTitle, getResourceTags, - getSearchSection, + LazyIconSearchView, SearchEmptyState, - sortItemsForDisplay, - sortSearchItems, - useFilteredIcons, - useSearchTagSelection, - useSectionTagsForDisplay + useSearchMenuState } from './searchUtils'; -import {getLibraryFromPage, getLibraryFromUrl} from './library'; import {IconSearchSkeleton, useIconFilter} from './IconSearchView'; import {type Library, TAB_DEFS} from './constants'; // @ts-ignore import {Page} from '@parcel/rsc'; -import React, {CSSProperties, lazy, Suspense, useEffect, useMemo, useRef, useState} from 'react'; +import React, {CSSProperties, Suspense, useCallback, useEffect, useRef} from 'react'; import {SearchTagGroups} from './SearchTagGroups'; import {style} from '@react-spectrum/s2/style' with { type: 'macro' }; import {Tab, TabList, TabPanel, Tabs} from './Tabs'; import {TextFieldRef} from '@react-types/textfield'; -export function stripMarkdown(description: string | undefined) { - return (description || '').replace(/\[(.*?)\]\(.*?\)/g, '$1'); -} - export const divider = style({ marginY: 8, marginStart: -8, @@ -50,7 +36,6 @@ export const divider = style({ width: '[3px]' }); -const IconSearchView = lazy(() => import('./IconSearchView').then(({IconSearchView}) => ({default: IconSearchView}))); interface SearchMenuProps { pages: Page[], @@ -77,16 +62,31 @@ function CloseButton({onClose}: {onClose: () => void}) { export function SearchMenu(props: SearchMenuProps) { let {pages, currentPage, onClose, overlayId, isSearchOpen} = props; - const currentLibrary = getLibraryFromPage(currentPage); - let [selectedLibrary, setSelectedLibrary] = useState(currentLibrary); - let [searchValue, setSearchValue] = useState(props.initialSearchValue); - - const orderedTabs = useMemo(() => getOrderedLibraries(currentPage), [currentPage]); - const searchRef = useRef | null>(null); + const iconFilter = useIconFilter(); + + const { + selectedLibrary, + setSelectedLibrary, + orderedLibraries: orderedTabs, + searchValue, + setSearchValue, + sectionTagsForDisplay, + selectedTagId, + handleTagSelectionChange, + filteredIcons, + isIconsSelected, + selectedItems, + selectedSectionName, + getPlaceholderText + } = useSearchMenuState({ + pages, + currentPage, + initialSearchValue: props.initialSearchValue, + initialTag: props.initialTag + }); // Auto-focus search field when menu opens - // We don't put autoFocus on the SearchField because it will cause a flicker when switching tabs useEffect(() => { const timer = setTimeout(() => { searchRef.current?.focus(); @@ -94,102 +94,8 @@ export function SearchMenu(props: SearchMenuProps) { return () => clearTimeout(timer); }, []); - // Transform pages data into component data structure - const transformedComponents = useMemo(() => { - if (!pages || !Array.isArray(pages)) { - return []; - } - - const components = pages - .filter(page => page.url && page.url.endsWith('.html') && getLibraryFromUrl(page.url) === selectedLibrary && !page.exports?.hideFromSearch) - .map(page => { - const name = page.url.replace(/^\//, '').replace(/\.html$/, ''); - const title = getPageTitle(page); - const section: string = getSearchSection(page); - const tags: string[] = (page.exports?.tags || page.exports?.keywords as string[]) || []; - const description: string = stripMarkdown(page.exports?.description); - const date: string | undefined = page.exports?.date; - return { - id: name, - name: title, - href: page.url, - section, - tags, - description, - date - }; - }); - - return components; - }, [pages, selectedLibrary]); - - // Build sections for the selected library - const sections = useMemo(() => { - const sectionNames = Array.from(new Set(transformedComponents.map(c => c.section || 'Components'))); - return sectionNames.map(sectionName => ({ - id: sectionName.toLowerCase(), - name: sectionName, - children: transformedComponents.filter(c => (c.section || 'Components') === sectionName) - })).sort((a, b) => { - if (a.id === 'components') { - return -1; - } - if (b.id === 'components') { - return 1; - } - return 0; - }); - }, [transformedComponents]); - - const sectionTags = useMemo(() => sections.map(s => ({id: s.id, name: s.name})), [sections]); - const resourceTags = useMemo(() => getResourceTags(selectedLibrary), [selectedLibrary]); - - const [selectedTagId, setSelectedTagId] = useSearchTagSelection( - searchValue, - sectionTags, - resourceTags, - props.initialTag || currentPage.exports?.section?.toLowerCase() || 'components' - ); - - const filteredIcons = useFilteredIcons(searchValue); - const iconFilter = useIconFilter(); - - let filteredComponents = useMemo(() => { - if (!searchValue) { - return sections; - } - - const allItems = sections.flatMap(section => section.children); - - const sortedItems = filterAndSortSearchItems(allItems, searchValue, createSearchOptions()); - - const resultsBySection = new Map(); - - sortedItems.forEach(item => { - const section = item.section || 'Components'; - if (!resultsBySection.has(section)) { - resultsBySection.set(section, []); - } - resultsBySection.get(section)!.push(item); - }); - - return sections - .map(section => ({ - ...section, - children: resultsBySection.get(section.name) || [] - })) - .filter(section => section.children.length > 0); - }, [sections, searchValue]); - - const sectionTagsForDisplay = useSectionTagsForDisplay( - sections, - searchValue, - selectedTagId, - resourceTags.map(t => t.id) - ); - - const handleTabSelectionChange = React.useCallback((key: Key) => { - setSelectedLibrary(key as typeof selectedLibrary); + const handleTabSelectionChange = useCallback((key: Key) => { + setSelectedLibrary(key as Library); // Focus main search field of the newly selected tab setTimeout(() => { const lib = key as Library; @@ -198,47 +104,7 @@ export function SearchMenu(props: SearchMenuProps) { searchRef.current.focus(); } }, 10); - }, []); - - const handleSectionSelectionChange = React.useCallback((keys: Iterable) => { - const firstKey = Array.from(keys)[0] as string; - if (firstKey) { - setSelectedTagId(firstKey); - } - }, [setSelectedTagId]); - - const handleIconSelectionChange = React.useCallback((keys: Iterable) => { - const firstKey = Array.from(keys)[0] as string; - if (firstKey) { - setSelectedTagId(firstKey); - } - }, [setSelectedTagId]); - - const selectedItems = useMemo(() => { - let items: typeof transformedComponents = []; - if (selectedTagId === 'all') { - items = filteredComponents.flatMap(s => s.children) || []; - if (searchValue.trim().length > 0) { - items = sortSearchItems(items, searchValue, createSearchOptions()); - } else { - items = sortItemsForDisplay(items, searchValue); - } - } else { - items = (filteredComponents.find(s => s.id === selectedTagId)?.children) || []; - items = sortItemsForDisplay(items, searchValue); - } - - return items; - }, [filteredComponents, selectedTagId, searchValue]); - - const selectedSectionName = useMemo(() => { - if (selectedTagId === 'all') { - return 'All'; - } - return (filteredComponents.find(s => s.id === selectedTagId)?.name) - || (sections.find(s => s.id === selectedTagId)?.name) - || 'Items'; - }, [filteredComponents, sections, selectedTagId]); + }, [setSelectedLibrary]); useEffect(() => { const handleNavigationStart = () => { @@ -278,13 +144,10 @@ export function SearchMenu(props: SearchMenuProps) { {orderedTabs.map((tab, i) => { const tabResourceTags = getResourceTags(tab.id); - const selectedResourceTag = tabResourceTags.find(tag => tag.id === selectedTagId); - const placeholderText = selectedResourceTag - ? `Search ${selectedResourceTag.name}` - : `Search ${tab.label}`; + const placeholderText = getPlaceholderText(tab.label); return ( - +
- {selectedTagId === 'icons' ? ( + onSectionSelectionChange={handleTagSelectionChange} + onResourceSelectionChange={handleTagSelectionChange} /> + {isIconsSelected ? (
}> - diff --git a/packages/dev/s2-docs/src/VisualExampleClient.tsx b/packages/dev/s2-docs/src/VisualExampleClient.tsx index 830df817e0d..8997d5a2e74 100644 --- a/packages/dev/s2-docs/src/VisualExampleClient.tsx +++ b/packages/dev/s2-docs/src/VisualExampleClient.tsx @@ -3,6 +3,7 @@ import {ActionButton, Avatar, Collection, ComboBox, ComboBoxItem, Content, ContextualHelp, Footer, Header, Heading, NotificationBadge, NumberField, Picker, PickerItem, PickerSection, RangeSlider, Slider, Switch, Text, TextField, ToggleButton, ToggleButtonGroup} from '@react-spectrum/s2'; import AddCircle from '@react-spectrum/s2/icons/AddCircle'; import {baseColor, focusRing, style, StyleString} from '@react-spectrum/s2/style' with { type: 'macro' }; +import {CenterBaseline} from '../../../@react-spectrum/s2/src/CenterBaseline'; import {CodePlatter, Pre, ShareUrlProvider} from './CodePlatter'; import {ExampleOutput} from './ExampleOutput'; import {ExampleSwitcherContext} from './ExampleSwitcher'; @@ -549,7 +550,15 @@ function Wrapper({control, children, styles, ref}: {control: PropControl, childr {control.name}   - {control.description ?
: null} + {control.description ? ( + + + + ) : null}
{children} diff --git a/packages/dev/s2-docs/src/searchUtils.tsx b/packages/dev/s2-docs/src/searchUtils.tsx index d7ba0ae707f..23710ff51ae 100644 --- a/packages/dev/s2-docs/src/searchUtils.tsx +++ b/packages/dev/s2-docs/src/searchUtils.tsx @@ -1,15 +1,16 @@ 'use client'; import {Content, Heading, IllustratedMessage} from '@react-spectrum/s2'; -import {getLibraryFromPage} from './library'; +import {getLibraryFromPage, getLibraryFromUrl} from './library'; // @ts-ignore import {iconList, useIconFilter} from './IconSearchView'; +import {Key} from 'react-aria-components'; import {type Library, TAB_DEFS} from './constants'; // eslint-disable-next-line monorepo/no-internal-import import NoSearchResults from '@react-spectrum/s2/illustrations/linear/NoSearchResults'; // @ts-ignore import {Page} from '@parcel/rsc'; -import React, {useEffect, useMemo, useRef, useState} from 'react'; +import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; export interface SearchableItem { @@ -39,6 +40,161 @@ export interface Tag { name: string } +/** + * Strips markdown link syntax from a string, keeping only the link text. + */ +export function stripMarkdown(description: string | undefined): string { + return (description || '').replace(/\[(.*?)\]\(.*?\)/g, '$1'); +} + +/** + * Transforms a page into a ComponentItem for search/display. + */ +export function transformPageToComponentItem(page: Page): ComponentItem { + const name = page.url.replace(/^\//, '').replace(/\.html$/, ''); + const title = getPageTitle(page); + const section: string = getSearchSection(page); + const tags: string[] = (page.exports?.tags || page.exports?.keywords as string[]) || []; + const description: string = stripMarkdown(page.exports?.description); + const date: string | undefined = page.exports?.date; + return { + id: name, + name: title, + href: page.url, + section, + tags, + description, + date + }; +} + +/** + * Builds sections from pages for a given library. + * Sorts sections with 'Components' first. + */ +export function buildSectionsFromPages(pages: Page[], library: Library): Section[] { + const filteredPages = pages.filter(page => + page.url && + page.url.endsWith('.html') && + getLibraryFromUrl(page.url) === library && + !page.exports?.hideFromSearch + ); + + const components = filteredPages.map(transformPageToComponentItem); + + const sectionNames = Array.from(new Set(components.map(c => c.section || 'Components'))); + + return sectionNames + .map(sectionName => ({ + id: sectionName.toLowerCase(), + name: sectionName, + children: components.filter(c => (c.section || 'Components') === sectionName) + })) + .sort((a, b) => { + if (a.id === 'components') { + return -1; + } + if (b.id === 'components') { + return 1; + } + return 0; + }); +} + +/** + * Gets items for a given section selection (handles 'all' and specific sections). + */ +export function getItemsForSection( + sections: Section[], + sectionId: string, + searchValue: string, + resourceTagIds: string[] = [] +): ComponentItem[] { + // Check if this is a resource tag (e.g., icons) - return empty, handled separately + if (resourceTagIds.includes(sectionId)) { + return []; + } + + let items: ComponentItem[]; + if (sectionId === 'all') { + items = sections.flatMap(s => s.children); + if (searchValue.trim().length > 0) { + items = sortSearchItems(items, searchValue, createSearchOptions()); + } else { + items = sortItemsForDisplay(items, searchValue); + } + } else { + items = sections.find(s => s.id === sectionId)?.children || []; + items = sortItemsForDisplay(items, searchValue); + } + + return items; +} + +/** + * Filters sections based on search value. + */ +export function filterSections(sections: Section[], searchValue: string): Section[] { + if (!searchValue) { + return sections; + } + + const allItems = sections.flatMap(section => section.children); + const sortedItems = filterAndSortSearchItems(allItems, searchValue, createSearchOptions()); + + const resultsBySection = new Map(); + sortedItems.forEach(item => { + const section = item.section || 'Components'; + if (!resultsBySection.has(section)) { + resultsBySection.set(section, []); + } + resultsBySection.get(section)!.push(item); + }); + + return sections + .map(section => ({ + ...section, + children: resultsBySection.get(section.name) || [] + })) + .filter(section => section.children.length > 0); +} + +/** + * Hook to build and manage sections for a library with search filtering. + */ +export function useLibrarySections(pages: Page[], library: Library, searchValue: string) { + const sections = useMemo( + () => buildSectionsFromPages(pages, library), + [pages, library] + ); + + const filteredSections = useMemo( + () => filterSections(sections, searchValue), + [sections, searchValue] + ); + + const getSectionNames = useCallback(() => { + return sections.map(s => s.name); + }, [sections]); + + return {sections, filteredSections, getSectionNames}; +} + +/** + * Creates search options for filtering/sorting Page objects directly. + */ +export function createSearchOptionsForPages(): SearchOptions { + return { + getName: (page: Page) => getPageTitle(page), + getTags: (page: Page) => page.exports?.tags || [], + getDate: (page: Page) => page.exports?.date, + shouldUseDateSort: (page: Page) => { + const section = getSearchSection(page); + return section === 'Blog' || section === 'Releases'; + } + }; +} + export interface SearchOptions { /** * Function to extract the name from an item. @@ -341,3 +497,174 @@ export function SearchEmptyState({searchValue, libraryLabel}: {searchValue: stri ); } + +export const LazyIconSearchView = React.lazy(() => + import('./IconSearchView').then(({IconSearchView}) => ({default: IconSearchView})) +); + +export interface SearchMenuStateOptions { + pages: Page[], + currentPage: Page, + initialSearchValue?: string, + initialTag?: string +} + +export interface SearchMenuState { + // Library state + selectedLibrary: Library, + setSelectedLibrary: (library: Library) => void, + orderedLibraries: ReturnType, + + // Search state + searchValue: string, + setSearchValue: (value: string) => void, + + // Section state + sections: Section[], + filteredSections: Section[], + sectionTags: Tag[], + sectionTagsForDisplay: Tag[], + + // Resource tags (icons, etc.) + resourceTags: Tag[], + resourceTagIds: string[], + + // Tag selection + selectedTagId: string, + setSelectedTagId: (id: string) => void, + handleTagSelectionChange: (keys: Iterable) => void, + + // Icons + filteredIcons: typeof iconList, + iconFilter: ReturnType, + isIconsSelected: boolean, + + // Computed items + selectedItems: ComponentItem[], + selectedSectionName: string, + + // Helpers + getPlaceholderText: (libraryLabel: string) => string +} + +export function useSearchMenuState(options: SearchMenuStateOptions): SearchMenuState { + const {pages, currentPage, initialSearchValue = '', initialTag} = options; + + // Library state + const currentLibrary = getLibraryFromPage(currentPage); + const [selectedLibrary, setSelectedLibrary] = useState(currentLibrary); + const orderedLibraries = useMemo(() => getOrderedLibraries(currentPage), [currentPage]); + + // Search state + const [searchValue, setSearchValue] = useState(initialSearchValue); + + // Build sections for the selected library + const {sections, filteredSections} = useLibrarySections( + pages || [], + selectedLibrary, + searchValue + ); + + // Section and resource tags + const sectionTags = useMemo(() => sections.map(s => ({id: s.id, name: s.name})), [sections]); + const resourceTags = useMemo(() => getResourceTags(selectedLibrary), [selectedLibrary]); + const resourceTagIds = useMemo(() => resourceTags.map(t => t.id), [resourceTags]); + + // Compute initial selected section + const initialSelectedSection = useMemo(() => { + const currentSection = sections.find(s => + s.children.some(c => c.href === currentPage.url) + ); + return initialTag || currentSection?.id || currentPage.exports?.section?.toLowerCase() || 'components'; + }, [initialTag, currentPage, sections]); + + // Tag selection + const [selectedTagId, setSelectedTagId] = useSearchTagSelection( + searchValue, + sectionTags, + resourceTags, + initialSelectedSection + ); + + // Section tags for display (includes "All" when searching) + const sectionTagsForDisplay = useSectionTagsForDisplay( + sections, + searchValue, + selectedTagId, + resourceTagIds + ); + + // Icons + const filteredIcons = useFilteredIcons(searchValue); + const iconFilter = useIconFilter(); + const isIconsSelected = selectedTagId === 'icons'; + + // Handler for tag selection change (works with TagGroup's onSelectionChange) + const handleTagSelectionChange = useCallback((keys: Iterable) => { + const firstKey = Array.from(keys)[0] as string; + if (firstKey) { + setSelectedTagId(firstKey); + } + }, [setSelectedTagId]); + + // Computed selected items + const selectedItems = useMemo(() => { + return getItemsForSection(filteredSections, selectedTagId, searchValue, resourceTagIds); + }, [filteredSections, selectedTagId, searchValue, resourceTagIds]); + + // Computed section name for aria-label + const selectedSectionName = useMemo(() => { + if (selectedTagId === 'all') { + return 'All'; + } + return (filteredSections.find(s => s.id === selectedTagId)?.name) + || (sections.find(s => s.id === selectedTagId)?.name) + || 'Items'; + }, [filteredSections, sections, selectedTagId]); + + // Helper to get placeholder text based on selected resource tag + const getPlaceholderText = useCallback((libraryLabel: string) => { + const selectedResourceTag = resourceTags.find(tag => tag.id === selectedTagId); + return selectedResourceTag + ? `Search ${selectedResourceTag.name}` + : `Search ${libraryLabel}`; + }, [resourceTags, selectedTagId]); + + return { + // Library state + selectedLibrary, + setSelectedLibrary, + orderedLibraries, + + // Search state + searchValue, + setSearchValue, + + // Section state + sections, + filteredSections, + sectionTags, + sectionTagsForDisplay, + + // Resource tags + resourceTags, + resourceTagIds, + + // Tag selection + selectedTagId, + setSelectedTagId, + handleTagSelectionChange, + + // Icons + filteredIcons, + iconFilter, + isIconsSelected, + + // Computed items + selectedItems, + selectedSectionName, + + // Helpers + getPlaceholderText + }; +} diff --git a/packages/dev/s2-docs/src/styleProperties.ts b/packages/dev/s2-docs/src/styleProperties.ts index 7df98cc25ab..ad1e6501f16 100644 --- a/packages/dev/s2-docs/src/styleProperties.ts +++ b/packages/dev/s2-docs/src/styleProperties.ts @@ -413,7 +413,7 @@ const shorthandMapping: {[key: string]: {values: string[], mapping: string[]}} = mapping: ['overflowX', 'overflowY', 'textOverflow', 'whiteSpace'] }, font: { - values: ['fontSize'], + values: [...fontSize], mapping: ['fontFamily', 'fontSize', 'fontWeight', 'lineHeight', 'color'] } }; @@ -465,18 +465,6 @@ export const spacingTypeValues = { negativeSpacing: negativeBaseSpacingValues }; -// a mapping of value to relative links that should be replaced in place -// opted NOT to link to Fonts from 'ui', 'code', etc since the visual example -// is so close to the area in the table where those are rendered -const relativeLinks: {[key: string]: string} = { - 'text-to-control': '#dimensions', - 'text-to-visual': '#dimensions', - 'edge-to-text': '#dimensions', - 'pill': '#dimensions', - 'baseColors': '#colors', - 'fontSize': '#text' -}; - // a mapping of value to mdn links that should be replaced in place const mdnTypeLinks: {[key: string]: string} = { 'string': 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String', @@ -546,8 +534,6 @@ export function getPropertyDefinitions(propertyCategory: string): {[key: string] if (mdnTypeLinks[value]) { links[value] = {href: mdnTypeLinks[value]}; - } else if (relativeLinks[value]) { - links[value] = {href: relativeLinks[value], isRelative: true}; } } } @@ -576,8 +562,6 @@ export function getShorthandDefinitions(): {[key: string]: StyleMacroPropertyDef if (mdnTypeLinks[value]) { links[value] = {href: mdnTypeLinks[value]}; - } else if (relativeLinks[value]) { - links[value] = {href: relativeLinks[value], isRelative: true}; } } diff --git a/packages/dev/s2-docs/src/types.tsx b/packages/dev/s2-docs/src/types.tsx index 015c5f53727..d9295b9278d 100644 --- a/packages/dev/s2-docs/src/types.tsx +++ b/packages/dev/s2-docs/src/types.tsx @@ -10,12 +10,15 @@ * governing permissions and limitations under the License. */ +import {Accordion, Disclosure, DisclosurePanel, DisclosureTitle} from '@react-spectrum/s2'; import Asterisk from '../../../@react-spectrum/s2/ui-icons/Asterisk'; +import {BackgroundColorsDisclosure, GlobalColorsDisclosure, SemanticColorsDisclosure, TextColorsDisclosure} from './S2Colors'; import {Code, styles as codeStyles} from './Code'; import {ColorLink, Link as SpectrumLink} from './Link'; import {getDoc} from 'globals-docs'; import Markdown from 'markdown-to-jsx'; import React, {ReactNode} from 'react'; +import {S2Typography} from './S2Typography'; import {spacingTypeValues} from './styleProperties'; import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; import {Table, TableBody, TableCell, TableColumn, TableHeader, TableRow} from './Table'; @@ -696,7 +699,7 @@ function TemplateLiteral({elements}: TTemplate) { ); } -const styleMacroTypeLinks = { +const styleMacroValueDesc = { 'baseSpacing': { description: 'Base spacing values in pixels, following a 4px grid. Will be converted to rem.', body: ( @@ -723,6 +726,48 @@ const styleMacroTypeLinks = { ) }, + 'text-to-control': { + description: 'Default spacing between text and a control (e.g., label and input). Scales with font size.' + }, + 'text-to-visual': { + description: 'Default spacing between text and a visual element (e.g., icon). Scales with font size.' + }, + 'edge-to-text': { + description: 'Default spacing between the edge of a control and its text. Relative to control height.' + }, + 'pill': { + description: 'Default spacing between the edge of a pill-shaped control and its text. Relative to control height.' + }, + 'baseColors': { + description: <>baseColors consists of the following values below:, + body: ( + <> + + + + ) + }, + 'fontSize': { + body: + }, + 'ui': { + description: 'Use within interactive UI components.' + }, + 'heading': { + description: 'Use for headings in content pages.' + }, + 'title': { + description: 'Use for titles within UI components such as cards or panels.' + }, + 'body': { + description: 'Use for the content of pages that are primarily text.' + }, + 'detail': { + description: 'Use for less important metadata.' + }, + 'code': { + description: 'Use for source code.' + }, 'LengthPercentage': { description: <>A CSS length value with percentage or viewport units. e.g. '50%', '100vw', '50vh' }, @@ -731,26 +776,6 @@ const styleMacroTypeLinks = { } }; -interface StyleMacroTypePopoverProps { - typeName: string, - description: ReactNode, - body?: ReactNode, - link?: string -} - -function StyleMacroTypePopover({typeName, description, body}: StyleMacroTypePopoverProps) { - return ( - - <> -

- {description} -

- {body} - -
- ); -} - interface StyleMacroPropertyDefinition { values: string[], additionalTypes?: string[], @@ -765,94 +790,182 @@ interface StyleMacroPropertiesProps { export function StyleMacroProperties({properties}: StyleMacroPropertiesProps) { let propertyNames = Object.keys(properties); - let hasMapping = Object.values(properties).some(p => p.mapping); return ( - - - - Property - Values - {hasMapping && Mapping} - - - - {propertyNames.map((propertyName, index) => { - let propDef = properties[propertyName]; - let values = propDef.values; - let links = propDef.links || {}; - - return ( - - - - - {propertyName} - - - - - {values.map((value, i) => ( - - {i > 0 && {' | '}} - {links[value] ? ( - - {value} - - ) : ( - '{value}' + + {propertyNames.map((propertyName, index) => { + let propDef = properties[propertyName]; + let values = propDef.values; + let links = propDef.links || {}; + + return ( + + + + {propertyName} + + + +
+ {/* for color and backgroundColor, skip values list and render disclosures directly since the contents of the disclosures cover the mapped values */} + {(() => { + if (propertyName === 'color') { + return ( + + + {styleMacroValueDesc['baseColors'].body} + + ); + } + if (propertyName === 'backgroundColor') { + return ( + + + {styleMacroValueDesc['baseColors'].body} + + ); + } + return ( +
+

Values

+ + {values.map((value, i) => { + let content; + if (links[value]) { + content = ( + + {value} + + ); + } else if (value === 'baseColors') { + content = {value}; + } else { + content = '{value}'; + } + + return ( + + {i > 0 && {' | '}} + {content} + + ); + })} + {/* for additional types properties (e.g. properties that have negative spacing or accept number/length percentage) we add them to the end */} + {propDef.additionalTypes && propDef.additionalTypes.map((typeName, i) => { + return ( + + {(values.length > 0 || i > 0) && {' | '}} + {typeName} + + ); + })} + +
+ ); + })()} + {values.map((value, i) => { + let valueDesc = styleMacroValueDesc[value]; + // special case handling for font and spacing specific value descriptions so they don't get rendered for + // other properties that may include the same values (e.g. heading in Colors) + // skip baseColors here as it will be rendered after + let shouldShowDescription = false; + if (value === 'fontSize' && (propertyName === 'fontSize' || propertyName === 'font')) { + shouldShowDescription = true; + } else if (['ui', 'heading', 'title', 'body', 'detail', 'code'].includes(value) && propertyName === 'lineHeight') { + shouldShowDescription = true; + } else if (['text-to-control', 'text-to-visual', 'edge-to-text', 'pill'].includes(value)) { + shouldShowDescription = true; + } + + if (shouldShowDescription && (valueDesc?.description || valueDesc?.body)) { + return ( +
+

+ + '{value}' + +

+ {valueDesc.description && ( +

+ {valueDesc.description} +

)} - - ))} - {propDef.additionalTypes && propDef.additionalTypes.map((typeName, i) => { - let typeLink = styleMacroTypeLinks[typeName]; - return ( - - {(values.length > 0 || i > 0) && {' | '}} - {/* eslint-disable-next-line no-nested-ternary */} - {typeLink ? ( - // only if the type link has a description and/or body do we want to render the type popover - // this is to make things like baseColor - typeLink.link && !typeLink.description && !typeLink.body ? ( - {typeName} - ) : ( - - ) - ) : undefined} - - ); - })} -
- - {hasMapping && ( - + {valueDesc.body} +
+ ); + } + return null; + })} + {/* show S2Typography for "fontSize" property and "font" shorthand specificatlly */} + {(propertyName === 'fontSize' || propertyName === 'font') && ( + + )} + {/* for other color property names show baseColors description since the value list is displayed still */} + {values.includes('baseColors') && styleMacroValueDesc['baseColors'] && (propertyName !== 'color' && propertyName !== 'backgroundColor') && ( +
+ {styleMacroValueDesc['baseColors'].description && ( +

+ {styleMacroValueDesc['baseColors'].description} +

+ )} + {styleMacroValueDesc['baseColors'].body} +
+ )} + {/* for the types that have descriptions, we add them below with the associated descriptions and/or mappings */} + {propDef.additionalTypes && propDef.additionalTypes.map((typeName, i) => { + let typeLink = styleMacroValueDesc[typeName]; + if (typeLink?.description || typeLink?.body) { + // dont render the type name for properties that only have one special value (e.g. baseSpacing) that has an associated description + // so that we don't double up on rendering the value name + let shouldSkipTypeName = values.length === 0 && propDef.additionalTypes?.length === 1; + + return ( +
+ {!shouldSkipTypeName && ( +

+ + {typeName} + +

+ )} + {typeLink.description && ( +

+ {typeLink.description} +

+ )} + {typeLink.body} +
+ ); + } + return null; + })} + {propDef.mapping && ( +
+

Maps to

- {propDef.mapping?.map((mappedProp, i) => ( + {propDef.mapping.map((mappedProp, i) => ( {i > 0 && {', '}} {mappedProp} ))} - +
)} - - {propDef.description && ( - - {propDef.description} - - )} - - ); - })} - -
+ {propDef.description && ( +
+ {propDef.description} +
+ )} +
+ + + ); + })} + ); } diff --git a/packages/react-aria-components/docs/CheckboxGroup.mdx b/packages/react-aria-components/docs/CheckboxGroup.mdx index b4d5082e6cb..1ecece634b0 100644 --- a/packages/react-aria-components/docs/CheckboxGroup.mdx +++ b/packages/react-aria-components/docs/CheckboxGroup.mdx @@ -13,7 +13,8 @@ export default Layout; import docs from 'docs:react-aria-components'; import statelyDocs from 'docs:@react-stately/checkbox'; import typesDocs from 'docs:@react-types/shared/src/events.d.ts'; -import {PropTable, HeaderInfo, TypeLink, PageDescription, StateTable, ContextTable} from '@react-spectrum/docs'; +import checkboxgroupUtil from 'docs:@react-aria/test-utils/src/checkboxgroup.ts'; +import {PropTable, HeaderInfo, TypeLink, PageDescription, StateTable, ContextTable, VersionBadge, ClassAPI} from '@react-spectrum/docs'; import styles from '@react-spectrum/docs/src/docs.css'; import packageData from 'react-aria-components/package.json'; import Anatomy from '@react-aria/checkbox/docs/checkboxgroup-anatomy.svg'; @@ -609,3 +610,40 @@ function SelectionCount() { ### Hooks If you need to customize things further, such as accessing internal state or customizing DOM structure, you can drop down to the lower level Hook-based API. See [useCheckboxGroup](useCheckboxGroup.html) for more details. + +## Testing + +### Test utils + +`@react-aria/test-utils` offers common checkbox group interaction utilities which you may find helpful when writing tests. See [here](./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 checkbox group tester and a sample of how you could use it in your test suite. + +```ts +// CheckboxGroup.test.ts +import {render} from '@testing-library/react'; +import {User} from '@react-aria/test-utils'; + +let testUtilUser = new User({interactionType: 'mouse', advanceTimer: jest.advanceTimersByTime}); +// ... + +it('CheckboxGroup can select multiple checkboxes', async function () { + // Render your test component/app and initialize the checkbox group tester + let {getByTestId} = render( + + ... + + ); + let checkboxGroupTester = testUtilUser.createTester('CheckboxGroup', {root: getByTestId('test-checkboxgroup')}); + expect(checkboxGroupTester.selectedCheckboxes).toHaveLength(0); + + await checkboxGroupTester.toggleCheckbox({checkbox: 0}); + expect(checkboxGroupTester.checkboxes[0]).toBeChecked(); + expect(checkboxGroupTester.selectedCheckboxes).toHaveLength(1); + + await checkboxGroupTester.toggleCheckbox({checkbox: 4}); + expect(checkboxGroupTester.checkboxes[4]).toBeChecked(); + expect(checkboxGroupTester.selectedCheckboxes).toHaveLength(2); +}); +``` + + diff --git a/packages/react-aria-components/docs/Dialog.mdx b/packages/react-aria-components/docs/Dialog.mdx index ddf239cc4be..55768ae9f51 100644 --- a/packages/react-aria-components/docs/Dialog.mdx +++ b/packages/react-aria-components/docs/Dialog.mdx @@ -12,8 +12,9 @@ export default Layout; import docs from 'docs:react-aria-components'; import typesDocs from 'docs:@react-types/overlays'; +import dialogUtil from 'docs:@react-aria/test-utils/src/dialog.ts'; import overlayStatelyDocs from 'docs:@react-stately/overlays'; -import {PropTable, HeaderInfo, TypeLink, PageDescription, StateTable, ContextTable} from '@react-spectrum/docs'; +import {PropTable, HeaderInfo, TypeLink, PageDescription, StateTable, ContextTable, VersionBadge, ClassAPI} from '@react-spectrum/docs'; import styles from '@react-spectrum/docs/src/docs.css'; import packageData from 'react-aria-components/package.json'; import Anatomy from '@react-aria/dialog/docs/anatomy.svg'; @@ -349,3 +350,42 @@ function CloseButton() { ### Hooks If you need to customize things further, such as accessing internal state or customizing DOM structure, you can drop down to the lower level Hook-based API. See [useDialog](useDialog.html) for more details. + +## Testing + +### Test utils + +`@react-aria/test-utils` offers common dialog interaction utilities which you may find helpful when writing tests. See [here](./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 dialog tester and a sample of how you could use it in your test suite. + +```ts +// Dialog.test.ts +import {render} from '@testing-library/react'; +import {User} from '@react-aria/test-utils'; + +let testUtilUser = new User({interactionType: 'mouse', advanceTimer: jest.advanceTimersByTime}); +// ... + +it('Dialog can be opened and closed', async function () { + // Render your test component/app and initialize the dialog tester + let {getByTestId, getByRole} = render( + + + + + ... + + + + ); + let button = getByRole('button'); + let dialogTester = testUtilUser.createTester('Dialog', {root: button, overlayType: 'modal'}); + await dialogTester.open(); + let dialog = dialogTester.dialog; + expect(dialog).toHaveAttribute('role', 'alertdialog'); + await dialogTester.close(); + expect(dialog).not.toBeInTheDocument(); +}); +``` + + diff --git a/packages/react-aria-components/docs/RadioGroup.mdx b/packages/react-aria-components/docs/RadioGroup.mdx index ac492baff71..3bafc1670e0 100644 --- a/packages/react-aria-components/docs/RadioGroup.mdx +++ b/packages/react-aria-components/docs/RadioGroup.mdx @@ -13,7 +13,8 @@ export default Layout; import docs from 'docs:react-aria-components'; import statelyDocs from 'docs:@react-stately/radio'; import typesDocs from 'docs:@react-types/shared/src/events.d.ts'; -import {PropTable, HeaderInfo, TypeLink, PageDescription, StateTable, ContextTable} from '@react-spectrum/docs'; +import radiogroupUtil from 'docs:@react-aria/test-utils/src/radiogroup.ts'; +import {PropTable, HeaderInfo, TypeLink, PageDescription, StateTable, ContextTable, VersionBadge, ClassAPI} from '@react-spectrum/docs'; import styles from '@react-spectrum/docs/src/docs.css'; import packageData from 'react-aria-components/package.json'; import Anatomy from '@react-aria/radio/docs/anatomy.svg'; @@ -630,3 +631,40 @@ RadioGroup provides a + +`@react-aria/test-utils` offers common radio group interaction utilities which you may find helpful when writing tests. See [here](./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 radio group tester and a sample of how you could use it in your test suite. + +```ts +// RadioGroup.test.ts +import {render} from '@testing-library/react'; +import {User} from '@react-aria/test-utils'; + +let testUtilUser = new User({interactionType: 'mouse', advanceTimer: jest.advanceTimersByTime}); +// ... + +it('RadioGroup can switch the selected radio', async function () { + // Render your test component/app and initialize the radiogroup tester + let {getByRole} = render( + + ... + + ); + + let radioGroupTester = testUtilUser.createTester('RadioGroup', {root: getByRole('radiogroup')}); + let radios = radioGroupTester.radios; + expect(radioGroupTester.selectedRadio).toBeFalsy(); + + await radioGroupTester.triggerRadio({radio: radios[0]}); + expect(radioGroupTester.selectedRadio).toBe(radios[0]); + + await radioGroupTester.triggerRadio({radio: radios[1]}); + expect(radioGroupTester.selectedRadio).toBe(radios[1]); +}); +``` + + diff --git a/packages/react-aria-components/docs/Tabs.mdx b/packages/react-aria-components/docs/Tabs.mdx index a1a73fde486..fedeb18bed2 100644 --- a/packages/react-aria-components/docs/Tabs.mdx +++ b/packages/react-aria-components/docs/Tabs.mdx @@ -123,7 +123,7 @@ import {Tabs, TabList, Tab, TabPanel, SelectionIndicator} from 'react-aria-compo position: absolute; transition-property: translate, width, height; transition-duration: 200ms; - + @media (prefers-reduced-motion: reduce) { transition: none; } @@ -815,7 +815,7 @@ If you need to customize things even further, such as accessing internal state o ### Test utils -`@react-aria/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 +`@react-aria/test-utils` offers common tabs interaction utilities which you may find helpful when writing tests. See [here](./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 diff --git a/packages/react-aria-components/docs/Tree.mdx b/packages/react-aria-components/docs/Tree.mdx index 3309a3d57e5..de7f7bdc4e1 100644 --- a/packages/react-aria-components/docs/Tree.mdx +++ b/packages/react-aria-components/docs/Tree.mdx @@ -2163,7 +2163,7 @@ If you need to customize things even further, such as accessing internal state o ### Test utils -`@react-aria/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 +`@react-aria/test-utils` offers common tree interaction utilities which you may find helpful when writing tests. See [here](./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 diff --git a/starters/docs/src/Table.css b/starters/docs/src/Table.css index 23082fecdc2..40cb074296f 100644 --- a/starters/docs/src/Table.css +++ b/starters/docs/src/Table.css @@ -55,6 +55,10 @@ transition-duration: 200ms; -webkit-tap-highlight-color: transparent; + &tr:last-child { + border-radius: 0 0 var(--radius) var(--radius); + } + &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); outline-offset: -2px; @@ -144,6 +148,14 @@ height: 100%; } + &:is(.react-aria-Row > :first-child) { + border-end-start-radius: var(--radius); + } + + &:is(.react-aria-Row > :last-child) { + border-end-end-radius: var(--radius); + } + &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); outline-offset: -2px; diff --git a/starters/docs/src/Toast.css b/starters/docs/src/Toast.css index 8668db0eff1..2830192a97e 100644 --- a/starters/docs/src/Toast.css +++ b/starters/docs/src/Toast.css @@ -20,7 +20,7 @@ display: flex; align-items: center; gap: var(--spacing-4); - background: var(--tint-1000); + background: var(--highlight-background); padding: var(--spacing-3) var(--spacing-4); border-radius: var(--radius-lg); outline: none; @@ -75,6 +75,7 @@ &[data-pressed] { background: var(--highlight-pressed); + box-shadow: none; } } } diff --git a/starters/tailwind/src/GridList.tsx b/starters/tailwind/src/GridList.tsx index b4c5afd05f6..24d576738d8 100644 --- a/starters/tailwind/src/GridList.tsx +++ b/starters/tailwind/src/GridList.tsx @@ -24,7 +24,7 @@ export function GridList( const itemStyles = tv({ extend: focusRing, - base: 'relative flex gap-3 cursor-default select-none py-2 px-3 text-sm text-neutral-900 dark:text-neutral-200 border-y dark:border-y-neutral-700 border-transparent first:border-t-0 last:border-b-0 first:rounded-t-lg last:rounded-b-lg -mb-px last:mb-0 -outline-offset-2', + base: 'relative flex gap-3 cursor-default select-none py-2 px-3 text-sm text-neutral-900 dark:text-neutral-200 border-t dark:border-t-neutral-700 border-transparent first:border-t-0 first:rounded-t-lg last:rounded-b-lg last:mb-0 -outline-offset-2', variants: { isSelected: { false: 'hover:bg-neutral-100 dark:hover:bg-neutral-700/60', diff --git a/starters/tailwind/src/Table.tsx b/starters/tailwind/src/Table.tsx index a1455d41cca..3abae4b4e7d 100644 --- a/starters/tailwind/src/Table.tsx +++ b/starters/tailwind/src/Table.tsx @@ -106,7 +106,7 @@ export function TableBody(props: TableBodyProps) { const rowStyles = tv({ extend: focusRing, - base: 'group/row relative cursor-default select-none -outline-offset-2 text-neutral-900 disabled:text-neutral-300 dark:text-neutral-200 dark:disabled:text-neutral-600 text-sm hover:bg-neutral-100 dark:hover:bg-neutral-800 selected:bg-blue-100 selected:hover:bg-blue-200 dark:selected:bg-blue-700/30 dark:selected:hover:bg-blue-700/40' + base: 'group/row relative cursor-default select-none -outline-offset-2 text-neutral-900 disabled:text-neutral-300 dark:text-neutral-200 dark:disabled:text-neutral-600 text-sm hover:bg-neutral-100 dark:hover:bg-neutral-800 selected:bg-blue-100 selected:hover:bg-blue-200 dark:selected:bg-blue-700/30 dark:selected:hover:bg-blue-700/40 last:rounded-b-lg' }); export function Row( @@ -135,7 +135,7 @@ export function Row( const cellStyles = tv({ extend: focusRing, - base: 'box-border border-b border-b-neutral-200 dark:border-b-neutral-700 group-last/row:border-b-0 [--selected-border:var(--color-blue-200)] dark:[--selected-border:var(--color-blue-900)] group-selected/row:border-(--selected-border) in-[:has(+[data-selected])]:border-(--selected-border) p-2 truncate -outline-offset-2' + base: 'box-border border-b border-b-neutral-200 dark:border-b-neutral-700 group-last/row:border-b-0 [--selected-border:var(--color-blue-200)] dark:[--selected-border:var(--color-blue-900)] group-selected/row:border-(--selected-border) in-[:has(+[data-selected])]:border-(--selected-border) p-2 truncate -outline-offset-2 group-last/row:first:rounded-bl-lg group-last/row:last:rounded-br-lg' }); export function Cell(props: CellProps) { diff --git a/starters/tailwind/src/Tree.tsx b/starters/tailwind/src/Tree.tsx index bd821a6036a..67bf9bb338d 100644 --- a/starters/tailwind/src/Tree.tsx +++ b/starters/tailwind/src/Tree.tsx @@ -16,7 +16,7 @@ import { composeTailwindRenderProps, focusRing } from './utils'; const itemStyles = tv({ extend: focusRing, - base: 'relative font-sans w-48 flex group gap-3 cursor-default select-none py-1 px-3 text-sm text-neutral-900 dark:text-neutral-200 bg-white dark:bg-neutral-900 border-y dark:border-y-neutral-700 border-transparent first:border-t-0 last:border-b-0 -mb-px last:mb-0 -outline-offset-2', + base: 'relative font-sans w-48 flex group gap-3 cursor-default select-none py-1 px-3 text-sm text-neutral-900 dark:text-neutral-200 bg-white dark:bg-neutral-900 border-t dark:border-t-neutral-700 border-transparent first:border-t-0 -outline-offset-2 first:rounded-t-lg last:rounded-b-lg', variants: { isSelected: { false: 'hover:bg-neutral-100 dark:hover:bg-neutral-800',