diff --git a/apps/fluent-tester/src/TestComponents/ContextualMenu/ContextualMenuTest.tsx b/apps/fluent-tester/src/TestComponents/ContextualMenu/ContextualMenuTest.tsx index afcb4d6fb0f..0fe829a156a 100644 --- a/apps/fluent-tester/src/TestComponents/ContextualMenu/ContextualMenuTest.tsx +++ b/apps/fluent-tester/src/TestComponents/ContextualMenu/ContextualMenuTest.tsx @@ -219,6 +219,7 @@ const NestedContextualMenu: React.FunctionComponent = () => { itemKey="4" onHoverIn={toggleShowSubmenu} componentRef={stdMenuItemRef} + expanded={showSubmenu} /> {showSubmenu && ( @@ -446,6 +447,7 @@ const ScrollViewContextualMenu: React.FunctionComponent = () => { itemKey="3" onHoverIn={toggleShowSubmenu} componentRef={stdMenuItemRef} + expanded={showSubmenu} /> {showSubmenu && ( { +export interface ContextualMenuItemProps extends Omit { /* ** A unique key-identifier for each menu item */ diff --git a/packages/components/ContextualMenu/src/SubmenuItem.tsx b/packages/components/ContextualMenu/src/SubmenuItem.tsx index f8687ebd855..b8906df2d74 100644 --- a/packages/components/ContextualMenu/src/SubmenuItem.tsx +++ b/packages/components/ContextualMenu/src/SubmenuItem.tsx @@ -27,6 +27,7 @@ export const SubmenuItem = compose({ const defaultComponentRef = React.useRef(null); const { disabled, + expanded, itemKey, icon, text, @@ -147,7 +148,27 @@ export const SubmenuItem = compose({ const onKeyDownProps = useKeyDownProps(showSubmenuOnKeyDown, ' ', 'Enter', 'ArrowLeft', 'ArrowRight'); const onAccTap = onAccessibilityTap ?? onItemPress; - // grab the styling information, referencing the state as well as the props + // Default accessibility actions to help screen readers announce expanded/collapsed state + // Only provide on win32 to follow platform-specific accessibility patterns + const defaultAccessibilityActions = React.useMemo(() => { + if (Platform.OS === ('win32' as any)) { + return [ + { name: 'Expand', label: 'Expand submenu' }, + { name: 'Collapse', label: 'Collapse submenu' }, + ]; + } + return []; + }, []); + + // Merge user accessibility actions with defaults + const finalAccessibilityActions = React.useMemo(() => { + const userActions = userProps.accessibilityActions; + if (userActions && userActions.length > 0) { + return [...defaultAccessibilityActions, ...userActions]; + } + return defaultAccessibilityActions; + }, [userProps.accessibilityActions, defaultAccessibilityActions]); + const styleProps = useStyling(userProps, (override: string) => state[override] || userProps[override]); // create the merged slot props const slotProps = mergeSettings(styleProps, { @@ -155,15 +176,16 @@ export const SubmenuItem = compose({ ref: cmRef, ...pressablePropsModified, ...onKeyDownProps, + ...rest, accessible: true, accessibilityLabel: accessibilityLabel, accessibilityRole: 'menuitem', - accessibilityState: { disabled: state.disabled ?? false, selected: state.selected }, + accessibilityState: { disabled: state.disabled ?? false, expanded: expanded ?? false, selected: state.selected }, accessibilityValue: { text: itemKey }, + accessibilityActions: finalAccessibilityActions, disabled, focusable: !disabled, onAccessibilityTap: onAccTap, - ...rest, }, content: { accessible: false, diff --git a/packages/components/ContextualMenu/src/SubmenuItem.types.ts b/packages/components/ContextualMenu/src/SubmenuItem.types.ts index e3d28429b7b..82e9d6f47ad 100644 --- a/packages/components/ContextualMenu/src/SubmenuItem.types.ts +++ b/packages/components/ContextualMenu/src/SubmenuItem.types.ts @@ -11,7 +11,12 @@ export const submenuItemName = 'SubmenuItem'; export interface SubmenuItemTokens extends ContextualMenuItemTokens { chevronColor?: string; } -export type SubmenuItemProps = ContextualMenuItemProps; +export interface SubmenuItemProps extends ContextualMenuItemProps { + /** + * Whether the submenu is currently expanded/visible + */ + expanded?: boolean; +} export type SubmenuItemState = ContextualMenuItemState;