diff --git a/change/@fluentui-react-tags-354a9ce9-45f4-4828-b874-203a374f06ef.json b/change/@fluentui-react-tags-354a9ce9-45f4-4828-b874-203a374f06ef.json new file mode 100644 index 00000000000000..47dfad2b30c9fc --- /dev/null +++ b/change/@fluentui-react-tags-354a9ce9-45f4-4828-b874-203a374f06ef.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "fix: decouple useTagGroupBase_unstable from Tabster; export contexts. This is technically a breaking change, but the prior Tabster coupling was a programmatic mistake and not intended public behavior. If you run into issues, please bump to the next version.", + "packageName": "@fluentui/react-tags", + "email": "vgenaev@gmail.com", + "dependentChangeType": "patch" +} diff --git a/packages/react-components/react-tags/library/etc/react-tags.api.md b/packages/react-components/react-tags/library/etc/react-tags.api.md index c32730f67ad424..f81ed54f5a5e60 100644 --- a/packages/react-components/react-tags/library/etc/react-tags.api.md +++ b/packages/react-components/react-tags/library/etc/react-tags.api.md @@ -16,7 +16,6 @@ import type { JSXElement } from '@fluentui/react-utilities'; import * as React_2 from 'react'; import type { Slot } from '@fluentui/react-utilities'; import type { SlotClassNames } from '@fluentui/react-utilities'; -import type { TabsterDOMAttribute } from '@fluentui/react-tabster'; // @public export const InteractionTag: ForwardRefComponent; @@ -290,7 +289,7 @@ export const useTagBase_unstable: (props: TagBaseProps, ref: React_2.Ref) => TagGroupState; // @public -export const useTagGroupBase_unstable: (props: TagGroupBaseProps, ref: React_2.Ref, options?: UseTagGroupBaseOptions) => TagGroupBaseState; +export const useTagGroupBase_unstable: (props: TagGroupBaseProps, ref: React_2.Ref) => TagGroupBaseState; // @public (undocumented) export const useTagGroupContext_unstable: () => TagGroupContextValue; diff --git a/packages/react-components/react-tags/library/src/components/TagGroup/useTagGroup.test.tsx b/packages/react-components/react-tags/library/src/components/TagGroup/useTagGroup.test.tsx index 61ccc24fa7f888..423b9c457012ef 100644 --- a/packages/react-components/react-tags/library/src/components/TagGroup/useTagGroup.test.tsx +++ b/packages/react-components/react-tags/library/src/components/TagGroup/useTagGroup.test.tsx @@ -3,6 +3,7 @@ import * as React from 'react'; import type { TabsterDOMAttribute } from '@fluentui/react-tabster'; import { useTagGroup_unstable, useTagGroupBase_unstable } from './useTagGroup'; +import type { TagGroupBaseProps } from './TagGroup.types'; describe('useTagGroup_unstable', () => { it('should default size to medium and appearance to filled', () => { @@ -13,7 +14,7 @@ describe('useTagGroup_unstable', () => { expect(result.current.appearance).toBe('filled'); }); - it('should spread Tabster arrow-navigation attributes onto root (via UseTagGroupBaseOptions)', () => { + it('should spread Tabster arrow-navigation attributes onto root', () => { const ref = React.createRef(); const { result } = renderHook(() => useTagGroup_unstable({}, ref)); @@ -29,31 +30,31 @@ describe('useTagGroup_unstable', () => { }); describe('useTagGroupBase_unstable', () => { - it('should NOT include arrow-navigation props when options omitted (true headless mode)', () => { + it('should NOT include arrow-navigation props by default (true headless mode)', () => { const ref = React.createRef(); const { result } = renderHook(() => useTagGroupBase_unstable({}, ref)); expect(result.current.root).not.toHaveProperty('data-tabster'); }); - it('should spread arrowNavigationProps option onto root when supplied', () => { + it('should spread arrow-navigation attributes onto root when passed via props', () => { const ref = React.createRef(); const arrowNavigationProps: TabsterDOMAttribute = { 'data-tabster': '{"mock":"value"}' }; - const { result } = renderHook(() => useTagGroupBase_unstable({}, ref, { arrowNavigationProps })); + const { result } = renderHook(() => + useTagGroupBase_unstable({ ...arrowNavigationProps } as TagGroupBaseProps, ref), + ); expect(result.current.root).toHaveProperty('data-tabster', '{"mock":"value"}'); }); - it('should call onAfterTagDismiss with the group container after a tag is dismissed', () => { - const onAfterTagDismiss = jest.fn(); + it('should call onDismiss when a tag is dismissed', () => { const onDismiss = jest.fn(); const ref = React.createRef(); - const { result } = renderHook(() => useTagGroupBase_unstable({ onDismiss }, ref, { onAfterTagDismiss })); + const { result } = renderHook(() => useTagGroupBase_unstable({ onDismiss }, ref)); const event = {} as React.MouseEvent; result.current.handleTagDismiss(event, { value: 'v1' }); expect(onDismiss).toHaveBeenCalledWith(event, { value: 'v1' }); - expect(onAfterTagDismiss).toHaveBeenCalledWith(null); }); it('should set aria-disabled on the root when disabled', () => { diff --git a/packages/react-components/react-tags/library/src/components/TagGroup/useTagGroup.ts b/packages/react-components/react-tags/library/src/components/TagGroup/useTagGroup.ts index e6b0b19aca97f1..a7f6dbcba922b5 100644 --- a/packages/react-components/react-tags/library/src/components/TagGroup/useTagGroup.ts +++ b/packages/react-components/react-tags/library/src/components/TagGroup/useTagGroup.ts @@ -14,12 +14,6 @@ import { useArrowNavigationGroup, useFocusFinders } from '@fluentui/react-tabste import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts'; import { interactionTagSecondaryClassNames } from '../InteractionTagSecondary/useInteractionTagSecondaryStyles.styles'; import type { TagValue } from '../../utils/types'; -import type { TabsterDOMAttribute } from '@fluentui/react-tabster'; - -type UseTagGroupBaseOptions = { - arrowNavigationProps?: TabsterDOMAttribute; - onAfterTagDismiss?: (container: HTMLElement | null) => void; -}; /** * Create the base state required to render TagGroup, without design-only props. @@ -30,7 +24,6 @@ type UseTagGroupBaseOptions = { export const useTagGroupBase_unstable = ( props: TagGroupBaseProps, ref: React.Ref, - options?: UseTagGroupBaseOptions, ): TagGroupBaseState => { const { onDismiss, @@ -43,8 +36,6 @@ export const useTagGroupBase_unstable = ( ...rest } = props; - const innerRef = React.useRef(undefined); - const [items, setItems] = useControllableState>({ defaultState: defaultSelectedValues, state: selectedValues, @@ -53,7 +44,6 @@ export const useTagGroupBase_unstable = ( const handleTagDismiss: TagGroupBaseState['handleTagDismiss'] = useEventCallback((e, data) => { onDismiss?.(e, data); - options?.onAfterTagDismiss?.(innerRef.current ?? null); }); const handleTagSelect: TagGroupBaseState['handleTagSelect'] = useEventCallback( @@ -79,13 +69,9 @@ export const useTagGroupBase_unstable = ( root: slot.always( getIntrinsicElementProps('div', { - // FIXME: - // `ref` is wrongly assigned to be `HTMLElement` instead of `HTMLDivElement` - // but since it would be a breaking change to fix it, we are casting ref to it's proper type - ref: useMergedRefs(ref, innerRef) as React.Ref, + ref, role, 'aria-disabled': disabled, - ...options?.arrowNavigationProps, ...rest, }), { elementType: 'div' }, @@ -114,8 +100,15 @@ export const useTagGroup_unstable = (props: TagGroupProps, ref: React.Ref { + const innerRef = React.useRef(null); + const mergedRef = useMergedRefs(ref, innerRef); + + const enhancedOnDismiss: TagGroupProps['onDismiss'] = useEventCallback((e, data) => { + props.onDismiss?.(e, data); + + const container = innerRef.current; const activeElement = targetDocument?.activeElement; + if (container?.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 }); @@ -136,7 +129,7 @@ export const useTagGroup_unstable = (props: TagGroupProps, ref: React.Ref