From 21c4dfb1129deed456a9b0817cc023813b33173b Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Sun, 12 Apr 2026 18:35:05 -0700 Subject: [PATCH 1/2] chore: Revert 9875 and move disabledBehavior example in TableView docs (#9914) * Revert "feat: default expand action for tableview and treeview (#9875)" This reverts commit bef549d82062681d69bfe0f942a45bfa3a2ee1e5. * remove disabled expandable row from docs example * move disabledBehavior example --- .../s2/stories/TableView.stories.tsx | 7 +-- .../s2/stories/TreeView.stories.tsx | 8 ++-- packages/dev/s2-docs/pages/s2/TableView.mdx | 7 ++- packages/react-aria-components/src/Table.tsx | 1 - .../react-aria-components/test/Tree.test.tsx | 30 ------------ .../{Treeble.test.tsx => Treeble.test.js} | 46 ++----------------- packages/react-aria/src/grid/useGridRow.ts | 30 +----------- .../src/gridlist/useGridListItem.ts | 7 +-- 8 files changed, 16 insertions(+), 120 deletions(-) rename packages/react-aria-components/test/{Treeble.test.tsx => Treeble.test.js} (93%) diff --git a/packages/@react-spectrum/s2/stories/TableView.stories.tsx b/packages/@react-spectrum/s2/stories/TableView.stories.tsx index f37a8976606..45ce91272e3 100644 --- a/packages/@react-spectrum/s2/stories/TableView.stories.tsx +++ b/packages/@react-spectrum/s2/stories/TableView.stories.tsx @@ -49,8 +49,9 @@ import User from '../s2wf-icons/S2_Icon_User_20_N.svg'; import {useTreeData} from 'react-stately/useTreeData'; let onActionFunc = action('onAction'); -let noOnAction = undefined; +let noOnAction = null; const onActionOptions = {onActionFunc, noOnAction}; + const events = ['onResizeStart', 'onResize', 'onResizeEnd', 'onSelectionChange', 'onSortChange']; const meta: Meta = { @@ -62,7 +63,7 @@ const meta: Meta = { tags: ['autodocs'], args: {...getActionArgs(events)}, argTypes: { - ...categorizeArgTypes('Events', ['onAction', 'onLoadMore', ...events]), + ...categorizeArgTypes('Events', ['onAction', 'onLoadMore', 'onResizeStart', 'onResize', 'onResizeEnd', 'onSelectionChange', 'onSortChange']), children: {table: {disable: true}}, onAction: { options: Object.keys(onActionOptions), // An array of serializable values @@ -1783,7 +1784,7 @@ export const TableWithNestedRows: StoryObj = { 5/22/1980 - + Applications Folder 4/7/2025 diff --git a/packages/@react-spectrum/s2/stories/TreeView.stories.tsx b/packages/@react-spectrum/s2/stories/TreeView.stories.tsx index d28392e2568..b9bebd80f52 100644 --- a/packages/@react-spectrum/s2/stories/TreeView.stories.tsx +++ b/packages/@react-spectrum/s2/stories/TreeView.stories.tsx @@ -45,9 +45,9 @@ import {useAsyncList} from 'react-stately/useAsyncList'; import {useListData} from 'react-stately/useListData'; let onActionFunc = action('onAction'); -let noOnAction = undefined; +let noOnAction = null; const onActionOptions = {onActionFunc, noOnAction}; -const events = ['onSelectionChange']; +const events = ['onSelectionChange', 'onAction']; const meta: Meta = { component: TreeView, @@ -57,7 +57,7 @@ const meta: Meta = { tags: ['autodocs'], args: {...getActionArgs(events)}, argTypes: { - ...categorizeArgTypes('Events', ['onAction', ...events]), + ...categorizeArgTypes('Events', events), children: {table: {disable: true}}, onAction: { options: Object.keys(onActionOptions), // An array of serializable values @@ -81,7 +81,7 @@ const TreeExampleStatic = (args: TreeViewProps): ReactElement => (
diff --git a/packages/dev/s2-docs/pages/s2/TableView.mdx b/packages/dev/s2-docs/pages/s2/TableView.mdx index 176e17fe035..6b40b756d0e 100644 --- a/packages/dev/s2-docs/pages/s2/TableView.mdx +++ b/packages/dev/s2-docs/pages/s2/TableView.mdx @@ -14,14 +14,13 @@ export const description = 'Displays data in rows and columns, with row selectio {docs.exports.TableView.description} -```tsx render docs={docs.exports.TableView} links={docs.links} props={['selectionMode', 'overflowMode', 'density', 'isQuiet', 'disabledBehavior']} initialProps={{'aria-label': 'Files', selectionMode: 'multiple', 'treeColumn': 'name', disabledBehavior: 'selection'}} type="s2" +```tsx render docs={docs.exports.TableView} links={docs.links} props={['selectionMode', 'overflowMode', 'density', 'isQuiet']} initialProps={{'aria-label': 'Files', selectionMode: 'multiple', 'treeColumn': 'name'}} type="s2" "use client"; import {TableView, TableHeader, Column, TableBody, Row, Cell} from '@react-spectrum/s2/TableView'; import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; Name @@ -644,7 +643,7 @@ export default function EditableTable(props) { Use `selectionMode` to enable single or multiple selection, and `selectedKeys` (matching each row's `id`) to control the selected rows. Return an [ActionBar](ActionBar) from `renderActionBar` to handle bulk actions, and use `onAction` for row navigation. Disable rows with `isDisabled`. See the [selection guide](selection) for details. -```tsx render docs={docs.exports.TableView} links={docs.links} props={['selectionMode', 'disallowEmptySelection']} initialProps={{selectionMode: 'multiple'}} wide type="s2" +```tsx render docs={docs.exports.TableView} links={docs.links} props={['selectionMode', 'disallowEmptySelection', 'disabledBehavior']} initialProps={{selectionMode: 'multiple', disabledBehavior: 'selection'}} wide type="s2" "use client"; import {TableView, TableHeader, Column, TableBody, Row, Cell, type Selection} from '@react-spectrum/s2/TableView'; import {ActionBar, ActionButton} from '@react-spectrum/s2/ActionBar'; @@ -975,4 +974,4 @@ function subscribe(fn) { ### EditableCell - \ No newline at end of file + diff --git a/packages/react-aria-components/src/Table.tsx b/packages/react-aria-components/src/Table.tsx index cfd6413d281..6d7179d6cd4 100644 --- a/packages/react-aria-components/src/Table.tsx +++ b/packages/react-aria-components/src/Table.tsx @@ -1377,7 +1377,6 @@ export const Row = /*#__PURE__*/ createBranchComponent( isFocusVisible: isFocusVisibleWithin, focusProps: focusWithinProps } = useFocusRing({within: true}); - let {hoverProps, isHovered} = useHover({ isDisabled: !states.allowsSelection && !states.hasAction, onHoverStart: props.onHoverStart, diff --git a/packages/react-aria-components/test/Tree.test.tsx b/packages/react-aria-components/test/Tree.test.tsx index d9028b55ddc..434ac46e58a 100644 --- a/packages/react-aria-components/test/Tree.test.tsx +++ b/packages/react-aria-components/test/Tree.test.tsx @@ -751,36 +751,6 @@ describe('Tree', () => { expect(onSelectionChange).toHaveBeenCalledTimes(0); }); - it('multi select should expand the row if anywhere on the row is clicked and there is no onAction provided', async () => { - let {getAllByRole} = render(); - let row = getAllByRole('row')[1]; - await user.hover(row); - expect(row).toHaveAttribute('data-hovered', 'true'); - - await user.click(row); - expect(row).toHaveAttribute('aria-expanded', 'true'); - }); - - it('single select should expand the row if anywhere on the row is clicked and there is no onAction provided', async () => { - let {getAllByRole} = render(); - let row = getAllByRole('row')[1]; - await user.hover(row); - expect(row).toHaveAttribute('data-hovered', 'true'); - - await user.click(row); - expect(row).toHaveAttribute('aria-expanded', 'true'); - }); - - it('no selection should expand the row if anywhere on the row is clicked and there is no onAction provided', async () => { - let {getAllByRole} = render(); - let row = getAllByRole('row')[1]; - await user.hover(row); - expect(row).toHaveAttribute('data-hovered', 'true'); - - await user.click(row); - expect(row).toHaveAttribute('aria-expanded', 'true'); - }); - it('should prevent Esc from clearing selection if escapeKeyBehavior is "none"', async () => { let {getAllByRole} = render(); diff --git a/packages/react-aria-components/test/Treeble.test.tsx b/packages/react-aria-components/test/Treeble.test.js similarity index 93% rename from packages/react-aria-components/test/Treeble.test.tsx rename to packages/react-aria-components/test/Treeble.test.js index bd87825ea02..404e2f6847c 100644 --- a/packages/react-aria-components/test/Treeble.test.tsx +++ b/packages/react-aria-components/test/Treeble.test.js @@ -98,15 +98,8 @@ function Example(props) { ); } -interface ReorderableTreebleItem { - id: string, - title: string, - type: string, - date: string, - children?: ReorderableTreebleItem[] -} function ReorderableTreeble(props) { - let tree = useTreeData({ + let tree = useTreeData({ initialItems: [ {id: '1', title: 'Documents', type: 'Directory', date: '10/20/2025', children: [ {id: '2', title: 'Project', type: 'Directory', date: '8/2/2025', children: [ @@ -121,7 +114,7 @@ function ReorderableTreeble(props) { ] }); - let {dragAndDropHooks} = useDragAndDrop<{value: ReorderableTreebleItem}>({ + let {dragAndDropHooks} = useDragAndDrop({ getItems: (keys, items) => items.map(item => ({'text/plain': item.value.title})), onMove(e) { if (e.target.dropPosition === 'before') { @@ -246,7 +239,7 @@ describe('Treeble', () => { expect(tester.rowHeaders[3]).toHaveTextContent('Job Posting'); }); - it.each(['mouse', 'touch', 'keyboard'] as const)('should expand a row with %s', async (interactionType) => { + it.each(['mouse', 'touch', 'keyboard'])('should expand a row with %s', async (interactionType) => { let tree = render(); let tester = utils.createTester('Table', {root: tree.getByTestId('treeble')}); @@ -536,39 +529,6 @@ describe('Treeble', () => { expect(onSelectionChange).toHaveBeenLastCalledWith(new Set(['games', 'mario', 'tetris'])); }); - it('supports expansion on disabled items with no action in disabledBehavior="selection" multiple selection', async () => { - let tree = render(); - let tester = utils.createTester('Table', {root: tree.getByTestId('treeble')}); - - await user.hover(tester.rows[1]); - expect(tester.rows[1]).toHaveAttribute('data-hovered', 'true'); - - await user.click(tester.rows[1]); - expect(tester.rows[1]).toHaveAttribute('aria-expanded', 'true'); - }); - - it('supports expansion on disabled items with no action in disabledBehavior="selection" single selection', async () => { - let tree = render(); - let tester = utils.createTester('Table', {root: tree.getByTestId('treeble')}); - - await user.hover(tester.rows[1]); - expect(tester.rows[1]).toHaveAttribute('data-hovered', 'true'); - - await user.click(tester.rows[1]); - expect(tester.rows[1]).toHaveAttribute('aria-expanded', 'true'); - }); - - it('supports expansion on disabled items with no action in disabledBehavior="selection" no selection', async () => { - let tree = render(); - let tester = utils.createTester('Table', {root: tree.getByTestId('treeble')}); - - await user.hover(tester.rows[1]); - expect(tester.rows[1]).toHaveAttribute('data-hovered', 'true'); - - await user.click(tester.rows[1]); - expect(tester.rows[1]).toHaveAttribute('aria-expanded', 'true'); - }); - it('should support drag and drop', async () => { let tree = render(); let tester = utils.createTester('Table', {root: tree.getByRole('treegrid')}); diff --git a/packages/react-aria/src/grid/useGridRow.ts b/packages/react-aria/src/grid/useGridRow.ts index 2489d76f5c7..1a66e8d0d7e 100644 --- a/packages/react-aria/src/grid/useGridRow.ts +++ b/packages/react-aria/src/grid/useGridRow.ts @@ -12,7 +12,7 @@ import {chain} from '../utils/chain'; -import {DOMAttributes, FocusableElement, Key, RefObject} from '@react-types/shared'; +import {DOMAttributes, FocusableElement, RefObject} from '@react-types/shared'; import {IGridCollection as GridCollection, GridNode} from 'react-stately/private/grid/GridCollection'; import {gridMap} from './utils'; import {GridState} from 'react-stately/private/grid/useGridState'; @@ -55,34 +55,6 @@ export function useGridRow, S extends GridState actions.onRowAction?.(node.key) : onAction; - - // Mirror useGridListItem: when no row action is provided, expandable tree-table rows use toggle as the - // primary action if selection is off or the row is selection-disabled (disabledKeys / selection behavior). - if ( - node != null && - 'treeColumn' in state && - state.treeColumn != null && - // I'd prefer if this was up in useTableRow, but onAction is a deprecated prop - // and maybe we'll move the expandable rows down into useGridRow eventually - 'toggleKey' in state && - typeof state.toggleKey === 'function' && - actions.onRowAction == null && - onAction == null - ) { - // adds the toggleKey type so it's not unknown below - let tableState = state as typeof state & {toggleKey: (key: Key) => void}; - let children = tableState.collection.getChildren?.(node.key); - let hasChildRows = [...(children ?? [])].length > 1; - let hasLink = state.selectionManager.isLink(node.key); - if ( - !hasLink && - hasChildRows && - ((state.disabledKeys.has(node.key) || node.props?.isDisabled) || - state.selectionManager.selectionMode === 'none')) { - onRowAction = () => tableState.toggleKey(node.key); - } - } - let {itemProps, ...states} = useSelectableItem({ selectionManager: state.selectionManager, key: node.key, diff --git a/packages/react-aria/src/gridlist/useGridListItem.ts b/packages/react-aria/src/gridlist/useGridListItem.ts index 844dbaafbd2..c2ab564852c 100644 --- a/packages/react-aria/src/gridlist/useGridListItem.ts +++ b/packages/react-aria/src/gridlist/useGridListItem.ts @@ -102,12 +102,7 @@ export function useGridListItem(props: AriaGridListItemOptions, state: ListSt let children = state.collection.getChildren?.(node.key); hasChildRows = hasChildRows || [...(children ?? [])].length > 1; - if ( - onAction == null && - !hasLink && - hasChildRows && - ((state.disabledKeys.has(node.key) || node.props?.isDisabled) || - state.selectionManager.selectionMode === 'none')) { + if (onAction == null && !hasLink && state.selectionManager.selectionMode === 'none' && hasChildRows) { onAction = () => state.toggleKey(node.key); } From 988c120714595eaa39c25df1ee8684cf53de713f Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Sun, 12 Apr 2026 18:38:57 -0700 Subject: [PATCH 2/2] docs: Update RangeCalendar for api consitency, add docs for nonce, and TagGroup onAction docs (#9910) * update RangeCalendar interactOutsideBehavior to commitBehavior * nonce docs * modify warning for missing dialog labelling * update taggroup selection docs to include actions * slight change to description copy for clarity * expandable tableview migration update and nits * review comments * add onAction to RAC Tags * add test for onAction set on Tag directly * fix combobox section className type --------- Co-authored-by: Devon Govett --- .../test/calendar/RangeCalendar.test.js | 32 +++++++++---------- packages/@react-spectrum/s2/src/ComboBox.tsx | 2 +- .../s2/src/DateRangePicker.tsx | 6 ++-- packages/@react-spectrum/s2/src/Picker.tsx | 2 +- packages/@react-spectrum/s2/style/index.ts | 6 ++-- .../pages/react-aria/RangeCalendar.mdx | 4 +-- .../dev/s2-docs/pages/react-aria/TagGroup.mdx | 26 ++++++++++++++- .../s2-docs/pages/react-aria/frameworks.mdx | 21 ++++++++++++ .../dev/s2-docs/pages/s2/RangeCalendar.mdx | 2 +- packages/dev/s2-docs/pages/s2/TagGroup.mdx | 26 ++++++++++++++- packages/dev/s2-docs/pages/s2/migrating.mdx | 5 +-- .../react-aria-components/src/TagGroup.tsx | 7 +++- .../test/TagGroup.test.js | 13 ++++++++ .../src/calendar/useRangeCalendar.ts | 16 +++++----- packages/react-aria/src/dialog/useDialog.ts | 2 +- 15 files changed, 127 insertions(+), 43 deletions(-) diff --git a/packages/@adobe/react-spectrum/test/calendar/RangeCalendar.test.js b/packages/@adobe/react-spectrum/test/calendar/RangeCalendar.test.js index d0d76b0371c..2b6b486505f 100644 --- a/packages/@adobe/react-spectrum/test/calendar/RangeCalendar.test.js +++ b/packages/@adobe/react-spectrum/test/calendar/RangeCalendar.test.js @@ -1549,14 +1549,14 @@ describe('RangeCalendar', () => { jest.setSystemTime(new Date('2025-11-01')); }); - it('should select the last hovered date when interactOutsideBehavior is "select"', async () => { + it('should select the last hovered date when commitBehavior is "select"', async () => { const onChange = jest.fn(); let {getByText, getAllByText} = render( + commitBehavior="select" /> ); await user.click(getByText('25')); @@ -1568,13 +1568,13 @@ describe('RangeCalendar', () => { expect(onChange).toHaveBeenCalledWith({start: new CalendarDate(2025, 11, 20), end: new CalendarDate(2025, 11, 25)}); }); - it('should clear the selection when interactOutsideBehavior is "clear"', async () => { + it('should clear the selection when commitBehavior is "clear"', async () => { const onChange = jest.fn(); let {getByText, getAllByText, getByRole} = render( + commitBehavior="clear" /> ); let startCell = getByRole('gridcell', {name: '25'}); @@ -1592,12 +1592,12 @@ describe('RangeCalendar', () => { expect(endCell).not.toHaveAttribute('aria-selected'); }); - it('should clear the selection when interactOutsideBehavior is "clear" no default selected range', async () => { + it('should clear the selection when commitBehavior is "clear" no default selected range', async () => { const onChange = jest.fn(); let {getByText, getByRole} = render( + commitBehavior="clear" /> ); let startCell = getByRole('gridcell', {name: '25'}); @@ -1614,13 +1614,13 @@ describe('RangeCalendar', () => { expect(endCell).not.toHaveAttribute('aria-selected'); }); - it('should reset to the initial range when interactOutsideBehavior is "reset"', async () => { + it('should reset to the initial range when commitBehavior is "reset"', async () => { const onChange = jest.fn(); let {getByText, getAllByText, getByRole} = render( + commitBehavior="reset" /> ); let originalStartCell = getByRole('gridcell', {name: '13'}); @@ -1645,13 +1645,13 @@ describe('RangeCalendar', () => { expect(originalEndCell).toHaveAttribute('aria-selected', 'true'); }); - it('should reset to the initial range when interactOutsideBehavior is "reset" (controlled value)', async () => { + it('should reset to the initial range when commitBehavior is "reset" (controlled value)', async () => { const onChange = jest.fn(); let {getByText, getAllByText, getByRole} = render( + commitBehavior="reset" /> ); let originalStartCell = getByRole('gridcell', {name: '13'}); @@ -1673,13 +1673,13 @@ describe('RangeCalendar', () => { }); describe('blur (e.g. tabbing away)', () => { - it('should select the hovered range when interactOutsideBehavior is "select" and calendar blurs', async () => { + it('should select the hovered range when commitBehavior is "select" and calendar blurs', async () => { const onChange = jest.fn(); let {getByText, getByRole} = render( + commitBehavior="select" /> ); let startCell = getByRole('gridcell', {name: '13'}); @@ -1701,13 +1701,13 @@ describe('RangeCalendar', () => { expect(newEndCell).toHaveAttribute('aria-selected', 'true'); }); - it('should clear the selection when interactOutsideBehavior is "clear" and calendar blurs', async () => { + it('should clear the selection when commitBehavior is "clear" and calendar blurs', async () => { const onChange = jest.fn(); let {getByText, getByRole} = render( + commitBehavior="clear" /> ); let startCell = getByRole('gridcell', {name: '13'}); @@ -1727,13 +1727,13 @@ describe('RangeCalendar', () => { expect(newEndCell).not.toHaveAttribute('aria-selected', 'true'); }); - it('should reset to the initial range when interactOutsideBehavior is "reset" and calendar blurs', async () => { + it('should reset to the initial range when commitBehavior is "reset" and calendar blurs', async () => { const onChange = jest.fn(); let {getByText, getByRole} = render( + commitBehavior="reset" /> ); let originalStartCell = getByRole('gridcell', {name: '13'}); diff --git a/packages/@react-spectrum/s2/src/ComboBox.tsx b/packages/@react-spectrum/s2/src/ComboBox.tsx index d1dbc0b0558..1c8eac6c5ec 100644 --- a/packages/@react-spectrum/s2/src/ComboBox.tsx +++ b/packages/@react-spectrum/s2/src/ComboBox.tsx @@ -432,7 +432,7 @@ export function ComboBoxItem(props: ComboBoxItemProps): ReactNode { ); } -export interface ComboBoxSectionProps extends Omit, keyof GlobalDOMAttributes> {} +export interface ComboBoxSectionProps extends Omit, 'style' | 'className' | 'render' | keyof GlobalDOMAttributes> {} export function ComboBoxSection(props: ComboBoxSectionProps): ReactNode { let {size} = useContext(InternalComboboxContext); return ( diff --git a/packages/@react-spectrum/s2/src/DateRangePicker.tsx b/packages/@react-spectrum/s2/src/DateRangePicker.tsx index a35a16354e5..b2b29c4395a 100644 --- a/packages/@react-spectrum/s2/src/DateRangePicker.tsx +++ b/packages/@react-spectrum/s2/src/DateRangePicker.tsx @@ -35,7 +35,7 @@ import {useSpectrumContextProps} from './useSpectrumContextProps'; export interface DateRangePickerProps extends Omit, 'children' | 'className' | 'style' | 'render' | keyof GlobalDOMAttributes>, - Pick, 'createCalendar' | 'pageBehavior' | 'firstDayOfWeek' | 'isDateUnavailable' | 'interactOutsideBehavior'>, + Pick, 'createCalendar' | 'pageBehavior' | 'firstDayOfWeek' | 'isDateUnavailable' | 'commitBehavior'>, Pick, StyleProps, SpectrumLabelableProps, @@ -80,7 +80,7 @@ export const DateRangePicker = /*#__PURE__*/ (forwardRef as forwardRefType)(func placeholderValue, maxVisibleMonths = 1, createCalendar, - interactOutsideBehavior, + commitBehavior, ...dateFieldProps } = props; let formContext = useContext(FormContext); @@ -155,7 +155,7 @@ export const DateRangePicker = /*#__PURE__*/ (forwardRef as forwardRefType)(func {showTimeField && (
diff --git a/packages/@react-spectrum/s2/src/Picker.tsx b/packages/@react-spectrum/s2/src/Picker.tsx index 34f5aa97b8b..1349668cfe1 100644 --- a/packages/@react-spectrum/s2/src/Picker.tsx +++ b/packages/@react-spectrum/s2/src/Picker.tsx @@ -740,7 +740,7 @@ function DefaultProvider({context, value, children}: {context: React.Context{children}; } -export interface PickerSectionProps extends Omit, 'style' | 'className' | 'render' | keyof GlobalDOMAttributes>, StyleProps {} +export interface PickerSectionProps extends Omit, 'style' | 'className' | 'render' | keyof GlobalDOMAttributes> {} export function PickerSection(props: PickerSectionProps): ReactNode { let {size} = useContext(InternalPickerContext); return ( diff --git a/packages/@react-spectrum/s2/style/index.ts b/packages/@react-spectrum/s2/style/index.ts index f21fc0ea971..ad6120f7d6c 100644 --- a/packages/@react-spectrum/s2/style/index.ts +++ b/packages/@react-spectrum/s2/style/index.ts @@ -28,7 +28,7 @@ export type {StyleString} from './types'; * * @example * ```tsx - * import {space} from '@react-spectrum/s2/style' with {type: 'macro'}; + * import {space, style} from '@react-spectrum/s2/style' with {type: 'macro'}; * * const styles = style({ * gap: space(12) // 12/16 = 0.75rem @@ -49,7 +49,7 @@ export function space(px: number): `[${string}]` { * * @example * ```tsx - * import {fontRelative} from '@react-spectrum/s2/style' with {type: 'macro'}; + * import {fontRelative, style} from '@react-spectrum/s2/style' with {type: 'macro'}; * * const styles = style({ * gap: fontRelative(2) // 2/14 = ~0.143em @@ -135,7 +135,7 @@ const iconSizes = { * ```tsx * import {iconStyle} from '@react-spectrum/s2/style' with {type: 'macro'}; * import Edit from '@react-spectrum/s2/icons/Edit'; - * + * * * ``` */ diff --git a/packages/dev/s2-docs/pages/react-aria/RangeCalendar.mdx b/packages/dev/s2-docs/pages/react-aria/RangeCalendar.mdx index 510d219a325..53a972761a4 100644 --- a/packages/dev/s2-docs/pages/react-aria/RangeCalendar.mdx +++ b/packages/dev/s2-docs/pages/react-aria/RangeCalendar.mdx @@ -21,14 +21,14 @@ export const description = 'Displays one or more date grids and allows users to component={VanillaRangeCalendar} docs={vanillaDocs.exports.RangeCalendar} links={vanillaDocs.links} - props={['isDisabled', 'interactOutsideBehavior']} + props={['isDisabled', 'commitBehavior']} type="vanilla" files={["starters/docs/src/RangeCalendar.tsx", "starters/docs/src/RangeCalendar.css"]} /> diff --git a/packages/dev/s2-docs/pages/react-aria/TagGroup.mdx b/packages/dev/s2-docs/pages/react-aria/TagGroup.mdx index 5aeaaecc903..972fefa34b5 100644 --- a/packages/dev/s2-docs/pages/react-aria/TagGroup.mdx +++ b/packages/dev/s2-docs/pages/react-aria/TagGroup.mdx @@ -101,7 +101,7 @@ import {TagGroup, Tag} from 'vanilla-starter/TagGroup'; Due to [HTML spec limitations](https://github.com/w3c/html-aria/issues/473), tags cannot be rendered as `` elements. React Aria handles link clicks with JavaScript and triggers native navigation. When using a client-side router, use the `onAction` event to programmatically trigger navigation instead of the `href` prop. -## Selection +## Selection and actions Use the `selectionMode` prop to enable single or multiple selection. The selected items can be controlled via the `selectedKeys` prop, matching the `id` prop of the items. Items can be disabled with the `isDisabled` prop. See the [selection guide](selection?component=TagGroup) for more details. @@ -137,6 +137,30 @@ function Example(props) { } ``` +Use the `onAction` prop to handle item actions. + +```tsx render wide +"use client"; +import {TagGroup, Tag} from 'vanilla-starter/TagGroup'; + +function Example() { + return ( + alert(`Clicked ${key}`)} + ///- end highlight -/// + > + Rock + Jazz + Pop + Classical + EDM + + ); +} +``` + ## Examples diff --git a/packages/dev/s2-docs/pages/react-aria/frameworks.mdx b/packages/dev/s2-docs/pages/react-aria/frameworks.mdx index a8f22dac396..04c1288126c 100644 --- a/packages/dev/s2-docs/pages/react-aria/frameworks.mdx +++ b/packages/dev/s2-docs/pages/react-aria/frameworks.mdx @@ -73,6 +73,9 @@ export const description = 'How to integrate with your framework.'; } ``` + + If you are using a [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) (CSP) with a nonce, add a `` tag to your document head, setting the `content` attribute to the generated nonce value. React Aria automatically reads the nonce from this tag. + @@ -184,6 +187,9 @@ export const description = 'How to integrate with your framework.'; }); ``` + + If you are using a [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) (CSP) with a nonce, add a `` tag to your document head, setting the `content` attribute to the generated nonce value. React Aria automatically reads the nonce from this tag. + @@ -213,6 +219,9 @@ export const description = 'How to integrate with your framework.'; } ``` + + If you are using a [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) (CSP) with a nonce, add a `` tag to your document head, setting the `content` attribute to the generated nonce value. React Aria automatically reads the nonce from this tag. + @@ -243,6 +252,9 @@ export const description = 'How to integrate with your framework.'; }); ``` + + If you are using a [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) (CSP) with a nonce, configure [`html.cspNonce`](https://vite.dev/config/shared-options.html#html-cspnonce) in `vite.config.ts`. React Aria automatically reads the nonce that Vite injects. + @@ -270,6 +282,9 @@ export const description = 'How to integrate with your framework.'; }; ``` + + If you are using a [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) (CSP) with a nonce, configure [`__webpack_nonce__`](https://webpack.js.org/guides/csp/) in your entry script. React Aria automatically reads the nonce from the webpack global. + @@ -297,6 +312,9 @@ export const description = 'How to integrate with your framework.'; }; ``` + + If you are using a [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) (CSP) with a nonce, add a `` tag to your document head, setting the `content` attribute to the generated nonce value. React Aria automatically reads the nonce from this tag. + @@ -324,6 +342,9 @@ export const description = 'How to integrate with your framework.'; }); ``` + + If you are using a [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) (CSP) with a nonce, add a `` tag to your document head, setting the `content` attribute to the generated nonce value. React Aria automatically reads the nonce from this tag. + diff --git a/packages/dev/s2-docs/pages/s2/RangeCalendar.mdx b/packages/dev/s2-docs/pages/s2/RangeCalendar.mdx index 1ee9a4285dc..d421d1130bc 100644 --- a/packages/dev/s2-docs/pages/s2/RangeCalendar.mdx +++ b/packages/dev/s2-docs/pages/s2/RangeCalendar.mdx @@ -15,7 +15,7 @@ export const description = 'Allows a user to select a contiguous range of dates. component={RangeCalendar} docs={docs.exports.RangeCalendar} links={docs.links} - props={['visibleMonths', 'pageBehavior', 'firstDayOfWeek', 'isDisabled', 'interactOutsideBehavior']} + props={['visibleMonths', 'pageBehavior', 'firstDayOfWeek', 'isDisabled', 'commitBehavior']} initialProps={{'aria-label': 'Trip dates'}} controlOptions={{ visibleMonths: { diff --git a/packages/dev/s2-docs/pages/s2/TagGroup.mdx b/packages/dev/s2-docs/pages/s2/TagGroup.mdx index c9bf4761b17..2bf7bd5f5f5 100644 --- a/packages/dev/s2-docs/pages/s2/TagGroup.mdx +++ b/packages/dev/s2-docs/pages/s2/TagGroup.mdx @@ -201,7 +201,7 @@ import {Link} from '@react-spectrum/s2/Link'; ``` -## Selection +## Selection and actions Use the `selectionMode` prop to enable single or multiple selection. The selected items can be controlled via the `selectedKeys` prop, matching the `id` prop of the items. Items can be disabled with the `isDisabled` prop. See the [selection guide](selection?component=TagGroup) for more details. @@ -235,6 +235,30 @@ function Example(props) { } ``` +Use the `onAction` prop to handle item actions. + +```tsx render wide +"use client"; +import {TagGroup, Tag} from '@react-spectrum/s2/TagGroup'; + +function Example() { + return ( + alert(`Clicked ${key}`)} + ///- end highlight -/// + > + Rock + Jazz + Pop + Classical + EDM + + ); +} +``` + ## Group actions diff --git a/packages/dev/s2-docs/pages/s2/migrating.mdx b/packages/dev/s2-docs/pages/s2/migrating.mdx index 60b56f423b7..aead11d60a0 100644 --- a/packages/dev/s2-docs/pages/s2/migrating.mdx +++ b/packages/dev/s2-docs/pages/s2/migrating.mdx @@ -368,10 +368,7 @@ No updates needed. - For dynamic tables, pass a `columns` prop into `Row` - For `Row`: Update dynamic render function to pass in `column` instead of `columnKey` - Move `loadingState` and `onLoadMore` from `TableBody` to `TableView` -- Comment out `UNSTABLE_allowsExpandableRows` (it has not been implemented yet) -- Comment out `UNSTABLE_onExpandedChange` (it has not been implemented yet) -- Comment out `UNSTABLE_expandedKeys` (it has not been implemented yet) -- Comment out `UNSTABLE_defaultExpandedKeys` (it has not been implemented yet) +- For expandable rows, use `treeColumn` to designate your expandable column. `defaultExpanded`, `expandedKeys`, and `onExpandedChange` can be used to set and update your expanded rows. See the [TableView](TableView#expandable-rows) documentation for more details. ### Tabs diff --git a/packages/react-aria-components/src/TagGroup.tsx b/packages/react-aria-components/src/TagGroup.tsx index d0fc46fcc55..491da128447 100644 --- a/packages/react-aria-components/src/TagGroup.tsx +++ b/packages/react-aria-components/src/TagGroup.tsx @@ -244,7 +244,12 @@ export interface TagProps extends RenderProps, LinkDOMPro */ textValue?: string, /** Whether the tag is disabled. */ - isDisabled?: boolean + isDisabled?: boolean, + /** + * Handler that is called when a user performs an action on the item. The exact user event depends on + * the collection's `selectionBehavior` prop and the interaction modality. + */ + onAction?: () => void } /** diff --git a/packages/react-aria-components/test/TagGroup.test.js b/packages/react-aria-components/test/TagGroup.test.js index 6ec421718ec..9964bd1a3c3 100644 --- a/packages/react-aria-components/test/TagGroup.test.js +++ b/packages/react-aria-components/test/TagGroup.test.js @@ -661,6 +661,19 @@ describe('TagGroup', () => { expect(onAction).toHaveBeenCalledTimes(1); }); + it('should support onAction on the items', async () => { + let onAction = jest.fn(); + let {getAllByRole} = renderTagGroup({selectionMode: 'none'}, {}, {onAction}); + let items = getAllByRole('row'); + + await user.click(items[0]); + expect(onAction).toHaveBeenCalledTimes(1); + onAction.mockReset(); + + await user.keyboard('{Enter}'); + expect(onAction).toHaveBeenCalledTimes(1); + }); + it('should support onAction with selectionMode = single, behaviour = replace', async () => { let onAction = jest.fn(); let {getAllByRole} = renderTagGroup({onAction, selectionMode: 'single', selectionBehavior: 'replace'}); diff --git a/packages/react-aria/src/calendar/useRangeCalendar.ts b/packages/react-aria/src/calendar/useRangeCalendar.ts index 609ab55ea9d..04be782ab93 100644 --- a/packages/react-aria/src/calendar/useRangeCalendar.ts +++ b/packages/react-aria/src/calendar/useRangeCalendar.ts @@ -20,7 +20,7 @@ import {useRef} from 'react'; export interface AriaRangeCalendarProps extends RangeCalendarProps, DOMProps, AriaLabelingProps { /** - * Controls the behavior when a pointer is released outside the calendar: + * Controls the behavior when a pointer is released outside the calendar or a blur occurs mid selection: * * - `clear`: clear the currently selected range of dates. * @@ -29,7 +29,7 @@ export interface AriaRangeCalendarProps extends RangeCalend * - `select`: select the currently hovered range of dates. * @default 'select' */ - interactOutsideBehavior?: 'clear' | 'reset' | 'select' + commitBehavior?: 'clear' | 'reset' | 'select' } /** @@ -37,7 +37,7 @@ export interface AriaRangeCalendarProps extends RangeCalend * A range calendar displays one or more date grids and allows users to select a contiguous range of dates. */ export function useRangeCalendar(props: AriaRangeCalendarProps, state: RangeCalendarState, ref: RefObject): CalendarAria { - let {interactOutsideBehavior = 'select', ...otherProps} = props; + let {commitBehavior = 'select', ...otherProps} = props; let res = useCalendarBase(otherProps, state); // We need to ignore virtual pointer events from VoiceOver due to these bugs. @@ -52,13 +52,13 @@ export function useRangeCalendar(props: AriaRangeCalendarPr isVirtualClick.current = e.width === 0 && e.height === 0; }); - const interactOutsideBehaviorMapping = { + const commitBehaviorMapping = { clear: () => state.clearSelection(), reset: () => state.setAnchorDate(null), select: () => state.selectFocusedDate() }; - // Execute method corresponding to `interactOutsideBehavior` when pressing or releasing a pointer outside the calendar body, + // Execute method corresponding to `commitBehavior` when pressing or releasing a pointer outside the calendar body, // except when pressing the next or previous buttons to switch months. let endDragging = (e: PointerEvent) => { if (isVirtualClick.current) { @@ -77,13 +77,13 @@ export function useRangeCalendar(props: AriaRangeCalendarPr isFocusWithin(ref.current) && (!nodeContains(ref.current, target) || !target.closest('button, [role="button"]')) ) { - interactOutsideBehaviorMapping[interactOutsideBehavior](); + commitBehaviorMapping[commitBehavior](); } }; useEvent(windowRef, 'pointerup', endDragging); - // Also execute method corresponding to `interactOutsideBehavior` on blur, + // Also execute method corresponding to `commitBehavior` on blur, // e.g. tabbing away from the calendar. res.calendarProps.onBlur = e => { if (!ref.current) { @@ -91,7 +91,7 @@ export function useRangeCalendar(props: AriaRangeCalendarPr } if ((!e.relatedTarget || !nodeContains(ref.current, e.relatedTarget)) && state.anchorDate) { - interactOutsideBehaviorMapping[interactOutsideBehavior](); + commitBehaviorMapping[commitBehavior](); } }; diff --git a/packages/react-aria/src/dialog/useDialog.ts b/packages/react-aria/src/dialog/useDialog.ts index 8d6f503252f..8a2015231fa 100644 --- a/packages/react-aria/src/dialog/useDialog.ts +++ b/packages/react-aria/src/dialog/useDialog.ts @@ -90,7 +90,7 @@ export function useDialog(props: AriaDialogProps, ref: RefObject