diff --git a/packages/@react-aria/tag/src/useTagGroup.ts b/packages/@react-aria/tag/src/useTagGroup.ts index 11b32c9508a..d8578594b7e 100644 --- a/packages/@react-aria/tag/src/useTagGroup.ts +++ b/packages/@react-aria/tag/src/useTagGroup.ts @@ -31,8 +31,11 @@ export interface TagGroupAria { errorMessageProps: DOMAttributes } -export interface AriaTagGroupProps extends CollectionBase, MultipleSelection, Pick, 'escapeKeyBehavior'>, DOMProps, LabelableProps, AriaLabelingProps, Omit { - /** How multiple selection should behave in the collection. */ +export interface AriaTagGroupProps extends CollectionBase, MultipleSelection, Pick, 'escapeKeyBehavior' | 'onAction'>, DOMProps, LabelableProps, AriaLabelingProps, Omit { + /** + * How multiple selection should behave in the collection. + * @default 'toggle' + */ selectionBehavior?: SelectionBehavior, /** Whether selection should occur on press up instead of press down. */ shouldSelectOnPressUp?: boolean, diff --git a/packages/@react-stately/selection/src/useMultipleSelectionState.ts b/packages/@react-stately/selection/src/useMultipleSelectionState.ts index e58d0823850..f7893f47764 100644 --- a/packages/@react-stately/selection/src/useMultipleSelectionState.ts +++ b/packages/@react-stately/selection/src/useMultipleSelectionState.ts @@ -31,11 +31,17 @@ function equalSets(setA, setB) { } export interface MultipleSelectionStateProps extends MultipleSelection { - /** How multiple selection should behave in the collection. */ + /** + * How multiple selection should behave in the collection. + * @default 'toggle' + */ selectionBehavior?: SelectionBehavior, /** Whether onSelectionChange should fire even if the new set of keys is the same as the last. */ allowDuplicateSelectionEvents?: boolean, - /** Whether `disabledKeys` applies to all interactions, or only selection. */ + /** + * Whether `disabledKeys` applies to all interactions, or only selection. + * @default 'all' + */ disabledBehavior?: DisabledBehavior } diff --git a/packages/react-aria-components/test/TagGroup.test.js b/packages/react-aria-components/test/TagGroup.test.js index adc4f07135b..94173ed3d73 100644 --- a/packages/react-aria-components/test/TagGroup.test.js +++ b/packages/react-aria-components/test/TagGroup.test.js @@ -521,6 +521,121 @@ describe('TagGroup', () => { expect(onRemove).toHaveBeenLastCalledWith(new Set(['dog'])); }); + it('should support onAction', async () => { + let onAction = jest.fn(); + let {getAllByRole} = renderTagGroup({onAction, selectionMode: 'none'}); + 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'}); + let items = getAllByRole('row'); + + await user.dblClick(items[0]); + expect(onAction).toHaveBeenCalledTimes(1); + onAction.mockReset(); + + await user.click(items[1]); + expect(onAction).not.toHaveBeenCalled(); + expect(items[1]).toHaveAttribute('aria-selected', 'true'); + + await user.dblClick(items[0]); + expect(onAction).toHaveBeenCalledTimes(1); + expect(items[0]).toHaveAttribute('aria-selected', 'false'); // should be true? + expect(items[1]).toHaveAttribute('aria-selected', 'false'); + onAction.mockReset(); + + await user.keyboard('{Enter}'); + expect(onAction).toHaveBeenCalledTimes(1); + expect(items[0]).toHaveAttribute('aria-selected', 'false'); // should be true? + expect(items[1]).toHaveAttribute('aria-selected', 'false'); + }); + + it('should support onAction with selectionMode = multiple, behaviour = replace', async () => { + let onAction = jest.fn(); + let {getAllByRole} = renderTagGroup({onAction, selectionMode: 'multiple', selectionBehavior: 'replace'}); + let items = getAllByRole('row'); + + await user.dblClick(items[0]); + expect(onAction).toHaveBeenCalledTimes(1); + onAction.mockReset(); + + await user.click(items[1]); + expect(onAction).not.toHaveBeenCalled(); + onAction.mockReset(); + expect(items[1]).toHaveAttribute('aria-selected', 'true'); + + await user.dblClick(items[0]); + expect(onAction).toHaveBeenCalledTimes(1); + expect(items[0]).toHaveAttribute('aria-selected', 'true'); + expect(items[1]).toHaveAttribute('aria-selected', 'false'); + onAction.mockReset(); + + await user.keyboard('{Enter}'); + expect(onAction).toHaveBeenCalledTimes(1); + expect(items[0]).toHaveAttribute('aria-selected', 'true'); + expect(items[1]).toHaveAttribute('aria-selected', 'false'); + }); + + // TODO: What do we want to do for this behaviour? Should we warn that it's not a valid combination? Or should it react to double click? + // Replace selectionBehavior works with double click, but toggle doesn't. + it.skip('should support onAction with selectionMode = single, behaviour = toggle', async () => { + let onAction = jest.fn(); + let {getAllByRole} = renderTagGroup({onAction, selectionMode: 'single'}); + let items = getAllByRole('row'); + + await user.dblClick(items[0]); + expect(onAction).toHaveBeenCalledTimes(1); + onAction.mockReset(); + + await user.click(items[1]); + expect(onAction).not.toHaveBeenCalled(); + expect(items[1]).toHaveAttribute('aria-selected', 'true'); + + await user.dblClick(items[0]); + expect(onAction).toHaveBeenCalledTimes(1); + expect(items[0]).not.toHaveAttribute('aria-selected'); + onAction.mockReset(); + + await user.keyboard('{Enter}'); + expect(onAction).not.toHaveBeenCalled(); + expect(items[0]).toHaveAttribute('aria-selected', 'true'); + expect(items[1]).not.toHaveAttribute('aria-selected'); + }); + + it.skip('should support onAction with selectionMode = multiple, behaviour = toggle', async () => { + let onAction = jest.fn(); + let {getAllByRole} = renderTagGroup({onAction, selectionMode: 'multiple'}); + let items = getAllByRole('row'); + + await user.dblClick(items[0]); + expect(onAction).toHaveBeenCalledTimes(1); + onAction.mockReset(); + + await user.click(items[1]); + expect(onAction).not.toHaveBeenCalled(); + onAction.mockReset(); + expect(items[1]).toHaveAttribute('aria-selected', 'true'); + + await user.dblClick(items[0]); + expect(onAction).toHaveBeenCalledTimes(1); + expect(items[0]).not.toHaveAttribute('aria-selected'); + onAction.mockReset(); + + await user.keyboard('{Enter}'); + expect(onAction).not.toHaveBeenCalled(); + expect(items[0]).toHaveAttribute('aria-selected', 'true'); + expect(items[1]).toHaveAttribute('aria-selected', 'true'); + }); + describe('shouldSelectOnPressUp', () => { it('should select an item on pressing down when shouldSelectOnPressUp is not provided', async () => { let onSelectionChange = jest.fn(); @@ -560,7 +675,7 @@ describe('TagGroup', () => { }); describe('press events', () => { - it.only.each` + it.each` interactionType ${'mouse'} ${'keyboard'}