From aaf20b678353d4cb91492a86e57ebd9a4107b016 Mon Sep 17 00:00:00 2001 From: Lingfan Gao Date: Fri, 24 Jan 2025 22:09:54 +0000 Subject: [PATCH 1/2] single state --- .../src/components/Option/useOption.tsx | 13 +++++++++- .../Option/useOptionStyles.styles.ts | 15 +++++++---- .../library/src/components/Menu/useMenu.tsx | 12 +++++++-- .../src/components/MenuItem/useMenuItem.tsx | 5 ++++ .../MenuItem/useMenuItemStyles.styles.ts | 7 ++--- .../src/components/MenuList/MenuList.types.ts | 2 ++ .../src/components/MenuList/useMenuList.ts | 26 +++++++++++++++++++ .../MenuList/useMenuListContextValues.ts | 11 +++++++- .../library/src/contexts/menuListContext.tsx | 7 +++++ 9 files changed, 86 insertions(+), 12 deletions(-) diff --git a/packages/react-components/react-combobox/library/src/components/Option/useOption.tsx b/packages/react-components/react-combobox/library/src/components/Option/useOption.tsx index 608262d15b63a1..9aee47d8ba729e 100644 --- a/packages/react-components/react-combobox/library/src/components/Option/useOption.tsx +++ b/packages/react-components/react-combobox/library/src/components/Option/useOption.tsx @@ -1,10 +1,11 @@ import * as React from 'react'; -import { getIntrinsicElementProps, useId, useMergedRefs, slot } from '@fluentui/react-utilities'; +import { getIntrinsicElementProps, useId, useMergedRefs, slot, useEventCallback } from '@fluentui/react-utilities'; import { useActiveDescendantContext } from '@fluentui/react-aria'; import { CheckmarkFilled, Checkmark12Filled } from '@fluentui/react-icons'; import { useListboxContext_unstable } from '../../contexts/ListboxContext'; import type { OptionValue } from '../../utils/OptionCollection.types'; import type { OptionProps, OptionState } from './Option.types'; +import { useSetKeyboardNavigation } from '@fluentui/react-tabster'; function getTextString(text: string | undefined, children: React.ReactNode) { if (text !== undefined) { @@ -87,6 +88,15 @@ export const useOption_unstable = (props: OptionProps, ref: React.Ref) => { + if (activeDescendantController.active() !== id) { + activeDescendantController.focus(id); + } + + setKeyboardNavigationState(false); + }); + // register option data with context React.useEffect(() => { if (id && optionRef.current) { @@ -114,6 +124,7 @@ export const useOption_unstable = (props: OptionProps, ref: React.Ref context.mouseInputState); React.useEffect(() => { if (open) { - focusFirst(); + // TODO this should not be called when user is moving the mouse around + // This only applies to submenus + // Submenu should only read this state if it's wrapped by a menulist + // MenuList should set this state + if (!mouseInputState?.isMouseInput()) { + focusFirst(); + } } else { if (!firstMount) { if (targetDocument?.activeElement === targetDocument?.body) { @@ -287,7 +295,7 @@ const useMenuOpenState = ( } // firstMount change should not re-run this effect // eslint-disable-next-line react-hooks/exhaustive-deps - }, [state.triggerRef, state.isSubmenu, open, focusFirst, targetDocument, state.menuPopoverRef]); + }, [state.triggerRef, state.isSubmenu, open, focusFirst, targetDocument, state.menuPopoverRef, mouseInputState]); return [open, setOpen] as const; }; diff --git a/packages/react-components/react-menu/library/src/components/MenuItem/useMenuItem.tsx b/packages/react-components/react-menu/library/src/components/MenuItem/useMenuItem.tsx index 750a3deeda900c..d757bed7f42b42 100644 --- a/packages/react-components/react-menu/library/src/components/MenuItem/useMenuItem.tsx +++ b/packages/react-components/react-menu/library/src/components/MenuItem/useMenuItem.tsx @@ -37,6 +37,7 @@ const ChevronLeftIcon = bundleIcon(ChevronLeftFilled, ChevronLeftRegular); export const useMenuItem_unstable = (props: MenuItemProps, ref: React.Ref>): MenuItemState => { const isSubmenuTrigger = useMenuTriggerContext_unstable(); const persistOnClickContext = useMenuContext_unstable(context => context.persistOnItemClick); + const mouseInputState = useMenuListContext_unstable(ctx => ctx.mouseInputState); const { as = 'div', disabled = false, hasSubmenu = isSubmenuTrigger, persistOnClick = persistOnClickContext } = props; const { hasIcons, hasCheckmarks } = useIconAndCheckmarkAlignment({ hasSubmenu }); const setOpen = useMenuContext_unstable(context => context.setOpen); @@ -75,6 +76,10 @@ export const useMenuItem_unstable = (props: MenuItemProps, ref: React.Ref { + if (!mouseInputState?.isMouseInput()) { + mouseInputState?.setMouseInput(true); + } + if (event.currentTarget.ownerDocument.activeElement !== event.currentTarget) { innerRef.current?.focus(); } diff --git a/packages/react-components/react-menu/library/src/components/MenuItem/useMenuItemStyles.styles.ts b/packages/react-components/react-menu/library/src/components/MenuItem/useMenuItemStyles.styles.ts index fca9ed9dd16d43..aa96dd661b70d5 100644 --- a/packages/react-components/react-menu/library/src/components/MenuItem/useMenuItemStyles.styles.ts +++ b/packages/react-components/react-menu/library/src/components/MenuItem/useMenuItemStyles.styles.ts @@ -36,7 +36,9 @@ const useRootBaseStyles = makeResetStyles({ cursor: 'pointer', gap: '4px', - ':hover': { + ...createFocusOutlineStyle(), + ':focus': { + outline: 'none', backgroundColor: tokens.colorNeutralBackground1Hover, color: tokens.colorNeutralForeground2Hover, @@ -62,6 +64,7 @@ const useRootBaseStyles = makeResetStyles({ [`& .${menuItemClassNames.subText}`]: { color: tokens.colorNeutralForeground3Pressed, }, + ...createFocusOutlineStyle({ style: { outlineColor: 'Highlight' } }), }, // High contrast styles @@ -71,11 +74,9 @@ const useRootBaseStyles = makeResetStyles({ borderColor: 'Highlight', color: 'Highlight', }, - ...createFocusOutlineStyle({ style: { outlineColor: 'Highlight' } }), }, userSelect: 'none', - ...createFocusOutlineStyle(), }); const useContentBaseStyles = makeResetStyles({ diff --git a/packages/react-components/react-menu/library/src/components/MenuList/MenuList.types.ts b/packages/react-components/react-menu/library/src/components/MenuList/MenuList.types.ts index 3aaa37a1275508..4c1c2ee30761a5 100644 --- a/packages/react-components/react-menu/library/src/components/MenuList/MenuList.types.ts +++ b/packages/react-components/react-menu/library/src/components/MenuList/MenuList.types.ts @@ -69,6 +69,8 @@ export type MenuListState = ComponentState & * States if the MenuList is inside MenuContext */ hasMenuContext?: boolean; + + mouseInputState?: MenuListContextValue['mouseInputState']; }; export type MenuListContextValues = { 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 9846dcff0a377e..9dc4d129ad5f4d 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 @@ -11,12 +11,15 @@ import { useFocusFinders, TabsterMoveFocusEventName, type TabsterMoveFocusEvent, + useOnKeyboardNavigationChange, + useSetKeyboardNavigation, } 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'; import { MenuContext } from '../../contexts/menuContext'; import type { MenuListProps, MenuListState } from './MenuList.types'; +import { useMenuListContext_unstable } from '../../contexts/menuListContext'; /** * Returns the props and state required to render the component @@ -133,6 +136,8 @@ export const useMenuList_unstable = (props: MenuListProps, ref: React.Ref { + const parentContext = useMenuListContext_unstable(ctx => ctx.mouseInputState); + const setKeyboardNavigationState = useSetKeyboardNavigation(); + const isMouseInputRef = React.useRef(!!parentContext?.isMouseInput()); + const setMouseInput = React.useCallback( + (isMouseInput: boolean) => { + setKeyboardNavigationState(!isMouseInput); + parentContext?.setMouseInput(isMouseInput); + isMouseInputRef.current = isMouseInput; + }, + [parentContext, setKeyboardNavigationState], + ); + + useOnKeyboardNavigationChange(isNavigatingWithKeyboard => { + setMouseInput(!isNavigatingWithKeyboard); + }); + + return { isMouseInput: () => isMouseInputRef.current, setMouseInput }; +}; diff --git a/packages/react-components/react-menu/library/src/components/MenuList/useMenuListContextValues.ts b/packages/react-components/react-menu/library/src/components/MenuList/useMenuListContextValues.ts index 150bc1a7e253bd..3925265e98906c 100644 --- a/packages/react-components/react-menu/library/src/components/MenuList/useMenuListContextValues.ts +++ b/packages/react-components/react-menu/library/src/components/MenuList/useMenuListContextValues.ts @@ -1,7 +1,15 @@ import type { MenuListContextValues, MenuListState } from './MenuList.types'; export function useMenuListContextValues_unstable(state: MenuListState): MenuListContextValues { - const { checkedValues, hasCheckmarks, hasIcons, selectRadio, setFocusByFirstCharacter, toggleCheckbox } = state; + const { + checkedValues, + hasCheckmarks, + hasIcons, + selectRadio, + setFocusByFirstCharacter, + toggleCheckbox, + mouseInputState, + } = state; // This context is created with "@fluentui/react-context-selector", these is no sense to memoize it const menuList = { @@ -11,6 +19,7 @@ export function useMenuListContextValues_unstable(state: MenuListState): MenuLis selectRadio, setFocusByFirstCharacter, toggleCheckbox, + mouseInputState, }; return { menuList }; diff --git a/packages/react-components/react-menu/library/src/contexts/menuListContext.tsx b/packages/react-components/react-menu/library/src/contexts/menuListContext.tsx index d4ffe7d62429da..3fc57b0e1c9e2f 100644 --- a/packages/react-components/react-menu/library/src/contexts/menuListContext.tsx +++ b/packages/react-components/react-menu/library/src/contexts/menuListContext.tsx @@ -4,6 +4,11 @@ import type { ContextSelector, Context } from '@fluentui/react-context-selector' import type { SelectableHandler } from '../selectable/index'; import type { MenuCheckedValueChangeData, MenuCheckedValueChangeEvent, MenuListProps } from '../components/index'; +export interface MouseInputState { + isMouseInput: () => boolean; + setMouseInput(isMouseInput: boolean): void; +} + export const MenuListContext: Context = createContext( undefined, ) as Context; @@ -15,6 +20,7 @@ const menuListContextDefaultValue: MenuListContextValue = { selectRadio: () => null, hasIcons: false, hasCheckmarks: false, + mouseInputState: { isMouseInput: () => false, setMouseInput: () => null }, }; /** @@ -34,6 +40,7 @@ export type MenuListContextValue = Pick void; + mouseInputState?: MouseInputState; }; export const MenuListProvider = MenuListContext.Provider; From d7f90186dc270bd43b8086e27ea4be5db1e9bbcb Mon Sep 17 00:00:00 2001 From: Lingfan Gao Date: Fri, 24 Jan 2025 22:34:21 +0000 Subject: [PATCH 2/2] update md --- .../react-components/react-menu/library/etc/react-menu.api.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/react-components/react-menu/library/etc/react-menu.api.md b/packages/react-components/react-menu/library/etc/react-menu.api.md index c6b8c4d7e27593..0999d4f48db13e 100644 --- a/packages/react-components/react-menu/library/etc/react-menu.api.md +++ b/packages/react-components/react-menu/library/etc/react-menu.api.md @@ -230,6 +230,7 @@ export type MenuListContextValue = Pick void; + mouseInputState?: MouseInputState; }; // @public (undocumented) @@ -260,6 +261,7 @@ export type MenuListState = ComponentState & Required; toggleCheckbox: SelectableHandler; hasMenuContext?: boolean; + mouseInputState?: MenuListContextValue['mouseInputState']; }; // @public