From 27a5232ac8b70d2741ac20bf3b4f4f59d90f7405 Mon Sep 17 00:00:00 2001 From: Adam Date: Tue, 15 Jul 2025 18:28:00 +0200 Subject: [PATCH 01/13] Add MenuGrid component, not yet complete --- .../react-menu/library/src/MenuGrid.ts | 9 +++ .../src/components/MenuGrid/MenuGrid.tsx | 24 ++++++++ .../src/components/MenuGrid/MenuGrid.types.ts | 14 +++++ .../library/src/components/MenuGrid/index.ts | 6 ++ .../components/MenuGrid/renderMenuGrid.tsx | 18 ++++++ .../src/components/MenuGrid/useMenuGrid.ts | 61 +++++++++++++++++++ .../MenuGrid/useMenuGridContextValues.ts | 8 +++ .../MenuGrid/useMenuGridStyles.styles.ts | 29 +++++++++ .../library/src/components/index.ts | 11 ++++ .../library/src/contexts/menuGridContext.tsx | 18 ++++++ .../react-menu/library/src/index.ts | 13 ++++ .../src/MenuGrid/MenuGridDefault.stories.tsx | 28 +++++++++ .../src/MenuGrid/MenuGridDescription.md | 1 + .../stories/src/MenuGrid/index.stories.tsx | 36 +++++++++++ .../CustomStyleHooksContext.ts | 1 + 15 files changed, 277 insertions(+) create mode 100644 packages/react-components/react-menu/library/src/MenuGrid.ts create mode 100644 packages/react-components/react-menu/library/src/components/MenuGrid/MenuGrid.tsx create mode 100644 packages/react-components/react-menu/library/src/components/MenuGrid/MenuGrid.types.ts create mode 100644 packages/react-components/react-menu/library/src/components/MenuGrid/index.ts create mode 100644 packages/react-components/react-menu/library/src/components/MenuGrid/renderMenuGrid.tsx create mode 100644 packages/react-components/react-menu/library/src/components/MenuGrid/useMenuGrid.ts create mode 100644 packages/react-components/react-menu/library/src/components/MenuGrid/useMenuGridContextValues.ts create mode 100644 packages/react-components/react-menu/library/src/components/MenuGrid/useMenuGridStyles.styles.ts create mode 100644 packages/react-components/react-menu/library/src/contexts/menuGridContext.tsx create mode 100644 packages/react-components/react-menu/stories/src/MenuGrid/MenuGridDefault.stories.tsx create mode 100644 packages/react-components/react-menu/stories/src/MenuGrid/MenuGridDescription.md create mode 100644 packages/react-components/react-menu/stories/src/MenuGrid/index.stories.tsx diff --git a/packages/react-components/react-menu/library/src/MenuGrid.ts b/packages/react-components/react-menu/library/src/MenuGrid.ts new file mode 100644 index 00000000000000..20529e8392d518 --- /dev/null +++ b/packages/react-components/react-menu/library/src/MenuGrid.ts @@ -0,0 +1,9 @@ +export type { MenuGridContextValues, MenuGridProps, MenuGridSlots, MenuGridState } from './components/MenuGrid/index'; +export { + MenuGrid, + menuGridClassNames, + renderMenuGrid_unstable, + useMenuGridContextValues_unstable, + useMenuGridStyles_unstable, + useMenuGrid_unstable, +} from './components/MenuGrid/index'; diff --git a/packages/react-components/react-menu/library/src/components/MenuGrid/MenuGrid.tsx b/packages/react-components/react-menu/library/src/components/MenuGrid/MenuGrid.tsx new file mode 100644 index 00000000000000..0eafbbdf7b2c67 --- /dev/null +++ b/packages/react-components/react-menu/library/src/components/MenuGrid/MenuGrid.tsx @@ -0,0 +1,24 @@ +import * as React from 'react'; +import { useMenuGrid_unstable } from './useMenuGrid'; +import { renderMenuGrid_unstable } from './renderMenuGrid'; +import { useMenuGridContextValues_unstable } from './useMenuGridContextValues'; +import { useMenuGridStyles_unstable } from './useMenuGridStyles.styles'; +import type { MenuGridProps } from './MenuGrid.types'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { useCustomStyleHook_unstable } from '@fluentui/react-shared-contexts'; + +/** + * Define a styled MenuGrid, using the `useMenuGrid_unstable` hook. + */ +export const MenuGrid: ForwardRefComponent = React.forwardRef((props, ref) => { + const state = useMenuGrid_unstable(props, ref); + const contextValues = useMenuGridContextValues_unstable(state); + + useMenuGridStyles_unstable(state); + + useCustomStyleHook_unstable('useMenuGridStyles_unstable')(state); + + return renderMenuGrid_unstable(state, contextValues); +}); + +MenuGrid.displayName = 'MenuGrid'; diff --git a/packages/react-components/react-menu/library/src/components/MenuGrid/MenuGrid.types.ts b/packages/react-components/react-menu/library/src/components/MenuGrid/MenuGrid.types.ts new file mode 100644 index 00000000000000..7c8b17622df2b7 --- /dev/null +++ b/packages/react-components/react-menu/library/src/components/MenuGrid/MenuGrid.types.ts @@ -0,0 +1,14 @@ +import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities'; +import type { MenuGridContextValue } from '../../contexts/menuGridContext'; + +export type MenuGridSlots = { + root: Slot<'div'>; +}; + +export type MenuGridProps = ComponentProps & {}; + +export type MenuGridState = ComponentState; + +export type MenuGridContextValues = { + menuGrid: MenuGridContextValue; +}; diff --git a/packages/react-components/react-menu/library/src/components/MenuGrid/index.ts b/packages/react-components/react-menu/library/src/components/MenuGrid/index.ts new file mode 100644 index 00000000000000..ce3c76a759b31a --- /dev/null +++ b/packages/react-components/react-menu/library/src/components/MenuGrid/index.ts @@ -0,0 +1,6 @@ +export { MenuGrid } from './MenuGrid'; +export type { MenuGridContextValues, MenuGridProps, MenuGridSlots, MenuGridState } from './MenuGrid.types'; +export { renderMenuGrid_unstable } from './renderMenuGrid'; +export { useMenuGrid_unstable } from './useMenuGrid'; +export { menuGridClassNames, useMenuGridStyles_unstable } from './useMenuGridStyles.styles'; +export { useMenuGridContextValues_unstable } from './useMenuGridContextValues'; diff --git a/packages/react-components/react-menu/library/src/components/MenuGrid/renderMenuGrid.tsx b/packages/react-components/react-menu/library/src/components/MenuGrid/renderMenuGrid.tsx new file mode 100644 index 00000000000000..3d2ae4615e1c65 --- /dev/null +++ b/packages/react-components/react-menu/library/src/components/MenuGrid/renderMenuGrid.tsx @@ -0,0 +1,18 @@ +/** @jsxRuntime automatic */ +/** @jsxImportSource @fluentui/react-jsx-runtime */ +import { assertSlots } from '@fluentui/react-utilities'; +import { MenuGridContextValues, MenuGridSlots, MenuGridState } from './MenuGrid.types'; +import { MenuGridProvider } from '../../contexts/menuGridContext'; + +/** + * Function that renders the final JSX of the component + */ +export const renderMenuGrid_unstable = (state: MenuGridState, contextValues: MenuGridContextValues) => { + assertSlots(state); + + return ( + + + + ); +}; diff --git a/packages/react-components/react-menu/library/src/components/MenuGrid/useMenuGrid.ts b/packages/react-components/react-menu/library/src/components/MenuGrid/useMenuGrid.ts new file mode 100644 index 00000000000000..6ef593a63ebc1c --- /dev/null +++ b/packages/react-components/react-menu/library/src/components/MenuGrid/useMenuGrid.ts @@ -0,0 +1,61 @@ +import * as React from 'react'; +import { useMergedRefs, getIntrinsicElementProps, slot } from '@fluentui/react-utilities'; +import { + useArrowNavigationGroup, + 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 { MenuContext } from '../../contexts/menuContext'; +import type { MenuGridProps, MenuGridState } from './MenuGrid.types'; + +/** + * Returns the props and state required to render the component + */ +export const useMenuGrid_unstable = (props: MenuGridProps, ref: React.Ref): MenuGridState => { + const { targetDocument } = useFluent(); + const hasMenuContext = useHasParentContext(MenuContext); + const focusAttributes = useArrowNavigationGroup({ circular: true }); + + const innerRef = React.useRef(null); + + React.useEffect(() => { + const element = innerRef.current; + + if (hasMenuContext && targetDocument && element) { + const onTabsterMoveFocus = (e: TabsterMoveFocusEvent) => { + const nextElement = e.detail.next; + + if (nextElement && element.contains(targetDocument.activeElement) && !element.contains(nextElement)) { + // Preventing Tabster from handling Tab press, useMenuPopover will handle it. + e.preventDefault(); + } + }; + + targetDocument.addEventListener(TabsterMoveFocusEventName, onTabsterMoveFocus); + + return () => { + targetDocument.removeEventListener(TabsterMoveFocusEventName, onTabsterMoveFocus); + }; + } + }, [innerRef, targetDocument, hasMenuContext]); + + return { + components: { + root: 'div', + }, + 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, + role: 'menu', + ...focusAttributes, + ...props, + }), + { elementType: 'div' }, + ), + }; +}; diff --git a/packages/react-components/react-menu/library/src/components/MenuGrid/useMenuGridContextValues.ts b/packages/react-components/react-menu/library/src/components/MenuGrid/useMenuGridContextValues.ts new file mode 100644 index 00000000000000..14687de5458b81 --- /dev/null +++ b/packages/react-components/react-menu/library/src/components/MenuGrid/useMenuGridContextValues.ts @@ -0,0 +1,8 @@ +import type { MenuGridContextValues, MenuGridState } from './MenuGrid.types'; + +export function useMenuGridContextValues_unstable(state: MenuGridState): MenuGridContextValues { + // This context is created with "@fluentui/react-context-selector", these is no sense to memoize it + const menuGrid = {}; + + return { menuGrid }; +} diff --git a/packages/react-components/react-menu/library/src/components/MenuGrid/useMenuGridStyles.styles.ts b/packages/react-components/react-menu/library/src/components/MenuGrid/useMenuGridStyles.styles.ts new file mode 100644 index 00000000000000..a4fd2f3cb1df62 --- /dev/null +++ b/packages/react-components/react-menu/library/src/components/MenuGrid/useMenuGridStyles.styles.ts @@ -0,0 +1,29 @@ +import type { SlotClassNames } from '@fluentui/react-utilities'; +import { mergeClasses, makeStyles } from '@griffel/react'; +import type { MenuGridSlots, MenuGridState } from './MenuGrid.types'; + +export const menuGridClassNames: SlotClassNames = { + root: 'fui-MenuGrid', +}; + +const useStyles = makeStyles({ + root: { + display: 'flex', + flexDirection: 'column', + gap: '2px', + }, + hasMenuContext: { + height: '100%', + }, +}); + +/** + * Apply styling to the Menu slots based on the state + */ +export const useMenuGridStyles_unstable = (state: MenuGridState): MenuGridState => { + 'use no memo'; + + const styles = useStyles(); + state.root.className = mergeClasses(menuGridClassNames.root, styles.root, state.root.className); + return state; +}; diff --git a/packages/react-components/react-menu/library/src/components/index.ts b/packages/react-components/react-menu/library/src/components/index.ts index eb7e7cd5f0785b..df18032a1348e0 100644 --- a/packages/react-components/react-menu/library/src/components/index.ts +++ b/packages/react-components/react-menu/library/src/components/index.ts @@ -6,6 +6,17 @@ export { useMenuItemStyles_unstable, useMenuItem_unstable, } from './MenuItem/index'; + +export type { MenuGridContextValues, MenuGridProps, MenuGridSlots, MenuGridState } from './MenuGrid/index'; +export { + MenuGrid, + menuGridClassNames, + renderMenuGrid_unstable, + useMenuGridContextValues_unstable, + useMenuGridStyles_unstable, + useMenuGrid_unstable, +} from './MenuGrid/index'; + export type { MenuCheckedValueChangeData, MenuCheckedValueChangeEvent, diff --git a/packages/react-components/react-menu/library/src/contexts/menuGridContext.tsx b/packages/react-components/react-menu/library/src/contexts/menuGridContext.tsx new file mode 100644 index 00000000000000..088c1ae9b8be9b --- /dev/null +++ b/packages/react-components/react-menu/library/src/contexts/menuGridContext.tsx @@ -0,0 +1,18 @@ +import { createContext, useContextSelector } from '@fluentui/react-context-selector'; +import type { ContextSelector, Context } from '@fluentui/react-context-selector'; + +export const MenuGridContext: Context = createContext( + undefined, +) as Context; + +const menuGridContextDefaultValue: MenuGridContextValue = {}; + +/** + * Context shared between MenuGrid and its children components + */ +export type MenuGridContextValue = {}; + +export const MenuGridProvider = MenuGridContext.Provider; + +export const useMenuGridContext_unstable = (selector: ContextSelector) => + useContextSelector(MenuGridContext, (ctx = menuGridContextDefaultValue) => selector(ctx)); diff --git a/packages/react-components/react-menu/library/src/index.ts b/packages/react-components/react-menu/library/src/index.ts index 1d2d53e93f1fa0..a2eca6ab85a466 100644 --- a/packages/react-components/react-menu/library/src/index.ts +++ b/packages/react-components/react-menu/library/src/index.ts @@ -3,6 +3,8 @@ export type { MenuContextValue } from './contexts/menuContext'; export { MenuTriggerContextProvider, useMenuTriggerContext_unstable } from './contexts/menuTriggerContext'; export { MenuGroupContextProvider, useMenuGroupContext_unstable } from './contexts/menuGroupContext'; export type { MenuGroupContextValue } from './contexts/menuGroupContext'; +export { MenuGridProvider, useMenuGridContext_unstable } from './contexts/menuGridContext'; +export type { MenuGridContextValue } from './contexts/menuGridContext'; export { MenuListProvider, useMenuListContext_unstable } from './contexts/menuListContext'; export type { MenuListContextValue } from './contexts/menuListContext'; @@ -67,6 +69,17 @@ export { useMenuItemRadio_unstable, } from './MenuItemRadio'; export type { MenuItemRadioProps, MenuItemRadioState } from './MenuItemRadio'; + +export { + MenuGrid, + menuGridClassNames, + renderMenuGrid_unstable, + useMenuGridContextValues_unstable, + useMenuGridStyles_unstable, + useMenuGrid_unstable, +} from './MenuGrid'; +export type { MenuGridContextValues, MenuGridProps, MenuGridSlots, MenuGridState } from './MenuGrid'; + export { MenuList, menuListClassNames, diff --git a/packages/react-components/react-menu/stories/src/MenuGrid/MenuGridDefault.stories.tsx b/packages/react-components/react-menu/stories/src/MenuGrid/MenuGridDefault.stories.tsx new file mode 100644 index 00000000000000..33a8df06eae549 --- /dev/null +++ b/packages/react-components/react-menu/stories/src/MenuGrid/MenuGridDefault.stories.tsx @@ -0,0 +1,28 @@ +import * as React from 'react'; +import { makeStyles, tokens, MenuList, MenuItem } from '@fluentui/react-components'; + +const useMenuListContainerStyles = makeStyles({ + container: { + backgroundColor: tokens.colorNeutralBackground1, + minWidth: '128px', + minHeight: '48px', + maxWidth: '300px', + width: 'max-content', + boxShadow: `${tokens.shadow16}`, + paddingTop: '4px', + paddingBottom: '4px', + }, +}); + +export const Default = () => { + const styles = useMenuListContainerStyles(); + return ( +
+ + Cut + Paste + Edit + +
+ ); +}; diff --git a/packages/react-components/react-menu/stories/src/MenuGrid/MenuGridDescription.md b/packages/react-components/react-menu/stories/src/MenuGrid/MenuGridDescription.md new file mode 100644 index 00000000000000..c70a09cfd34ff8 --- /dev/null +++ b/packages/react-components/react-menu/stories/src/MenuGrid/MenuGridDescription.md @@ -0,0 +1 @@ +A menu list displays a list of actions. It is usually rendered inside of the Menu component. diff --git a/packages/react-components/react-menu/stories/src/MenuGrid/index.stories.tsx b/packages/react-components/react-menu/stories/src/MenuGrid/index.stories.tsx new file mode 100644 index 00000000000000..f551ec409327e0 --- /dev/null +++ b/packages/react-components/react-menu/stories/src/MenuGrid/index.stories.tsx @@ -0,0 +1,36 @@ +import { + MenuDivider, + MenuGroup, + MenuGroupHeader, + MenuItem, + MenuItemCheckbox, + MenuItemLink, + MenuItemRadio, + MenuGrid, + MenuSplitGroup, +} from '@fluentui/react-components'; +import descriptionMd from './MenuGridDescription.md'; + +export { Default } from './MenuGridDefault.stories'; + +export default { + title: 'Components/Menu/MenuGrid', + component: MenuGrid, + subcomponents: { + MenuDivider, + MenuGroup, + MenuGroupHeader, + MenuItem, + MenuItemCheckbox, + MenuItemLink, + MenuItemRadio, + MenuSplitGroup, + }, + parameters: { + docs: { + description: { + component: [descriptionMd].join('\n'), + }, + }, + }, +}; diff --git a/packages/react-components/react-shared-contexts/library/src/CustomStyleHooksContext/CustomStyleHooksContext.ts b/packages/react-components/react-shared-contexts/library/src/CustomStyleHooksContext/CustomStyleHooksContext.ts index d8011339c8d3ce..1bcdeba4bc2991 100644 --- a/packages/react-components/react-shared-contexts/library/src/CustomStyleHooksContext/CustomStyleHooksContext.ts +++ b/packages/react-components/react-shared-contexts/library/src/CustomStyleHooksContext/CustomStyleHooksContext.ts @@ -88,6 +88,7 @@ export type CustomStyleHooksContextValue = Partial<{ useListboxStyles_unstable: CustomStyleHook; useMenuButtonStyles_unstable: CustomStyleHook; useMenuDividerStyles_unstable: CustomStyleHook; + useMenuGridStyles_unstable: CustomStyleHook; useMenuGroupHeaderStyles_unstable: CustomStyleHook; useMenuGroupStyles_unstable: CustomStyleHook; useMenuItemCheckboxStyles_unstable: CustomStyleHook; From 98cd41d7ccbc471aa1200d511e323aadc0fe3cba Mon Sep 17 00:00:00 2001 From: Adam Date: Wed, 16 Jul 2025 13:35:32 +0200 Subject: [PATCH 02/13] Create storybook for MenuGrid --- .../src/components/MenuGrid/useMenuGrid.ts | 2 +- .../src/MenuGrid/MenuGridDefault.stories.tsx | 7 +-- .../src/MenuGrid/MenuGridDescription.md | 2 +- .../MenuGrid/MenuGridWithSubmenu.stories.tsx | 48 +++++++++++++++++++ .../stories/src/MenuGrid/index.stories.tsx | 23 ++------- 5 files changed, 58 insertions(+), 24 deletions(-) create mode 100644 packages/react-components/react-menu/stories/src/MenuGrid/MenuGridWithSubmenu.stories.tsx diff --git a/packages/react-components/react-menu/library/src/components/MenuGrid/useMenuGrid.ts b/packages/react-components/react-menu/library/src/components/MenuGrid/useMenuGrid.ts index 6ef593a63ebc1c..5c5426f19321cd 100644 --- a/packages/react-components/react-menu/library/src/components/MenuGrid/useMenuGrid.ts +++ b/packages/react-components/react-menu/library/src/components/MenuGrid/useMenuGrid.ts @@ -51,7 +51,7 @@ export const useMenuGrid_unstable = (props: MenuGridProps, ref: React.Ref, - role: 'menu', + role: 'grid', ...focusAttributes, ...props, }), diff --git a/packages/react-components/react-menu/stories/src/MenuGrid/MenuGridDefault.stories.tsx b/packages/react-components/react-menu/stories/src/MenuGrid/MenuGridDefault.stories.tsx index 33a8df06eae549..9600a30df6be81 100644 --- a/packages/react-components/react-menu/stories/src/MenuGrid/MenuGridDefault.stories.tsx +++ b/packages/react-components/react-menu/stories/src/MenuGrid/MenuGridDefault.stories.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; -import { makeStyles, tokens, MenuList, MenuItem } from '@fluentui/react-components'; +import { makeStyles, tokens, MenuItem } from '@fluentui/react-components'; +import { MenuGrid } from '@fluentui/react-menu'; const useMenuListContainerStyles = makeStyles({ container: { @@ -18,11 +19,11 @@ export const Default = () => { const styles = useMenuListContainerStyles(); return (
- + Cut Paste Edit - +
); }; diff --git a/packages/react-components/react-menu/stories/src/MenuGrid/MenuGridDescription.md b/packages/react-components/react-menu/stories/src/MenuGrid/MenuGridDescription.md index c70a09cfd34ff8..fc29078f893591 100644 --- a/packages/react-components/react-menu/stories/src/MenuGrid/MenuGridDescription.md +++ b/packages/react-components/react-menu/stories/src/MenuGrid/MenuGridDescription.md @@ -1 +1 @@ -A menu list displays a list of actions. It is usually rendered inside of the Menu component. +A menu grid displays a complex menu structured as grid with more actions. It is usually rendered inside of the Menu component. diff --git a/packages/react-components/react-menu/stories/src/MenuGrid/MenuGridWithSubmenu.stories.tsx b/packages/react-components/react-menu/stories/src/MenuGrid/MenuGridWithSubmenu.stories.tsx new file mode 100644 index 00000000000000..ff82b40816c2fc --- /dev/null +++ b/packages/react-components/react-menu/stories/src/MenuGrid/MenuGridWithSubmenu.stories.tsx @@ -0,0 +1,48 @@ +import * as React from 'react'; +import { makeStyles, tokens, MenuList, MenuItem, Menu, MenuPopover, MenuTrigger } from '@fluentui/react-components'; + +const useMenuListContainerStyles = makeStyles({ + container: { + backgroundColor: tokens.colorNeutralBackground1, + minWidth: '128px', + minHeight: '48px', + maxWidth: '300px', + width: 'max-content', + boxShadow: `${tokens.shadow16}`, + paddingTop: '4px', + paddingBottom: '4px', + }, +}); + +export const WithSubmenu = () => { + const styles = useMenuListContainerStyles(); + return ( +
+ + Cut + Paste + Edit + + + Preferences + + + + Cut + Paste + Edit + + + + +
+ ); +}; + +WithSubmenu.parameters = { + docs: { + description: { + story: ['A `MenuGrid` row can open a submenu using a button provided as one of the row actions'].join('\n'), + }, + }, +}; diff --git a/packages/react-components/react-menu/stories/src/MenuGrid/index.stories.tsx b/packages/react-components/react-menu/stories/src/MenuGrid/index.stories.tsx index f551ec409327e0..f1312577f9ed1b 100644 --- a/packages/react-components/react-menu/stories/src/MenuGrid/index.stories.tsx +++ b/packages/react-components/react-menu/stories/src/MenuGrid/index.stories.tsx @@ -1,30 +1,15 @@ -import { - MenuDivider, - MenuGroup, - MenuGroupHeader, - MenuItem, - MenuItemCheckbox, - MenuItemLink, - MenuItemRadio, - MenuGrid, - MenuSplitGroup, -} from '@fluentui/react-components'; +import { MenuGrid } from '@fluentui/react-menu'; import descriptionMd from './MenuGridDescription.md'; export { Default } from './MenuGridDefault.stories'; +export { WithSubmenu } from './MenuGridWithSubmenu.stories'; export default { title: 'Components/Menu/MenuGrid', component: MenuGrid, subcomponents: { - MenuDivider, - MenuGroup, - MenuGroupHeader, - MenuItem, - MenuItemCheckbox, - MenuItemLink, - MenuItemRadio, - MenuSplitGroup, + // MenuGridGroup, + // MenuGridGroupHeader, }, parameters: { docs: { From 7af3674f7531eebf690d41787a5c9d96a921bc03 Mon Sep 17 00:00:00 2001 From: Adam Date: Wed, 16 Jul 2025 16:11:36 +0200 Subject: [PATCH 03/13] Add MenuGridCell component, not yet complete --- .../react-menu/library/src/MenuGridCell.ts | 14 +++++++++++ .../src/components/MenuGrid/useMenuGrid.ts | 2 +- .../components/MenuGridCell/MenuGridCell.tsx | 24 ++++++++++++++++++ .../MenuGridCell/MenuGridCell.types.ts | 14 +++++++++++ .../src/components/MenuGridCell/index.ts | 11 ++++++++ .../MenuGridCell/renderMenuGridCell.tsx | 19 ++++++++++++++ .../MenuGridCell/useMenuGridCell.ts | 25 +++++++++++++++++++ .../useMenuGridCellContextValues.ts | 8 ++++++ .../useMenuGridCellStyles.styles.ts | 15 +++++++++++ .../src/contexts/menuGridCellContext.ts | 16 ++++++++++++ .../react-menu/library/src/index.ts | 16 ++++++++++++ .../src/MenuGrid/MenuGridDefault.stories.tsx | 10 ++++---- .../CustomStyleHooksContext.ts | 1 + 13 files changed, 169 insertions(+), 6 deletions(-) create mode 100644 packages/react-components/react-menu/library/src/MenuGridCell.ts create mode 100644 packages/react-components/react-menu/library/src/components/MenuGridCell/MenuGridCell.tsx create mode 100644 packages/react-components/react-menu/library/src/components/MenuGridCell/MenuGridCell.types.ts create mode 100644 packages/react-components/react-menu/library/src/components/MenuGridCell/index.ts create mode 100644 packages/react-components/react-menu/library/src/components/MenuGridCell/renderMenuGridCell.tsx create mode 100644 packages/react-components/react-menu/library/src/components/MenuGridCell/useMenuGridCell.ts create mode 100644 packages/react-components/react-menu/library/src/components/MenuGridCell/useMenuGridCellContextValues.ts create mode 100644 packages/react-components/react-menu/library/src/components/MenuGridCell/useMenuGridCellStyles.styles.ts create mode 100644 packages/react-components/react-menu/library/src/contexts/menuGridCellContext.ts diff --git a/packages/react-components/react-menu/library/src/MenuGridCell.ts b/packages/react-components/react-menu/library/src/MenuGridCell.ts new file mode 100644 index 00000000000000..627bd74bb85e95 --- /dev/null +++ b/packages/react-components/react-menu/library/src/MenuGridCell.ts @@ -0,0 +1,14 @@ +export type { + MenuGridCellContextValues, + MenuGridCellProps, + MenuGridCellSlots, + MenuGridCellState, +} from './components/MenuGridCell/index'; +export { + MenuGridCell, + menuGridCellClassNames, + renderMenuGridCell_unstable, + useMenuGridCellContextValues_unstable, + useMenuGridCellStyles_unstable, + useMenuGridCell_unstable, +} from './components/MenuGridCell/index'; diff --git a/packages/react-components/react-menu/library/src/components/MenuGrid/useMenuGrid.ts b/packages/react-components/react-menu/library/src/components/MenuGrid/useMenuGrid.ts index 5c5426f19321cd..b7aeead5cef570 100644 --- a/packages/react-components/react-menu/library/src/components/MenuGrid/useMenuGrid.ts +++ b/packages/react-components/react-menu/library/src/components/MenuGrid/useMenuGrid.ts @@ -16,7 +16,7 @@ import type { MenuGridProps, MenuGridState } from './MenuGrid.types'; export const useMenuGrid_unstable = (props: MenuGridProps, ref: React.Ref): MenuGridState => { const { targetDocument } = useFluent(); const hasMenuContext = useHasParentContext(MenuContext); - const focusAttributes = useArrowNavigationGroup({ circular: true }); + const focusAttributes = useArrowNavigationGroup({ axis: 'grid' }); const innerRef = React.useRef(null); diff --git a/packages/react-components/react-menu/library/src/components/MenuGridCell/MenuGridCell.tsx b/packages/react-components/react-menu/library/src/components/MenuGridCell/MenuGridCell.tsx new file mode 100644 index 00000000000000..10e9a4f7a8b04a --- /dev/null +++ b/packages/react-components/react-menu/library/src/components/MenuGridCell/MenuGridCell.tsx @@ -0,0 +1,24 @@ +import * as React from 'react'; +import { useMenuGridCell_unstable } from './useMenuGridCell'; +import { renderMenuGridCell_unstable } from './renderMenuGridCell'; +import { useMenuGridCellContextValues_unstable } from './useMenuGridCellContextValues'; +import type { MenuGridCellProps } from './MenuGridCell.types'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { useMenuGridCellStyles_unstable } from './useMenuGridCellStyles.styles'; +import { useCustomStyleHook_unstable } from '@fluentui/react-shared-contexts'; + +/** + * Define a MenuGridCell, using the `useMenuGridCell_unstable` hook. + */ +export const MenuGridCell: ForwardRefComponent = React.forwardRef((props, ref) => { + const state = useMenuGridCell_unstable(props, ref); + const contextValues = useMenuGridCellContextValues_unstable(state); + + useMenuGridCellStyles_unstable(state); + + useCustomStyleHook_unstable('useMenuGridCellStyles_unstable')(state); + + return renderMenuGridCell_unstable(state, contextValues); +}); + +MenuGridCell.displayName = 'MenuGroup'; diff --git a/packages/react-components/react-menu/library/src/components/MenuGridCell/MenuGridCell.types.ts b/packages/react-components/react-menu/library/src/components/MenuGridCell/MenuGridCell.types.ts new file mode 100644 index 00000000000000..8b30e00869e515 --- /dev/null +++ b/packages/react-components/react-menu/library/src/components/MenuGridCell/MenuGridCell.types.ts @@ -0,0 +1,14 @@ +import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities'; +import type { MenuGridCellContextValue } from '../../contexts/menuGridCellContext'; + +export type MenuGridCellSlots = { + root: Slot<'div'>; +}; + +export type MenuGridCellProps = ComponentProps; + +export type MenuGridCellState = ComponentState; + +export type MenuGridCellContextValues = { + menuGridCell: MenuGridCellContextValue; +}; diff --git a/packages/react-components/react-menu/library/src/components/MenuGridCell/index.ts b/packages/react-components/react-menu/library/src/components/MenuGridCell/index.ts new file mode 100644 index 00000000000000..6962ef35b9c8d9 --- /dev/null +++ b/packages/react-components/react-menu/library/src/components/MenuGridCell/index.ts @@ -0,0 +1,11 @@ +export type { + MenuGridCellContextValues, + MenuGridCellProps, + MenuGridCellSlots, + MenuGridCellState, +} from './MenuGridCell.types'; +export { MenuGridCell } from './MenuGridCell'; +export { renderMenuGridCell_unstable } from './renderMenuGridCell'; +export { useMenuGridCell_unstable } from './useMenuGridCell'; +export { useMenuGridCellContextValues_unstable } from './useMenuGridCellContextValues'; +export { menuGridCellClassNames, useMenuGridCellStyles_unstable } from './useMenuGridCellStyles.styles'; diff --git a/packages/react-components/react-menu/library/src/components/MenuGridCell/renderMenuGridCell.tsx b/packages/react-components/react-menu/library/src/components/MenuGridCell/renderMenuGridCell.tsx new file mode 100644 index 00000000000000..0c3b39771937b6 --- /dev/null +++ b/packages/react-components/react-menu/library/src/components/MenuGridCell/renderMenuGridCell.tsx @@ -0,0 +1,19 @@ +/** @jsxRuntime automatic */ +/** @jsxImportSource @fluentui/react-jsx-runtime */ +import { assertSlots } from '@fluentui/react-utilities'; +import { MenuGridCellContextValues, MenuGridCellSlots, MenuGridCellState } from './MenuGridCell.types'; +import { MenuGridCellContextProvider } from '../../contexts/menuGridCellContext'; + +/** + * Redefine the render function to add slots. Reuse the menugroup structure but add + * slots to children. + */ +export const renderMenuGridCell_unstable = (state: MenuGridCellState, contextValues: MenuGridCellContextValues) => { + assertSlots(state); + + return ( + + + + ); +}; diff --git a/packages/react-components/react-menu/library/src/components/MenuGridCell/useMenuGridCell.ts b/packages/react-components/react-menu/library/src/components/MenuGridCell/useMenuGridCell.ts new file mode 100644 index 00000000000000..30b51ec1b4c62b --- /dev/null +++ b/packages/react-components/react-menu/library/src/components/MenuGridCell/useMenuGridCell.ts @@ -0,0 +1,25 @@ +import * as React from 'react'; +import { getIntrinsicElementProps, slot } from '@fluentui/react-utilities'; +import { MenuGridCellProps, MenuGridCellState } from './MenuGridCell.types'; + +/** + * Given user props, returns state and render function for a MenuGridCell. + */ +export function useMenuGridCell_unstable(props: MenuGridCellProps, ref: React.Ref): MenuGridCellState { + return { + components: { + root: 'div', + }, + 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: ref as React.Ref, + role: 'gridcell', + ...props, + }), + { elementType: 'div' }, + ), + }; +} diff --git a/packages/react-components/react-menu/library/src/components/MenuGridCell/useMenuGridCellContextValues.ts b/packages/react-components/react-menu/library/src/components/MenuGridCell/useMenuGridCellContextValues.ts new file mode 100644 index 00000000000000..e9b67e91d1fd13 --- /dev/null +++ b/packages/react-components/react-menu/library/src/components/MenuGridCell/useMenuGridCellContextValues.ts @@ -0,0 +1,8 @@ +import * as React from 'react'; +import type { MenuGridCellContextValues, MenuGridCellState } from './MenuGridCell.types'; + +export function useMenuGridCellContextValues_unstable(state: MenuGridCellState): MenuGridCellContextValues { + const menuGridCell = React.useMemo(() => ({}), []); + + return { menuGridCell }; +} diff --git a/packages/react-components/react-menu/library/src/components/MenuGridCell/useMenuGridCellStyles.styles.ts b/packages/react-components/react-menu/library/src/components/MenuGridCell/useMenuGridCellStyles.styles.ts new file mode 100644 index 00000000000000..159d9d6164fcfd --- /dev/null +++ b/packages/react-components/react-menu/library/src/components/MenuGridCell/useMenuGridCellStyles.styles.ts @@ -0,0 +1,15 @@ +import type { SlotClassNames } from '@fluentui/react-utilities'; +import { mergeClasses } from '@griffel/react'; +import type { MenuGridCellSlots, MenuGridCellState } from './MenuGridCell.types'; + +export const menuGridCellClassNames: SlotClassNames = { + root: 'fui-MenuGridCell', +}; + +export const useMenuGridCellStyles_unstable = (state: MenuGridCellState): MenuGridCellState => { + 'use no memo'; + + state.root.className = mergeClasses(menuGridCellClassNames.root, state.root.className); + + return state; +}; diff --git a/packages/react-components/react-menu/library/src/contexts/menuGridCellContext.ts b/packages/react-components/react-menu/library/src/contexts/menuGridCellContext.ts new file mode 100644 index 00000000000000..ee3a40f7d2168d --- /dev/null +++ b/packages/react-components/react-menu/library/src/contexts/menuGridCellContext.ts @@ -0,0 +1,16 @@ +import * as React from 'react'; + +const MenuGridCellContext = React.createContext( + undefined, +) as React.Context; + +const menuGridCellContextDefaultValue: MenuGridCellContextValue = {}; + +/** + * Context + */ +export type MenuGridCellContextValue = {}; + +export const MenuGridCellContextProvider = MenuGridCellContext.Provider; +export const useMenuGridCellContext_unstable = () => + React.useContext(MenuGridCellContext) ?? menuGridCellContextDefaultValue; diff --git a/packages/react-components/react-menu/library/src/index.ts b/packages/react-components/react-menu/library/src/index.ts index a2eca6ab85a466..86465a4f7d22b8 100644 --- a/packages/react-components/react-menu/library/src/index.ts +++ b/packages/react-components/react-menu/library/src/index.ts @@ -5,6 +5,8 @@ export { MenuGroupContextProvider, useMenuGroupContext_unstable } from './contex export type { MenuGroupContextValue } from './contexts/menuGroupContext'; export { MenuGridProvider, useMenuGridContext_unstable } from './contexts/menuGridContext'; export type { MenuGridContextValue } from './contexts/menuGridContext'; +export { MenuGridCellContextProvider, useMenuGridCellContext_unstable } from './contexts/menuGridCellContext'; +export type { MenuGridCellContextValue } from './contexts/menuGridCellContext'; export { MenuListProvider, useMenuListContext_unstable } from './contexts/menuListContext'; export type { MenuListContextValue } from './contexts/menuListContext'; @@ -79,6 +81,20 @@ export { useMenuGrid_unstable, } from './MenuGrid'; export type { MenuGridContextValues, MenuGridProps, MenuGridSlots, MenuGridState } from './MenuGrid'; +export { + MenuGridCell, + menuGridCellClassNames, + renderMenuGridCell_unstable, + useMenuGridCellContextValues_unstable, + useMenuGridCellStyles_unstable, + useMenuGridCell_unstable, +} from './MenuGridCell'; +export type { + MenuGridCellContextValues, + MenuGridCellProps, + MenuGridCellSlots, + MenuGridCellState, +} from './MenuGridCell'; export { MenuList, diff --git a/packages/react-components/react-menu/stories/src/MenuGrid/MenuGridDefault.stories.tsx b/packages/react-components/react-menu/stories/src/MenuGrid/MenuGridDefault.stories.tsx index 9600a30df6be81..8a63a12300c9bf 100644 --- a/packages/react-components/react-menu/stories/src/MenuGrid/MenuGridDefault.stories.tsx +++ b/packages/react-components/react-menu/stories/src/MenuGrid/MenuGridDefault.stories.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; -import { makeStyles, tokens, MenuItem } from '@fluentui/react-components'; -import { MenuGrid } from '@fluentui/react-menu'; +import { makeStyles, tokens } from '@fluentui/react-components'; +import { MenuGrid, MenuGridCell } from '@fluentui/react-menu'; const useMenuListContainerStyles = makeStyles({ container: { @@ -20,9 +20,9 @@ export const Default = () => { return (
- Cut - Paste - Edit + Cut + Paste + Edit
); diff --git a/packages/react-components/react-shared-contexts/library/src/CustomStyleHooksContext/CustomStyleHooksContext.ts b/packages/react-components/react-shared-contexts/library/src/CustomStyleHooksContext/CustomStyleHooksContext.ts index 1bcdeba4bc2991..10263fabc8b3cb 100644 --- a/packages/react-components/react-shared-contexts/library/src/CustomStyleHooksContext/CustomStyleHooksContext.ts +++ b/packages/react-components/react-shared-contexts/library/src/CustomStyleHooksContext/CustomStyleHooksContext.ts @@ -89,6 +89,7 @@ export type CustomStyleHooksContextValue = Partial<{ useMenuButtonStyles_unstable: CustomStyleHook; useMenuDividerStyles_unstable: CustomStyleHook; useMenuGridStyles_unstable: CustomStyleHook; + useMenuGridCellStyles_unstable: CustomStyleHook; useMenuGroupHeaderStyles_unstable: CustomStyleHook; useMenuGroupStyles_unstable: CustomStyleHook; useMenuItemCheckboxStyles_unstable: CustomStyleHook; From 7f10d1583514e4a0c43aee29a60c9ab9af5afc16 Mon Sep 17 00:00:00 2001 From: Adam Date: Wed, 16 Jul 2025 18:45:22 +0200 Subject: [PATCH 04/13] Add MenuGridRow and change tabster attributes in story --- .../react-menu/library/src/MenuGridRow.ts | 14 ++++++ .../src/components/MenuGrid/MenuGrid.types.ts | 9 +++- .../src/components/MenuGrid/useMenuGrid.ts | 44 +++---------------- .../MenuGrid/useMenuGridContextValues.ts | 5 ++- .../components/MenuGridRow/MenuGridRow.tsx | 24 ++++++++++ .../MenuGridRow/MenuGridRow.types.ts | 14 ++++++ .../src/components/MenuGridRow/index.ts | 11 +++++ .../MenuGridRow/renderMenuGridRow.tsx | 19 ++++++++ .../components/MenuGridRow/useMenuGridRow.ts | 31 +++++++++++++ .../useMenuGridRowContextValues.ts | 8 ++++ .../useMenuGridRowStyles.styles.ts | 15 +++++++ .../library/src/contexts/menuGridContext.tsx | 20 ++++++--- .../src/contexts/menuGridRowContext.ts | 16 +++++++ .../react-menu/library/src/index.ts | 11 +++++ .../src/MenuGrid/MenuGridDefault.stories.tsx | 25 ++++++++--- .../CustomStyleHooksContext.ts | 1 + 16 files changed, 216 insertions(+), 51 deletions(-) create mode 100644 packages/react-components/react-menu/library/src/MenuGridRow.ts create mode 100644 packages/react-components/react-menu/library/src/components/MenuGridRow/MenuGridRow.tsx create mode 100644 packages/react-components/react-menu/library/src/components/MenuGridRow/MenuGridRow.types.ts create mode 100644 packages/react-components/react-menu/library/src/components/MenuGridRow/index.ts create mode 100644 packages/react-components/react-menu/library/src/components/MenuGridRow/renderMenuGridRow.tsx create mode 100644 packages/react-components/react-menu/library/src/components/MenuGridRow/useMenuGridRow.ts create mode 100644 packages/react-components/react-menu/library/src/components/MenuGridRow/useMenuGridRowContextValues.ts create mode 100644 packages/react-components/react-menu/library/src/components/MenuGridRow/useMenuGridRowStyles.styles.ts create mode 100644 packages/react-components/react-menu/library/src/contexts/menuGridRowContext.ts diff --git a/packages/react-components/react-menu/library/src/MenuGridRow.ts b/packages/react-components/react-menu/library/src/MenuGridRow.ts new file mode 100644 index 00000000000000..f65318a727fe26 --- /dev/null +++ b/packages/react-components/react-menu/library/src/MenuGridRow.ts @@ -0,0 +1,14 @@ +export type { + MenuGridRowContextValues, + MenuGridRowProps, + MenuGridRowSlots, + MenuGridRowState, +} from './components/MenuGridRow/index'; +export { + MenuGridRow, + menuGridRowClassNames, + renderMenuGridRow_unstable, + useMenuGridRowContextValues_unstable, + useMenuGridRowStyles_unstable, + useMenuGridRow_unstable, +} from './components/MenuGridRow/index'; diff --git a/packages/react-components/react-menu/library/src/components/MenuGrid/MenuGrid.types.ts b/packages/react-components/react-menu/library/src/components/MenuGrid/MenuGrid.types.ts index 7c8b17622df2b7..346422a26613f3 100644 --- a/packages/react-components/react-menu/library/src/components/MenuGrid/MenuGrid.types.ts +++ b/packages/react-components/react-menu/library/src/components/MenuGrid/MenuGrid.types.ts @@ -1,4 +1,6 @@ import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities'; +import { TabsterDOMAttribute } from '@fluentui/react-tabster'; + import type { MenuGridContextValue } from '../../contexts/menuGridContext'; export type MenuGridSlots = { @@ -7,7 +9,12 @@ export type MenuGridSlots = { export type MenuGridProps = ComponentProps & {}; -export type MenuGridState = ComponentState; +export type MenuGridState = ComponentState & { + /** + * Tabster row attributes applied to the `MenuGridRow` components + */ + tableRowTabsterAttribute: TabsterDOMAttribute | null; +}; export type MenuGridContextValues = { menuGrid: MenuGridContextValue; diff --git a/packages/react-components/react-menu/library/src/components/MenuGrid/useMenuGrid.ts b/packages/react-components/react-menu/library/src/components/MenuGrid/useMenuGrid.ts index b7aeead5cef570..d46ada04bd968b 100644 --- a/packages/react-components/react-menu/library/src/components/MenuGrid/useMenuGrid.ts +++ b/packages/react-components/react-menu/library/src/components/MenuGrid/useMenuGrid.ts @@ -1,45 +1,13 @@ import * as React from 'react'; -import { useMergedRefs, getIntrinsicElementProps, slot } from '@fluentui/react-utilities'; -import { - useArrowNavigationGroup, - 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 { MenuContext } from '../../contexts/menuContext'; +import { getIntrinsicElementProps, slot } from '@fluentui/react-utilities'; +import { useTableCompositeNavigation } from '@fluentui/react-components'; import type { MenuGridProps, MenuGridState } from './MenuGrid.types'; /** * Returns the props and state required to render the component */ export const useMenuGrid_unstable = (props: MenuGridProps, ref: React.Ref): MenuGridState => { - const { targetDocument } = useFluent(); - const hasMenuContext = useHasParentContext(MenuContext); - const focusAttributes = useArrowNavigationGroup({ axis: 'grid' }); - - const innerRef = React.useRef(null); - - React.useEffect(() => { - const element = innerRef.current; - - if (hasMenuContext && targetDocument && element) { - const onTabsterMoveFocus = (e: TabsterMoveFocusEvent) => { - const nextElement = e.detail.next; - - if (nextElement && element.contains(targetDocument.activeElement) && !element.contains(nextElement)) { - // Preventing Tabster from handling Tab press, useMenuPopover will handle it. - e.preventDefault(); - } - }; - - targetDocument.addEventListener(TabsterMoveFocusEventName, onTabsterMoveFocus); - - return () => { - targetDocument.removeEventListener(TabsterMoveFocusEventName, onTabsterMoveFocus); - }; - } - }, [innerRef, targetDocument, hasMenuContext]); + const { tableRowTabsterAttribute, tableTabsterAttribute, onTableKeyDown } = useTableCompositeNavigation(); return { components: { @@ -50,12 +18,14 @@ export const useMenuGrid_unstable = (props: MenuGridProps, ref: React.Ref, + ref: ref as React.Ref, role: 'grid', - ...focusAttributes, + onKeyDown: onTableKeyDown, + ...tableTabsterAttribute, ...props, }), { elementType: 'div' }, ), + tableRowTabsterAttribute, }; }; diff --git a/packages/react-components/react-menu/library/src/components/MenuGrid/useMenuGridContextValues.ts b/packages/react-components/react-menu/library/src/components/MenuGrid/useMenuGridContextValues.ts index 14687de5458b81..564d3cd723fe9c 100644 --- a/packages/react-components/react-menu/library/src/components/MenuGrid/useMenuGridContextValues.ts +++ b/packages/react-components/react-menu/library/src/components/MenuGrid/useMenuGridContextValues.ts @@ -1,8 +1,9 @@ +import * as React from 'react'; import type { MenuGridContextValues, MenuGridState } from './MenuGrid.types'; export function useMenuGridContextValues_unstable(state: MenuGridState): MenuGridContextValues { - // This context is created with "@fluentui/react-context-selector", these is no sense to memoize it - const menuGrid = {}; + const { tableRowTabsterAttribute } = state; + const menuGrid = React.useMemo(() => ({ tableRowTabsterAttribute }), [tableRowTabsterAttribute]); return { menuGrid }; } diff --git a/packages/react-components/react-menu/library/src/components/MenuGridRow/MenuGridRow.tsx b/packages/react-components/react-menu/library/src/components/MenuGridRow/MenuGridRow.tsx new file mode 100644 index 00000000000000..2c62167901618b --- /dev/null +++ b/packages/react-components/react-menu/library/src/components/MenuGridRow/MenuGridRow.tsx @@ -0,0 +1,24 @@ +import * as React from 'react'; +import { useMenuGridRow_unstable } from './useMenuGridRow'; +import { renderMenuGridRow_unstable } from './renderMenuGridRow'; +import { useMenuGridRowContextValues_unstable } from './useMenuGridRowContextValues'; +import type { MenuGridRowProps } from './MenuGridRow.types'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { useMenuGridRowStyles_unstable } from './useMenuGridRowStyles.styles'; +import { useCustomStyleHook_unstable } from '@fluentui/react-shared-contexts'; + +/** + * Define a MenuGridRow, using the `useMenuGridRow_unstable` hook. + */ +export const MenuGridRow: ForwardRefComponent = React.forwardRef((props, ref) => { + const state = useMenuGridRow_unstable(props, ref); + const contextValues = useMenuGridRowContextValues_unstable(state); + + useMenuGridRowStyles_unstable(state); + + useCustomStyleHook_unstable('useMenuGridRowStyles_unstable')(state); + + return renderMenuGridRow_unstable(state, contextValues); +}); + +MenuGridRow.displayName = 'MenuGroup'; diff --git a/packages/react-components/react-menu/library/src/components/MenuGridRow/MenuGridRow.types.ts b/packages/react-components/react-menu/library/src/components/MenuGridRow/MenuGridRow.types.ts new file mode 100644 index 00000000000000..195f9b2be89e06 --- /dev/null +++ b/packages/react-components/react-menu/library/src/components/MenuGridRow/MenuGridRow.types.ts @@ -0,0 +1,14 @@ +import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities'; +import type { MenuGridRowContextValue } from '../../contexts/menuGridRowContext'; + +export type MenuGridRowSlots = { + root: Slot<'div'>; +}; + +export type MenuGridRowProps = ComponentProps; + +export type MenuGridRowState = ComponentState; + +export type MenuGridRowContextValues = { + menuGridRow: MenuGridRowContextValue; +}; diff --git a/packages/react-components/react-menu/library/src/components/MenuGridRow/index.ts b/packages/react-components/react-menu/library/src/components/MenuGridRow/index.ts new file mode 100644 index 00000000000000..3a1de2af794fc0 --- /dev/null +++ b/packages/react-components/react-menu/library/src/components/MenuGridRow/index.ts @@ -0,0 +1,11 @@ +export type { + MenuGridRowContextValues, + MenuGridRowProps, + MenuGridRowSlots, + MenuGridRowState, +} from './MenuGridRow.types'; +export { MenuGridRow } from './MenuGridRow'; +export { renderMenuGridRow_unstable } from './renderMenuGridRow'; +export { useMenuGridRow_unstable } from './useMenuGridRow'; +export { useMenuGridRowContextValues_unstable } from './useMenuGridRowContextValues'; +export { menuGridRowClassNames, useMenuGridRowStyles_unstable } from './useMenuGridRowStyles.styles'; diff --git a/packages/react-components/react-menu/library/src/components/MenuGridRow/renderMenuGridRow.tsx b/packages/react-components/react-menu/library/src/components/MenuGridRow/renderMenuGridRow.tsx new file mode 100644 index 00000000000000..34d9f1e2de071f --- /dev/null +++ b/packages/react-components/react-menu/library/src/components/MenuGridRow/renderMenuGridRow.tsx @@ -0,0 +1,19 @@ +/** @jsxRuntime automatic */ +/** @jsxImportSource @fluentui/react-jsx-runtime */ +import { assertSlots } from '@fluentui/react-utilities'; +import { MenuGridRowContextValues, MenuGridRowSlots, MenuGridRowState } from './MenuGridRow.types'; +import { MenuGridRowContextProvider } from '../../contexts/menuGridRowContext'; + +/** + * Redefine the render function to add slots. Reuse the menugroup structure but add + * slots to children. + */ +export const renderMenuGridRow_unstable = (state: MenuGridRowState, contextValues: MenuGridRowContextValues) => { + assertSlots(state); + + return ( + + + + ); +}; diff --git a/packages/react-components/react-menu/library/src/components/MenuGridRow/useMenuGridRow.ts b/packages/react-components/react-menu/library/src/components/MenuGridRow/useMenuGridRow.ts new file mode 100644 index 00000000000000..bdbc17bb31e36b --- /dev/null +++ b/packages/react-components/react-menu/library/src/components/MenuGridRow/useMenuGridRow.ts @@ -0,0 +1,31 @@ +import * as React from 'react'; +import { getIntrinsicElementProps, slot } from '@fluentui/react-utilities'; + +import { useMenuGridContext_unstable } from '../../contexts/menuGridContext'; +import { MenuGridRowProps, MenuGridRowState } from './MenuGridRow.types'; + +/** + * Given user props, returns state and render function for a MenuGridRow. + */ +export function useMenuGridRow_unstable(props: MenuGridRowProps, ref: React.Ref): MenuGridRowState { + const { tableRowTabsterAttribute } = useMenuGridContext_unstable(); + + return { + components: { + root: 'div', + }, + 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: ref as React.Ref, + role: 'row', + tabIndex: 0, + ...tableRowTabsterAttribute, + ...props, + }), + { elementType: 'div' }, + ), + }; +} diff --git a/packages/react-components/react-menu/library/src/components/MenuGridRow/useMenuGridRowContextValues.ts b/packages/react-components/react-menu/library/src/components/MenuGridRow/useMenuGridRowContextValues.ts new file mode 100644 index 00000000000000..d8761061ff461d --- /dev/null +++ b/packages/react-components/react-menu/library/src/components/MenuGridRow/useMenuGridRowContextValues.ts @@ -0,0 +1,8 @@ +import * as React from 'react'; +import type { MenuGridRowContextValues, MenuGridRowState } from './MenuGridRow.types'; + +export function useMenuGridRowContextValues_unstable(state: MenuGridRowState): MenuGridRowContextValues { + const menuGridRow = React.useMemo(() => ({}), []); + + return { menuGridRow }; +} diff --git a/packages/react-components/react-menu/library/src/components/MenuGridRow/useMenuGridRowStyles.styles.ts b/packages/react-components/react-menu/library/src/components/MenuGridRow/useMenuGridRowStyles.styles.ts new file mode 100644 index 00000000000000..be4221ab83edb1 --- /dev/null +++ b/packages/react-components/react-menu/library/src/components/MenuGridRow/useMenuGridRowStyles.styles.ts @@ -0,0 +1,15 @@ +import type { SlotClassNames } from '@fluentui/react-utilities'; +import { mergeClasses } from '@griffel/react'; +import type { MenuGridRowSlots, MenuGridRowState } from './MenuGridRow.types'; + +export const menuGridRowClassNames: SlotClassNames = { + root: 'fui-MenuGridRow', +}; + +export const useMenuGridRowStyles_unstable = (state: MenuGridRowState): MenuGridRowState => { + 'use no memo'; + + state.root.className = mergeClasses(menuGridRowClassNames.root, state.root.className); + + return state; +}; diff --git a/packages/react-components/react-menu/library/src/contexts/menuGridContext.tsx b/packages/react-components/react-menu/library/src/contexts/menuGridContext.tsx index 088c1ae9b8be9b..facbd1c12b7531 100644 --- a/packages/react-components/react-menu/library/src/contexts/menuGridContext.tsx +++ b/packages/react-components/react-menu/library/src/contexts/menuGridContext.tsx @@ -1,18 +1,26 @@ -import { createContext, useContextSelector } from '@fluentui/react-context-selector'; -import type { ContextSelector, Context } from '@fluentui/react-context-selector'; +import * as React from 'react'; +import { createContext } from '@fluentui/react-context-selector'; +import type { Context } from '@fluentui/react-context-selector'; +import { TabsterDOMAttribute } from '@fluentui/react-tabster'; export const MenuGridContext: Context = createContext( undefined, ) as Context; -const menuGridContextDefaultValue: MenuGridContextValue = {}; +const menuGridContextDefaultValue: MenuGridContextValue = { + tableRowTabsterAttribute: null, +}; /** * Context shared between MenuGrid and its children components */ -export type MenuGridContextValue = {}; +export type MenuGridContextValue = { + /** + * Tabster row attributes applied to the `MenuGridRow` components + */ + tableRowTabsterAttribute: TabsterDOMAttribute | null; +}; export const MenuGridProvider = MenuGridContext.Provider; -export const useMenuGridContext_unstable = (selector: ContextSelector) => - useContextSelector(MenuGridContext, (ctx = menuGridContextDefaultValue) => selector(ctx)); +export const useMenuGridContext_unstable = () => React.useContext(MenuGridContext) ?? menuGridContextDefaultValue; diff --git a/packages/react-components/react-menu/library/src/contexts/menuGridRowContext.ts b/packages/react-components/react-menu/library/src/contexts/menuGridRowContext.ts new file mode 100644 index 00000000000000..d4abb93919fbe6 --- /dev/null +++ b/packages/react-components/react-menu/library/src/contexts/menuGridRowContext.ts @@ -0,0 +1,16 @@ +import * as React from 'react'; + +const MenuGridRowContext = React.createContext( + undefined, +) as React.Context; + +const menuGridRowContextDefaultValue: MenuGridRowContextValue = {}; + +/** + * Context + */ +export type MenuGridRowContextValue = {}; + +export const MenuGridRowContextProvider = MenuGridRowContext.Provider; +export const useMenuGridRowContext_unstable = () => + React.useContext(MenuGridRowContext) ?? menuGridRowContextDefaultValue; diff --git a/packages/react-components/react-menu/library/src/index.ts b/packages/react-components/react-menu/library/src/index.ts index 86465a4f7d22b8..f91fabda5f85f1 100644 --- a/packages/react-components/react-menu/library/src/index.ts +++ b/packages/react-components/react-menu/library/src/index.ts @@ -7,6 +7,8 @@ export { MenuGridProvider, useMenuGridContext_unstable } from './contexts/menuGr export type { MenuGridContextValue } from './contexts/menuGridContext'; export { MenuGridCellContextProvider, useMenuGridCellContext_unstable } from './contexts/menuGridCellContext'; export type { MenuGridCellContextValue } from './contexts/menuGridCellContext'; +export { MenuGridRowContextProvider, useMenuGridRowContext_unstable } from './contexts/menuGridRowContext'; +export type { MenuGridRowContextValue } from './contexts/menuGridRowContext'; export { MenuListProvider, useMenuListContext_unstable } from './contexts/menuListContext'; export type { MenuListContextValue } from './contexts/menuListContext'; @@ -95,6 +97,15 @@ export type { MenuGridCellSlots, MenuGridCellState, } from './MenuGridCell'; +export { + MenuGridRow, + menuGridRowClassNames, + renderMenuGridRow_unstable, + useMenuGridRowContextValues_unstable, + useMenuGridRowStyles_unstable, + useMenuGridRow_unstable, +} from './MenuGridRow'; +export type { MenuGridRowContextValues, MenuGridRowProps, MenuGridRowSlots, MenuGridRowState } from './MenuGridRow'; export { MenuList, diff --git a/packages/react-components/react-menu/stories/src/MenuGrid/MenuGridDefault.stories.tsx b/packages/react-components/react-menu/stories/src/MenuGrid/MenuGridDefault.stories.tsx index 8a63a12300c9bf..fcb86e29595523 100644 --- a/packages/react-components/react-menu/stories/src/MenuGrid/MenuGridDefault.stories.tsx +++ b/packages/react-components/react-menu/stories/src/MenuGrid/MenuGridDefault.stories.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; -import { makeStyles, tokens } from '@fluentui/react-components'; -import { MenuGrid, MenuGridCell } from '@fluentui/react-menu'; +import { Button, makeStyles, tokens } from '@fluentui/react-components'; +import { MenuGrid, MenuGridCell, MenuGridRow } from '@fluentui/react-menu'; const useMenuListContainerStyles = makeStyles({ container: { @@ -20,9 +20,24 @@ export const Default = () => { return (
- Cut - Paste - Edit + + First row + + + + + + + + + Second row + + + + + + +
); diff --git a/packages/react-components/react-shared-contexts/library/src/CustomStyleHooksContext/CustomStyleHooksContext.ts b/packages/react-components/react-shared-contexts/library/src/CustomStyleHooksContext/CustomStyleHooksContext.ts index 10263fabc8b3cb..4543c8a5858a85 100644 --- a/packages/react-components/react-shared-contexts/library/src/CustomStyleHooksContext/CustomStyleHooksContext.ts +++ b/packages/react-components/react-shared-contexts/library/src/CustomStyleHooksContext/CustomStyleHooksContext.ts @@ -90,6 +90,7 @@ export type CustomStyleHooksContextValue = Partial<{ useMenuDividerStyles_unstable: CustomStyleHook; useMenuGridStyles_unstable: CustomStyleHook; useMenuGridCellStyles_unstable: CustomStyleHook; + useMenuGridRowStyles_unstable: CustomStyleHook; useMenuGroupHeaderStyles_unstable: CustomStyleHook; useMenuGroupStyles_unstable: CustomStyleHook; useMenuItemCheckboxStyles_unstable: CustomStyleHook; From 5e127ff30db91b6685942ba15eabc4445877aec0 Mon Sep 17 00:00:00 2001 From: Adam Date: Wed, 16 Jul 2025 19:03:39 +0200 Subject: [PATCH 05/13] Add subcomponents for MenuGrid --- .../react-menu/stories/src/MenuGrid/index.stories.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/react-components/react-menu/stories/src/MenuGrid/index.stories.tsx b/packages/react-components/react-menu/stories/src/MenuGrid/index.stories.tsx index f1312577f9ed1b..ef0181cfae9edb 100644 --- a/packages/react-components/react-menu/stories/src/MenuGrid/index.stories.tsx +++ b/packages/react-components/react-menu/stories/src/MenuGrid/index.stories.tsx @@ -1,4 +1,4 @@ -import { MenuGrid } from '@fluentui/react-menu'; +import { MenuGrid, MenuGridRow, MenuGridCell } from '@fluentui/react-menu'; import descriptionMd from './MenuGridDescription.md'; export { Default } from './MenuGridDefault.stories'; @@ -8,8 +8,8 @@ export default { title: 'Components/Menu/MenuGrid', component: MenuGrid, subcomponents: { - // MenuGridGroup, - // MenuGridGroupHeader, + MenuGridRow, + MenuGridCell, }, parameters: { docs: { From e43743d0f4a4bf681f3a3e249f495859b13bb773 Mon Sep 17 00:00:00 2001 From: Adam Date: Wed, 16 Jul 2025 19:21:10 +0200 Subject: [PATCH 06/13] Fix composite navigation in MenuGrid --- .../contexts/{menuGridContext.tsx => menuGridContext.ts} | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) rename packages/react-components/react-menu/library/src/contexts/{menuGridContext.tsx => menuGridContext.ts} (69%) diff --git a/packages/react-components/react-menu/library/src/contexts/menuGridContext.tsx b/packages/react-components/react-menu/library/src/contexts/menuGridContext.ts similarity index 69% rename from packages/react-components/react-menu/library/src/contexts/menuGridContext.tsx rename to packages/react-components/react-menu/library/src/contexts/menuGridContext.ts index facbd1c12b7531..ff7313261bcf4a 100644 --- a/packages/react-components/react-menu/library/src/contexts/menuGridContext.tsx +++ b/packages/react-components/react-menu/library/src/contexts/menuGridContext.ts @@ -1,11 +1,9 @@ import * as React from 'react'; -import { createContext } from '@fluentui/react-context-selector'; -import type { Context } from '@fluentui/react-context-selector'; import { TabsterDOMAttribute } from '@fluentui/react-tabster'; -export const MenuGridContext: Context = createContext( +export const MenuGridContext = React.createContext( undefined, -) as Context; +) as React.Context; const menuGridContextDefaultValue: MenuGridContextValue = { tableRowTabsterAttribute: null, From c2fa069c88af2d286cfb76e17ce1cce5ef1f1d3c Mon Sep 17 00:00:00 2001 From: Adam Date: Wed, 16 Jul 2025 19:33:13 +0200 Subject: [PATCH 07/13] Refactor MenuGridContextProvider name --- .../library/src/components/MenuGrid/renderMenuGrid.tsx | 6 +++--- .../react-menu/library/src/contexts/menuGridContext.ts | 2 +- packages/react-components/react-menu/library/src/index.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/react-components/react-menu/library/src/components/MenuGrid/renderMenuGrid.tsx b/packages/react-components/react-menu/library/src/components/MenuGrid/renderMenuGrid.tsx index 3d2ae4615e1c65..d69f99dd754a1d 100644 --- a/packages/react-components/react-menu/library/src/components/MenuGrid/renderMenuGrid.tsx +++ b/packages/react-components/react-menu/library/src/components/MenuGrid/renderMenuGrid.tsx @@ -2,7 +2,7 @@ /** @jsxImportSource @fluentui/react-jsx-runtime */ import { assertSlots } from '@fluentui/react-utilities'; import { MenuGridContextValues, MenuGridSlots, MenuGridState } from './MenuGrid.types'; -import { MenuGridProvider } from '../../contexts/menuGridContext'; +import { MenuGridContextProvider } from '../../contexts/menuGridContext'; /** * Function that renders the final JSX of the component @@ -11,8 +11,8 @@ export const renderMenuGrid_unstable = (state: MenuGridState, contextValues: Men assertSlots(state); return ( - + - + ); }; diff --git a/packages/react-components/react-menu/library/src/contexts/menuGridContext.ts b/packages/react-components/react-menu/library/src/contexts/menuGridContext.ts index ff7313261bcf4a..c2ae1fdc89aa2f 100644 --- a/packages/react-components/react-menu/library/src/contexts/menuGridContext.ts +++ b/packages/react-components/react-menu/library/src/contexts/menuGridContext.ts @@ -19,6 +19,6 @@ export type MenuGridContextValue = { tableRowTabsterAttribute: TabsterDOMAttribute | null; }; -export const MenuGridProvider = MenuGridContext.Provider; +export const MenuGridContextProvider = MenuGridContext.Provider; export const useMenuGridContext_unstable = () => React.useContext(MenuGridContext) ?? menuGridContextDefaultValue; diff --git a/packages/react-components/react-menu/library/src/index.ts b/packages/react-components/react-menu/library/src/index.ts index f91fabda5f85f1..b5b2dc17c01ccf 100644 --- a/packages/react-components/react-menu/library/src/index.ts +++ b/packages/react-components/react-menu/library/src/index.ts @@ -3,7 +3,7 @@ export type { MenuContextValue } from './contexts/menuContext'; export { MenuTriggerContextProvider, useMenuTriggerContext_unstable } from './contexts/menuTriggerContext'; export { MenuGroupContextProvider, useMenuGroupContext_unstable } from './contexts/menuGroupContext'; export type { MenuGroupContextValue } from './contexts/menuGroupContext'; -export { MenuGridProvider, useMenuGridContext_unstable } from './contexts/menuGridContext'; +export { MenuGridContextProvider, useMenuGridContext_unstable } from './contexts/menuGridContext'; export type { MenuGridContextValue } from './contexts/menuGridContext'; export { MenuGridCellContextProvider, useMenuGridCellContext_unstable } from './contexts/menuGridCellContext'; export type { MenuGridCellContextValue } from './contexts/menuGridCellContext'; From b1e52b852dd4e1dfe0c49111ae6414465affc071 Mon Sep 17 00:00:00 2001 From: Adam Date: Wed, 16 Jul 2025 20:59:26 +0200 Subject: [PATCH 08/13] Add MenuGridRowGroup component --- .../library/src/MenuGridRowGroup.ts | 14 +++++++ .../MenuGridRowGroup/MenuGridRowGroup.tsx | 24 +++++++++++ .../MenuGridRowGroup.types.ts | 14 +++++++ .../src/components/MenuGridRowGroup/index.ts | 11 +++++ .../renderMenuGridRowGroup.tsx | 22 ++++++++++ .../MenuGridRowGroup/useMenuGridRowGroup.ts | 28 +++++++++++++ .../useMenuGridRowGroupContextValues.ts | 8 ++++ .../useMenuGridRowGroupStyles.styles.ts | 15 +++++++ .../src/contexts/menuGridRowGroupContext.ts | 16 ++++++++ .../react-menu/library/src/index.ts | 19 +++++++++ .../src/MenuGrid/MenuGridDefault.stories.tsx | 40 ++++++++++--------- .../stories/src/MenuGrid/index.stories.tsx | 5 ++- .../CustomStyleHooksContext.ts | 1 + 13 files changed, 196 insertions(+), 21 deletions(-) create mode 100644 packages/react-components/react-menu/library/src/MenuGridRowGroup.ts create mode 100644 packages/react-components/react-menu/library/src/components/MenuGridRowGroup/MenuGridRowGroup.tsx create mode 100644 packages/react-components/react-menu/library/src/components/MenuGridRowGroup/MenuGridRowGroup.types.ts create mode 100644 packages/react-components/react-menu/library/src/components/MenuGridRowGroup/index.ts create mode 100644 packages/react-components/react-menu/library/src/components/MenuGridRowGroup/renderMenuGridRowGroup.tsx create mode 100644 packages/react-components/react-menu/library/src/components/MenuGridRowGroup/useMenuGridRowGroup.ts create mode 100644 packages/react-components/react-menu/library/src/components/MenuGridRowGroup/useMenuGridRowGroupContextValues.ts create mode 100644 packages/react-components/react-menu/library/src/components/MenuGridRowGroup/useMenuGridRowGroupStyles.styles.ts create mode 100644 packages/react-components/react-menu/library/src/contexts/menuGridRowGroupContext.ts diff --git a/packages/react-components/react-menu/library/src/MenuGridRowGroup.ts b/packages/react-components/react-menu/library/src/MenuGridRowGroup.ts new file mode 100644 index 00000000000000..7294ee0debd81e --- /dev/null +++ b/packages/react-components/react-menu/library/src/MenuGridRowGroup.ts @@ -0,0 +1,14 @@ +export type { + MenuGridRowGroupContextValues, + MenuGridRowGroupProps, + MenuGridRowGroupSlots, + MenuGridRowGroupState, +} from './components/MenuGridRowGroup/index'; +export { + MenuGridRowGroup, + menuGridRowGroupClassNames, + renderMenuGridRowGroup_unstable, + useMenuGridRowGroupContextValues_unstable, + useMenuGridRowGroupStyles_unstable, + useMenuGridRowGroup_unstable, +} from './components/MenuGridRowGroup/index'; diff --git a/packages/react-components/react-menu/library/src/components/MenuGridRowGroup/MenuGridRowGroup.tsx b/packages/react-components/react-menu/library/src/components/MenuGridRowGroup/MenuGridRowGroup.tsx new file mode 100644 index 00000000000000..d8a6a2fba45661 --- /dev/null +++ b/packages/react-components/react-menu/library/src/components/MenuGridRowGroup/MenuGridRowGroup.tsx @@ -0,0 +1,24 @@ +import * as React from 'react'; +import { useMenuGridRowGroup_unstable } from './useMenuGridRowGroup'; +import { renderMenuGridRowGroup_unstable } from './renderMenuGridRowGroup'; +import { useMenuGridRowGroupContextValues_unstable } from './useMenuGridRowGroupContextValues'; +import type { MenuGridRowGroupProps } from './MenuGridRowGroup.types'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { useMenuGridRowGroupStyles_unstable } from './useMenuGridRowGroupStyles.styles'; +import { useCustomStyleHook_unstable } from '@fluentui/react-shared-contexts'; + +/** + * Define a MenuGridRowGroup, using the `useMenuGridRowGroup_unstable` hook. + */ +export const MenuGridRowGroup: ForwardRefComponent = React.forwardRef((props, ref) => { + const state = useMenuGridRowGroup_unstable(props, ref); + const contextValues = useMenuGridRowGroupContextValues_unstable(state); + + useMenuGridRowGroupStyles_unstable(state); + + useCustomStyleHook_unstable('useMenuGridRowGroupStyles_unstable')(state); + + return renderMenuGridRowGroup_unstable(state, contextValues); +}); + +MenuGridRowGroup.displayName = 'MenuGroup'; diff --git a/packages/react-components/react-menu/library/src/components/MenuGridRowGroup/MenuGridRowGroup.types.ts b/packages/react-components/react-menu/library/src/components/MenuGridRowGroup/MenuGridRowGroup.types.ts new file mode 100644 index 00000000000000..5cfa1e6015ecad --- /dev/null +++ b/packages/react-components/react-menu/library/src/components/MenuGridRowGroup/MenuGridRowGroup.types.ts @@ -0,0 +1,14 @@ +import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities'; +import type { MenuGridRowGroupContextValue } from '../../contexts/menuGridRowGroupContext'; + +export type MenuGridRowGroupSlots = { + root: Slot<'div'>; +}; + +export type MenuGridRowGroupProps = ComponentProps; + +export type MenuGridRowGroupState = ComponentState; + +export type MenuGridRowGroupContextValues = { + menuGridRowGroup: MenuGridRowGroupContextValue; +}; diff --git a/packages/react-components/react-menu/library/src/components/MenuGridRowGroup/index.ts b/packages/react-components/react-menu/library/src/components/MenuGridRowGroup/index.ts new file mode 100644 index 00000000000000..e5e2beb14b5819 --- /dev/null +++ b/packages/react-components/react-menu/library/src/components/MenuGridRowGroup/index.ts @@ -0,0 +1,11 @@ +export type { + MenuGridRowGroupContextValues, + MenuGridRowGroupProps, + MenuGridRowGroupSlots, + MenuGridRowGroupState, +} from './MenuGridRowGroup.types'; +export { MenuGridRowGroup } from './MenuGridRowGroup'; +export { renderMenuGridRowGroup_unstable } from './renderMenuGridRowGroup'; +export { useMenuGridRowGroup_unstable } from './useMenuGridRowGroup'; +export { useMenuGridRowGroupContextValues_unstable } from './useMenuGridRowGroupContextValues'; +export { menuGridRowGroupClassNames, useMenuGridRowGroupStyles_unstable } from './useMenuGridRowGroupStyles.styles'; diff --git a/packages/react-components/react-menu/library/src/components/MenuGridRowGroup/renderMenuGridRowGroup.tsx b/packages/react-components/react-menu/library/src/components/MenuGridRowGroup/renderMenuGridRowGroup.tsx new file mode 100644 index 00000000000000..0eb17b3a3ae070 --- /dev/null +++ b/packages/react-components/react-menu/library/src/components/MenuGridRowGroup/renderMenuGridRowGroup.tsx @@ -0,0 +1,22 @@ +/** @jsxRuntime automatic */ +/** @jsxImportSource @fluentui/react-jsx-runtime */ +import { assertSlots } from '@fluentui/react-utilities'; +import { MenuGridRowGroupContextValues, MenuGridRowGroupSlots, MenuGridRowGroupState } from './MenuGridRowGroup.types'; +import { MenuGridRowGroupContextProvider } from '../../contexts/menuGridRowGroupContext'; + +/** + * Redefine the render function to add slots. Reuse the menugroup structure but add + * slots to children. + */ +export const renderMenuGridRowGroup_unstable = ( + state: MenuGridRowGroupState, + contextValues: MenuGridRowGroupContextValues, +) => { + assertSlots(state); + + return ( + + + + ); +}; diff --git a/packages/react-components/react-menu/library/src/components/MenuGridRowGroup/useMenuGridRowGroup.ts b/packages/react-components/react-menu/library/src/components/MenuGridRowGroup/useMenuGridRowGroup.ts new file mode 100644 index 00000000000000..76d3b546440d9d --- /dev/null +++ b/packages/react-components/react-menu/library/src/components/MenuGridRowGroup/useMenuGridRowGroup.ts @@ -0,0 +1,28 @@ +import * as React from 'react'; +import { getIntrinsicElementProps, slot } from '@fluentui/react-utilities'; +import { MenuGridRowGroupProps, MenuGridRowGroupState } from './MenuGridRowGroup.types'; + +/** + * Given user props, returns state and render function for a MenuGridRowGroup. + */ +export function useMenuGridRowGroup_unstable( + props: MenuGridRowGroupProps, + ref: React.Ref, +): MenuGridRowGroupState { + return { + components: { + root: 'div', + }, + 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: ref as React.Ref, + role: 'rowgroup', + ...props, + }), + { elementType: 'div' }, + ), + }; +} diff --git a/packages/react-components/react-menu/library/src/components/MenuGridRowGroup/useMenuGridRowGroupContextValues.ts b/packages/react-components/react-menu/library/src/components/MenuGridRowGroup/useMenuGridRowGroupContextValues.ts new file mode 100644 index 00000000000000..fa409ab5bc901b --- /dev/null +++ b/packages/react-components/react-menu/library/src/components/MenuGridRowGroup/useMenuGridRowGroupContextValues.ts @@ -0,0 +1,8 @@ +import * as React from 'react'; +import type { MenuGridRowGroupContextValues, MenuGridRowGroupState } from './MenuGridRowGroup.types'; + +export function useMenuGridRowGroupContextValues_unstable(state: MenuGridRowGroupState): MenuGridRowGroupContextValues { + const menuGridRowGroup = React.useMemo(() => ({}), []); + + return { menuGridRowGroup }; +} diff --git a/packages/react-components/react-menu/library/src/components/MenuGridRowGroup/useMenuGridRowGroupStyles.styles.ts b/packages/react-components/react-menu/library/src/components/MenuGridRowGroup/useMenuGridRowGroupStyles.styles.ts new file mode 100644 index 00000000000000..d92b0942d36d60 --- /dev/null +++ b/packages/react-components/react-menu/library/src/components/MenuGridRowGroup/useMenuGridRowGroupStyles.styles.ts @@ -0,0 +1,15 @@ +import type { SlotClassNames } from '@fluentui/react-utilities'; +import { mergeClasses } from '@griffel/react'; +import type { MenuGridRowGroupSlots, MenuGridRowGroupState } from './MenuGridRowGroup.types'; + +export const menuGridRowGroupClassNames: SlotClassNames = { + root: 'fui-MenuGridRowGroup', +}; + +export const useMenuGridRowGroupStyles_unstable = (state: MenuGridRowGroupState): MenuGridRowGroupState => { + 'use no memo'; + + state.root.className = mergeClasses(menuGridRowGroupClassNames.root, state.root.className); + + return state; +}; diff --git a/packages/react-components/react-menu/library/src/contexts/menuGridRowGroupContext.ts b/packages/react-components/react-menu/library/src/contexts/menuGridRowGroupContext.ts new file mode 100644 index 00000000000000..864f2b5557fb14 --- /dev/null +++ b/packages/react-components/react-menu/library/src/contexts/menuGridRowGroupContext.ts @@ -0,0 +1,16 @@ +import * as React from 'react'; + +const MenuGridRowGroupContext = React.createContext( + undefined, +) as React.Context; + +const menuGridRowGroupContextDefaultValue: MenuGridRowGroupContextValue = {}; + +/** + * Context + */ +export type MenuGridRowGroupContextValue = {}; + +export const MenuGridRowGroupContextProvider = MenuGridRowGroupContext.Provider; +export const useMenuGridRowGroupContext_unstable = () => + React.useContext(MenuGridRowGroupContext) ?? menuGridRowGroupContextDefaultValue; diff --git a/packages/react-components/react-menu/library/src/index.ts b/packages/react-components/react-menu/library/src/index.ts index b5b2dc17c01ccf..bbbd5979e8a449 100644 --- a/packages/react-components/react-menu/library/src/index.ts +++ b/packages/react-components/react-menu/library/src/index.ts @@ -9,6 +9,11 @@ export { MenuGridCellContextProvider, useMenuGridCellContext_unstable } from './ export type { MenuGridCellContextValue } from './contexts/menuGridCellContext'; export { MenuGridRowContextProvider, useMenuGridRowContext_unstable } from './contexts/menuGridRowContext'; export type { MenuGridRowContextValue } from './contexts/menuGridRowContext'; +export { + MenuGridRowGroupContextProvider, + useMenuGridRowGroupContext_unstable, +} from './contexts/menuGridRowGroupContext'; +export type { MenuGridRowGroupContextValue } from './contexts/menuGridRowGroupContext'; export { MenuListProvider, useMenuListContext_unstable } from './contexts/menuListContext'; export type { MenuListContextValue } from './contexts/menuListContext'; @@ -106,6 +111,20 @@ export { useMenuGridRow_unstable, } from './MenuGridRow'; export type { MenuGridRowContextValues, MenuGridRowProps, MenuGridRowSlots, MenuGridRowState } from './MenuGridRow'; +export { + MenuGridRowGroup, + menuGridRowGroupClassNames, + renderMenuGridRowGroup_unstable, + useMenuGridRowGroupContextValues_unstable, + useMenuGridRowGroupStyles_unstable, + useMenuGridRowGroup_unstable, +} from './MenuGridRowGroup'; +export type { + MenuGridRowGroupContextValues, + MenuGridRowGroupProps, + MenuGridRowGroupSlots, + MenuGridRowGroupState, +} from './MenuGridRowGroup'; export { MenuList, diff --git a/packages/react-components/react-menu/stories/src/MenuGrid/MenuGridDefault.stories.tsx b/packages/react-components/react-menu/stories/src/MenuGrid/MenuGridDefault.stories.tsx index fcb86e29595523..fb05dc81973a66 100644 --- a/packages/react-components/react-menu/stories/src/MenuGrid/MenuGridDefault.stories.tsx +++ b/packages/react-components/react-menu/stories/src/MenuGrid/MenuGridDefault.stories.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { Button, makeStyles, tokens } from '@fluentui/react-components'; -import { MenuGrid, MenuGridCell, MenuGridRow } from '@fluentui/react-menu'; +import { MenuGrid, MenuGridCell, MenuGridRow, MenuGridRowGroup } from '@fluentui/react-menu'; const useMenuListContainerStyles = makeStyles({ container: { @@ -20,24 +20,26 @@ export const Default = () => { return (
- - First row - - - - - - - - - Second row - - - - - - - + + + First row + + + + + + + + + Second row + + + + + + + +
); diff --git a/packages/react-components/react-menu/stories/src/MenuGrid/index.stories.tsx b/packages/react-components/react-menu/stories/src/MenuGrid/index.stories.tsx index ef0181cfae9edb..64c511b0d6c65c 100644 --- a/packages/react-components/react-menu/stories/src/MenuGrid/index.stories.tsx +++ b/packages/react-components/react-menu/stories/src/MenuGrid/index.stories.tsx @@ -1,4 +1,4 @@ -import { MenuGrid, MenuGridRow, MenuGridCell } from '@fluentui/react-menu'; +import { MenuGrid, MenuGridCell, MenuGridRow, MenuGridRowGroup } from '@fluentui/react-menu'; import descriptionMd from './MenuGridDescription.md'; export { Default } from './MenuGridDefault.stories'; @@ -8,8 +8,9 @@ export default { title: 'Components/Menu/MenuGrid', component: MenuGrid, subcomponents: { - MenuGridRow, MenuGridCell, + MenuGridRow, + MenuGridRowGroup, }, parameters: { docs: { diff --git a/packages/react-components/react-shared-contexts/library/src/CustomStyleHooksContext/CustomStyleHooksContext.ts b/packages/react-components/react-shared-contexts/library/src/CustomStyleHooksContext/CustomStyleHooksContext.ts index 4543c8a5858a85..148bd43a3a669a 100644 --- a/packages/react-components/react-shared-contexts/library/src/CustomStyleHooksContext/CustomStyleHooksContext.ts +++ b/packages/react-components/react-shared-contexts/library/src/CustomStyleHooksContext/CustomStyleHooksContext.ts @@ -91,6 +91,7 @@ export type CustomStyleHooksContextValue = Partial<{ useMenuGridStyles_unstable: CustomStyleHook; useMenuGridCellStyles_unstable: CustomStyleHook; useMenuGridRowStyles_unstable: CustomStyleHook; + useMenuGridRowGroupStyles_unstable: CustomStyleHook; useMenuGroupHeaderStyles_unstable: CustomStyleHook; useMenuGroupStyles_unstable: CustomStyleHook; useMenuItemCheckboxStyles_unstable: CustomStyleHook; From 21aee8f06916e8e813f5670468ab2ac461c18c4b Mon Sep 17 00:00:00 2001 From: Adam Date: Thu, 17 Jul 2025 12:25:14 +0200 Subject: [PATCH 09/13] Add MenuGridRowGroupHeader component and improve story for MenuGrid --- .../library/src/MenuGridRowGroupHeader.ts | 14 +++++ .../MenuGridRowGroup.types.ts | 7 ++- .../MenuGridRowGroup/useMenuGridRowGroup.ts | 6 ++- .../useMenuGridRowGroupContextValues.ts | 3 +- .../MenuGridRowGroupHeader.tsx | 26 ++++++++++ .../MenuGridRowGroupHeader.types.ts | 14 +++++ .../MenuGridRowGroupHeader/index.ts | 14 +++++ .../renderMenuGridRowGroupHeader.tsx | 26 ++++++++++ .../useMenuGridRowGroupHeader.ts | 34 +++++++++++++ .../useMenuGridRowGroupHeaderContextValues.ts | 10 ++++ .../useMenuGridRowGroupHeaderStyles.styles.ts | 17 +++++++ .../src/contexts/menuGridRowGroupContext.ts | 13 +++-- .../contexts/menuGridRowGroupHeaderContext.ts | 16 ++++++ .../react-menu/library/src/index.ts | 19 +++++++ .../src/MenuGrid/MenuGridDefault.stories.tsx | 51 ++++++++++++------- .../stories/src/MenuGrid/index.stories.tsx | 3 +- .../CustomStyleHooksContext.ts | 1 + 17 files changed, 248 insertions(+), 26 deletions(-) create mode 100644 packages/react-components/react-menu/library/src/MenuGridRowGroupHeader.ts create mode 100644 packages/react-components/react-menu/library/src/components/MenuGridRowGroupHeader/MenuGridRowGroupHeader.tsx create mode 100644 packages/react-components/react-menu/library/src/components/MenuGridRowGroupHeader/MenuGridRowGroupHeader.types.ts create mode 100644 packages/react-components/react-menu/library/src/components/MenuGridRowGroupHeader/index.ts create mode 100644 packages/react-components/react-menu/library/src/components/MenuGridRowGroupHeader/renderMenuGridRowGroupHeader.tsx create mode 100644 packages/react-components/react-menu/library/src/components/MenuGridRowGroupHeader/useMenuGridRowGroupHeader.ts create mode 100644 packages/react-components/react-menu/library/src/components/MenuGridRowGroupHeader/useMenuGridRowGroupHeaderContextValues.ts create mode 100644 packages/react-components/react-menu/library/src/components/MenuGridRowGroupHeader/useMenuGridRowGroupHeaderStyles.styles.ts create mode 100644 packages/react-components/react-menu/library/src/contexts/menuGridRowGroupHeaderContext.ts diff --git a/packages/react-components/react-menu/library/src/MenuGridRowGroupHeader.ts b/packages/react-components/react-menu/library/src/MenuGridRowGroupHeader.ts new file mode 100644 index 00000000000000..6c7e79dd6d9dfb --- /dev/null +++ b/packages/react-components/react-menu/library/src/MenuGridRowGroupHeader.ts @@ -0,0 +1,14 @@ +export type { + MenuGridRowGroupHeaderContextValues, + MenuGridRowGroupHeaderProps, + MenuGridRowGroupHeaderSlots, + MenuGridRowGroupHeaderState, +} from './components/MenuGridRowGroupHeader/index'; +export { + MenuGridRowGroupHeader, + menuGridRowGroupHeaderClassNames, + renderMenuGridRowGroupHeader_unstable, + useMenuGridRowGroupHeaderContextValues_unstable, + useMenuGridRowGroupHeaderStyles_unstable, + useMenuGridRowGroupHeader_unstable, +} from './components/MenuGridRowGroupHeader/index'; diff --git a/packages/react-components/react-menu/library/src/components/MenuGridRowGroup/MenuGridRowGroup.types.ts b/packages/react-components/react-menu/library/src/components/MenuGridRowGroup/MenuGridRowGroup.types.ts index 5cfa1e6015ecad..0eabceaddf3ac3 100644 --- a/packages/react-components/react-menu/library/src/components/MenuGridRowGroup/MenuGridRowGroup.types.ts +++ b/packages/react-components/react-menu/library/src/components/MenuGridRowGroup/MenuGridRowGroup.types.ts @@ -7,7 +7,12 @@ export type MenuGridRowGroupSlots = { export type MenuGridRowGroupProps = ComponentProps; -export type MenuGridRowGroupState = ComponentState; +export type MenuGridRowGroupState = ComponentState & { + /** + * id applied to the DOM element of `MenuGridRowGroupHeader` + */ + headerId: string; +}; export type MenuGridRowGroupContextValues = { menuGridRowGroup: MenuGridRowGroupContextValue; diff --git a/packages/react-components/react-menu/library/src/components/MenuGridRowGroup/useMenuGridRowGroup.ts b/packages/react-components/react-menu/library/src/components/MenuGridRowGroup/useMenuGridRowGroup.ts index 76d3b546440d9d..56350ebd899919 100644 --- a/packages/react-components/react-menu/library/src/components/MenuGridRowGroup/useMenuGridRowGroup.ts +++ b/packages/react-components/react-menu/library/src/components/MenuGridRowGroup/useMenuGridRowGroup.ts @@ -1,5 +1,5 @@ import * as React from 'react'; -import { getIntrinsicElementProps, slot } from '@fluentui/react-utilities'; +import { getIntrinsicElementProps, useId, slot } from '@fluentui/react-utilities'; import { MenuGridRowGroupProps, MenuGridRowGroupState } from './MenuGridRowGroup.types'; /** @@ -9,6 +9,8 @@ export function useMenuGridRowGroup_unstable( props: MenuGridRowGroupProps, ref: React.Ref, ): MenuGridRowGroupState { + const headerId = useId('menu-grid-row-group-header'); + return { components: { root: 'div', @@ -20,9 +22,11 @@ export function useMenuGridRowGroup_unstable( // but since it would be a breaking change to fix it, we are casting ref to it's proper type ref: ref as React.Ref, role: 'rowgroup', + 'aria-labelledby': headerId, ...props, }), { elementType: 'div' }, ), + headerId, }; } diff --git a/packages/react-components/react-menu/library/src/components/MenuGridRowGroup/useMenuGridRowGroupContextValues.ts b/packages/react-components/react-menu/library/src/components/MenuGridRowGroup/useMenuGridRowGroupContextValues.ts index fa409ab5bc901b..cae82709de23ac 100644 --- a/packages/react-components/react-menu/library/src/components/MenuGridRowGroup/useMenuGridRowGroupContextValues.ts +++ b/packages/react-components/react-menu/library/src/components/MenuGridRowGroup/useMenuGridRowGroupContextValues.ts @@ -2,7 +2,8 @@ import * as React from 'react'; import type { MenuGridRowGroupContextValues, MenuGridRowGroupState } from './MenuGridRowGroup.types'; export function useMenuGridRowGroupContextValues_unstable(state: MenuGridRowGroupState): MenuGridRowGroupContextValues { - const menuGridRowGroup = React.useMemo(() => ({}), []); + const { headerId } = state; + const menuGridRowGroup = React.useMemo(() => ({ headerId }), [headerId]); return { menuGridRowGroup }; } diff --git a/packages/react-components/react-menu/library/src/components/MenuGridRowGroupHeader/MenuGridRowGroupHeader.tsx b/packages/react-components/react-menu/library/src/components/MenuGridRowGroupHeader/MenuGridRowGroupHeader.tsx new file mode 100644 index 00000000000000..be2722082e4110 --- /dev/null +++ b/packages/react-components/react-menu/library/src/components/MenuGridRowGroupHeader/MenuGridRowGroupHeader.tsx @@ -0,0 +1,26 @@ +import * as React from 'react'; +import { useMenuGridRowGroupHeader_unstable } from './useMenuGridRowGroupHeader'; +import { renderMenuGridRowGroupHeader_unstable } from './renderMenuGridRowGroupHeader'; +import { useMenuGridRowGroupHeaderContextValues_unstable } from './useMenuGridRowGroupHeaderContextValues'; +import type { MenuGridRowGroupHeaderProps } from './MenuGridRowGroupHeader.types'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { useMenuGridRowGroupHeaderStyles_unstable } from './useMenuGridRowGroupHeaderStyles.styles'; +import { useCustomStyleHook_unstable } from '@fluentui/react-shared-contexts'; + +/** + * Define a MenuGridRowGroupHeader, using the `useMenuGridRowGroupHeader_unstable` hook. + */ +export const MenuGridRowGroupHeader: ForwardRefComponent = React.forwardRef( + (props, ref) => { + const state = useMenuGridRowGroupHeader_unstable(props, ref); + const contextValues = useMenuGridRowGroupHeaderContextValues_unstable(state); + + useMenuGridRowGroupHeaderStyles_unstable(state); + + useCustomStyleHook_unstable('useMenuGridRowGroupHeaderStyles_unstable')(state); + + return renderMenuGridRowGroupHeader_unstable(state, contextValues); + }, +); + +MenuGridRowGroupHeader.displayName = 'MenuGroup'; diff --git a/packages/react-components/react-menu/library/src/components/MenuGridRowGroupHeader/MenuGridRowGroupHeader.types.ts b/packages/react-components/react-menu/library/src/components/MenuGridRowGroupHeader/MenuGridRowGroupHeader.types.ts new file mode 100644 index 00000000000000..3c24b9d9f9e3c7 --- /dev/null +++ b/packages/react-components/react-menu/library/src/components/MenuGridRowGroupHeader/MenuGridRowGroupHeader.types.ts @@ -0,0 +1,14 @@ +import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities'; +import type { MenuGridRowGroupHeaderContextValue } from '../../contexts/menuGridRowGroupHeaderContext'; + +export type MenuGridRowGroupHeaderSlots = { + root: Slot<'div'>; +}; + +export type MenuGridRowGroupHeaderProps = ComponentProps; + +export type MenuGridRowGroupHeaderState = ComponentState; + +export type MenuGridRowGroupHeaderContextValues = { + menuGridRowGroupHeader: MenuGridRowGroupHeaderContextValue; +}; diff --git a/packages/react-components/react-menu/library/src/components/MenuGridRowGroupHeader/index.ts b/packages/react-components/react-menu/library/src/components/MenuGridRowGroupHeader/index.ts new file mode 100644 index 00000000000000..86a0d6aeea9a71 --- /dev/null +++ b/packages/react-components/react-menu/library/src/components/MenuGridRowGroupHeader/index.ts @@ -0,0 +1,14 @@ +export type { + MenuGridRowGroupHeaderContextValues, + MenuGridRowGroupHeaderProps, + MenuGridRowGroupHeaderSlots, + MenuGridRowGroupHeaderState, +} from './MenuGridRowGroupHeader.types'; +export { MenuGridRowGroupHeader } from './MenuGridRowGroupHeader'; +export { renderMenuGridRowGroupHeader_unstable } from './renderMenuGridRowGroupHeader'; +export { useMenuGridRowGroupHeader_unstable } from './useMenuGridRowGroupHeader'; +export { useMenuGridRowGroupHeaderContextValues_unstable } from './useMenuGridRowGroupHeaderContextValues'; +export { + menuGridRowGroupHeaderClassNames, + useMenuGridRowGroupHeaderStyles_unstable, +} from './useMenuGridRowGroupHeaderStyles.styles'; diff --git a/packages/react-components/react-menu/library/src/components/MenuGridRowGroupHeader/renderMenuGridRowGroupHeader.tsx b/packages/react-components/react-menu/library/src/components/MenuGridRowGroupHeader/renderMenuGridRowGroupHeader.tsx new file mode 100644 index 00000000000000..9c57995be685c8 --- /dev/null +++ b/packages/react-components/react-menu/library/src/components/MenuGridRowGroupHeader/renderMenuGridRowGroupHeader.tsx @@ -0,0 +1,26 @@ +/** @jsxRuntime automatic */ +/** @jsxImportSource @fluentui/react-jsx-runtime */ +import { assertSlots } from '@fluentui/react-utilities'; +import { + MenuGridRowGroupHeaderContextValues, + MenuGridRowGroupHeaderSlots, + MenuGridRowGroupHeaderState, +} from './MenuGridRowGroupHeader.types'; +import { MenuGridRowGroupHeaderContextProvider } from '../../contexts/menuGridRowGroupHeaderContext'; + +/** + * Redefine the render function to add slots. Reuse the menugroup structure but add + * slots to children. + */ +export const renderMenuGridRowGroupHeader_unstable = ( + state: MenuGridRowGroupHeaderState, + contextValues: MenuGridRowGroupHeaderContextValues, +) => { + assertSlots(state); + + return ( + + + + ); +}; diff --git a/packages/react-components/react-menu/library/src/components/MenuGridRowGroupHeader/useMenuGridRowGroupHeader.ts b/packages/react-components/react-menu/library/src/components/MenuGridRowGroupHeader/useMenuGridRowGroupHeader.ts new file mode 100644 index 00000000000000..e9517a3f2fcfa1 --- /dev/null +++ b/packages/react-components/react-menu/library/src/components/MenuGridRowGroupHeader/useMenuGridRowGroupHeader.ts @@ -0,0 +1,34 @@ +import * as React from 'react'; +import { getIntrinsicElementProps, slot } from '@fluentui/react-utilities'; + +import { useMenuGridRowGroupContext_unstable } from '../../contexts/menuGridRowGroupContext'; +import { MenuGridRowGroupHeaderProps, MenuGridRowGroupHeaderState } from './MenuGridRowGroupHeader.types'; + +/** + * Given user props, returns state and render function for a MenuGridRowGroupHeader. + */ +export function useMenuGridRowGroupHeader_unstable( + props: MenuGridRowGroupHeaderProps, + ref: React.Ref, +): MenuGridRowGroupHeaderState { + const { headerId: id } = useMenuGridRowGroupContext_unstable(); + + return { + components: { + root: 'div', + }, + 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: ref as React.Ref, + role: 'presentation', + id, + 'aria-hidden': true, + ...props, + }), + { elementType: 'div' }, + ), + }; +} diff --git a/packages/react-components/react-menu/library/src/components/MenuGridRowGroupHeader/useMenuGridRowGroupHeaderContextValues.ts b/packages/react-components/react-menu/library/src/components/MenuGridRowGroupHeader/useMenuGridRowGroupHeaderContextValues.ts new file mode 100644 index 00000000000000..41290ac94a541e --- /dev/null +++ b/packages/react-components/react-menu/library/src/components/MenuGridRowGroupHeader/useMenuGridRowGroupHeaderContextValues.ts @@ -0,0 +1,10 @@ +import * as React from 'react'; +import type { MenuGridRowGroupHeaderContextValues, MenuGridRowGroupHeaderState } from './MenuGridRowGroupHeader.types'; + +export function useMenuGridRowGroupHeaderContextValues_unstable( + state: MenuGridRowGroupHeaderState, +): MenuGridRowGroupHeaderContextValues { + const menuGridRowGroupHeader = React.useMemo(() => ({}), []); + + return { menuGridRowGroupHeader }; +} diff --git a/packages/react-components/react-menu/library/src/components/MenuGridRowGroupHeader/useMenuGridRowGroupHeaderStyles.styles.ts b/packages/react-components/react-menu/library/src/components/MenuGridRowGroupHeader/useMenuGridRowGroupHeaderStyles.styles.ts new file mode 100644 index 00000000000000..3a29b298354cc3 --- /dev/null +++ b/packages/react-components/react-menu/library/src/components/MenuGridRowGroupHeader/useMenuGridRowGroupHeaderStyles.styles.ts @@ -0,0 +1,17 @@ +import type { SlotClassNames } from '@fluentui/react-utilities'; +import { mergeClasses } from '@griffel/react'; +import type { MenuGridRowGroupHeaderSlots, MenuGridRowGroupHeaderState } from './MenuGridRowGroupHeader.types'; + +export const menuGridRowGroupHeaderClassNames: SlotClassNames = { + root: 'fui-MenuGridRowGroupHeader', +}; + +export const useMenuGridRowGroupHeaderStyles_unstable = ( + state: MenuGridRowGroupHeaderState, +): MenuGridRowGroupHeaderState => { + 'use no memo'; + + state.root.className = mergeClasses(menuGridRowGroupHeaderClassNames.root, state.root.className); + + return state; +}; diff --git a/packages/react-components/react-menu/library/src/contexts/menuGridRowGroupContext.ts b/packages/react-components/react-menu/library/src/contexts/menuGridRowGroupContext.ts index 864f2b5557fb14..3a608dc6f13003 100644 --- a/packages/react-components/react-menu/library/src/contexts/menuGridRowGroupContext.ts +++ b/packages/react-components/react-menu/library/src/contexts/menuGridRowGroupContext.ts @@ -4,12 +4,19 @@ const MenuGridRowGroupContext = React.createContext; -const menuGridRowGroupContextDefaultValue: MenuGridRowGroupContextValue = {}; +const menuGridRowGroupContextDefaultValue: MenuGridRowGroupContextValue = { + headerId: '', +}; /** - * Context + * Context used to guarantee correct aria-relationship between header */ -export type MenuGridRowGroupContextValue = {}; +export type MenuGridRowGroupContextValue = { + /** + * Element id applied to the `MenuGridRowGroupHeader` component + */ + headerId: string; +}; export const MenuGridRowGroupContextProvider = MenuGridRowGroupContext.Provider; export const useMenuGridRowGroupContext_unstable = () => diff --git a/packages/react-components/react-menu/library/src/contexts/menuGridRowGroupHeaderContext.ts b/packages/react-components/react-menu/library/src/contexts/menuGridRowGroupHeaderContext.ts new file mode 100644 index 00000000000000..4f11e74af95000 --- /dev/null +++ b/packages/react-components/react-menu/library/src/contexts/menuGridRowGroupHeaderContext.ts @@ -0,0 +1,16 @@ +import * as React from 'react'; + +const MenuGridRowGroupHeaderContext = React.createContext( + undefined, +) as React.Context; + +const menuGridRowGroupHeaderContextDefaultValue: MenuGridRowGroupHeaderContextValue = {}; + +/** + * Context + */ +export type MenuGridRowGroupHeaderContextValue = {}; + +export const MenuGridRowGroupHeaderContextProvider = MenuGridRowGroupHeaderContext.Provider; +export const useMenuGridRowGroupHeaderContext_unstable = () => + React.useContext(MenuGridRowGroupHeaderContext) ?? menuGridRowGroupHeaderContextDefaultValue; diff --git a/packages/react-components/react-menu/library/src/index.ts b/packages/react-components/react-menu/library/src/index.ts index bbbd5979e8a449..db877f69100a03 100644 --- a/packages/react-components/react-menu/library/src/index.ts +++ b/packages/react-components/react-menu/library/src/index.ts @@ -14,6 +14,11 @@ export { useMenuGridRowGroupContext_unstable, } from './contexts/menuGridRowGroupContext'; export type { MenuGridRowGroupContextValue } from './contexts/menuGridRowGroupContext'; +export { + MenuGridRowGroupHeaderContextProvider, + useMenuGridRowGroupHeaderContext_unstable, +} from './contexts/menuGridRowGroupHeaderContext'; +export type { MenuGridRowGroupHeaderContextValue } from './contexts/menuGridRowGroupHeaderContext'; export { MenuListProvider, useMenuListContext_unstable } from './contexts/menuListContext'; export type { MenuListContextValue } from './contexts/menuListContext'; @@ -125,6 +130,20 @@ export type { MenuGridRowGroupSlots, MenuGridRowGroupState, } from './MenuGridRowGroup'; +export { + MenuGridRowGroupHeader, + menuGridRowGroupHeaderClassNames, + renderMenuGridRowGroupHeader_unstable, + useMenuGridRowGroupHeaderContextValues_unstable, + useMenuGridRowGroupHeaderStyles_unstable, + useMenuGridRowGroupHeader_unstable, +} from './MenuGridRowGroupHeader'; +export type { + MenuGridRowGroupHeaderContextValues, + MenuGridRowGroupHeaderProps, + MenuGridRowGroupHeaderSlots, + MenuGridRowGroupHeaderState, +} from './MenuGridRowGroupHeader'; export { MenuList, diff --git a/packages/react-components/react-menu/stories/src/MenuGrid/MenuGridDefault.stories.tsx b/packages/react-components/react-menu/stories/src/MenuGrid/MenuGridDefault.stories.tsx index fb05dc81973a66..351ffee960025f 100644 --- a/packages/react-components/react-menu/stories/src/MenuGrid/MenuGridDefault.stories.tsx +++ b/packages/react-components/react-menu/stories/src/MenuGrid/MenuGridDefault.stories.tsx @@ -1,6 +1,11 @@ import * as React from 'react'; import { Button, makeStyles, tokens } from '@fluentui/react-components'; -import { MenuGrid, MenuGridCell, MenuGridRow, MenuGridRowGroup } from '@fluentui/react-menu'; +import { MenuGrid, MenuGridCell, MenuGridRow, MenuGridRowGroup, MenuGridRowGroupHeader } from '@fluentui/react-menu'; + +const items = { + people: ['Olivia Carter', 'Liam Thompson', 'Sophia Martinez', 'Noah Patel', 'Emma Robinson'], + agentsAndBots: ['Facilitator', 'Copilot'], +}; const useMenuListContainerStyles = makeStyles({ container: { @@ -21,24 +26,32 @@ export const Default = () => {
- - First row - - - - - - - - - Second row - - - - - - - + People + {items.people.map((name, index) => ( + + {name} + + + + + + + + ))} + + + Agents and bots + {items.agentsAndBots.map((name, index) => ( + + {name} + + + + + + + + ))}
diff --git a/packages/react-components/react-menu/stories/src/MenuGrid/index.stories.tsx b/packages/react-components/react-menu/stories/src/MenuGrid/index.stories.tsx index 64c511b0d6c65c..6b14a109433edb 100644 --- a/packages/react-components/react-menu/stories/src/MenuGrid/index.stories.tsx +++ b/packages/react-components/react-menu/stories/src/MenuGrid/index.stories.tsx @@ -1,4 +1,4 @@ -import { MenuGrid, MenuGridCell, MenuGridRow, MenuGridRowGroup } from '@fluentui/react-menu'; +import { MenuGrid, MenuGridCell, MenuGridRow, MenuGridRowGroup, MenuGridRowGroupHeader } from '@fluentui/react-menu'; import descriptionMd from './MenuGridDescription.md'; export { Default } from './MenuGridDefault.stories'; @@ -11,6 +11,7 @@ export default { MenuGridCell, MenuGridRow, MenuGridRowGroup, + MenuGridRowGroupHeader, }, parameters: { docs: { diff --git a/packages/react-components/react-shared-contexts/library/src/CustomStyleHooksContext/CustomStyleHooksContext.ts b/packages/react-components/react-shared-contexts/library/src/CustomStyleHooksContext/CustomStyleHooksContext.ts index 148bd43a3a669a..5314ffe13db8e2 100644 --- a/packages/react-components/react-shared-contexts/library/src/CustomStyleHooksContext/CustomStyleHooksContext.ts +++ b/packages/react-components/react-shared-contexts/library/src/CustomStyleHooksContext/CustomStyleHooksContext.ts @@ -92,6 +92,7 @@ export type CustomStyleHooksContextValue = Partial<{ useMenuGridCellStyles_unstable: CustomStyleHook; useMenuGridRowStyles_unstable: CustomStyleHook; useMenuGridRowGroupStyles_unstable: CustomStyleHook; + useMenuGridRowGroupHeaderStyles_unstable: CustomStyleHook; useMenuGroupHeaderStyles_unstable: CustomStyleHook; useMenuGroupStyles_unstable: CustomStyleHook; useMenuItemCheckboxStyles_unstable: CustomStyleHook; From 55c281fe3fd9e92d96817f2a05b3a6f121d31da3 Mon Sep 17 00:00:00 2001 From: Adam Date: Mon, 21 Jul 2025 15:42:17 +0200 Subject: [PATCH 10/13] Improve MenuGrid WithSubmenu story example --- .../src/MenuGrid/MenuGridDefault.stories.tsx | 78 ++++++++----------- .../MenuGrid/MenuGridWithSubmenu.stories.tsx | 62 +++++++-------- 2 files changed, 59 insertions(+), 81 deletions(-) diff --git a/packages/react-components/react-menu/stories/src/MenuGrid/MenuGridDefault.stories.tsx b/packages/react-components/react-menu/stories/src/MenuGrid/MenuGridDefault.stories.tsx index 351ffee960025f..64fabadf3b7a9f 100644 --- a/packages/react-components/react-menu/stories/src/MenuGrid/MenuGridDefault.stories.tsx +++ b/packages/react-components/react-menu/stories/src/MenuGrid/MenuGridDefault.stories.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { Button, makeStyles, tokens } from '@fluentui/react-components'; +import { Button } from '@fluentui/react-components'; import { MenuGrid, MenuGridCell, MenuGridRow, MenuGridRowGroup, MenuGridRowGroupHeader } from '@fluentui/react-menu'; const items = { @@ -7,53 +7,37 @@ const items = { agentsAndBots: ['Facilitator', 'Copilot'], }; -const useMenuListContainerStyles = makeStyles({ - container: { - backgroundColor: tokens.colorNeutralBackground1, - minWidth: '128px', - minHeight: '48px', - maxWidth: '300px', - width: 'max-content', - boxShadow: `${tokens.shadow16}`, - paddingTop: '4px', - paddingBottom: '4px', - }, -}); - export const Default = () => { - const styles = useMenuListContainerStyles(); return ( -
- - - People - {items.people.map((name, index) => ( - - {name} - - - - - - - - ))} - - - Agents and bots - {items.agentsAndBots.map((name, index) => ( - - {name} - - - - - - - - ))} - - -
+ + + People + {items.people.map((name, index) => ( + + {name} + + + + + + + + ))} + + + Agents and bots + {items.agentsAndBots.map((name, index) => ( + + {name} + + + + + + + + ))} + + ); }; diff --git a/packages/react-components/react-menu/stories/src/MenuGrid/MenuGridWithSubmenu.stories.tsx b/packages/react-components/react-menu/stories/src/MenuGrid/MenuGridWithSubmenu.stories.tsx index ff82b40816c2fc..e843df1dfffd0e 100644 --- a/packages/react-components/react-menu/stories/src/MenuGrid/MenuGridWithSubmenu.stories.tsx +++ b/packages/react-components/react-menu/stories/src/MenuGrid/MenuGridWithSubmenu.stories.tsx @@ -1,48 +1,42 @@ import * as React from 'react'; -import { makeStyles, tokens, MenuList, MenuItem, Menu, MenuPopover, MenuTrigger } from '@fluentui/react-components'; +import { Button, Menu, MenuTrigger, MenuList, MenuItem, MenuPopover } from '@fluentui/react-components'; +import { MenuGrid, MenuGridCell, MenuGridRow } from '@fluentui/react-menu'; -const useMenuListContainerStyles = makeStyles({ - container: { - backgroundColor: tokens.colorNeutralBackground1, - minWidth: '128px', - minHeight: '48px', - maxWidth: '300px', - width: 'max-content', - boxShadow: `${tokens.shadow16}`, - paddingTop: '4px', - paddingBottom: '4px', - }, -}); +const items = ['Olivia Carter', 'Liam Thompson', 'Sophia Martinez', 'Noah Patel', 'Emma Robinson']; export const WithSubmenu = () => { - const styles = useMenuListContainerStyles(); return ( -
- - Cut - Paste - Edit - - - Preferences - - - - Cut - Paste - Edit - - - - -
+ + {items.map((name, index) => ( + + {name} + + + + + + + + Show profile + Audio call + Video call + Remove + + + + + + ))} + ); }; WithSubmenu.parameters = { docs: { description: { - story: ['A `MenuGrid` row can open a submenu using a button provided as one of the row actions'].join('\n'), + story: [ + 'If you need to provide a submenu for a `MenuGrid` item, use a menu button, e.g. "More actions", placed into its own `MenuGridCell`', + ].join('\n'), }, }, }; From 86eafaf557c95bcf4003d6e4d6d533e1dfa17155 Mon Sep 17 00:00:00 2001 From: Adam Date: Mon, 21 Jul 2025 19:17:46 +0200 Subject: [PATCH 11/13] Fix Menu button opening on arrow down --- .../MenuGrid/MenuGridWithSubmenu.stories.tsx | 41 +++++++++++++------ 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/packages/react-components/react-menu/stories/src/MenuGrid/MenuGridWithSubmenu.stories.tsx b/packages/react-components/react-menu/stories/src/MenuGrid/MenuGridWithSubmenu.stories.tsx index e843df1dfffd0e..59b329918f997f 100644 --- a/packages/react-components/react-menu/stories/src/MenuGrid/MenuGridWithSubmenu.stories.tsx +++ b/packages/react-components/react-menu/stories/src/MenuGrid/MenuGridWithSubmenu.stories.tsx @@ -1,9 +1,36 @@ import * as React from 'react'; import { Button, Menu, MenuTrigger, MenuList, MenuItem, MenuPopover } from '@fluentui/react-components'; +import type { MenuProps } from '@fluentui/react-components'; import { MenuGrid, MenuGridCell, MenuGridRow } from '@fluentui/react-menu'; const items = ['Olivia Carter', 'Liam Thompson', 'Sophia Martinez', 'Noah Patel', 'Emma Robinson']; +const Submenu = () => { + const [open, setOpen] = React.useState(false); + const onOpenChange: MenuProps['onOpenChange'] = (e, data) => { + if (e.type === 'keydown' && (e as KeyboardEvent).key === 'ArrowDown') { + return; + } + setOpen(data.open); + }; + + return ( + + + + + + + Show profile + Audio call + Video call + Remove + + + + ); +}; + export const WithSubmenu = () => { return ( @@ -11,19 +38,7 @@ export const WithSubmenu = () => { {name} - - - - - - - Show profile - Audio call - Video call - Remove - - - + ))} From 4c55dbf0059a71a89d4ed12dcb457012d0c3533f Mon Sep 17 00:00:00 2001 From: Adam Date: Mon, 21 Jul 2025 19:58:31 +0200 Subject: [PATCH 12/13] More proper way to check for menu trigger key down --- .../stories/src/MenuGrid/MenuGridWithSubmenu.stories.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-components/react-menu/stories/src/MenuGrid/MenuGridWithSubmenu.stories.tsx b/packages/react-components/react-menu/stories/src/MenuGrid/MenuGridWithSubmenu.stories.tsx index 59b329918f997f..77d8aad987617d 100644 --- a/packages/react-components/react-menu/stories/src/MenuGrid/MenuGridWithSubmenu.stories.tsx +++ b/packages/react-components/react-menu/stories/src/MenuGrid/MenuGridWithSubmenu.stories.tsx @@ -8,7 +8,7 @@ const items = ['Olivia Carter', 'Liam Thompson', 'Sophia Martinez', 'Noah Patel' const Submenu = () => { const [open, setOpen] = React.useState(false); const onOpenChange: MenuProps['onOpenChange'] = (e, data) => { - if (e.type === 'keydown' && (e as KeyboardEvent).key === 'ArrowDown') { + if (data.type === 'menuTriggerKeyDown' && (e as KeyboardEvent).key === 'ArrowDown') { return; } setOpen(data.open); From eefed32026029e8b417adba462bb49bdcb5b1ec0 Mon Sep 17 00:00:00 2001 From: Adam Date: Mon, 21 Jul 2025 20:57:21 +0200 Subject: [PATCH 13/13] Add Complex menu story for Menu component and label MenuGrid using trigger id --- .../src/components/MenuGrid/useMenuGrid.ts | 3 + .../src/Menu/MenuComplexMenu.stories.tsx | 59 +++++++++++++++++++ .../stories/src/Menu/index.stories.tsx | 7 +++ 3 files changed, 69 insertions(+) create mode 100644 packages/react-components/react-menu/stories/src/Menu/MenuComplexMenu.stories.tsx diff --git a/packages/react-components/react-menu/library/src/components/MenuGrid/useMenuGrid.ts b/packages/react-components/react-menu/library/src/components/MenuGrid/useMenuGrid.ts index d46ada04bd968b..960e9e5c19fe3d 100644 --- a/packages/react-components/react-menu/library/src/components/MenuGrid/useMenuGrid.ts +++ b/packages/react-components/react-menu/library/src/components/MenuGrid/useMenuGrid.ts @@ -2,11 +2,13 @@ import * as React from 'react'; import { getIntrinsicElementProps, slot } from '@fluentui/react-utilities'; import { useTableCompositeNavigation } from '@fluentui/react-components'; import type { MenuGridProps, MenuGridState } from './MenuGrid.types'; +import { useMenuContext_unstable } from '../../contexts/menuContext'; /** * Returns the props and state required to render the component */ export const useMenuGrid_unstable = (props: MenuGridProps, ref: React.Ref): MenuGridState => { + const triggerId = useMenuContext_unstable(context => context.triggerId); const { tableRowTabsterAttribute, tableTabsterAttribute, onTableKeyDown } = useTableCompositeNavigation(); return { @@ -20,6 +22,7 @@ export const useMenuGrid_unstable = (props: MenuGridProps, ref: React.Ref, role: 'grid', + 'aria-labelledby': triggerId, onKeyDown: onTableKeyDown, ...tableTabsterAttribute, ...props, diff --git a/packages/react-components/react-menu/stories/src/Menu/MenuComplexMenu.stories.tsx b/packages/react-components/react-menu/stories/src/Menu/MenuComplexMenu.stories.tsx new file mode 100644 index 00000000000000..b09d57d514c311 --- /dev/null +++ b/packages/react-components/react-menu/stories/src/Menu/MenuComplexMenu.stories.tsx @@ -0,0 +1,59 @@ +import * as React from 'react'; +import { Button, Menu, MenuPopover, MenuTrigger } from '@fluentui/react-components'; +import { MenuGrid, MenuGridCell, MenuGridRow, MenuGridRowGroup, MenuGridRowGroupHeader } from '@fluentui/react-menu'; + +const items = { + people: ['Olivia Carter', 'Liam Thompson', 'Sophia Martinez', 'Noah Patel', 'Emma Robinson'], + agentsAndBots: ['Facilitator', 'Copilot'], +}; + +export const ComplexMenu = () => { + return ( + + + + + + + + + People + {items.people.map((name, index) => ( + + {name} + + + + + + + + ))} + + + Agents and bots + {items.agentsAndBots.map((name, index) => ( + + {name} + + + + + + + + ))} + + + + + ); +}; + +ComplexMenu.parameters = { + docs: { + description: { + story: ['For complex menus with more actions, use the `MenuGrid` component'].join('\n'), + }, + }, +}; diff --git a/packages/react-components/react-menu/stories/src/Menu/index.stories.tsx b/packages/react-components/react-menu/stories/src/Menu/index.stories.tsx index f6bf01427c187b..5b8faa155938c3 100644 --- a/packages/react-components/react-menu/stories/src/Menu/index.stories.tsx +++ b/packages/react-components/react-menu/stories/src/Menu/index.stories.tsx @@ -13,6 +13,7 @@ import { MenuSplitGroup, MenuTrigger, } from '@fluentui/react-components'; +import { MenuGrid, MenuGridCell, MenuGridRow, MenuGridRowGroup, MenuGridRowGroupHeader } from '@fluentui/react-menu'; import descriptionMd from './MenuDescription.md'; import bestPracticesMd from './MenuBestPractices.md'; @@ -42,6 +43,7 @@ export { RenderFunctionTrigger } from './MenuRenderFunctionTrigger.stories'; export { MemoizedMenuItems } from './MenuMemoizedMenuItems.stories'; export { SplitMenuItem } from './MenuSplitMenuItem.stories'; export { MenuTriggerWithTooltip } from './MenuTriggerWithTooltip.stories'; +export { ComplexMenu } from './MenuComplexMenu.stories'; export default { title: 'Components/Menu/Menu', @@ -59,6 +61,11 @@ export default { MenuSplitGroup, MenuTrigger, MenuItemSwitch, + MenuGrid, + MenuGridCell, + MenuGridRow, + MenuGridRowGroup, + MenuGridRowGroupHeader, }, parameters: { docs: {