-
Notifications
You must be signed in to change notification settings - Fork 1.3k
TagGroup: Add useTagGroupState and fix focus issues #3798
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
e30722a
d7614e3
78e360e
09f297a
22f9f5f
1bec84c
5e7df52
022828f
50a723d
6cc253a
15a8b32
31ca965
a8202bf
fa66c91
3aad4db
d5449f7
f3f64d7
f746d65
55d704e
72ebeef
cc44d18
e0d4bdb
1df22b3
3faa12f
8223690
a2dcdc5
45fd98f
e4be115
2b72dbc
c757e9f
6463bbf
30a808d
4b4ec67
c61e143
34e0046
db61320
9c5cd8a
ee5d3aa
2eebbf8
734492e
57d796a
274d027
7137dca
d9fe732
507195a
7f36829
4365154
9ee9a63
bad3da5
d13c41c
47fc203
aec919e
288e8e3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,3 @@ | ||
| { | ||
| "remove": "Remove" | ||
| "remove": "Press Space or Delete to remove tag." | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,25 +10,26 @@ | |
| * governing permissions and limitations under the License. | ||
| */ | ||
|
|
||
| import {GridCollection} from '@react-types/grid'; | ||
| import {GridKeyboardDelegate} from '@react-aria/grid'; | ||
| import {Collection, Direction, KeyboardDelegate} from '@react-types/shared'; | ||
| import {Key} from 'react'; | ||
|
|
||
| export class TagKeyboardDelegate<T> extends GridKeyboardDelegate<T, GridCollection<T>> { | ||
| getFirstKey() { | ||
| let key = this.collection.getFirstKey(); | ||
| let item = this.collection.getItem(key); | ||
| export class TagKeyboardDelegate<T> implements KeyboardDelegate { | ||
| private collection: Collection<T>; | ||
| private direction: Direction; | ||
|
|
||
| return [...item.childNodes][0].key; | ||
| constructor(collection: Collection<T>, direction: Direction) { | ||
| this.collection = collection; | ||
| this.direction = direction; | ||
| } | ||
|
|
||
| getLastKey() { | ||
| let key = this.collection.getLastKey(); | ||
| let item = this.collection.getItem(key); | ||
|
|
||
| return [...item.childNodes][0].key; | ||
| getFirstKey() { | ||
| return this.collection.getFirstKey(); | ||
| } | ||
|
|
||
| getLastKey() { | ||
| return this.collection.getLastKey(); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is likely to behave oddly when in the collapsed mode |
||
| } | ||
|
|
||
| getKeyRightOf(key: Key) { | ||
| return this.direction === 'rtl' ? this.getKeyAbove(key) : this.getKeyBelow(key); | ||
| } | ||
|
|
@@ -43,27 +44,12 @@ export class TagKeyboardDelegate<T> extends GridKeyboardDelegate<T, GridCollecti | |
| return; | ||
| } | ||
|
|
||
| // If focus was on a cell, start searching from the parent row | ||
| if (this.isCell(startItem)) { | ||
| key = startItem.parentKey; | ||
| } | ||
|
|
||
| // Find the next item | ||
| key = this.findNextKey(key); | ||
| key = this.collection.getKeyAfter(key); | ||
| if (key != null) { | ||
| // If focus was on a cell, focus the cell with the same index in the next row. | ||
| if (this.isCell(startItem)) { | ||
| let item = this.collection.getItem(key); | ||
|
|
||
| return [...item.childNodes][startItem.index].key; | ||
| } | ||
|
|
||
| // Otherwise, focus the next row | ||
| if (this.focusMode === 'row') { | ||
| return key; | ||
| } | ||
| return key; | ||
| } else { | ||
| return this.getFirstKey(); | ||
| return this.collection.getFirstKey(); | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -73,26 +59,12 @@ export class TagKeyboardDelegate<T> extends GridKeyboardDelegate<T, GridCollecti | |
| return; | ||
| } | ||
|
|
||
| // If focus is on a cell, start searching from the parent row | ||
| if (this.isCell(startItem)) { | ||
| key = startItem.parentKey; | ||
| } | ||
|
|
||
| // Find the previous item | ||
| key = this.findPreviousKey(key); | ||
| key = this.collection.getKeyBefore(key); | ||
| if (key != null) { | ||
| // If focus was on a cell, focus the cell with the same index in the previous row. | ||
| if (this.isCell(startItem)) { | ||
| let item = this.collection.getItem(key); | ||
| return [...item.childNodes][startItem.index].key; | ||
| } | ||
|
|
||
| // Otherwise, focus the previous row | ||
| if (this.focusMode === 'row') { | ||
| return key; | ||
| } | ||
| return key; | ||
| } else { | ||
| return this.getLastKey(); | ||
| return this.collection.getLastKey(); | ||
| } | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,77 +10,78 @@ | |
| * governing permissions and limitations under the License. | ||
| */ | ||
|
|
||
| import {ButtonHTMLAttributes, KeyboardEvent} from 'react'; | ||
| import {AriaButtonProps} from '@react-types/button'; | ||
| import {chain, filterDOMProps, mergeProps, useId} from '@react-aria/utils'; | ||
| import {DOMAttributes} from '@react-types/shared'; | ||
| import {filterDOMProps, mergeProps, useId} from '@react-aria/utils'; | ||
| import {GridState} from '@react-stately/grid'; | ||
| // @ts-ignore | ||
| import intlMessages from '../intl/*.json'; | ||
| import {KeyboardEvent} from 'react'; | ||
| import type {TagGroupState} from '@react-stately/tag'; | ||
| import {TagProps} from '@react-types/tag'; | ||
| import {useGridCell, useGridRow} from '@react-aria/grid'; | ||
| import {useGridListItem} from '@react-aria/gridlist'; | ||
| import {useLocalizedStringFormatter} from '@react-aria/i18n'; | ||
|
|
||
|
|
||
| export interface TagAria { | ||
| labelProps: DOMAttributes, | ||
| tagProps: DOMAttributes, | ||
| tagRowProps: DOMAttributes, | ||
| clearButtonProps: ButtonHTMLAttributes<HTMLButtonElement> | ||
| clearButtonProps: AriaButtonProps | ||
| } | ||
|
|
||
| export function useTag(props: TagProps<any>, state: GridState<any, any>): TagAria { | ||
| let {isFocused} = props; | ||
| const { | ||
| /** | ||
| * Provides the behavior and accessibility implementation for a tag component. | ||
| * @param props - Props to be applied to the tag. | ||
| * @param state - State for the tag group, as returned by `useTagGroupState`. | ||
| */ | ||
| export function useTag<T>(props: TagProps<T>, state: TagGroupState<T>): TagAria { | ||
| let { | ||
| isFocused, | ||
| allowsRemoving, | ||
| onRemove, | ||
| item, | ||
| tagRef, | ||
| tagRowRef | ||
| } = props; | ||
| const stringFormatter = useLocalizedStringFormatter(intlMessages); | ||
| const removeString = stringFormatter.format('remove'); | ||
| const labelId = useId(); | ||
| const buttonId = useId(); | ||
| let stringFormatter = useLocalizedStringFormatter(intlMessages); | ||
| let removeString = stringFormatter.format('remove'); | ||
| let labelId = useId(); | ||
| let buttonId = useId(); | ||
|
|
||
| let {rowProps} = useGridRow({ | ||
| let {rowProps, gridCellProps} = useGridListItem({ | ||
| node: item | ||
| }, state, tagRowRef); | ||
| // Don't want the row to be focusable or accessible via keyboard | ||
| // eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
| let {tabIndex, ...otherRowProps} = rowProps; | ||
|
|
||
| let {gridCellProps} = useGridCell({ | ||
| node: [...item.childNodes][0], | ||
| focusMode: 'cell' | ||
| }, state, tagRef); | ||
| // We want the group to handle keyboard navigation between tags. | ||
| delete rowProps.onKeyDownCapture; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We could set it to undefined down there, but it would do the same thing. And I think we want |
||
|
|
||
| let onRemove = chain(props.onRemove, state.onRemove); | ||
|
|
||
| function onKeyDown(e: KeyboardEvent) { | ||
| let onKeyDown = (e: KeyboardEvent) => { | ||
| if (e.key === 'Delete' || e.key === 'Backspace' || e.key === ' ') { | ||
| onRemove(item.childNodes[0].key); | ||
| onRemove(item.key); | ||
| e.preventDefault(); | ||
| } | ||
| } | ||
| const pressProps = { | ||
| onPress: () => onRemove?.(item.childNodes[0].key) | ||
| }; | ||
|
|
||
| isFocused = isFocused || state.selectionManager.focusedKey === item.childNodes[0].key; | ||
| isFocused = isFocused || state.selectionManager.focusedKey === item.key; | ||
| let domProps = filterDOMProps(props); | ||
| return { | ||
| clearButtonProps: mergeProps(pressProps, { | ||
| clearButtonProps: { | ||
| 'aria-label': removeString, | ||
| 'aria-labelledby': `${buttonId} ${labelId}`, | ||
| id: buttonId | ||
| }), | ||
| id: buttonId, | ||
| onPress: () => allowsRemoving && onRemove ? onRemove(item.key) : null | ||
| }, | ||
| labelProps: { | ||
| id: labelId | ||
| }, | ||
| tagRowProps: otherRowProps, | ||
| tagRowProps: { | ||
| ...rowProps, | ||
| tabIndex: (isFocused || state.selectionManager.focusedKey == null) ? 0 : -1, | ||
| onKeyDown: allowsRemoving ? onKeyDown : null | ||
| }, | ||
| tagProps: mergeProps(domProps, gridCellProps, { | ||
| 'aria-errormessage': props['aria-errormessage'], | ||
| 'aria-label': props['aria-label'], | ||
| onKeyDown: allowsRemoving ? onKeyDown : null, | ||
| tabIndex: (isFocused || state.selectionManager.focusedKey == null) ? 0 : -1 | ||
| 'aria-label': props['aria-label'] | ||
| }) | ||
| }; | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.