diff --git a/packages/react-components/react-tags/package.json b/packages/react-components/react-tags/package.json index 07133dd18720ea..b917884a8a08fb 100644 --- a/packages/react-components/react-tags/package.json +++ b/packages/react-components/react-tags/package.json @@ -39,6 +39,7 @@ "@fluentui/react-avatar": "^9.5.5", "@fluentui/react-icons": "^2.0.203", "@fluentui/react-jsx-runtime": "9.0.0-alpha.6", + "@fluentui/react-shared-contexts": "^9.5.0", "@fluentui/react-tabster": "^9.7.5", "@fluentui/react-theme": "^9.1.8", "@fluentui/react-utilities": "^9.9.2", diff --git a/packages/react-components/react-tags/src/components/TagGroup/useTagGroup.ts b/packages/react-components/react-tags/src/components/TagGroup/useTagGroup.ts index 5da937f4212447..1cdf4937d92e36 100644 --- a/packages/react-components/react-tags/src/components/TagGroup/useTagGroup.ts +++ b/packages/react-components/react-tags/src/components/TagGroup/useTagGroup.ts @@ -1,6 +1,9 @@ import * as React from 'react'; -import { getNativeElementProps, useEventCallback } from '@fluentui/react-utilities'; +import { getNativeElementProps, useEventCallback, useMergedRefs } from '@fluentui/react-utilities'; import type { TagGroupProps, TagGroupState } from './TagGroup.types'; +import { useArrowNavigationGroup, useFocusFinders } from '@fluentui/react-tabster'; +import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts'; +import { tagButtonClassNames } from '../TagButton/useTagButtonStyles.styles'; /** * Create the state required to render TagGroup. @@ -14,10 +17,37 @@ import type { TagGroupProps, TagGroupState } from './TagGroup.types'; export const useTagGroup_unstable = (props: TagGroupProps, ref: React.Ref): TagGroupState => { const { onDismiss, size = 'medium' } = props; + const innerRef = React.useRef(); + const { targetDocument } = useFluent(); + const { findNextFocusable, findPrevFocusable } = useFocusFinders(); + const handleTagDismiss = useEventCallback((e: React.MouseEvent | React.KeyboardEvent, id: string) => { onDismiss?.(e, { dismissedTagValue: id }); - // TODO set focus after tag dismiss + // set focus after tag dismiss + const activeElement = targetDocument?.activeElement; + if (innerRef.current?.contains(activeElement as HTMLElement)) { + // focus on next tag only if the active element is within the current tag group + const next = findNextFocusable(activeElement as HTMLElement, { container: innerRef.current }); + if (next) { + next.focus(); + return; + } + + // if there is no next focusable, focus on the previous focusable + if (activeElement?.className.includes(tagButtonClassNames.dismissButton)) { + const prev = findPrevFocusable(activeElement.parentElement as HTMLElement, { container: innerRef.current }); + prev?.focus(); + } else { + const prev = findPrevFocusable(activeElement as HTMLElement, { container: innerRef.current }); + prev?.focus(); + } + } + }); + + const arrowNavigationProps = useArrowNavigationGroup({ + circular: true, + axis: 'both', }); return { @@ -30,9 +60,10 @@ export const useTagGroup_unstable = (props: TagGroupProps, ref: React.Ref { const defaultItems = [ - { value: '1', children: 'Tag 1' }, - { value: '2', children: 'Tag 2' }, - { value: 'tagButton-foo', children: 'Foo' }, - { value: 'tagButton-bar', children: 'Bar' }, + { value: '1', children: 'Tag 1', 'aria-label': 'Tag1, remove' }, + { value: '2', children: 'Tag 2', 'aria-label': 'Tag2, remove' }, + { value: 'tagButton-foo', children: 'Foo', dismissButton: { 'aria-label': 'Foo, remove' } }, + { value: 'tagButton-bar', children: 'Bar', dismissButton: { 'aria-label': 'Bar, remove' } }, ]; const [items, setItems] = React.useState>(defaultItems);