diff --git a/apps/public-docsite-v9/src/shims/MenuShim.tsx b/apps/public-docsite-v9/src/shims/MenuShim.tsx index 64f2a6272955a9..f7aaec192ef3f7 100644 --- a/apps/public-docsite-v9/src/shims/MenuShim.tsx +++ b/apps/public-docsite-v9/src/shims/MenuShim.tsx @@ -15,6 +15,7 @@ import { MenuItemCheckbox, MenuItemCheckboxProps, MenuProps, + MenuDividerProps, } from '@fluentui/react-components'; export const shimMenuProps = (props: IContextualMenuProps): Partial => { @@ -54,7 +55,7 @@ const shimMenuHeaderProps = (props: IContextualMenuItem): MenuGroupHeaderProps = export const MenuItemShim = (props: IContextualMenuItem) => { if (props.itemType === ContextualMenuItemType.Divider) { const shimProps = shimMenuItemProps(props); - return ; + return ; } if (props.itemType === ContextualMenuItemType.Section) { diff --git a/change/@fluentui-react-menu-c4342a62-8136-40bb-83a5-5269a00d5b85.json b/change/@fluentui-react-menu-c4342a62-8136-40bb-83a5-5269a00d5b85.json new file mode 100644 index 00000000000000..d930958b13411b --- /dev/null +++ b/change/@fluentui-react-menu-c4342a62-8136-40bb-83a5-5269a00d5b85.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "feat: replace keydown handlers by useARIAButtonShorthand on MenuItem", + "packageName": "@fluentui/react-menu", + "email": "bernardo.sunderhus@gmail.com", + "dependentChangeType": "patch" +} diff --git a/packages/react-components/react-menu/etc/react-menu.api.md b/packages/react-components/react-menu/etc/react-menu.api.md index 942b71dc533be8..a52f4defae9d09 100644 --- a/packages/react-components/react-menu/etc/react-menu.api.md +++ b/packages/react-components/react-menu/etc/react-menu.api.md @@ -6,7 +6,9 @@ /// +import type { ARIAButtonElement } from '@fluentui/react-aria'; import { ARIAButtonResultProps } from '@fluentui/react-aria'; +import type { ARIAButtonSlotProps } from '@fluentui/react-aria'; import { ARIAButtonType } from '@fluentui/react-aria'; import type { ComponentProps } from '@fluentui/react-utilities'; import type { ComponentState } from '@fluentui/react-utilities'; @@ -128,7 +130,6 @@ export const menuItemClassNames: SlotClassNames; // @public (undocumented) export type MenuItemProps = ComponentProps> & { - disabled?: boolean; hasSubmenu?: boolean; persistOnClick?: boolean; }; @@ -153,14 +154,12 @@ export type MenuItemSelectableProps = { // @public export type MenuItemSelectableState = MenuItemSelectableProps & { - checkedItems: string[]; - onCheckedValueChange: (e: React_2.MouseEvent | React_2.KeyboardEvent, name: string, checkedItems: string[]) => void; checked: boolean; }; // @public (undocumented) export type MenuItemSlots = { - root: Slot<'div'>; + root: Slot>; icon?: Slot<'span'>; checkmark?: Slot<'span'>; submenuIndicator?: Slot<'span'>; @@ -169,7 +168,9 @@ export type MenuItemSlots = { }; // @public (undocumented) -export type MenuItemState = ComponentState & Pick; +export type MenuItemState = ComponentState & Required> & { + isNativeButton: boolean; +}; // @public export const MenuList: ForwardRefComponent; @@ -398,16 +399,16 @@ export const useMenuGroupHeaderStyles_unstable: (state: MenuGroupHeaderState) => export const useMenuGroupStyles_unstable: (state: MenuGroupState) => MenuGroupState; // @public -export const useMenuItem_unstable: (props: MenuItemProps, ref: React_2.Ref) => MenuItemState; +export const useMenuItem_unstable: (props: MenuItemProps, ref: React_2.Ref>) => MenuItemState; // @public -export const useMenuItemCheckbox_unstable: (props: MenuItemCheckboxProps, ref: React_2.Ref) => MenuItemCheckboxState; +export const useMenuItemCheckbox_unstable: (props: MenuItemCheckboxProps, ref: React_2.Ref>) => MenuItemCheckboxState; // @public (undocumented) export const useMenuItemCheckboxStyles_unstable: (state: MenuItemCheckboxState) => void; // @public -export const useMenuItemRadio_unstable: (props: MenuItemRadioProps, ref: React_2.Ref) => MenuItemRadioState; +export const useMenuItemRadio_unstable: (props: MenuItemRadioProps, ref: React_2.Ref>) => MenuItemRadioState; // @public (undocumented) export const useMenuItemRadioStyles_unstable: (state: MenuItemRadioState) => void; diff --git a/packages/react-components/react-menu/src/components/Menu/Menu.types.ts b/packages/react-components/react-menu/src/components/Menu/Menu.types.ts index 13594a9624b134..9df615f6633439 100644 --- a/packages/react-components/react-menu/src/components/Menu/Menu.types.ts +++ b/packages/react-components/react-menu/src/components/Menu/Menu.types.ts @@ -162,6 +162,8 @@ export type MenuOpenChangeData = { bubble?: boolean; /** * Indicates whether the change of state was a keyboard interaction + * @deprecated + * This should not be used, since `Enter`, `Space` and click should be interpreted as the same thing as a click */ keyboard?: boolean; open: boolean; diff --git a/packages/react-components/react-menu/src/components/Menu/useMenu.tsx b/packages/react-components/react-menu/src/components/Menu/useMenu.tsx index 2a2abc95cb4e4d..ca677101ab140b 100644 --- a/packages/react-components/react-menu/src/components/Menu/useMenu.tsx +++ b/packages/react-components/react-menu/src/components/Menu/useMenu.tsx @@ -14,6 +14,7 @@ import { useMenuContext_unstable } from '../../contexts/menuContext'; import { MENU_ENTER_EVENT, useOnMenuMouseEnter } from '../../utils/index'; import { useIsSubmenu } from '../../utils/useIsSubmenu'; import type { MenuOpenChangeData, MenuOpenEvents, MenuProps, MenuState } from './Menu.types'; +import { Tab } from '@fluentui/keyboard-keys'; /** * Create the state required to render Menu. @@ -160,7 +161,6 @@ const useMenuOpenState = ( const parentSetOpen = useMenuContext_unstable(context => context.setOpen); const onOpenChange: MenuState['onOpenChange'] = useEventCallback((e, data) => state.onOpenChange?.(e, data)); - const shouldHandleKeyboardRef = React.useRef(false); const shouldHandleTabRef = React.useRef(false); const pressedShiftRef = React.useRef(false); const setOpenTimeout = React.useRef(0); @@ -183,10 +183,9 @@ const useMenuOpenState = ( state.setContextTarget(undefined); } - if (data.keyboard) { - shouldHandleKeyboardRef.current = true; - shouldHandleTabRef.current = (e as React.KeyboardEvent).key === 'Tab'; - pressedShiftRef.current = (e as React.KeyboardEvent).shiftKey; + if (e.type === 'keydown' && (e as React.KeyboardEvent).key === Tab) { + shouldHandleTabRef.current = true; + pressedShiftRef.current = (e as React.KeyboardEvent).shiftKey; } if (data.bubble) { @@ -288,7 +287,7 @@ const useMenuOpenState = ( focusFirst(); } - if (shouldHandleKeyboardRef.current && !open) { + if (!open) { if (shouldHandleTabRef.current && !state.isSubmenu) { pressedShiftRef.current ? focusBeforeMenuTrigger() : focusAfterMenuTrigger(); } else { @@ -296,7 +295,6 @@ const useMenuOpenState = ( } } - shouldHandleKeyboardRef.current = false; shouldHandleTabRef.current = false; pressedShiftRef.current = false; }, [state.triggerRef, state.isSubmenu, open, focusFirst, focusAfterMenuTrigger, focusBeforeMenuTrigger]); diff --git a/packages/react-components/react-menu/src/components/MenuItem/MenuItem.test.tsx b/packages/react-components/react-menu/src/components/MenuItem/MenuItem.test.tsx index ed5ce32d663759..a2ab4a39789493 100644 --- a/packages/react-components/react-menu/src/components/MenuItem/MenuItem.test.tsx +++ b/packages/react-components/react-menu/src/components/MenuItem/MenuItem.test.tsx @@ -7,11 +7,12 @@ import { isConformant } from '../../common/isConformant'; import { MenuTriggerContextProvider } from '../../contexts/menuTriggerContext'; import { MenuListProvider } from '../../contexts/menuListContext'; import { mockUseMenuContext } from '../../common/mockUseMenuContext'; +import type { MenuItemProps } from './MenuItem.types'; jest.mock('../../contexts/menuContext'); describe('MenuItem', () => { - isConformant({ + isConformant({ Component: MenuItem, displayName: 'MenuItem', testOptions: { diff --git a/packages/react-components/react-menu/src/components/MenuItem/MenuItem.types.ts b/packages/react-components/react-menu/src/components/MenuItem/MenuItem.types.ts index d139a467a17bbf..cb8b0a30e6e31f 100644 --- a/packages/react-components/react-menu/src/components/MenuItem/MenuItem.types.ts +++ b/packages/react-components/react-menu/src/components/MenuItem/MenuItem.types.ts @@ -1,7 +1,8 @@ +import type { ARIAButtonSlotProps } from '@fluentui/react-aria'; import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities'; export type MenuItemSlots = { - root: Slot<'div'>; + root: Slot>; /** * Icon slot rendered before children content @@ -32,13 +33,6 @@ export type MenuItemSlots = { }; export type MenuItemProps = ComponentProps> & { - /** - * Applies disabled styles to menu item but remains focusable - * - * @default false - */ - disabled?: boolean; - /** * If the menu item is a trigger for a submenu * @@ -55,4 +49,6 @@ export type MenuItemProps = ComponentProps> & { }; export type MenuItemState = ComponentState & - Pick; + Required> & { + isNativeButton: boolean; + }; diff --git a/packages/react-components/react-menu/src/components/MenuItem/__snapshots__/MenuItem.test.tsx.snap b/packages/react-components/react-menu/src/components/MenuItem/__snapshots__/MenuItem.test.tsx.snap index cdc34029f58a07..dc257c5e2d570c 100644 --- a/packages/react-components/react-menu/src/components/MenuItem/__snapshots__/MenuItem.test.tsx.snap +++ b/packages/react-components/react-menu/src/components/MenuItem/__snapshots__/MenuItem.test.tsx.snap @@ -5,6 +5,7 @@ exports[`MenuItem renders a default state 1`] = ` className="fui-MenuItem" onClick={[Function]} onKeyDown={[Function]} + onKeyUp={[Function]} onMouseEnter={[Function]} role="menuitem" tabIndex={0} diff --git a/packages/react-components/react-menu/src/components/MenuItem/useCharacterSearch.ts b/packages/react-components/react-menu/src/components/MenuItem/useCharacterSearch.ts index be836ff0a1f5c4..216c2aa947e42d 100644 --- a/packages/react-components/react-menu/src/components/MenuItem/useCharacterSearch.ts +++ b/packages/react-components/react-menu/src/components/MenuItem/useCharacterSearch.ts @@ -1,15 +1,15 @@ import * as React from 'react'; import { useMenuListContext_unstable } from '../../contexts/menuListContext'; import type { MenuItemState } from '../../components/index'; +import type { ARIAButtonElementIntersection } from '@fluentui/react-aria'; export const useCharacterSearch = (state: MenuItemState, ref: React.RefObject) => { const setFocusByFirstCharacter = useMenuListContext_unstable(context => context.setFocusByFirstCharacter); - const { onKeyDown: onKeyDownBase } = state.root; - state.root.onKeyDown = e => { - if (onKeyDownBase) { - onKeyDownBase(e); - } + const { onKeyDown: originalOnKeyDown } = state.root; + + state.root.onKeyDown = (e: React.KeyboardEvent) => { + originalOnKeyDown?.(e); if (e.key?.length > 1) { return; diff --git a/packages/react-components/react-menu/src/components/MenuItem/useMenuItem.tsx b/packages/react-components/react-menu/src/components/MenuItem/useMenuItem.tsx index c7bda1d14abd62..3fd141f34239a2 100644 --- a/packages/react-components/react-menu/src/components/MenuItem/useMenuItem.tsx +++ b/packages/react-components/react-menu/src/components/MenuItem/useMenuItem.tsx @@ -1,11 +1,5 @@ import * as React from 'react'; -import { - useEventCallback, - shouldPreventDefaultOnKeyDown, - resolveShorthand, - useMergedRefs, - getNativeElementProps, -} from '@fluentui/react-utilities'; +import { useEventCallback, resolveShorthand, useMergedRefs, getNativeElementProps } from '@fluentui/react-utilities'; import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts'; import { useCharacterSearch } from './useCharacterSearch'; import { useMenuTriggerContext_unstable } from '../../contexts/menuTriggerContext'; @@ -19,6 +13,9 @@ import { import { useMenuListContext_unstable } from '../../contexts/menuListContext'; import { useMenuContext_unstable } from '../../contexts/menuContext'; import type { MenuItemProps, MenuItemState } from './MenuItem.types'; +import type { ARIAButtonElement, ARIAButtonElementIntersection, ARIAButtonSlotProps } from '@fluentui/react-aria'; +import { useARIAButtonShorthand } from '@fluentui/react-aria'; +import { Enter, Space } from '@fluentui/keyboard-keys'; const ChevronRightIcon = bundleIcon(ChevronRightFilled, ChevronRightRegular); const ChevronLeftIcon = bundleIcon(ChevronLeftFilled, ChevronLeftRegular); @@ -26,22 +23,30 @@ const ChevronLeftIcon = bundleIcon(ChevronLeftFilled, ChevronLeftRegular); /** * Returns the props and state required to render the component */ -export const useMenuItem_unstable = (props: MenuItemProps, ref: React.Ref): MenuItemState => { +export const useMenuItem_unstable = (props: MenuItemProps, ref: React.Ref>): MenuItemState => { + const isSubmenuTrigger = useMenuTriggerContext_unstable(); + const persistOnClickContext = useMenuContext_unstable(context => context.persistOnItemClick); + const { + as = 'div', + disabled, + disabledFocusable, + hasSubmenu = isSubmenuTrigger, + persistOnClick = persistOnClickContext, + } = props; const hasIcons = useMenuListContext_unstable(context => context.hasIcons); const hasCheckmarks = useMenuListContext_unstable(context => context.hasCheckmarks); const setOpen = useMenuContext_unstable(context => context.setOpen); - const persistOnClickContext = useMenuContext_unstable(context => context.persistOnItemClick); - const dismissedWithKeyboardRef = React.useRef(false); - - const isSubmenuTrigger = useMenuTriggerContext_unstable(); - const hasSubmenu = props.hasSubmenu ?? isSubmenuTrigger; const { dir } = useFluent(); - const innerRef = React.useRef(null); + const innerRef = React.useRef>(null); + const dismissedWithKeyboardRef = React.useRef(false); + + const isDisabled = Boolean(disabled || disabledFocusable); const state: MenuItemState = { hasSubmenu, - ...props, + disabled: isDisabled, + persistOnClick, components: { root: 'div', icon: 'span', @@ -50,13 +55,40 @@ export const useMenuItem_unstable = (props: MenuItemProps, ref: React.Ref>( + { disabled: false, disabledFocusable: isDisabled, as }, + { + required: true, + defaultProps: { + role: 'menuitem', + ...props, + ref: useMergedRefs(ref, innerRef) as React.Ref>, + onKeyDown: useEventCallback(event => { + props.onKeyDown?.(event); + if (!event.isDefaultPrevented() && (event.key === Space || event.key === Enter)) { + dismissedWithKeyboardRef.current = true; + } + }), + onMouseEnter: useEventCallback(event => { + innerRef.current?.focus(); + + props.onMouseEnter?.(event); + }), + onClick: useEventCallback(event => { + if (!hasSubmenu && !persistOnClick) { + setOpen(event, { open: false, keyboard: dismissedWithKeyboardRef.current, bubble: true }); + dismissedWithKeyboardRef.current = false; + } + + props.onClick?.(event); + }), + }, + }, + ), + ), icon: resolveShorthand(props.icon, { required: hasIcons }), checkmark: resolveShorthand(props.checkmark, { required: hasCheckmarks }), submenuIndicator: resolveShorthand(props.submenuIndicator, { @@ -71,52 +103,6 @@ export const useMenuItem_unstable = (props: MenuItemProps, ref: React.Ref { - if (shouldPreventDefaultOnKeyDown(e)) { - if (state.disabled) { - e.preventDefault(); - e.stopPropagation(); - return; - } - - dismissedWithKeyboardRef.current = true; - e.preventDefault(); - (e.target as HTMLElement)?.click(); - } - - onKeyDownOriginal?.(e); - }; - - state.root.onClick = e => { - if (state.disabled) { - e.preventDefault(); - e.stopPropagation(); - return; - } - - let shouldPersist = persistOnClickContext; - // prop wins over context; - if (state.persistOnClick !== undefined && persistOnClickContext !== state.persistOnClick) { - shouldPersist = state.persistOnClick; - } - - if (!hasSubmenu && !shouldPersist) { - setOpen(e, { open: false, keyboard: dismissedWithKeyboardRef.current, bubble: true }); - dismissedWithKeyboardRef.current = false; - } - - onClickOriginal?.(e); - }; - - const { onMouseEnter: onMouseEnterOriginal } = state.root; - state.root.onMouseEnter = useEventCallback(e => { - innerRef.current?.focus(); - - onMouseEnterOriginal?.(e); - }); - useCharacterSearch(state, innerRef); return state; }; diff --git a/packages/react-components/react-menu/src/components/MenuItem/useMenuItemStyles.ts b/packages/react-components/react-menu/src/components/MenuItem/useMenuItemStyles.ts index 181c21cbfaa6c5..aef4ebebb075a5 100644 --- a/packages/react-components/react-menu/src/components/MenuItem/useMenuItemStyles.ts +++ b/packages/react-components/react-menu/src/components/MenuItem/useMenuItemStyles.ts @@ -3,7 +3,7 @@ import { iconFilledClassName, iconRegularClassName } from '@fluentui/react-icons import { createFocusOutlineStyle } from '@fluentui/react-tabster'; import { tokens } from '@fluentui/react-theme'; import { useCheckmarkStyles_unstable } from '../../selectable/index'; -import { MenuItemCheckboxState } from '../MenuItemCheckbox/index'; +import type { MenuItemCheckboxState } from '../MenuItemCheckbox/index'; import type { MenuItemSlots, MenuItemState } from './MenuItem.types'; import type { SlotClassNames } from '@fluentui/react-utilities'; @@ -18,6 +18,20 @@ export const menuItemClassNames: SlotClassNames = { const useStyles = makeStyles({ focusIndicator: createFocusOutlineStyle(), + // TODO: this should be extracted to another package + resetButton: { + boxSizing: 'content-box', + backgroundColor: 'inherit', + color: 'inherit', + fontFamily: 'inherit', + fontSize: 'inherit', + lineHeight: 'normal', + ...shorthands.borderColor('transparent'), + ...shorthands.overflow('visible'), + ...shorthands.padding(0), + WebkitAppearance: 'button', + textAlign: 'unset', + }, root: { ...shorthands.borderRadius(tokens.borderRadiusMedium), position: 'relative', @@ -101,6 +115,7 @@ export const useMenuItemStyles_unstable = (state: MenuItemState) => { const styles = useStyles(); state.root.className = mergeClasses( menuItemClassNames.root, + state.isNativeButton && styles.resetButton, styles.root, styles.focusIndicator, state.disabled && styles.disabled, diff --git a/packages/react-components/react-menu/src/components/MenuItemCheckbox/MenuItemCheckbox.test.tsx b/packages/react-components/react-menu/src/components/MenuItemCheckbox/MenuItemCheckbox.test.tsx index bf02772003f98d..9d74e3676875aa 100644 --- a/packages/react-components/react-menu/src/components/MenuItemCheckbox/MenuItemCheckbox.test.tsx +++ b/packages/react-components/react-menu/src/components/MenuItemCheckbox/MenuItemCheckbox.test.tsx @@ -7,11 +7,12 @@ import { MenuItemCheckbox } from './MenuItemCheckbox'; import { MenuListProvider } from '../../contexts/menuListContext'; import { mockUseMenuContext } from '../../common/mockUseMenuContext'; import type { MenuListContextValue } from '../../contexts/menuListContext'; +import type { MenuItemCheckboxProps } from './MenuItemCheckbox.types'; jest.mock('../../contexts/menuContext'); describe('MenuItemCheckbox conformance', () => { - isConformant({ + isConformant({ Component: MenuItemCheckbox, requiredProps: { name: 'checkbox', diff --git a/packages/react-components/react-menu/src/components/MenuItemCheckbox/__snapshots__/MenuItemCheckbox.test.tsx.snap b/packages/react-components/react-menu/src/components/MenuItemCheckbox/__snapshots__/MenuItemCheckbox.test.tsx.snap index 60f2445d7e0dd7..11ef6ab00e2895 100644 --- a/packages/react-components/react-menu/src/components/MenuItemCheckbox/__snapshots__/MenuItemCheckbox.test.tsx.snap +++ b/packages/react-components/react-menu/src/components/MenuItemCheckbox/__snapshots__/MenuItemCheckbox.test.tsx.snap @@ -7,6 +7,7 @@ exports[`MenuItemCheckbox conformance renders a default state 1`] = ` name="checkbox" onClick={[Function]} onKeyDown={[Function]} + onKeyUp={[Function]} onMouseEnter={[Function]} role="menuitemcheckbox" tabIndex={0} diff --git a/packages/react-components/react-menu/src/components/MenuItemCheckbox/useMenuItemCheckbox.tsx b/packages/react-components/react-menu/src/components/MenuItemCheckbox/useMenuItemCheckbox.tsx index 14e4acb2f8e6c9..426ce40b6d1e81 100644 --- a/packages/react-components/react-menu/src/components/MenuItemCheckbox/useMenuItemCheckbox.tsx +++ b/packages/react-components/react-menu/src/components/MenuItemCheckbox/useMenuItemCheckbox.tsx @@ -4,45 +4,42 @@ import { Checkmark16Filled } from '@fluentui/react-icons'; import { useMenuListContext_unstable } from '../../contexts/menuListContext'; import { useMenuItem_unstable } from '../MenuItem/useMenuItem'; import type { MenuItemCheckboxProps, MenuItemCheckboxState } from './MenuItemCheckbox.types'; +import type { ARIAButtonElement, ARIAButtonElementIntersection } from '@fluentui/react-aria'; /** Returns the props and state required to render the component */ export const useMenuItemCheckbox_unstable = ( props: MenuItemCheckboxProps, - ref: React.Ref, + ref: React.Ref>, ): MenuItemCheckboxState => { - const state = useMenuItem_unstable( - { - role: 'menuitemcheckbox', - persistOnClick: true, - ...props, - checkmark: resolveShorthand(props.checkmark, { - defaultProps: { children: }, - required: true, - }), - }, - ref, - ) as MenuItemCheckboxState; - const toggleCheckbox = useMenuListContext_unstable(context => context.toggleCheckbox); - const { onClick: onClickOriginal } = state.root; + const { name, value } = props; + const checked = useMenuListContext_unstable(context => { - const checkedItems = context.checkedValues?.[state.name] || []; - return checkedItems.indexOf(state.value) !== -1; + const checkedItems = context.checkedValues?.[name] || []; + return checkedItems.indexOf(value) !== -1; }); - state.checked = checked; - state.root['aria-checked'] = state.checked; - - // MenuItem state already transforms keyDown to click events - state.root.onClick = e => { - if (state.disabled) { - e.preventDefault(); - e.stopPropagation(); - return; - } - - toggleCheckbox?.(e, state.name, state.value, state.checked); - onClickOriginal?.(e); + const state: MenuItemCheckboxState = { + ...useMenuItem_unstable( + { + role: 'menuitemcheckbox', + persistOnClick: true, + ...props, + 'aria-checked': checked, + checkmark: resolveShorthand(props.checkmark, { + defaultProps: { children: }, + required: true, + }), + onClick: (e: React.MouseEvent>) => { + toggleCheckbox?.(e, name, value, checked); + props.onClick?.(e); + }, + }, + ref, + ), + name, + value, + checked, }; return state; diff --git a/packages/react-components/react-menu/src/components/MenuItemCheckbox/useMenuItemCheckboxStyles.ts b/packages/react-components/react-menu/src/components/MenuItemCheckbox/useMenuItemCheckboxStyles.ts index c15df7f5f74f9a..fbd280b4ecb398 100644 --- a/packages/react-components/react-menu/src/components/MenuItemCheckbox/useMenuItemCheckboxStyles.ts +++ b/packages/react-components/react-menu/src/components/MenuItemCheckbox/useMenuItemCheckboxStyles.ts @@ -1,8 +1,8 @@ -import type { SlotClassNames } from '@fluentui/react-utilities'; import { mergeClasses } from '@griffel/react'; import { useCheckmarkStyles_unstable } from '../../selectable/index'; -import { MenuItemSlots } from '../index'; import { useMenuItemStyles_unstable } from '../MenuItem/useMenuItemStyles'; +import type { SlotClassNames } from '@fluentui/react-utilities'; +import type { MenuItemSlots } from '../index'; import type { MenuItemCheckboxState } from './MenuItemCheckbox.types'; export const menuItemCheckboxClassNames: SlotClassNames> = { diff --git a/packages/react-components/react-menu/src/components/MenuItemRadio/MenuItemRadio.test.tsx b/packages/react-components/react-menu/src/components/MenuItemRadio/MenuItemRadio.test.tsx index accb416c5a2e0b..99c06620852fc7 100644 --- a/packages/react-components/react-menu/src/components/MenuItemRadio/MenuItemRadio.test.tsx +++ b/packages/react-components/react-menu/src/components/MenuItemRadio/MenuItemRadio.test.tsx @@ -6,9 +6,10 @@ import { MenuItemRadio } from './MenuItemRadio'; import { isConformant } from '../../common/isConformant'; import { MenuListProvider } from '../../contexts/menuListContext'; import type { MenuListContextValue } from '../../contexts/menuListContext'; +import type { MenuItemRadioProps } from './MenuItemRadio.types'; describe('MenuItemRadio', () => { - isConformant({ + isConformant({ Component: MenuItemRadio, requiredProps: { name: 'radio', diff --git a/packages/react-components/react-menu/src/components/MenuItemRadio/__snapshots__/MenuItemRadio.test.tsx.snap b/packages/react-components/react-menu/src/components/MenuItemRadio/__snapshots__/MenuItemRadio.test.tsx.snap index ead6b7f87d85ab..e226fa65f94b52 100644 --- a/packages/react-components/react-menu/src/components/MenuItemRadio/__snapshots__/MenuItemRadio.test.tsx.snap +++ b/packages/react-components/react-menu/src/components/MenuItemRadio/__snapshots__/MenuItemRadio.test.tsx.snap @@ -7,6 +7,7 @@ exports[`MenuItemRadio renders a default state 1`] = ` name="radio" onClick={[Function]} onKeyDown={[Function]} + onKeyUp={[Function]} onMouseEnter={[Function]} role="menuitemradio" tabIndex={0} diff --git a/packages/react-components/react-menu/src/components/MenuItemRadio/useMenuItemRadio.tsx b/packages/react-components/react-menu/src/components/MenuItemRadio/useMenuItemRadio.tsx index ee0f0f85df279e..b8e89dc42ce230 100644 --- a/packages/react-components/react-menu/src/components/MenuItemRadio/useMenuItemRadio.tsx +++ b/packages/react-components/react-menu/src/components/MenuItemRadio/useMenuItemRadio.tsx @@ -4,51 +4,43 @@ import { Checkmark16Filled } from '@fluentui/react-icons'; import { useMenuListContext_unstable } from '../../contexts/menuListContext'; import { useMenuItem_unstable } from '../MenuItem/useMenuItem'; import type { MenuItemRadioProps, MenuItemRadioState } from './MenuItemRadio.types'; +import type { ARIAButtonElement, ARIAButtonElementIntersection } from '@fluentui/react-aria'; /** * Given user props, returns state and render function for a MenuItemRadio. */ export const useMenuItemRadio_unstable = ( props: MenuItemRadioProps, - ref: React.Ref, + ref: React.Ref>, ): MenuItemRadioState => { - const radioProps = { - role: 'menuitemradio', - }; - - const state = useMenuItem_unstable( - { - ...radioProps, - ...props, - checkmark: resolveShorthand(props.checkmark, { - defaultProps: { children: }, - required: true, - }), - }, - ref, - ) as MenuItemRadioState; + const { name, value } = props; - const selectRadio = useMenuListContext_unstable(context => context.selectRadio); - const { onClick: onClickOriginal } = state.root; const checked = useMenuListContext_unstable(context => { - const checkedItems = context.checkedValues?.[state.name] || []; - return checkedItems.indexOf(state.value) !== -1; + const checkedItems = context.checkedValues?.[name] || []; + return checkedItems.indexOf(value) !== -1; }); - state.checked = checked; - state.root['aria-checked'] = state.checked; - - // MenuItem state already transforms keyDown to click events - state.root.onClick = e => { - if (state.disabled) { - e.preventDefault(); - e.stopPropagation(); - return; - } + const selectRadio = useMenuListContext_unstable(context => context.selectRadio); - selectRadio?.(e, state.name, state.value, state.checked); - onClickOriginal?.(e); + return { + ...useMenuItem_unstable( + { + ...props, + role: 'menuitemradio', + 'aria-checked': checked, + checkmark: resolveShorthand(props.checkmark, { + defaultProps: { children: }, + required: true, + }), + onClick: (e: React.MouseEvent>) => { + selectRadio?.(e, name, value, checked); + props.onClick?.(e); + }, + }, + ref, + ), + checked, + name, + value, }; - - return state; }; diff --git a/packages/react-components/react-menu/src/components/MenuItemRadio/useMenuItemRadioStyles.ts b/packages/react-components/react-menu/src/components/MenuItemRadio/useMenuItemRadioStyles.ts index 18acac6c2758bc..7da6c66881a013 100644 --- a/packages/react-components/react-menu/src/components/MenuItemRadio/useMenuItemRadioStyles.ts +++ b/packages/react-components/react-menu/src/components/MenuItemRadio/useMenuItemRadioStyles.ts @@ -1,8 +1,8 @@ -import type { SlotClassNames } from '@fluentui/react-utilities'; import { mergeClasses } from '@griffel/react'; import { useCheckmarkStyles_unstable } from '../../selectable/index'; -import { MenuItemSlots } from '../index'; import { useMenuItemStyles_unstable } from '../MenuItem/useMenuItemStyles'; +import type { SlotClassNames } from '@fluentui/react-utilities'; +import type { MenuItemSlots } from '../index'; import type { MenuItemRadioState } from './MenuItemRadio.types'; export const menuItemRadioClassNames: SlotClassNames> = { diff --git a/packages/react-components/react-menu/src/selectable/types.ts b/packages/react-components/react-menu/src/selectable/types.ts index 9b2412c2a2a5d4..a4ba3cd381b494 100644 --- a/packages/react-components/react-menu/src/selectable/types.ts +++ b/packages/react-components/react-menu/src/selectable/types.ts @@ -28,16 +28,6 @@ export type MenuItemSelectableProps = { * State for selectable menu items */ export type MenuItemSelectableState = MenuItemSelectableProps & { - /** - * Checked items for a value with `name` - */ - checkedItems: string[]; - - /** - * Callback when checked items changes for a given value with `name` - */ - onCheckedValueChange: (e: React.MouseEvent | React.KeyboardEvent, name: string, checkedItems: string[]) => void; - /** * Selectable is checked */ diff --git a/packages/react-components/react-overflow/src/stories/Overflow/utils.stories.tsx b/packages/react-components/react-overflow/src/stories/Overflow/utils.stories.tsx index d139f8ef1209b3..19d7439f794da0 100644 --- a/packages/react-components/react-overflow/src/stories/Overflow/utils.stories.tsx +++ b/packages/react-components/react-overflow/src/stories/Overflow/utils.stories.tsx @@ -84,7 +84,8 @@ export const TestOverflowMenuItem: React.FC = props = return null; } - return Item {id}; + // As an union between button props and div props may be conflicting, casting is required + return Item {id}; }; export const TestOverflowMenuDivider: React.FC<{ diff --git a/packages/react-components/react-tabs/src/stories/Tabs/TabListWithOverflow.stories.tsx b/packages/react-components/react-tabs/src/stories/Tabs/TabListWithOverflow.stories.tsx index b072106985ffc9..bab394eb2a9f6e 100644 --- a/packages/react-components/react-tabs/src/stories/Tabs/TabListWithOverflow.stories.tsx +++ b/packages/react-components/react-tabs/src/stories/Tabs/TabListWithOverflow.stories.tsx @@ -27,6 +27,7 @@ import { MoreHorizontalRegular, } from '@fluentui/react-icons'; import { useIsOverflowItemVisible, useOverflowMenu, Overflow, OverflowItem } from '@fluentui/react-overflow'; +import type { ARIAButtonElement } from '@fluentui/react-aria'; //----- Example Tab Data -----// @@ -88,7 +89,7 @@ const tabs: ExampleTab[] = [ type OverflowMenuItemProps = { tab: ExampleTab; - onClick: React.MouseEventHandler; + onClick: React.MouseEventHandler>; }; /** diff --git a/packages/react-components/react-toolbar/src/stories/Toolbar/ToolbarOverflow.stories.tsx b/packages/react-components/react-toolbar/src/stories/Toolbar/ToolbarOverflow.stories.tsx index 68e2a45b532927..922622d6775ecd 100644 --- a/packages/react-components/react-toolbar/src/stories/Toolbar/ToolbarOverflow.stories.tsx +++ b/packages/react-components/react-toolbar/src/stories/Toolbar/ToolbarOverflow.stories.tsx @@ -41,7 +41,7 @@ export const ToolbarOverflowMenuItem: React.FC = p return }> Italic; } - return Item {id}; + return Item {id}; }; export const ToolbarMenuOverflowDivider: React.FC<{