From 2280cd555cd21ee5a42e983a49f58331011186ef Mon Sep 17 00:00:00 2001 From: Sarah Higley Date: Tue, 31 Mar 2026 12:49:22 -0700 Subject: [PATCH 1/3] POC focusgroup in breadcrumb, carousel, and menulist --- .../src/components/Breadcrumb/useBreadcrumb.ts | 13 +++---------- .../src/components/CarouselNav/useCarouselNav.ts | 11 +---------- .../CarouselNavButton/useCarouselNavButton.ts | 9 ++------- .../library/src/components/MenuList/useMenuList.ts | 10 ++-------- 4 files changed, 8 insertions(+), 35 deletions(-) diff --git a/packages/react-components/react-breadcrumb/library/src/components/Breadcrumb/useBreadcrumb.ts b/packages/react-components/react-breadcrumb/library/src/components/Breadcrumb/useBreadcrumb.ts index ecfce4fcfaf862..b965202d39c10e 100644 --- a/packages/react-components/react-breadcrumb/library/src/components/Breadcrumb/useBreadcrumb.ts +++ b/packages/react-components/react-breadcrumb/library/src/components/Breadcrumb/useBreadcrumb.ts @@ -3,7 +3,6 @@ import * as React from 'react'; import { getIntrinsicElementProps, slot } from '@fluentui/react-utilities'; import type { BreadcrumbBaseProps, BreadcrumbBaseState, BreadcrumbProps, BreadcrumbState } from './Breadcrumb.types'; -import { TabsterDOMAttribute, useArrowNavigationGroup } from '@fluentui/react-tabster'; /** * Create the state required to render Breadcrumb. @@ -62,19 +61,13 @@ export const useBreadcrumbBase_unstable = ( /** * Hook to get accessibility attributes for Breadcrumb component, such as roving tab index. - * Based on Tabster's useArrowNavigationGroup. + * Based on the HTML focusgroup attribute. * * @param focusMode - whether the Breadcrumb uses arrow key navigation or tab key navigation * @returns Tabster DOM attributes */ export const useBreadcrumbA11yBehavior_unstable = ({ focusMode, -}: Pick): Partial => { - const focusAttributes = useArrowNavigationGroup({ - circular: true, - axis: 'horizontal', - memorizeCurrent: true, - }); - - return focusMode === 'arrow' ? focusAttributes : {}; +}: Pick): { focusgroup?: string } => { + return focusMode === 'arrow' ? { focusgroup: 'toolbar inline wrap' } : {}; }; diff --git a/packages/react-components/react-carousel/library/src/components/CarouselNav/useCarouselNav.ts b/packages/react-components/react-carousel/library/src/components/CarouselNav/useCarouselNav.ts index 32a6c64f4bc78a..fa150dfe75550f 100644 --- a/packages/react-components/react-carousel/library/src/components/CarouselNav/useCarouselNav.ts +++ b/packages/react-components/react-carousel/library/src/components/CarouselNav/useCarouselNav.ts @@ -1,6 +1,5 @@ 'use client'; -import { useArrowNavigationGroup } from '@fluentui/react-tabster'; import { getIntrinsicElementProps, slot, useIsomorphicLayoutEffect } from '@fluentui/react-utilities'; import * as React from 'react'; @@ -20,14 +19,6 @@ import { useControllableState } from '@fluentui/react-utilities'; export const useCarouselNav_unstable = (props: CarouselNavProps, ref: React.Ref): CarouselNavState => { const { appearance } = props; - const focusableGroupAttr = useArrowNavigationGroup({ - circular: false, - axis: 'horizontal', - memorizeCurrent: false, - // eslint-disable-next-line @typescript-eslint/naming-convention - unstable_hasDefault: true, - }); - // Users can choose controlled or uncontrolled, if uncontrolled, the default is initialized by carousel context const [totalSlides, setTotalSlides] = useControllableState({ state: props.totalSlides, @@ -54,7 +45,7 @@ export const useCarouselNav_unstable = (props: CarouselNavProps, ref: React.Ref< ref, role: 'tablist', ...props, - ...focusableGroupAttr, + focusgroup: 'tablist nowrap nomemory', children: null, }), { elementType: 'div' }, diff --git a/packages/react-components/react-carousel/library/src/components/CarouselNavButton/useCarouselNavButton.ts b/packages/react-components/react-carousel/library/src/components/CarouselNavButton/useCarouselNavButton.ts index 24e2d4d8e8be05..01ddc25752d7b1 100644 --- a/packages/react-components/react-carousel/library/src/components/CarouselNavButton/useCarouselNavButton.ts +++ b/packages/react-components/react-carousel/library/src/components/CarouselNavButton/useCarouselNavButton.ts @@ -1,7 +1,6 @@ 'use client'; import { type ARIAButtonElement, type ARIAButtonSlotProps, useARIAButtonProps } from '@fluentui/react-aria'; -import { useTabsterAttributes } from '@fluentui/react-tabster'; import { getIntrinsicElementProps, isHTMLElement, @@ -51,12 +50,8 @@ export const useCarouselNavButton_unstable = ( resetAutoplay(); }); - const defaultTabProps = useTabsterAttributes({ - focusable: { isDefault: selected }, - }); - const buttonRef = React.useRef(undefined); - const _carouselButton = slot.always( + const _carouselButton = slot.always( getIntrinsicElementProps(as, useARIAButtonProps(props.as, props)), { elementType: 'button', @@ -65,7 +60,7 @@ export const useCarouselNavButton_unstable = ( role: 'tab', type: 'button', 'aria-selected': selected, - ...defaultTabProps, + focusGroupStart: true, }, }, ); diff --git a/packages/react-components/react-menu/library/src/components/MenuList/useMenuList.ts b/packages/react-components/react-menu/library/src/components/MenuList/useMenuList.ts index 059387d90ff042..14b430307673c0 100644 --- a/packages/react-components/react-menu/library/src/components/MenuList/useMenuList.ts +++ b/packages/react-components/react-menu/library/src/components/MenuList/useMenuList.ts @@ -8,12 +8,7 @@ import { getIntrinsicElementProps, slot, } from '@fluentui/react-utilities'; -import { - useArrowNavigationGroup, - useFocusFinders, - TabsterMoveFocusEventName, - type TabsterMoveFocusEvent, -} from '@fluentui/react-tabster'; +import { useFocusFinders, TabsterMoveFocusEventName, type TabsterMoveFocusEvent } from '@fluentui/react-tabster'; import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts'; import { useHasParentContext } from '@fluentui/react-context-selector'; import { useMenuContext_unstable } from '../../contexts/menuContext'; @@ -29,7 +24,6 @@ export const useMenuList_unstable = (props: MenuListProps, ref: React.Ref, role: 'menu', 'aria-labelledby': menuContext.triggerId, - ...focusAttributes, + focusgroup: 'menu nomemory wrap', ...props, }), { elementType: 'div' }, From 03e2a6ec4f26468723c7fe64e118f2f09a1c099d Mon Sep 17 00:00:00 2001 From: Sarah Higley Date: Wed, 1 Apr 2026 16:51:01 -0700 Subject: [PATCH 2/3] feat: migrate tabster's useArrowNavigationGroup to focusgroup --- .../src/components/Accordion/useAccordion.ts | 6 +----- .../useCarouselNavImageButton.ts | 7 +------ .../components/CarouselSlider/useCarouselSlider.ts | 4 ++++ .../library/src/components/List/useList.ts | 8 +++----- .../library/src/components/ListItem/useListItem.tsx | 7 +------ .../library/src/components/List/List/useList.ts | 6 ++++-- .../components/NavDrawerBody/useNavDrawerBody.ts | 1 + .../src/components/SwatchPicker/useSwatchPicker.ts | 5 +++-- .../react-tabs/library/src/components/Tab/useTab.ts | 11 ++++++----- .../library/src/components/TabList/useTabList.ts | 13 +++---------- .../components/TagPickerGroup/useTagPickerGroup.ts | 9 +-------- .../library/src/components/TagGroup/useTagGroup.ts | 12 +++--------- .../useTeachingPopoverCarouselNav.tsx | 12 +----------- .../useTeachingPopoverCarouselNavButton.tsx | 7 +------ .../library/src/components/Toolbar/useToolbar.ts | 8 ++------ .../components/TreeItemLayout/useTreeItemLayout.tsx | 5 ++--- .../stories/src/Tree/TreeActions.stories.tsx | 2 +- .../stories/src/Tree/TreeBestPractices.md | 2 +- 18 files changed, 39 insertions(+), 86 deletions(-) diff --git a/packages/react-components/react-accordion/library/src/components/Accordion/useAccordion.ts b/packages/react-components/react-accordion/library/src/components/Accordion/useAccordion.ts index ce6ebf7e6b0bc8..c66748f2813783 100644 --- a/packages/react-components/react-accordion/library/src/components/Accordion/useAccordion.ts +++ b/packages/react-components/react-accordion/library/src/components/Accordion/useAccordion.ts @@ -4,7 +4,6 @@ import * as React from 'react'; import { useControllableState, useEventCallback, slot } from '@fluentui/react-utilities'; import type { AccordionBaseProps, AccordionBaseState, AccordionProps, AccordionState } from './Accordion.types'; import type { AccordionItemValue } from '../AccordionItem/AccordionItem.types'; -import { useArrowNavigationGroup } from '@fluentui/react-tabster'; import { AccordionRequestToggleData } from '../../contexts/accordion'; /** @@ -24,10 +23,7 @@ export const useAccordion_unstable = ( const state = useAccordionBase_unstable(baseProps, ref); /** FIXME: deprecated will be removed after navigation prop is removed */ - const arrowNavigationProps = useArrowNavigationGroup({ - circular: navigation === 'circular', - tabbable: true, - }); + const arrowNavigationProps = { focusgroup: navigation === 'circular' ? 'block wrap' : 'block nowrap' }; return { navigation, diff --git a/packages/react-components/react-carousel/library/src/components/CarouselNavImageButton/useCarouselNavImageButton.ts b/packages/react-components/react-carousel/library/src/components/CarouselNavImageButton/useCarouselNavImageButton.ts index f999ea2c6d6d39..2e439f80746092 100644 --- a/packages/react-components/react-carousel/library/src/components/CarouselNavImageButton/useCarouselNavImageButton.ts +++ b/packages/react-components/react-carousel/library/src/components/CarouselNavImageButton/useCarouselNavImageButton.ts @@ -9,7 +9,6 @@ import { useIsomorphicLayoutEffect, useMergedRefs, } from '@fluentui/react-utilities'; -import { useTabsterAttributes } from '@fluentui/react-tabster'; import * as React from 'react'; import { useCarouselNavIndexContext } from '../CarouselNav/CarouselNavIndexContext'; @@ -44,10 +43,6 @@ export const useCarouselNavImageButton_unstable = ( } }); - const defaultTabProps = useTabsterAttributes({ - focusable: { isDefault: selected }, - }); - const buttonRef = React.useRef(undefined); const _carouselButton = slot.always( getIntrinsicElementProps(as, useARIAButtonProps(props.as, props)), @@ -58,7 +53,7 @@ export const useCarouselNavImageButton_unstable = ( role: 'tab', type: 'button', 'aria-selected': selected, - ...defaultTabProps, + focusGroupStart: selected, }, }, ); diff --git a/packages/react-components/react-carousel/library/src/components/CarouselSlider/useCarouselSlider.ts b/packages/react-components/react-carousel/library/src/components/CarouselSlider/useCarouselSlider.ts index c5fcb003ff0a60..e0fe0899a7a126 100644 --- a/packages/react-components/react-carousel/library/src/components/CarouselSlider/useCarouselSlider.ts +++ b/packages/react-components/react-carousel/library/src/components/CarouselSlider/useCarouselSlider.ts @@ -22,6 +22,10 @@ export const useCarouselSlider_unstable = ( ): CarouselSliderState => { const { cardFocus = false } = props; const circular = useCarouselContext(ctx => ctx.circular); + + // this can't yet be a focusgroup because we don't have a way + // to set focusgroup=none on descendants of the CarouselCard + // without adding an extra wrapping element const focusableGroupAttr = useArrowNavigationGroup({ circular, axis: 'horizontal', diff --git a/packages/react-components/react-list/library/src/components/List/useList.ts b/packages/react-components/react-list/library/src/components/List/useList.ts index 11d483513545c3..a5e0e673554fa4 100644 --- a/packages/react-components/react-list/library/src/components/List/useList.ts +++ b/packages/react-components/react-list/library/src/components/List/useList.ts @@ -8,7 +8,7 @@ import { useControllableState, useEventCallback, } from '@fluentui/react-utilities'; -import { useArrowNavigationGroup, useFocusFinders } from '@fluentui/react-tabster'; +import { useFocusFinders } from '@fluentui/react-tabster'; import { ListProps, ListState } from './List.types'; import { useListSelection } from '../../hooks/useListSelection'; import { @@ -38,10 +38,8 @@ export const useList_unstable = ( const as = props.as || navigationMode === 'composite' ? 'div' : DEFAULT_ROOT_EL_TYPE; - const arrowNavigationAttributes = useArrowNavigationGroup({ - axis: 'vertical', - memorizeCurrent: true, - }); + // using "toolbar" here because we explicitly set roles based on other logic + const arrowNavigationAttributes = { focusgroup: 'toolbar block nowrap' }; const [selectionState, setSelectionState] = useControllableState({ state: selectedItems, diff --git a/packages/react-components/react-list/library/src/components/ListItem/useListItem.tsx b/packages/react-components/react-list/library/src/components/ListItem/useListItem.tsx index 86bb27e1e5e968..2362b8d5f082e1 100644 --- a/packages/react-components/react-list/library/src/components/ListItem/useListItem.tsx +++ b/packages/react-components/react-list/library/src/components/ListItem/useListItem.tsx @@ -6,7 +6,6 @@ import { MoverMoveFocusEvent, GroupperMoveFocusActions, MoverKeys, - useArrowNavigationGroup, useFocusableGroup, useMergedTabsterAttributes_unstable, type TabsterDOMAttribute, @@ -178,12 +177,7 @@ export const useListItem_unstable = ( toggleItem?.(e, value); }); - const arrowNavigationAttributes = useArrowNavigationGroup({ - axis: 'horizontal', - }); - const tabsterAttributes = useMergedTabsterAttributes_unstable( - focusableItems ? arrowNavigationAttributes : {}, focusableGroupAttrs, props as Partial, ); @@ -198,6 +192,7 @@ export const useListItem_unstable = ( 'aria-selected': isSelected, 'aria-disabled': (disabledSelection && !onAction) || undefined, }), + ...(focusableItems && { focusgroup: 'toolbar inline nowrap' }), ...props, ...tabsterAttributes, onKeyDown: handleKeyDown, diff --git a/packages/react-components/react-migration-v0-v9/library/src/components/List/List/useList.ts b/packages/react-components/react-migration-v0-v9/library/src/components/List/List/useList.ts index fb04c92eff0116..77865d50629b19 100644 --- a/packages/react-components/react-migration-v0-v9/library/src/components/List/List/useList.ts +++ b/packages/react-components/react-migration-v0-v9/library/src/components/List/List/useList.ts @@ -37,10 +37,12 @@ export const useList_unstable = (props: ListProps, ref: React.Ref, ): NavDrawerBodyState => { const { tabbable } = useNavContext_unstable(); + // cannot use focusgroup here because of the tabbable behavior const focusAttributes = useArrowNavigationGroup({ axis: 'vertical', circular: true, diff --git a/packages/react-components/react-swatch-picker/library/src/components/SwatchPicker/useSwatchPicker.ts b/packages/react-components/react-swatch-picker/library/src/components/SwatchPicker/useSwatchPicker.ts index 413fc0a6d509f4..f891489e4a9931 100644 --- a/packages/react-components/react-swatch-picker/library/src/components/SwatchPicker/useSwatchPicker.ts +++ b/packages/react-components/react-swatch-picker/library/src/components/SwatchPicker/useSwatchPicker.ts @@ -36,11 +36,12 @@ export const useSwatchPicker_unstable = ( } = props; const isGrid = layout === 'grid'; - const focusAttributes = useArrowNavigationGroup({ + const gridFocusAttributes = useArrowNavigationGroup({ circular: true, - axis: isGrid ? 'grid-linear' : 'both', + axis: 'grid-linear', memorizeCurrent: true, }); + const focusAttributes = isGrid ? gridFocusAttributes : { focusgroup: 'radiogroup inline block wrap' }; const role = isGrid ? 'grid' : 'radiogroup'; diff --git a/packages/react-components/react-tabs/library/src/components/Tab/useTab.ts b/packages/react-components/react-tabs/library/src/components/Tab/useTab.ts index 19c80fb9db51f0..5fcb654eae6a5a 100644 --- a/packages/react-components/react-tabs/library/src/components/Tab/useTab.ts +++ b/packages/react-components/react-tabs/library/src/components/Tab/useTab.ts @@ -1,7 +1,6 @@ 'use client'; import * as React from 'react'; -import { type TabsterDOMAttribute, useTabsterAttributes } from '@fluentui/react-tabster'; import { mergeCallbacks, useEventCallback, useMergedRefs, slot, omit } from '@fluentui/react-utilities'; import type { TabProps, TabState, TabBaseProps, TabBaseState } from './Tab.types'; import { useTabListContext_unstable } from '../TabList'; @@ -122,8 +121,10 @@ export const useTabBase_unstable = (props: TabBaseProps, ref: React.Ref): TabsterDOMAttribute => { - return useTabsterAttributes({ - focusable: { isDefault: selected }, - }); +export const useTabA11yBehavior_unstable = ({ + selected, +}: Pick): { focusGroupStart: boolean } => { + return { + focusGroupStart: selected, + }; }; diff --git a/packages/react-components/react-tabs/library/src/components/TabList/useTabList.ts b/packages/react-components/react-tabs/library/src/components/TabList/useTabList.ts index 8187999c5e6950..8995a0322e7f80 100644 --- a/packages/react-components/react-tabs/library/src/components/TabList/useTabList.ts +++ b/packages/react-components/react-tabs/library/src/components/TabList/useTabList.ts @@ -1,7 +1,6 @@ 'use client'; import * as React from 'react'; -import { type TabsterDOMAttribute, useArrowNavigationGroup } from '@fluentui/react-tabster'; import { useControllableState, useEventCallback, useMergedRefs, slot } from '@fluentui/react-utilities'; import type { TabRegisterData, @@ -143,19 +142,13 @@ export const useTabListBase_unstable = (props: TabListBaseProps, ref: React.Ref< /** * Hook to get accessibility attributes for TabList component, such as roving tab index. - * Based on Tabster's useArrowNavigationGroup. + * Based on the HTML focusgroup attribute. * * @param vertical - whether the TabList is vertical * @returns Tabster DOM attributes */ export const useTabListA11yBehavior_unstable = ({ vertical, -}: Pick): TabsterDOMAttribute => { - return useArrowNavigationGroup({ - circular: true, - axis: vertical ? 'vertical' : 'horizontal', - memorizeCurrent: false, - // eslint-disable-next-line @typescript-eslint/naming-convention - unstable_hasDefault: true, - }); +}: Pick): { focusgroup: string } => { + return { focusgroup: vertical ? 'tablist block wrap nomemory' : 'tablist inline wrap nomemory' }; }; diff --git a/packages/react-components/react-tag-picker/library/src/components/TagPickerGroup/useTagPickerGroup.ts b/packages/react-components/react-tag-picker/library/src/components/TagPickerGroup/useTagPickerGroup.ts index 29601b0aa3da85..47e6b4128da594 100644 --- a/packages/react-components/react-tag-picker/library/src/components/TagPickerGroup/useTagPickerGroup.ts +++ b/packages/react-components/react-tag-picker/library/src/components/TagPickerGroup/useTagPickerGroup.ts @@ -6,7 +6,6 @@ import { useTagGroup_unstable } from '@fluentui/react-tags'; import { useTagPickerContext_unstable } from '../../contexts/TagPickerContext'; import { isHTMLElement, useEventCallback, useMergedRefs } from '@fluentui/react-utilities'; import { tagPickerAppearanceToTagAppearance, tagPickerSizeToTagSize } from '../../utils/tagPicker2Tag'; -import { useArrowNavigationGroup } from '@fluentui/react-tabster'; import { ArrowRight } from '@fluentui/keyboard-keys'; /** @@ -31,18 +30,12 @@ export const useTagPickerGroup_unstable = ( const appearance = useTagPickerContext_unstable(ctx => ctx.appearance); const disabled = useTagPickerContext_unstable(ctx => ctx.disabled); - const arrowNavigationProps = useArrowNavigationGroup({ - circular: false, - axis: 'both', - memorizeCurrent: true, - }); - const state = useTagGroup_unstable( { role: 'listbox', disabled, ...props, - ...arrowNavigationProps, + focusgroup: 'listbox inline block nowrap', size, appearance: tagPickerAppearanceToTagAppearance(appearance), dismissible: true, 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 12981abc9b15d9..ff98bad0c908e0 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 @@ -10,7 +10,7 @@ import { slot, } from '@fluentui/react-utilities'; import type { TagGroupProps, TagGroupState } from './TagGroup.types'; -import { useArrowNavigationGroup, useFocusFinders } from '@fluentui/react-tabster'; +import { useFocusFinders } from '@fluentui/react-tabster'; import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts'; import { interactionTagSecondaryClassNames } from '../InteractionTagSecondary/useInteractionTagSecondaryStyles.styles'; import type { TagValue } from '../../utils/types'; @@ -32,7 +32,7 @@ export const useTagGroup_unstable = (props: TagGroupProps, ref: React.Ref, role, 'aria-disabled': disabled, - ...arrowNavigationProps, + focusgroup: 'toolbar inline block wrap', ...rest, }), { elementType: 'div' }, diff --git a/packages/react-components/react-teaching-popover/library/src/components/TeachingPopoverCarouselNav/useTeachingPopoverCarouselNav.tsx b/packages/react-components/react-teaching-popover/library/src/components/TeachingPopoverCarouselNav/useTeachingPopoverCarouselNav.tsx index 6fb21717da50e1..c07ef7ca3802e9 100644 --- a/packages/react-components/react-teaching-popover/library/src/components/TeachingPopoverCarouselNav/useTeachingPopoverCarouselNav.tsx +++ b/packages/react-components/react-teaching-popover/library/src/components/TeachingPopoverCarouselNav/useTeachingPopoverCarouselNav.tsx @@ -1,6 +1,5 @@ 'use client'; -import { useArrowNavigationGroup } from '@fluentui/react-tabster'; import { getIntrinsicElementProps, slot } from '@fluentui/react-utilities'; import * as React from 'react'; @@ -19,13 +18,6 @@ export const useTeachingPopoverCarouselNav_unstable = ( props: TeachingPopoverCarouselNavProps, ref: React.Ref, ): TeachingPopoverCarouselNavState => { - const focusableGroupAttr = useArrowNavigationGroup({ - circular: false, - axis: 'horizontal', - memorizeCurrent: false, - // eslint-disable-next-line @typescript-eslint/naming-convention - unstable_hasDefault: true, - }); const values = useCarouselValues_unstable(snapshot => snapshot); return { @@ -37,10 +29,8 @@ export const useTeachingPopoverCarouselNav_unstable = ( root: slot.always( getIntrinsicElementProps('div', { ref, - role: 'tablist', - tabIndex: 0, + focusgroup: 'tablist inline nowrap nomemory', ...props, - ...focusableGroupAttr, children: null, }), { elementType: 'div' }, diff --git a/packages/react-components/react-teaching-popover/library/src/components/TeachingPopoverCarouselNavButton/useTeachingPopoverCarouselNavButton.tsx b/packages/react-components/react-teaching-popover/library/src/components/TeachingPopoverCarouselNavButton/useTeachingPopoverCarouselNavButton.tsx index bd68823063b03c..5f32002dded6da 100644 --- a/packages/react-components/react-teaching-popover/library/src/components/TeachingPopoverCarouselNavButton/useTeachingPopoverCarouselNavButton.tsx +++ b/packages/react-components/react-teaching-popover/library/src/components/TeachingPopoverCarouselNavButton/useTeachingPopoverCarouselNavButton.tsx @@ -3,7 +3,6 @@ import * as React from 'react'; import { ARIAButtonSlotProps, useARIAButtonProps } from '@fluentui/react-aria'; import { usePopoverContext_unstable } from '@fluentui/react-popover'; -import { useTabsterAttributes } from '@fluentui/react-tabster'; import { getIntrinsicElementProps, isHTMLElement, slot, useEventCallback } from '@fluentui/react-utilities'; import type { @@ -42,10 +41,6 @@ export const useTeachingPopoverCarouselNavButton_unstable = ( } }); - const defaultTabProps = useTabsterAttributes({ - focusable: { isDefault: isSelected }, - }); - const _carouselButton = slot.always>( getIntrinsicElementProps(as, useARIAButtonProps(props.as, props)), { @@ -55,7 +50,7 @@ export const useTeachingPopoverCarouselNavButton_unstable = ( role: 'tab', type: 'button', 'aria-selected': `${!!isSelected}`, - ...defaultTabProps, + focusGroupStart: isSelected, }, }, ); diff --git a/packages/react-components/react-toolbar/library/src/components/Toolbar/useToolbar.ts b/packages/react-components/react-toolbar/library/src/components/Toolbar/useToolbar.ts index b022a1a3287c74..029f3105ec7850 100644 --- a/packages/react-components/react-toolbar/library/src/components/Toolbar/useToolbar.ts +++ b/packages/react-components/react-toolbar/library/src/components/Toolbar/useToolbar.ts @@ -10,7 +10,6 @@ import type { ToolbarState, UninitializedToolbarState, } from './Toolbar.types'; -import { TabsterDOMAttribute, useArrowNavigationGroup } from '@fluentui/react-tabster'; /** * Create the state required to render Toolbar. @@ -141,9 +140,6 @@ const useToolbarSelectableState = ( * @internal * @returns - Tabster DOM attributes for arrow navigation */ -export const useToolbarArrowNavigationProps_unstable = (): TabsterDOMAttribute => { - return useArrowNavigationGroup({ - circular: true, - axis: 'both', - }); +export const useToolbarArrowNavigationProps_unstable = (): { focusgroup: string } => { + return { focusgroup: 'toolbar inline block wrap' }; }; diff --git a/packages/react-components/react-tree/library/src/components/TreeItemLayout/useTreeItemLayout.tsx b/packages/react-components/react-tree/library/src/components/TreeItemLayout/useTreeItemLayout.tsx index 61f10ad5a275f9..e3c3f6727c77af 100644 --- a/packages/react-components/react-tree/library/src/components/TreeItemLayout/useTreeItemLayout.tsx +++ b/packages/react-components/react-tree/library/src/components/TreeItemLayout/useTreeItemLayout.tsx @@ -20,7 +20,7 @@ import type { import { Checkbox, CheckboxProps } from '@fluentui/react-checkbox'; import { Radio, RadioProps } from '@fluentui/react-radio'; import { TreeItemChevron } from '../TreeItemChevron'; -import { useArrowNavigationGroup, useIsNavigatingWithKeyboard } from '@fluentui/react-tabster'; +import { useIsNavigatingWithKeyboard } from '@fluentui/react-tabster'; import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts'; /** @@ -152,10 +152,9 @@ export const useTreeItemLayout_unstable = ( if (expandIcon) { expandIcon.ref = expandIconRefs; } - const arrowNavigationProps = useArrowNavigationGroup({ circular: navigationMode === 'tree', axis: 'horizontal' }); const actions = isActionsVisible ? slot.optional(props.actions, { - defaultProps: { ...arrowNavigationProps, role: 'toolbar' }, + defaultProps: { focusgroup: navigationMode === 'tree' ? 'toolbar inline wrap' : 'toolbar inline nowrap' }, elementType: 'div', }) : undefined; diff --git a/packages/react-components/react-tree/stories/src/Tree/TreeActions.stories.tsx b/packages/react-components/react-tree/stories/src/Tree/TreeActions.stories.tsx index 516e2537f9cb47..5c0d93830c8198 100644 --- a/packages/react-components/react-tree/stories/src/Tree/TreeActions.stories.tsx +++ b/packages/react-components/react-tree/stories/src/Tree/TreeActions.stories.tsx @@ -111,7 +111,7 @@ Actions.parameters = { In addition to \`aside\` slot, both tree item layouts support \`actions\` slot that can be used for tasks such as edit, rename, or triggering a menu. \`actions\` and \`aside\` slots are positioned on the exact same spot, so they won't be visible at the same time. \`aside\` slot is visible by default meanwhile \`actions\` slot are only visible when the tree item is active (by hovering or by navigating to it). \`actions\` slot supports a \`visible\` prop to force visibility of the actions. -The \`actions\` slot has a \`role="toolbar"\` and ensures proper horizontal navigation with the keyboard by using [\`useArrowNavigationGroup\`](https://react.fluentui.dev/?path=/docs/utilities-focus-management-usearrownavigationgroup--default). +The \`actions\` slot has a \`role="toolbar"\` and ensures proper horizontal navigation with the keyboard by using [\`focusgroup\`](https://open-ui.org/components/scoped-focusgroup.explainer/). > ⚠️ Although \`actions\` are easy to navigate, they're not an expected pattern according to [WAI-ARIA](https://www.w3.org/WAI/ARIA/apg/patterns/treeview/).providing a context menu with the same functionalities as the actions is recommended to ensure your tree item is accessible. diff --git a/packages/react-components/react-tree/stories/src/Tree/TreeBestPractices.md b/packages/react-components/react-tree/stories/src/Tree/TreeBestPractices.md index 31a8c41495668b..4d18ef23898d3d 100644 --- a/packages/react-components/react-tree/stories/src/Tree/TreeBestPractices.md +++ b/packages/react-components/react-tree/stories/src/Tree/TreeBestPractices.md @@ -15,6 +15,6 @@ - **Make `actions` or additional functionality in tree items accessible with a context menu:** - ⚠️ `actions` slot do not adhere to keyboard navigation standards! Use `aria-description` or `aria-describedby` on tree items to indicate this interaction, you should explain your user how to interact with `actions` slot. - - the `actions` slot will have `role="toolbar"` and are accessible with horizontal keyboard navigation using [\`useArrowNavigationGroup\`](https://react.fluentui.dev/?path=/docs/utilities-focus-management-usearrownavigationgroup--default) by default. + - the `actions` slot will have `role="toolbar"` and are accessible with horizontal keyboard navigation using [\`focusgroup\`](https://open-ui.org/components/scoped-focusgroup.explainer/) by default. - **Use `aria-selected=true` once a treeitem is selected in custom behaviors** Some tree utilization might use the selection feature for navigation purposes, in this case, the `aria-selected` attribute should be set to `true` once the treeitem is the current active item to indicate that it is selected for the navigation. From f8d5ec1e6e9bd8038accaad11c1c1117e3d345d8 Mon Sep 17 00:00:00 2001 From: Sarah Higley Date: Fri, 3 Apr 2026 11:37:28 -0700 Subject: [PATCH 3/3] fix focusgroupstart --- .../src/components/CarouselNavButton/useCarouselNavButton.ts | 4 ++-- .../CarouselNavImageButton/useCarouselNavImageButton.ts | 2 +- .../react-tabs/library/src/components/Tab/useTab.ts | 4 ++-- .../useTeachingPopoverCarouselNavButton.tsx | 2 +- .../react-components/react-utilities/src/utils/properties.ts | 1 + 5 files changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/react-components/react-carousel/library/src/components/CarouselNavButton/useCarouselNavButton.ts b/packages/react-components/react-carousel/library/src/components/CarouselNavButton/useCarouselNavButton.ts index 01ddc25752d7b1..e5fb74a391b34b 100644 --- a/packages/react-components/react-carousel/library/src/components/CarouselNavButton/useCarouselNavButton.ts +++ b/packages/react-components/react-carousel/library/src/components/CarouselNavButton/useCarouselNavButton.ts @@ -51,7 +51,7 @@ export const useCarouselNavButton_unstable = ( }); const buttonRef = React.useRef(undefined); - const _carouselButton = slot.always( + const _carouselButton = slot.always( getIntrinsicElementProps(as, useARIAButtonProps(props.as, props)), { elementType: 'button', @@ -60,7 +60,7 @@ export const useCarouselNavButton_unstable = ( role: 'tab', type: 'button', 'aria-selected': selected, - focusGroupStart: true, + focusgroupstart: selected ? 'true' : undefined, }, }, ); diff --git a/packages/react-components/react-carousel/library/src/components/CarouselNavImageButton/useCarouselNavImageButton.ts b/packages/react-components/react-carousel/library/src/components/CarouselNavImageButton/useCarouselNavImageButton.ts index 2e439f80746092..31ec53244328ac 100644 --- a/packages/react-components/react-carousel/library/src/components/CarouselNavImageButton/useCarouselNavImageButton.ts +++ b/packages/react-components/react-carousel/library/src/components/CarouselNavImageButton/useCarouselNavImageButton.ts @@ -53,7 +53,7 @@ export const useCarouselNavImageButton_unstable = ( role: 'tab', type: 'button', 'aria-selected': selected, - focusGroupStart: selected, + focusgroupstart: selected ? 'true' : undefined, }, }, ); diff --git a/packages/react-components/react-tabs/library/src/components/Tab/useTab.ts b/packages/react-components/react-tabs/library/src/components/Tab/useTab.ts index 5fcb654eae6a5a..b57f0c1760e1bc 100644 --- a/packages/react-components/react-tabs/library/src/components/Tab/useTab.ts +++ b/packages/react-components/react-tabs/library/src/components/Tab/useTab.ts @@ -123,8 +123,8 @@ export const useTabBase_unstable = (props: TabBaseProps, ref: React.Ref): { focusGroupStart: boolean } => { +}: Pick): { focusgroupstart: 'true' | undefined } => { return { - focusGroupStart: selected, + focusgroupstart: selected ? 'true' : undefined, }; }; diff --git a/packages/react-components/react-teaching-popover/library/src/components/TeachingPopoverCarouselNavButton/useTeachingPopoverCarouselNavButton.tsx b/packages/react-components/react-teaching-popover/library/src/components/TeachingPopoverCarouselNavButton/useTeachingPopoverCarouselNavButton.tsx index 5f32002dded6da..b9ae79665610a3 100644 --- a/packages/react-components/react-teaching-popover/library/src/components/TeachingPopoverCarouselNavButton/useTeachingPopoverCarouselNavButton.tsx +++ b/packages/react-components/react-teaching-popover/library/src/components/TeachingPopoverCarouselNavButton/useTeachingPopoverCarouselNavButton.tsx @@ -50,7 +50,7 @@ export const useTeachingPopoverCarouselNavButton_unstable = ( role: 'tab', type: 'button', 'aria-selected': `${!!isSelected}`, - focusGroupStart: isSelected, + focusgroupstart: isSelected ? 'true' : undefined, }, }, ); diff --git a/packages/react-components/react-utilities/src/utils/properties.ts b/packages/react-components/react-utilities/src/utils/properties.ts index 810d11119b648f..f71a7200a9592c 100644 --- a/packages/react-components/react-utilities/src/utils/properties.ts +++ b/packages/react-components/react-utilities/src/utils/properties.ts @@ -120,6 +120,7 @@ export const baseElementProperties = toObjectMap([ 'lang', // global 'popover', // global 'focusgroup', // global + 'focusgroupstart', // global 'ref', // global 'role', // global 'style', // global