Skip to content

Commit 2d45210

Browse files
minzzang144snowystingeryihuiliao
authored
fix: prevent disabled Tag from being removed with keyboard nav (#7394)
* fix: useTag hook * chore: revert useTag hook * fix: useTag hook(2) * refactor: useTag * test: useTag hook * rename variable and test behaviour --------- Co-authored-by: GitHub <[email protected]> Co-authored-by: Yihui Liao <[email protected]>
1 parent f14ca9b commit 2d45210

File tree

2 files changed

+45
-4
lines changed

2 files changed

+45
-4
lines changed

packages/@react-aria/tag/src/useTag.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,13 @@ export function useTag<T>(props: AriaTagProps<T>, state: ListState<T>, ref: RefO
6161
// eslint-disable-next-line @typescript-eslint/no-unused-vars
6262
let {descriptionProps: _, ...stateWithoutDescription} = states;
6363

64+
let isDisabled = state.disabledKeys.has(item.key) || item.props.isDisabled;
6465
let onKeyDown = (e: KeyboardEvent) => {
6566
if (e.key === 'Delete' || e.key === 'Backspace') {
67+
if (isDisabled) {
68+
return;
69+
}
70+
6671
e.preventDefault();
6772
if (state.selectionManager.isSelected(item.key)) {
6873
onRemove?.(new Set(state.selectionManager.selectedKeys));
@@ -79,20 +84,26 @@ export function useTag<T>(props: AriaTagProps<T>, state: ListState<T>, ref: RefO
7984
let description = onRemove && (modality === 'keyboard' || modality === 'virtual') ? stringFormatter.format('removeDescription') : '';
8085
let descProps = useDescription(description);
8186

82-
let isFocused = item.key === state.selectionManager.focusedKey;
87+
let isItemFocused = item.key === state.selectionManager.focusedKey;
88+
let isFocused = state.selectionManager.focusedKey != null;
89+
let tabIndex = -1;
90+
if (!isDisabled && (isItemFocused || !isFocused)) {
91+
tabIndex = 0;
92+
}
93+
8394
let domProps = filterDOMProps(item.props);
8495
let linkProps = useSyntheticLinkProps(item.props);
8596
return {
8697
removeButtonProps: {
8798
'aria-label': stringFormatter.format('removeButtonLabel'),
8899
'aria-labelledby': `${buttonId} ${rowProps.id}`,
89-
isDisabled: state.disabledKeys.has(item.key) || item.props.isDisabled,
100+
isDisabled,
90101
id: buttonId,
91102
onPress: () => onRemove ? onRemove(new Set([item.key])) : null,
92103
excludeFromTabOrder: true
93104
},
94105
rowProps: mergeProps(rowProps, domProps, linkProps, {
95-
tabIndex: (isFocused || state.selectionManager.focusedKey == null) ? 0 : -1,
106+
tabIndex,
96107
onKeyDown: onRemove ? onKeyDown : undefined,
97108
'aria-describedby': descProps['aria-describedby']
98109
}),

packages/react-aria-components/test/TagGroup.test.js

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
* governing permissions and limitations under the License.
1111
*/
1212

13+
import {act, fireEvent, mockClickDefault, pointerMap, render} from '@react-spectrum/test-utils-internal';
1314
import {Button, Label, RouterProvider, Tag, TagGroup, TagList, Text} from '../';
14-
import {fireEvent, mockClickDefault, pointerMap, render} from '@react-spectrum/test-utils-internal';
1515
import React from 'react';
1616
import {useListData} from '@react-stately/data';
1717
import userEvent from '@testing-library/user-event';
@@ -419,4 +419,34 @@ describe('TagGroup', () => {
419419
await user.keyboard('{Backspace}');
420420
expect(grid).toHaveFocus();
421421
});
422+
423+
it('disabled tags should not be deletable', async () => {
424+
let onRemove = jest.fn();
425+
let tree = render(
426+
<>
427+
<TagGroup data-testid="group" onRemove={onRemove} disabledKeys={['cat', 'dog', 'kangaroo']}>
428+
<Label>Test</Label>
429+
<TagList>
430+
<RemovableTag id="cat">Cat</RemovableTag>
431+
<RemovableTag id="dog">Dog</RemovableTag>
432+
<RemovableTag id="kangaroo">Kangaroo</RemovableTag>
433+
</TagList>
434+
<Text slot="description">Description</Text>
435+
<Text slot="errorMessage">Error</Text>
436+
</TagGroup>
437+
<Button>Click here first</Button>
438+
</>
439+
);
440+
let items = tree.getAllByRole('row');
441+
442+
act(() => items[2].focus());
443+
await user.keyboard('{Backspace}');
444+
445+
expect(onRemove).not.toHaveBeenCalled();
446+
447+
await user.click(tree.getAllByRole('button')[3]);
448+
await user.tab({shift: true});
449+
450+
expect(document.activeElement).toBe(tree.getByRole('grid'));
451+
});
422452
});

0 commit comments

Comments
 (0)