From 76233ad2d9efdcd37e2528bfbba9c9c22152d759 Mon Sep 17 00:00:00 2001 From: mainframev Date: Thu, 11 Jun 2026 22:27:49 +0200 Subject: [PATCH 1/5] feat(react-headless-components-preview): add headless Overflow component --- ...-4bdc7f70-88bc-4b7a-8c35-7ddd600b9a3c.json | 7 + .../library/etc/overflow.api.md | 91 ++++++++++++ .../library/package.json | 7 + .../src/components/Overflow/Overflow.test.tsx | 93 ++++++++++++ .../src/components/Overflow/Overflow.tsx | 20 +++ .../src/components/Overflow/Overflow.types.ts | 19 +++ .../library/src/components/Overflow/index.ts | 31 ++++ .../src/components/Overflow/renderOverflow.ts | 1 + .../src/components/Overflow/useOverflow.ts | 1 + .../Overflow/useOverflowContextValues.ts | 1 + .../library/src/overflow.ts | 30 ++++ .../src/Overflow/OverflowDefault.stories.tsx | 26 ++++ .../src/Overflow/OverflowDescription.md | 22 +++ .../OverflowLargerDividers.stories.tsx | 55 +++++++ .../stories/src/Overflow/OverflowMenu.tsx | 92 ++++++++++++ .../src/Overflow/OverflowPinned.stories.tsx | 47 ++++++ .../OverflowPriorityWithDividers.stories.tsx | 140 ++++++++++++++++++ .../OverflowReverseDomOrder.stories.tsx | 26 ++++ .../src/Overflow/OverflowVertical.stories.tsx | 25 ++++ .../stories/src/Overflow/index.stories.tsx | 24 +++ .../stories/src/Overflow/overflow.module.css | 131 ++++++++++++++++ 21 files changed, 889 insertions(+) create mode 100644 change/@fluentui-react-headless-components-preview-4bdc7f70-88bc-4b7a-8c35-7ddd600b9a3c.json create mode 100644 packages/react-components/react-headless-components-preview/library/etc/overflow.api.md create mode 100644 packages/react-components/react-headless-components-preview/library/src/components/Overflow/Overflow.test.tsx create mode 100644 packages/react-components/react-headless-components-preview/library/src/components/Overflow/Overflow.tsx create mode 100644 packages/react-components/react-headless-components-preview/library/src/components/Overflow/Overflow.types.ts create mode 100644 packages/react-components/react-headless-components-preview/library/src/components/Overflow/index.ts create mode 100644 packages/react-components/react-headless-components-preview/library/src/components/Overflow/renderOverflow.ts create mode 100644 packages/react-components/react-headless-components-preview/library/src/components/Overflow/useOverflow.ts create mode 100644 packages/react-components/react-headless-components-preview/library/src/components/Overflow/useOverflowContextValues.ts create mode 100644 packages/react-components/react-headless-components-preview/library/src/overflow.ts create mode 100644 packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowDefault.stories.tsx create mode 100644 packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowDescription.md create mode 100644 packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowLargerDividers.stories.tsx create mode 100644 packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowMenu.tsx create mode 100644 packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowPinned.stories.tsx create mode 100644 packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowPriorityWithDividers.stories.tsx create mode 100644 packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowReverseDomOrder.stories.tsx create mode 100644 packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowVertical.stories.tsx create mode 100644 packages/react-components/react-headless-components-preview/stories/src/Overflow/index.stories.tsx create mode 100644 packages/react-components/react-headless-components-preview/stories/src/Overflow/overflow.module.css diff --git a/change/@fluentui-react-headless-components-preview-4bdc7f70-88bc-4b7a-8c35-7ddd600b9a3c.json b/change/@fluentui-react-headless-components-preview-4bdc7f70-88bc-4b7a-8c35-7ddd600b9a3c.json new file mode 100644 index 00000000000000..1f1b3ce0383ea2 --- /dev/null +++ b/change/@fluentui-react-headless-components-preview-4bdc7f70-88bc-4b7a-8c35-7ddd600b9a3c.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "feat: add headless Overflow component", + "packageName": "@fluentui/react-headless-components-preview", + "email": "vgenaev@gmail.com", + "dependentChangeType": "patch" +} diff --git a/packages/react-components/react-headless-components-preview/library/etc/overflow.api.md b/packages/react-components/react-headless-components-preview/library/etc/overflow.api.md new file mode 100644 index 00000000000000..c061f5e6218544 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/etc/overflow.api.md @@ -0,0 +1,91 @@ +## API Report File for "@fluentui/react-headless-components-preview" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { DATA_OVERFLOW_DIVIDER } from '@fluentui/react-overflow'; +import { DATA_OVERFLOW_ITEM } from '@fluentui/react-overflow'; +import { DATA_OVERFLOW_MENU } from '@fluentui/react-overflow'; +import { DATA_OVERFLOWING } from '@fluentui/react-overflow'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import type { OnOverflowChangeData as OnOverflowChangeData_2 } from '@fluentui/react-overflow'; +import { OverflowComponentState } from '@fluentui/react-overflow'; +import { OverflowContextValues } from '@fluentui/react-overflow'; +import { OverflowDivider } from '@fluentui/react-overflow'; +import { OverflowDividerProps } from '@fluentui/react-overflow'; +import { OverflowItem } from '@fluentui/react-overflow'; +import { OverflowItemProps } from '@fluentui/react-overflow'; +import type { OverflowProps as OverflowProps_2 } from '@fluentui/react-overflow'; +import { OverflowReorderObserver } from '@fluentui/react-overflow'; +import { OverflowState } from '@fluentui/react-overflow'; +import { renderOverflow_unstable as renderOverflow } from '@fluentui/react-overflow'; +import { useIsOverflowGroupVisible } from '@fluentui/react-overflow'; +import { useIsOverflowItemVisible } from '@fluentui/react-overflow'; +import { useOverflow_unstable as useOverflow } from '@fluentui/react-overflow'; +import { useOverflowContext } from '@fluentui/react-overflow'; +import { useOverflowContextValues_unstable as useOverflowContextValues } from '@fluentui/react-overflow'; +import { useOverflowCount } from '@fluentui/react-overflow'; +import { useOverflowDivider } from '@fluentui/react-overflow'; +import { useOverflowItem } from '@fluentui/react-overflow'; +import { useOverflowMenu } from '@fluentui/react-overflow'; +import { useOverflowVisibility } from '@fluentui/react-overflow'; + +export { DATA_OVERFLOW_DIVIDER } + +export { DATA_OVERFLOW_ITEM } + +export { DATA_OVERFLOW_MENU } + +export { DATA_OVERFLOWING } + +// @public (undocumented) +export type OnOverflowChangeData = OnOverflowChangeData_2; + +// @public +export const Overflow: ForwardRefComponent; + +export { OverflowComponentState } + +export { OverflowContextValues } + +export { OverflowDivider } + +export { OverflowDividerProps } + +export { OverflowItem } + +export { OverflowItemProps } + +// @public +export type OverflowProps = OverflowProps_2; + +export { OverflowReorderObserver } + +export { OverflowState } + +export { renderOverflow } + +export { useIsOverflowGroupVisible } + +export { useIsOverflowItemVisible } + +export { useOverflow } + +export { useOverflowContext } + +export { useOverflowContextValues } + +export { useOverflowCount } + +export { useOverflowDivider } + +export { useOverflowItem } + +export { useOverflowMenu } + +export { useOverflowVisibility } + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/react-components/react-headless-components-preview/library/package.json b/packages/react-components/react-headless-components-preview/library/package.json index 54c7d57430ef4d..ce51dc84fe63b9 100644 --- a/packages/react-components/react-headless-components-preview/library/package.json +++ b/packages/react-components/react-headless-components-preview/library/package.json @@ -41,6 +41,7 @@ "@fluentui/react-message-bar": "^9.7.1", "@fluentui/react-teaching-popover": "^9.7.0", "@fluentui/react-nav": "^9.4.0", + "@fluentui/react-overflow": "^9.8.0", "@fluentui/react-persona": "^9.7.4", "@fluentui/react-popover": "^9.14.3", "@fluentui/react-positioning": "^9.22.2", @@ -206,6 +207,12 @@ "import": "./lib/nav.js", "require": "./lib-commonjs/nav.js" }, + "./overflow": { + "types": "./dist/overflow.d.ts", + "node": "./lib-commonjs/overflow.js", + "import": "./lib/overflow.js", + "require": "./lib-commonjs/overflow.js" + }, "./persona": { "types": "./dist/persona.d.ts", "node": "./lib-commonjs/persona.js", diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Overflow/Overflow.test.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Overflow/Overflow.test.tsx new file mode 100644 index 00000000000000..3f998485b18f05 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Overflow/Overflow.test.tsx @@ -0,0 +1,93 @@ +import * as React from 'react'; +import { render } from '@testing-library/react'; +import { isConformant } from '../../testing/isConformant'; +import { Overflow } from './Overflow'; +import { OverflowItem, useOverflowMenu } from './index'; + +describe('Overflow', () => { + beforeAll(() => { + // jsdom does not implement ResizeObserver, which the overflow manager observes the container with. + // https://github.com/jsdom/jsdom/issues/3368 + global.ResizeObserver = class ResizeObserver { + public observe() { + /* noop */ + } + public unobserve() { + /* noop */ + } + public disconnect() { + /* noop */ + } + }; + }); + + isConformant({ + Component: Overflow, + displayName: 'Overflow', + // Overflow is renderless (clones its single child) and provides context, so the slot/render + // based conformance assertions do not apply. + disabledTests: ['component-handles-ref', 'component-has-root-ref', 'component-handles-classname'], + requiredProps: { children:
} as object, + }); + + it('forwards the ref to the single child', () => { + const overflowRef = jest.fn(); + const childRef = jest.fn(); + + render( + +
+ , + ); + + expect(overflowRef).toHaveBeenCalledWith(expect.objectContaining({ id: 'child', tagName: 'DIV' })); + expect(childRef).toHaveBeenCalledWith(expect.objectContaining({ id: 'child', tagName: 'DIV' })); + }); + + it('does not add any built-in className to the cloned child', () => { + const { container } = render( + +
+ , + ); + + // The headless Overflow must preserve only the child's own className — no Griffel/fui-* classes. + expect((container.firstChild as HTMLElement).className).toBe('my-container'); + }); + + it('registers overflow items on the child element', () => { + const { getByText } = render( + +
+ + + +
+
, + ); + + expect(getByText('foo')).toHaveAttribute('data-overflow-item'); + }); + + it('marks the overflow menu element with data-overflow-menu for styling', () => { + const Menu: React.FC = () => { + const { ref } = useOverflowMenu(); + return ( + + ); + }; + + const { getByTestId } = render( + +
+ +
+
, + ); + + // Headless: the engine sets the data attribute; consumers style it (e.g. `flex-shrink: 0`). + expect(getByTestId('menu')).toHaveAttribute('data-overflow-menu'); + }); +}); diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Overflow/Overflow.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Overflow/Overflow.tsx new file mode 100644 index 00000000000000..21d24f2d440afa --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Overflow/Overflow.tsx @@ -0,0 +1,20 @@ +'use client'; + +import * as React from 'react'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import type { OverflowProps } from './Overflow.types'; +import { useOverflow } from './useOverflow'; +import { useOverflowContextValues } from './useOverflowContextValues'; +import { renderOverflow } from './renderOverflow'; + +/** + * Provides an overflow context for `OverflowItem` descendants without any built-in styling. + */ +export const Overflow: ForwardRefComponent = React.forwardRef((props, ref) => { + const state = useOverflow(props, ref); + const contextValues = useOverflowContextValues(state); + + return renderOverflow(state, contextValues); +}); + +Overflow.displayName = 'Overflow'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Overflow/Overflow.types.ts b/packages/react-components/react-headless-components-preview/library/src/components/Overflow/Overflow.types.ts new file mode 100644 index 00000000000000..094453517aa155 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Overflow/Overflow.types.ts @@ -0,0 +1,19 @@ +import type { + OverflowProps as OverflowBaseProps, + OnOverflowChangeData as OnOverflowChangeDataBase, +} from '@fluentui/react-overflow'; + +export type { + OverflowItemProps, + OverflowDividerProps, + OverflowState, + OverflowComponentState, + OverflowContextValues, +} from '@fluentui/react-overflow'; + +/** + * Overflow Props + */ +export type OverflowProps = OverflowBaseProps; + +export type OnOverflowChangeData = OnOverflowChangeDataBase; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Overflow/index.ts b/packages/react-components/react-headless-components-preview/library/src/components/Overflow/index.ts new file mode 100644 index 00000000000000..412c2c84e80ee3 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Overflow/index.ts @@ -0,0 +1,31 @@ +export { Overflow } from './Overflow'; +export { useOverflow } from './useOverflow'; +export { useOverflowContextValues } from './useOverflowContextValues'; +export { renderOverflow } from './renderOverflow'; +export type { + OverflowProps, + OverflowState, + OverflowComponentState, + OverflowContextValues, + OnOverflowChangeData, + OverflowItemProps, + OverflowDividerProps, +} from './Overflow.types'; + +export { + OverflowItem, + OverflowDivider, + OverflowReorderObserver, + useOverflowMenu, + useOverflowContext, + useOverflowCount, + useOverflowItem, + useOverflowDivider, + useIsOverflowItemVisible, + useIsOverflowGroupVisible, + useOverflowVisibility, + DATA_OVERFLOWING, + DATA_OVERFLOW_ITEM, + DATA_OVERFLOW_MENU, + DATA_OVERFLOW_DIVIDER, +} from '@fluentui/react-overflow'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Overflow/renderOverflow.ts b/packages/react-components/react-headless-components-preview/library/src/components/Overflow/renderOverflow.ts new file mode 100644 index 00000000000000..672409cd1b3f36 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Overflow/renderOverflow.ts @@ -0,0 +1 @@ +export { renderOverflow_unstable as renderOverflow } from '@fluentui/react-overflow'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Overflow/useOverflow.ts b/packages/react-components/react-headless-components-preview/library/src/components/Overflow/useOverflow.ts new file mode 100644 index 00000000000000..5ad327e9e5e408 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Overflow/useOverflow.ts @@ -0,0 +1 @@ +export { useOverflow_unstable as useOverflow } from '@fluentui/react-overflow'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Overflow/useOverflowContextValues.ts b/packages/react-components/react-headless-components-preview/library/src/components/Overflow/useOverflowContextValues.ts new file mode 100644 index 00000000000000..c8e580b001b4c7 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Overflow/useOverflowContextValues.ts @@ -0,0 +1 @@ +export { useOverflowContextValues_unstable as useOverflowContextValues } from '@fluentui/react-overflow'; diff --git a/packages/react-components/react-headless-components-preview/library/src/overflow.ts b/packages/react-components/react-headless-components-preview/library/src/overflow.ts new file mode 100644 index 00000000000000..58763b12e7955e --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/overflow.ts @@ -0,0 +1,30 @@ +export { + Overflow, + useOverflow, + useOverflowContextValues, + renderOverflow, + useOverflowMenu, + OverflowItem, + OverflowDivider, + OverflowReorderObserver, + useOverflowContext, + useOverflowCount, + useOverflowItem, + useOverflowDivider, + useIsOverflowItemVisible, + useIsOverflowGroupVisible, + useOverflowVisibility, + DATA_OVERFLOWING, + DATA_OVERFLOW_ITEM, + DATA_OVERFLOW_MENU, + DATA_OVERFLOW_DIVIDER, +} from './components/Overflow'; +export type { + OverflowProps, + OverflowState, + OverflowComponentState, + OverflowContextValues, + OnOverflowChangeData, + OverflowItemProps, + OverflowDividerProps, +} from './components/Overflow'; diff --git a/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowDefault.stories.tsx b/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowDefault.stories.tsx new file mode 100644 index 00000000000000..42f9146e9aa8d9 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowDefault.stories.tsx @@ -0,0 +1,26 @@ +import * as React from 'react'; +import { Overflow, OverflowItem } from '@fluentui/react-headless-components-preview/overflow'; + +import { OverflowMenu } from './OverflowMenu'; +import styles from './overflow.module.css'; + +const itemIds = Array.from({ length: 10 }, (_, i) => (i + 1).toString()); + +/** + * Drag the dashed box's right edge to resize. Items that no longer fit are hidden and the `+N` + * button reflects the overflow count; click it to see the overflowed items. + */ +export const Default = (): React.ReactNode => ( +
+ +
+ {itemIds.map(id => ( + + + + ))} + +
+
+
+); diff --git a/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowDescription.md b/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowDescription.md new file mode 100644 index 00000000000000..8b653af994be45 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowDescription.md @@ -0,0 +1,22 @@ +Overflow detects when its items no longer fit their container and moves the extras into an overflow +menu. It ships no styling — it only sets data attributes, which you style. Two of those rules are +required for the overflow engine to work, so add them alongside your own styling: + +```css +[data-overflowing] { + display: none; +} /* removes overflowed items from layout */ +[data-overflow-menu] { + flex-shrink: 0; +} /* keeps the menu at full size for measurement */ +``` + +Compose it from: + +- `Overflow` — the root; provides context and clones its single child to measure it. +- `OverflowItem` — marks a child as an overflow item (give each a unique `id`). +- `useOverflowMenu` — registers the overflow menu element and reports the overflow count. + +Additional hooks (`useOverflowCount`, `useIsOverflowItemVisible`, `useIsOverflowGroupVisible`, +`useOverflowVisibility`, `useOverflowContext`) and the `OverflowDivider` / `OverflowReorderObserver` +components are re-exported from `@fluentui/react-overflow` for advanced scenarios. diff --git a/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowLargerDividers.stories.tsx b/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowLargerDividers.stories.tsx new file mode 100644 index 00000000000000..84f8b8030379a5 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowLargerDividers.stories.tsx @@ -0,0 +1,55 @@ +import * as React from 'react'; +import { Overflow, OverflowItem, OverflowDivider } from '@fluentui/react-headless-components-preview/overflow'; + +import { OverflowMenu } from './OverflowMenu'; +import styles from './overflow.module.css'; + +const GroupDivider: React.FC<{ groupId: string }> = ({ groupId }) => ( + +
+ +); + +const menuIds = ['1', 'divider-1', '2', 'divider-2', '3', '4', 'divider-3', '5', '6', '7', 'divider-4', '8']; + +/** + * `OverflowDivider` registers a divider with a `groupId` so its width is included in the overflow + * calculation. Group dividers are hidden (and rendered in the menu) once their group overflows. + */ +export const LargerDividers = (): React.ReactNode => ( +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+); diff --git a/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowMenu.tsx b/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowMenu.tsx new file mode 100644 index 00000000000000..547be18113fabd --- /dev/null +++ b/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowMenu.tsx @@ -0,0 +1,92 @@ +/* eslint-disable import/no-extraneous-dependencies -- story-support module; deps are provided by the stories build */ +'use client'; + +import * as React from 'react'; +import { + useOverflowMenu, + useIsOverflowItemVisible, + useIsOverflowGroupVisible, +} from '@fluentui/react-headless-components-preview/overflow'; + +import styles from './overflow.module.css'; + +export const OverflowMenuItem: React.FC<{ id: string; onClick?: () => void }> = ({ id, onClick }) => { + // Only the overflowed (hidden) items are listed in the menu. + const isVisible = useIsOverflowItemVisible(id); + + if (isVisible) { + return null; + } + + return ( + + ); +}; + +export const OverflowMenuDivider: React.FC<{ groupId: string }> = ({ groupId }) => { + const groupVisibility = useIsOverflowGroupVisible(groupId); + + if (groupVisibility === 'visible') { + return null; + } + + return
; +}; + +/** + * Renders a `+N` button that opens a popover listing the overflowed items. Mirrors the styled + * Overflow's overflow menu, but built from the headless hooks with no Griffel. Entries in `ids` + * prefixed with `divider-` render a group divider (when that group is overflowing). + * + * The popover uses `position: fixed` so it isn't clipped by the resizable container's `overflow`. + */ +export const OverflowMenu: React.FC<{ ids: string[]; onItemClick?: (id: string) => void }> = ({ ids, onItemClick }) => { + const { ref, overflowCount, isOverflowing } = useOverflowMenu(); + const [position, setPosition] = React.useState<{ top: number; left: number }>(); + + if (!isOverflowing) { + return null; + } + + const toggle = () => { + if (position) { + setPosition(undefined); + return; + } + const rect = ref.current?.getBoundingClientRect(); + setPosition(rect ? { top: rect.bottom + 4, left: rect.left } : undefined); + }; + + return ( + <> + + {position && ( +
+ {ids.map(id => + id.startsWith('divider-') ? ( + + ) : ( + onItemClick(id))} /> + ), + )} +
+ )} + + ); +}; diff --git a/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowPinned.stories.tsx b/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowPinned.stories.tsx new file mode 100644 index 00000000000000..7edf6c5f98617c --- /dev/null +++ b/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowPinned.stories.tsx @@ -0,0 +1,47 @@ +import * as React from 'react'; +import { Overflow, OverflowItem } from '@fluentui/react-headless-components-preview/overflow'; + +import { OverflowMenu } from './OverflowMenu'; +import styles from './overflow.module.css'; + +const itemIds = Array.from({ length: 10 }, (_, i) => (i + 1).toString()); + +/** + * An item can be pinned (always visible, never overflows) via the `pinned` prop on `OverflowItem` — + * useful for selection scenarios. Click items (or menu entries) to toggle their pinned state. + */ +export const Pinned = (): React.ReactNode => { + const [selected, setSelected] = React.useState>(() => new Set(['6'])); + + const toggle = (id: string) => + setSelected(prev => { + const next = new Set(prev); + if (next.has(id)) { + next.delete(id); + } else { + next.add(id); + } + return next; + }); + + return ( +
+ +
+ {itemIds.map(id => ( + + + + ))} + +
+
+
+ ); +}; diff --git a/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowPriorityWithDividers.stories.tsx b/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowPriorityWithDividers.stories.tsx new file mode 100644 index 00000000000000..0df3b1796e5da4 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowPriorityWithDividers.stories.tsx @@ -0,0 +1,140 @@ +import * as React from 'react'; +import { + Overflow, + OverflowItem, + useOverflowMenu, + useIsOverflowGroupVisible, +} from '@fluentui/react-headless-components-preview/overflow'; + +import { OverflowMenuItem } from './OverflowMenu'; +import styles from './overflow.module.css'; + +const GROUPS = { ONE: 1, TWO: 2, THREE: 3, FOUR: 4, FIVE: 5 }; + +const menuIds = ['6', 'divider-1', '7', 'divider-2', '4', '5', 'divider-3', '1', '2', '3', 'divider-4', '8']; + +/** In-container divider — hidden once its whole group has overflowed. */ +const ContainerGroupDivider: React.FC<{ groupId: number }> = ({ groupId }) => { + const groupVisibility = useIsOverflowGroupVisible(groupId.toString()); + + if (groupVisibility === 'hidden') { + return null; + } + + return
; +}; + +/** + * Menu divider — because priority differs from DOM order, a divider may be needed in the menu only + * when an overflowing group precedes another overflowing group. This mirrors the styled story's + * reference implementation. + */ +const MenuGroupDivider: React.FC<{ groupId: number }> = ({ groupId }) => { + const groupVisibilities = Object.values(GROUPS).map(group => ({ + group, + // eslint-disable-next-line react-hooks/rules-of-hooks + visibility: useIsOverflowGroupVisible(group.toString()), + })); + + const currentGroupPosition = groupVisibilities.findIndex(x => x.group === groupId); + const precedesOverflowingGroup = groupVisibilities + .slice(currentGroupPosition + 1) + .some(groupVisibility => groupVisibility.visibility !== 'visible'); + + if (groupVisibilities[currentGroupPosition].visibility === 'visible' || !precedesOverflowingGroup) { + return null; + } + + return
; +}; + +const PriorityOverflowMenu: React.FC<{ ids: string[] }> = ({ ids }) => { + const { ref, overflowCount, isOverflowing } = useOverflowMenu(); + const [position, setPosition] = React.useState<{ top: number; left: number }>(); + + if (!isOverflowing) { + return null; + } + + const toggle = () => { + if (position) { + setPosition(undefined); + return; + } + const rect = ref.current?.getBoundingClientRect(); + setPosition(rect ? { top: rect.bottom + 4, left: rect.left } : undefined); + }; + + return ( + <> + + {position && ( +
+ {ids.map(id => + id.startsWith('divider-') ? ( + + ) : ( + + ), + )} +
+ )} + + ); +}; + +/** + * Overflow groups respect item `priority`. Managing divider visibility here is non-trivial because + * dividers can appear both in the container and the menu — read the code carefully before adopting. + */ +export const PriorityWithDividers = (): React.ReactNode => ( +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+); diff --git a/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowReverseDomOrder.stories.tsx b/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowReverseDomOrder.stories.tsx new file mode 100644 index 00000000000000..237e0b77827bfc --- /dev/null +++ b/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowReverseDomOrder.stories.tsx @@ -0,0 +1,26 @@ +import * as React from 'react'; +import { Overflow, OverflowItem } from '@fluentui/react-headless-components-preview/overflow'; + +import { OverflowMenu } from './OverflowMenu'; +import styles from './overflow.module.css'; + +const itemIds = Array.from({ length: 10 }, (_, i) => (i + 1).toString()); + +/** + * Overflow can happen in reverse DOM order via `overflowDirection="start"` — here the menu is the + * first child and items overflow from the start of the container. + */ +export const ReverseDomOrder = (): React.ReactNode => ( +
+ +
+ + {itemIds.map(id => ( + + + + ))} +
+
+
+); diff --git a/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowVertical.stories.tsx b/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowVertical.stories.tsx new file mode 100644 index 00000000000000..05c7b41002989c --- /dev/null +++ b/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowVertical.stories.tsx @@ -0,0 +1,25 @@ +import * as React from 'react'; +import { Overflow, OverflowItem } from '@fluentui/react-headless-components-preview/overflow'; + +import { OverflowMenu } from './OverflowMenu'; +import styles from './overflow.module.css'; + +const itemIds = Array.from({ length: 10 }, (_, i) => (i + 1).toString()); + +/** + * Use the `overflowAxis` prop to switch orientation. Drag the dashed box's bottom edge to resize. + */ +export const Vertical = (): React.ReactNode => ( +
+ +
+ {itemIds.map(id => ( + + + + ))} + +
+
+
+); diff --git a/packages/react-components/react-headless-components-preview/stories/src/Overflow/index.stories.tsx b/packages/react-components/react-headless-components-preview/stories/src/Overflow/index.stories.tsx new file mode 100644 index 00000000000000..259849e6d7d052 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/stories/src/Overflow/index.stories.tsx @@ -0,0 +1,24 @@ +import { Overflow, OverflowItem } from '@fluentui/react-headless-components-preview/overflow'; + +import descriptionMd from './OverflowDescription.md'; +export { Default } from './OverflowDefault.stories'; +export { Vertical } from './OverflowVertical.stories'; +export { ReverseDomOrder } from './OverflowReverseDomOrder.stories'; +export { LargerDividers } from './OverflowLargerDividers.stories'; +export { Pinned } from './OverflowPinned.stories'; +export { PriorityWithDividers } from './OverflowPriorityWithDividers.stories'; + +export default { + title: 'Components/Overflow', + component: Overflow, + subcomponents: { + OverflowItem, + }, + parameters: { + docs: { + description: { + component: descriptionMd, + }, + }, + }, +}; diff --git a/packages/react-components/react-headless-components-preview/stories/src/Overflow/overflow.module.css b/packages/react-components/react-headless-components-preview/stories/src/Overflow/overflow.module.css new file mode 100644 index 00000000000000..1c220e3094b88f --- /dev/null +++ b/packages/react-components/react-headless-components-preview/stories/src/Overflow/overflow.module.css @@ -0,0 +1,131 @@ +/* + * The headless Overflow ships no styling — it only sets the `data-overflowing` and + * `data-overflow-menu` attributes. Consumers provide the two layout rules the overflow engine + * relies on (below), plus any presentational styling. + */ + +.container [data-overflowing] { + display: none; +} + +.container [data-overflow-menu] { + flex-shrink: 0; +} + +.resizer { + resize: horizontal; + overflow: hidden; + width: 360px; + max-width: 100%; + padding: 8px; + border: 1px dashed var(--border); + border-radius: var(--radius-lg); + background: var(--bg-elev); +} + +.resizerVertical { + resize: vertical; + overflow: hidden; + height: 220px; + max-height: 100%; + width: max-content; + padding: 8px; + border: 1px dashed var(--border); + border-radius: var(--radius-lg); + background: var(--bg-elev); +} + +.container { + position: relative; + display: flex; + align-items: center; + gap: 4px; + min-width: 0; + overflow: hidden; +} + +.vertical { + flex-direction: column; + align-items: stretch; + height: 100%; +} + +.item { + flex: none; + padding: 6px 12px; + border-radius: var(--radius-pill); + background: var(--surface-muted); + color: var(--text); + border: none; + white-space: nowrap; + cursor: pointer; +} + +.item:hover { + background: var(--accent); + color: var(--accent-contrast); +} + +.itemSelected { + background: var(--accent); + color: var(--accent-contrast); +} + +.menu { + flex: none; + padding: 6px 12px; + border-radius: var(--radius-pill); + background: var(--accent); + color: var(--accent-contrast); + border: none; + cursor: pointer; +} + +.divider { + flex: none; + align-self: stretch; + width: 1px; + margin: 0 4px; + background: var(--border); +} + +.vertical .divider { + width: auto; + height: 1px; + margin: 4px 0; +} + +.menuPopover { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 140px; + padding: 4px; + margin: 0; + list-style: none; + background: var(--bg-elev); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-1); +} + +.menuItem { + text-align: left; + padding: 6px 10px; + border: none; + background: transparent; + color: var(--text); + border-radius: var(--radius-pill); + cursor: pointer; + white-space: nowrap; +} + +.menuItem:hover { + background: var(--surface-muted); +} + +.menuDivider { + height: 1px; + margin: 4px 0; + background: var(--border); +} From 747370ff4e264d1accdf3180c716a573adf143b9 Mon Sep 17 00:00:00 2001 From: mainframev Date: Tue, 16 Jun 2026 15:17:30 +0200 Subject: [PATCH 2/5] fix: address fixes after review --- .../library/etc/overflow.api.md | 10 ++- .../src/components/Overflow/Overflow.test.tsx | 16 ----- .../src/components/Overflow/Overflow.types.ts | 14 +--- .../stories/src/Overflow/OverflowMenu.tsx | 67 ++++++++--------- .../OverflowPriorityWithDividers.stories.tsx | 71 ++++++++----------- .../stories/src/Overflow/overflow.module.css | 25 +++---- 6 files changed, 78 insertions(+), 125 deletions(-) diff --git a/packages/react-components/react-headless-components-preview/library/etc/overflow.api.md b/packages/react-components/react-headless-components-preview/library/etc/overflow.api.md index c061f5e6218544..7329351f567d67 100644 --- a/packages/react-components/react-headless-components-preview/library/etc/overflow.api.md +++ b/packages/react-components/react-headless-components-preview/library/etc/overflow.api.md @@ -9,14 +9,14 @@ import { DATA_OVERFLOW_ITEM } from '@fluentui/react-overflow'; import { DATA_OVERFLOW_MENU } from '@fluentui/react-overflow'; import { DATA_OVERFLOWING } from '@fluentui/react-overflow'; import type { ForwardRefComponent } from '@fluentui/react-utilities'; -import type { OnOverflowChangeData as OnOverflowChangeData_2 } from '@fluentui/react-overflow'; +import { OnOverflowChangeData } from '@fluentui/react-overflow'; import { OverflowComponentState } from '@fluentui/react-overflow'; import { OverflowContextValues } from '@fluentui/react-overflow'; import { OverflowDivider } from '@fluentui/react-overflow'; import { OverflowDividerProps } from '@fluentui/react-overflow'; import { OverflowItem } from '@fluentui/react-overflow'; import { OverflowItemProps } from '@fluentui/react-overflow'; -import type { OverflowProps as OverflowProps_2 } from '@fluentui/react-overflow'; +import { OverflowProps } from '@fluentui/react-overflow'; import { OverflowReorderObserver } from '@fluentui/react-overflow'; import { OverflowState } from '@fluentui/react-overflow'; import { renderOverflow_unstable as renderOverflow } from '@fluentui/react-overflow'; @@ -39,8 +39,7 @@ export { DATA_OVERFLOW_MENU } export { DATA_OVERFLOWING } -// @public (undocumented) -export type OnOverflowChangeData = OnOverflowChangeData_2; +export { OnOverflowChangeData } // @public export const Overflow: ForwardRefComponent; @@ -57,8 +56,7 @@ export { OverflowItem } export { OverflowItemProps } -// @public -export type OverflowProps = OverflowProps_2; +export { OverflowProps } export { OverflowReorderObserver } diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Overflow/Overflow.test.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Overflow/Overflow.test.tsx index 3f998485b18f05..6098fd19644c2a 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/Overflow/Overflow.test.tsx +++ b/packages/react-components/react-headless-components-preview/library/src/components/Overflow/Overflow.test.tsx @@ -5,22 +5,6 @@ import { Overflow } from './Overflow'; import { OverflowItem, useOverflowMenu } from './index'; describe('Overflow', () => { - beforeAll(() => { - // jsdom does not implement ResizeObserver, which the overflow manager observes the container with. - // https://github.com/jsdom/jsdom/issues/3368 - global.ResizeObserver = class ResizeObserver { - public observe() { - /* noop */ - } - public unobserve() { - /* noop */ - } - public disconnect() { - /* noop */ - } - }; - }); - isConformant({ Component: Overflow, displayName: 'Overflow', diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Overflow/Overflow.types.ts b/packages/react-components/react-headless-components-preview/library/src/components/Overflow/Overflow.types.ts index 094453517aa155..d6a60afd6096fc 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/Overflow/Overflow.types.ts +++ b/packages/react-components/react-headless-components-preview/library/src/components/Overflow/Overflow.types.ts @@ -1,19 +1,9 @@ -import type { - OverflowProps as OverflowBaseProps, - OnOverflowChangeData as OnOverflowChangeDataBase, -} from '@fluentui/react-overflow'; - export type { + OverflowProps, + OnOverflowChangeData, OverflowItemProps, OverflowDividerProps, OverflowState, OverflowComponentState, OverflowContextValues, } from '@fluentui/react-overflow'; - -/** - * Overflow Props - */ -export type OverflowProps = OverflowBaseProps; - -export type OnOverflowChangeData = OnOverflowChangeDataBase; diff --git a/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowMenu.tsx b/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowMenu.tsx index 547be18113fabd..e2c98f037031cd 100644 --- a/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowMenu.tsx +++ b/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowMenu.tsx @@ -7,6 +7,14 @@ import { useIsOverflowItemVisible, useIsOverflowGroupVisible, } from '@fluentui/react-headless-components-preview/overflow'; +import { + Menu, + MenuTrigger, + MenuPopover, + MenuList, + MenuItem, + MenuDivider, +} from '@fluentui/react-headless-components-preview/menu'; import styles from './overflow.module.css'; @@ -19,9 +27,9 @@ export const OverflowMenuItem: React.FC<{ id: string; onClick?: () => void }> = } return ( - + ); }; @@ -32,52 +40,35 @@ export const OverflowMenuDivider: React.FC<{ groupId: string }> = ({ groupId }) return null; } - return
; + return ; }; /** - * Renders a `+N` button that opens a popover listing the overflowed items. Mirrors the styled - * Overflow's overflow menu, but built from the headless hooks with no Griffel. Entries in `ids` - * prefixed with `divider-` render a group divider (when that group is overflowing). + * Renders a `+N` button that opens a menu listing the overflowed items. Mirrors the styled + * Overflow's overflow menu, but built from the headless `Menu` — which provides the correct + * `menu`/`menuitem` semantics and keyboard navigation — and the headless overflow hooks, with no + * Griffel. Entries in `ids` prefixed with `divider-` render a group divider (when that group is + * overflowing). * - * The popover uses `position: fixed` so it isn't clipped by the resizable container's `overflow`. + * `MenuPopover` renders through a portal but preserves React context, so the overflow hooks inside + * still read the `Overflow` root's context. */ export const OverflowMenu: React.FC<{ ids: string[]; onItemClick?: (id: string) => void }> = ({ ids, onItemClick }) => { const { ref, overflowCount, isOverflowing } = useOverflowMenu(); - const [position, setPosition] = React.useState<{ top: number; left: number }>(); if (!isOverflowing) { return null; } - const toggle = () => { - if (position) { - setPosition(undefined); - return; - } - const rect = ref.current?.getBoundingClientRect(); - setPosition(rect ? { top: rect.bottom + 4, left: rect.left } : undefined); - }; - return ( - <> - - {position && ( -
+ + + + + + {ids.map(id => id.startsWith('divider-') ? ( @@ -85,8 +76,8 @@ export const OverflowMenu: React.FC<{ ids: string[]; onItemClick?: (id: string) onItemClick(id))} /> ), )} -
- )} - + + + ); }; diff --git a/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowPriorityWithDividers.stories.tsx b/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowPriorityWithDividers.stories.tsx index 0df3b1796e5da4..c29ef850142dd8 100644 --- a/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowPriorityWithDividers.stories.tsx +++ b/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowPriorityWithDividers.stories.tsx @@ -4,7 +4,15 @@ import { OverflowItem, useOverflowMenu, useIsOverflowGroupVisible, + useOverflowVisibility, } from '@fluentui/react-headless-components-preview/overflow'; +import { + Menu, + MenuTrigger, + MenuPopover, + MenuList, + MenuDivider, +} from '@fluentui/react-headless-components-preview/menu'; import { OverflowMenuItem } from './OverflowMenu'; import styles from './overflow.module.css'; @@ -28,62 +36,43 @@ const ContainerGroupDivider: React.FC<{ groupId: number }> = ({ groupId }) => { * Menu divider — because priority differs from DOM order, a divider may be needed in the menu only * when an overflowing group precedes another overflowing group. This mirrors the styled story's * reference implementation. + * + * Every group's visibility is read from a single `useOverflowVisibility()` subscription rather than + * calling `useIsOverflowGroupVisible` once per group — the latter would call a hook inside a loop + * and violate the rules of hooks. */ const MenuGroupDivider: React.FC<{ groupId: number }> = ({ groupId }) => { - const groupVisibilities = Object.values(GROUPS).map(group => ({ - group, - // eslint-disable-next-line react-hooks/rules-of-hooks - visibility: useIsOverflowGroupVisible(group.toString()), - })); + const { groupVisibility } = useOverflowVisibility(); + const groups = Object.values(GROUPS); - const currentGroupPosition = groupVisibilities.findIndex(x => x.group === groupId); - const precedesOverflowingGroup = groupVisibilities + const currentGroupPosition = groups.indexOf(groupId); + const precedesOverflowingGroup = groups .slice(currentGroupPosition + 1) - .some(groupVisibility => groupVisibility.visibility !== 'visible'); + .some(group => groupVisibility[group.toString()] !== 'visible'); - if (groupVisibilities[currentGroupPosition].visibility === 'visible' || !precedesOverflowingGroup) { + if (groupVisibility[groupId.toString()] === 'visible' || !precedesOverflowingGroup) { return null; } - return
; + return ; }; const PriorityOverflowMenu: React.FC<{ ids: string[] }> = ({ ids }) => { const { ref, overflowCount, isOverflowing } = useOverflowMenu(); - const [position, setPosition] = React.useState<{ top: number; left: number }>(); if (!isOverflowing) { return null; } - const toggle = () => { - if (position) { - setPosition(undefined); - return; - } - const rect = ref.current?.getBoundingClientRect(); - setPosition(rect ? { top: rect.bottom + 4, left: rect.left } : undefined); - }; - return ( - <> - - {position && ( -
+ + + + + + {ids.map(id => id.startsWith('divider-') ? ( @@ -91,9 +80,9 @@ const PriorityOverflowMenu: React.FC<{ ids: string[] }> = ({ ids }) => { ), )} -
- )} - + + + ); }; diff --git a/packages/react-components/react-headless-components-preview/stories/src/Overflow/overflow.module.css b/packages/react-components/react-headless-components-preview/stories/src/Overflow/overflow.module.css index 1c220e3094b88f..0d0e1a5e300350 100644 --- a/packages/react-components/react-headless-components-preview/stories/src/Overflow/overflow.module.css +++ b/packages/react-components/react-headless-components-preview/stories/src/Overflow/overflow.module.css @@ -1,9 +1,3 @@ -/* - * The headless Overflow ships no styling — it only sets the `data-overflowing` and - * `data-overflow-menu` attributes. Consumers provide the two layout rules the overflow engine - * relies on (below), plus any presentational styling. - */ - .container [data-overflowing] { display: none; } @@ -96,20 +90,25 @@ } .menuPopover { - display: flex; - flex-direction: column; - gap: 2px; min-width: 140px; padding: 4px; - margin: 0; - list-style: none; background: var(--bg-elev); border: 1px solid var(--border); border-radius: var(--radius-lg); box-shadow: var(--shadow-1); } +.menuList { + display: flex; + flex-direction: column; + gap: 2px; + outline: none; +} + .menuItem { + display: flex; + align-items: center; + width: 100%; text-align: left; padding: 6px 10px; border: none; @@ -118,9 +117,11 @@ border-radius: var(--radius-pill); cursor: pointer; white-space: nowrap; + outline: none; } -.menuItem:hover { +.menuItem:hover, +.menuItem:focus-visible { background: var(--surface-muted); } From 684bdf64c64ab65a18aacb6d42e34f88d565e833 Mon Sep 17 00:00:00 2001 From: mainframev Date: Tue, 16 Jun 2026 16:04:24 +0200 Subject: [PATCH 3/5] fixup! fix: address fixes after review --- .../library/bundle-size/AllComponents.fixture.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/react-components/react-headless-components-preview/library/bundle-size/AllComponents.fixture.js b/packages/react-components/react-headless-components-preview/library/bundle-size/AllComponents.fixture.js index 81812ffed92a06..f7488248bed5ff 100644 --- a/packages/react-components/react-headless-components-preview/library/bundle-size/AllComponents.fixture.js +++ b/packages/react-components/react-headless-components-preview/library/bundle-size/AllComponents.fixture.js @@ -20,6 +20,7 @@ import * as Link from '@fluentui/react-headless-components-preview/link'; import * as Menu from '@fluentui/react-headless-components-preview/menu'; import * as MessageBar from '@fluentui/react-headless-components-preview/message-bar'; import * as Nav from '@fluentui/react-headless-components-preview/nav'; +import * as Overflow from '@fluentui/react-headless-components-preview/overflow'; import * as Persona from '@fluentui/react-headless-components-preview/persona'; import * as Popover from '@fluentui/react-headless-components-preview/popover'; import * as ProgressBar from '@fluentui/react-headless-components-preview/progress-bar'; @@ -67,6 +68,7 @@ console.log({ Menu, MessageBar, Nav, + Overflow, Persona, Popover, ProgressBar, From bc12e7f0f38c74700061dff3a0a16609acb2653c Mon Sep 17 00:00:00 2001 From: mainframev Date: Tue, 16 Jun 2026 17:29:54 +0200 Subject: [PATCH 4/5] fixup! fix: address fixes after review --- .../src/Overflow/OverflowDefault.stories.tsx | 45 +++++++++- .../OverflowLargerDividers.stories.tsx | 66 ++++++++++++++- .../stories/src/Overflow/OverflowMenu.tsx | 83 ------------------- .../src/Overflow/OverflowPinned.stories.tsx | 49 ++++++++++- .../OverflowPriorityWithDividers.stories.tsx | 15 +++- .../OverflowReverseDomOrder.stories.tsx | 45 +++++++++- .../src/Overflow/OverflowVertical.stories.tsx | 45 +++++++++- 7 files changed, 250 insertions(+), 98 deletions(-) delete mode 100644 packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowMenu.tsx diff --git a/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowDefault.stories.tsx b/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowDefault.stories.tsx index 42f9146e9aa8d9..04e0994031e899 100644 --- a/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowDefault.stories.tsx +++ b/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowDefault.stories.tsx @@ -1,11 +1,52 @@ import * as React from 'react'; -import { Overflow, OverflowItem } from '@fluentui/react-headless-components-preview/overflow'; +import { + Overflow, + OverflowItem, + useOverflowMenu, + useIsOverflowItemVisible, +} from '@fluentui/react-headless-components-preview/overflow'; +import { Menu, MenuTrigger, MenuPopover, MenuList, MenuItem } from '@fluentui/react-headless-components-preview/menu'; -import { OverflowMenu } from './OverflowMenu'; import styles from './overflow.module.css'; const itemIds = Array.from({ length: 10 }, (_, i) => (i + 1).toString()); +const OverflowMenuItem = ({ id }: { id: string }): React.ReactNode => { + // Only the overflowed (hidden) items are listed in the menu. + const isVisible = useIsOverflowItemVisible(id); + return isVisible ? null : Item {id}; +}; + +/** + * `+N` button that opens a headless `Menu` listing the overflowed items. `MenuPopover` renders + * through a portal but preserves React context, so the overflow hooks inside still read the + * `Overflow` root's context. + */ +const OverflowMenu = ({ ids }: { ids: string[] }): React.ReactNode => { + const { ref, overflowCount, isOverflowing } = useOverflowMenu(); + + if (!isOverflowing) { + return null; + } + + return ( + + + + + + + {ids.map(id => ( + + ))} + + + + ); +}; + /** * Drag the dashed box's right edge to resize. Items that no longer fit are hidden and the `+N` * button reflects the overflow count; click it to see the overflowed items. diff --git a/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowLargerDividers.stories.tsx b/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowLargerDividers.stories.tsx index 84f8b8030379a5..8c762d92b52ebe 100644 --- a/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowLargerDividers.stories.tsx +++ b/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowLargerDividers.stories.tsx @@ -1,10 +1,24 @@ import * as React from 'react'; -import { Overflow, OverflowItem, OverflowDivider } from '@fluentui/react-headless-components-preview/overflow'; +import { + Overflow, + OverflowItem, + OverflowDivider, + useOverflowMenu, + useIsOverflowItemVisible, + useIsOverflowGroupVisible, +} from '@fluentui/react-headless-components-preview/overflow'; +import { + Menu, + MenuTrigger, + MenuPopover, + MenuList, + MenuItem, + MenuDivider, +} from '@fluentui/react-headless-components-preview/menu'; -import { OverflowMenu } from './OverflowMenu'; import styles from './overflow.module.css'; -const GroupDivider: React.FC<{ groupId: string }> = ({ groupId }) => ( +const GroupDivider = ({ groupId }: { groupId: string }): React.ReactNode => (
@@ -12,6 +26,52 @@ const GroupDivider: React.FC<{ groupId: string }> = ({ groupId }) => ( const menuIds = ['1', 'divider-1', '2', 'divider-2', '3', '4', 'divider-3', '5', '6', '7', 'divider-4', '8']; +const OverflowMenuItem = ({ id }: { id: string }): React.ReactNode => { + // Only the overflowed (hidden) items are listed in the menu. + const isVisible = useIsOverflowItemVisible(id); + return isVisible ? null : Item {id}; +}; + +const OverflowMenuDivider = ({ groupId }: { groupId: string }): React.ReactNode => { + const groupVisibility = useIsOverflowGroupVisible(groupId); + return groupVisibility === 'visible' ? null : ; +}; + +/** + * `+N` button that opens a headless `Menu` listing the overflowed items. `MenuPopover` renders + * through a portal but preserves React context, so the overflow hooks inside still read the + * `Overflow` root's context. Entries prefixed with `divider-` render a group divider (when that + * group is overflowing). + */ +const OverflowMenu = ({ ids }: { ids: string[] }): React.ReactNode => { + const { ref, overflowCount, isOverflowing } = useOverflowMenu(); + + if (!isOverflowing) { + return null; + } + + return ( + + + + + + + {ids.map(id => + id.startsWith('divider-') ? ( + + ) : ( + + ), + )} + + + + ); +}; + /** * `OverflowDivider` registers a divider with a `groupId` so its width is included in the overflow * calculation. Group dividers are hidden (and rendered in the menu) once their group overflows. diff --git a/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowMenu.tsx b/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowMenu.tsx deleted file mode 100644 index e2c98f037031cd..00000000000000 --- a/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowMenu.tsx +++ /dev/null @@ -1,83 +0,0 @@ -/* eslint-disable import/no-extraneous-dependencies -- story-support module; deps are provided by the stories build */ -'use client'; - -import * as React from 'react'; -import { - useOverflowMenu, - useIsOverflowItemVisible, - useIsOverflowGroupVisible, -} from '@fluentui/react-headless-components-preview/overflow'; -import { - Menu, - MenuTrigger, - MenuPopover, - MenuList, - MenuItem, - MenuDivider, -} from '@fluentui/react-headless-components-preview/menu'; - -import styles from './overflow.module.css'; - -export const OverflowMenuItem: React.FC<{ id: string; onClick?: () => void }> = ({ id, onClick }) => { - // Only the overflowed (hidden) items are listed in the menu. - const isVisible = useIsOverflowItemVisible(id); - - if (isVisible) { - return null; - } - - return ( - - Item {id} - - ); -}; - -export const OverflowMenuDivider: React.FC<{ groupId: string }> = ({ groupId }) => { - const groupVisibility = useIsOverflowGroupVisible(groupId); - - if (groupVisibility === 'visible') { - return null; - } - - return ; -}; - -/** - * Renders a `+N` button that opens a menu listing the overflowed items. Mirrors the styled - * Overflow's overflow menu, but built from the headless `Menu` — which provides the correct - * `menu`/`menuitem` semantics and keyboard navigation — and the headless overflow hooks, with no - * Griffel. Entries in `ids` prefixed with `divider-` render a group divider (when that group is - * overflowing). - * - * `MenuPopover` renders through a portal but preserves React context, so the overflow hooks inside - * still read the `Overflow` root's context. - */ -export const OverflowMenu: React.FC<{ ids: string[]; onItemClick?: (id: string) => void }> = ({ ids, onItemClick }) => { - const { ref, overflowCount, isOverflowing } = useOverflowMenu(); - - if (!isOverflowing) { - return null; - } - - return ( - - - - - - - {ids.map(id => - id.startsWith('divider-') ? ( - - ) : ( - onItemClick(id))} /> - ), - )} - - - - ); -}; diff --git a/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowPinned.stories.tsx b/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowPinned.stories.tsx index 7edf6c5f98617c..936cf1462334ab 100644 --- a/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowPinned.stories.tsx +++ b/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowPinned.stories.tsx @@ -1,11 +1,56 @@ import * as React from 'react'; -import { Overflow, OverflowItem } from '@fluentui/react-headless-components-preview/overflow'; +import { + Overflow, + OverflowItem, + useOverflowMenu, + useIsOverflowItemVisible, +} from '@fluentui/react-headless-components-preview/overflow'; +import { Menu, MenuTrigger, MenuPopover, MenuList, MenuItem } from '@fluentui/react-headless-components-preview/menu'; -import { OverflowMenu } from './OverflowMenu'; import styles from './overflow.module.css'; const itemIds = Array.from({ length: 10 }, (_, i) => (i + 1).toString()); +const OverflowMenuItem = ({ id, onClick }: { id: string; onClick: () => void }): React.ReactNode => { + // Only the overflowed (hidden) items are listed in the menu. + const isVisible = useIsOverflowItemVisible(id); + return isVisible ? null : ( + + Item {id} + + ); +}; + +/** + * `+N` button that opens a headless `Menu` listing the overflowed items. `MenuPopover` renders + * through a portal but preserves React context, so the overflow hooks inside still read the + * `Overflow` root's context. + */ +const OverflowMenu = ({ ids, onItemClick }: { ids: string[]; onItemClick: (id: string) => void }): React.ReactNode => { + const { ref, overflowCount, isOverflowing } = useOverflowMenu(); + + if (!isOverflowing) { + return null; + } + + return ( + + + + + + + {ids.map(id => ( + onItemClick(id)} /> + ))} + + + + ); +}; + /** * An item can be pinned (always visible, never overflows) via the `pinned` prop on `OverflowItem` — * useful for selection scenarios. Click items (or menu entries) to toggle their pinned state. diff --git a/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowPriorityWithDividers.stories.tsx b/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowPriorityWithDividers.stories.tsx index c29ef850142dd8..48ae09a47cfe5b 100644 --- a/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowPriorityWithDividers.stories.tsx +++ b/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowPriorityWithDividers.stories.tsx @@ -3,6 +3,7 @@ import { Overflow, OverflowItem, useOverflowMenu, + useIsOverflowItemVisible, useIsOverflowGroupVisible, useOverflowVisibility, } from '@fluentui/react-headless-components-preview/overflow'; @@ -11,18 +12,24 @@ import { MenuTrigger, MenuPopover, MenuList, + MenuItem, MenuDivider, } from '@fluentui/react-headless-components-preview/menu'; -import { OverflowMenuItem } from './OverflowMenu'; import styles from './overflow.module.css'; const GROUPS = { ONE: 1, TWO: 2, THREE: 3, FOUR: 4, FIVE: 5 }; const menuIds = ['6', 'divider-1', '7', 'divider-2', '4', '5', 'divider-3', '1', '2', '3', 'divider-4', '8']; +const OverflowMenuItem = ({ id }: { id: string }): React.ReactNode => { + // Only the overflowed (hidden) items are listed in the menu. + const isVisible = useIsOverflowItemVisible(id); + return isVisible ? null : Item {id}; +}; + /** In-container divider — hidden once its whole group has overflowed. */ -const ContainerGroupDivider: React.FC<{ groupId: number }> = ({ groupId }) => { +const ContainerGroupDivider = ({ groupId }: { groupId: number }): React.ReactNode => { const groupVisibility = useIsOverflowGroupVisible(groupId.toString()); if (groupVisibility === 'hidden') { @@ -41,7 +48,7 @@ const ContainerGroupDivider: React.FC<{ groupId: number }> = ({ groupId }) => { * calling `useIsOverflowGroupVisible` once per group — the latter would call a hook inside a loop * and violate the rules of hooks. */ -const MenuGroupDivider: React.FC<{ groupId: number }> = ({ groupId }) => { +const MenuGroupDivider = ({ groupId }: { groupId: number }): React.ReactNode => { const { groupVisibility } = useOverflowVisibility(); const groups = Object.values(GROUPS); @@ -57,7 +64,7 @@ const MenuGroupDivider: React.FC<{ groupId: number }> = ({ groupId }) => { return ; }; -const PriorityOverflowMenu: React.FC<{ ids: string[] }> = ({ ids }) => { +const PriorityOverflowMenu = ({ ids }: { ids: string[] }): React.ReactNode => { const { ref, overflowCount, isOverflowing } = useOverflowMenu(); if (!isOverflowing) { diff --git a/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowReverseDomOrder.stories.tsx b/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowReverseDomOrder.stories.tsx index 237e0b77827bfc..8303fcfeb75de4 100644 --- a/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowReverseDomOrder.stories.tsx +++ b/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowReverseDomOrder.stories.tsx @@ -1,11 +1,52 @@ import * as React from 'react'; -import { Overflow, OverflowItem } from '@fluentui/react-headless-components-preview/overflow'; +import { + Overflow, + OverflowItem, + useOverflowMenu, + useIsOverflowItemVisible, +} from '@fluentui/react-headless-components-preview/overflow'; +import { Menu, MenuTrigger, MenuPopover, MenuList, MenuItem } from '@fluentui/react-headless-components-preview/menu'; -import { OverflowMenu } from './OverflowMenu'; import styles from './overflow.module.css'; const itemIds = Array.from({ length: 10 }, (_, i) => (i + 1).toString()); +const OverflowMenuItem = ({ id }: { id: string }): React.ReactNode => { + // Only the overflowed (hidden) items are listed in the menu. + const isVisible = useIsOverflowItemVisible(id); + return isVisible ? null : Item {id}; +}; + +/** + * `+N` button that opens a headless `Menu` listing the overflowed items. `MenuPopover` renders + * through a portal but preserves React context, so the overflow hooks inside still read the + * `Overflow` root's context. + */ +const OverflowMenu = ({ ids }: { ids: string[] }): React.ReactNode => { + const { ref, overflowCount, isOverflowing } = useOverflowMenu(); + + if (!isOverflowing) { + return null; + } + + return ( + + + + + + + {ids.map(id => ( + + ))} + + + + ); +}; + /** * Overflow can happen in reverse DOM order via `overflowDirection="start"` — here the menu is the * first child and items overflow from the start of the container. diff --git a/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowVertical.stories.tsx b/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowVertical.stories.tsx index 05c7b41002989c..34ddc1d8f4ce7d 100644 --- a/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowVertical.stories.tsx +++ b/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowVertical.stories.tsx @@ -1,11 +1,52 @@ import * as React from 'react'; -import { Overflow, OverflowItem } from '@fluentui/react-headless-components-preview/overflow'; +import { + Overflow, + OverflowItem, + useOverflowMenu, + useIsOverflowItemVisible, +} from '@fluentui/react-headless-components-preview/overflow'; +import { Menu, MenuTrigger, MenuPopover, MenuList, MenuItem } from '@fluentui/react-headless-components-preview/menu'; -import { OverflowMenu } from './OverflowMenu'; import styles from './overflow.module.css'; const itemIds = Array.from({ length: 10 }, (_, i) => (i + 1).toString()); +const OverflowMenuItem = ({ id }: { id: string }): React.ReactNode => { + // Only the overflowed (hidden) items are listed in the menu. + const isVisible = useIsOverflowItemVisible(id); + return isVisible ? null : Item {id}; +}; + +/** + * `+N` button that opens a headless `Menu` listing the overflowed items. `MenuPopover` renders + * through a portal but preserves React context, so the overflow hooks inside still read the + * `Overflow` root's context. + */ +const OverflowMenu = ({ ids }: { ids: string[] }): React.ReactNode => { + const { ref, overflowCount, isOverflowing } = useOverflowMenu(); + + if (!isOverflowing) { + return null; + } + + return ( + + + + + + + {ids.map(id => ( + + ))} + + + + ); +}; + /** * Use the `overflowAxis` prop to switch orientation. Drag the dashed box's bottom edge to resize. */ From a5c53093f6b88378f19a097327bd0b368371880b Mon Sep 17 00:00:00 2001 From: mainframev Date: Thu, 18 Jun 2026 16:49:56 +0200 Subject: [PATCH 5/5] fix: use ReactElement return type in Overflow stories for React 17 compat --- .../stories/src/Overflow/OverflowDefault.stories.tsx | 6 +++--- .../src/Overflow/OverflowLargerDividers.stories.tsx | 10 +++++----- .../stories/src/Overflow/OverflowPinned.stories.tsx | 12 +++++++++--- .../OverflowPriorityWithDividers.stories.tsx | 10 +++++----- .../src/Overflow/OverflowReverseDomOrder.stories.tsx | 6 +++--- .../src/Overflow/OverflowVertical.stories.tsx | 6 +++--- 6 files changed, 28 insertions(+), 22 deletions(-) diff --git a/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowDefault.stories.tsx b/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowDefault.stories.tsx index 04e0994031e899..04cb21daa950e0 100644 --- a/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowDefault.stories.tsx +++ b/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowDefault.stories.tsx @@ -11,7 +11,7 @@ import styles from './overflow.module.css'; const itemIds = Array.from({ length: 10 }, (_, i) => (i + 1).toString()); -const OverflowMenuItem = ({ id }: { id: string }): React.ReactNode => { +const OverflowMenuItem = ({ id }: { id: string }): React.ReactElement | null => { // Only the overflowed (hidden) items are listed in the menu. const isVisible = useIsOverflowItemVisible(id); return isVisible ? null : Item {id}; @@ -22,7 +22,7 @@ const OverflowMenuItem = ({ id }: { id: string }): React.ReactNode => { * through a portal but preserves React context, so the overflow hooks inside still read the * `Overflow` root's context. */ -const OverflowMenu = ({ ids }: { ids: string[] }): React.ReactNode => { +const OverflowMenu = ({ ids }: { ids: string[] }): React.ReactElement | null => { const { ref, overflowCount, isOverflowing } = useOverflowMenu(); if (!isOverflowing) { @@ -51,7 +51,7 @@ const OverflowMenu = ({ ids }: { ids: string[] }): React.ReactNode => { * Drag the dashed box's right edge to resize. Items that no longer fit are hidden and the `+N` * button reflects the overflow count; click it to see the overflowed items. */ -export const Default = (): React.ReactNode => ( +export const Default = (): React.ReactElement => (
diff --git a/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowLargerDividers.stories.tsx b/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowLargerDividers.stories.tsx index 8c762d92b52ebe..8478a30055cf32 100644 --- a/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowLargerDividers.stories.tsx +++ b/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowLargerDividers.stories.tsx @@ -18,7 +18,7 @@ import { import styles from './overflow.module.css'; -const GroupDivider = ({ groupId }: { groupId: string }): React.ReactNode => ( +const GroupDivider = ({ groupId }: { groupId: string }): React.ReactElement => (
@@ -26,13 +26,13 @@ const GroupDivider = ({ groupId }: { groupId: string }): React.ReactNode => ( const menuIds = ['1', 'divider-1', '2', 'divider-2', '3', '4', 'divider-3', '5', '6', '7', 'divider-4', '8']; -const OverflowMenuItem = ({ id }: { id: string }): React.ReactNode => { +const OverflowMenuItem = ({ id }: { id: string }): React.ReactElement | null => { // Only the overflowed (hidden) items are listed in the menu. const isVisible = useIsOverflowItemVisible(id); return isVisible ? null : Item {id}; }; -const OverflowMenuDivider = ({ groupId }: { groupId: string }): React.ReactNode => { +const OverflowMenuDivider = ({ groupId }: { groupId: string }): React.ReactElement | null => { const groupVisibility = useIsOverflowGroupVisible(groupId); return groupVisibility === 'visible' ? null : ; }; @@ -43,7 +43,7 @@ const OverflowMenuDivider = ({ groupId }: { groupId: string }): React.ReactNode * `Overflow` root's context. Entries prefixed with `divider-` render a group divider (when that * group is overflowing). */ -const OverflowMenu = ({ ids }: { ids: string[] }): React.ReactNode => { +const OverflowMenu = ({ ids }: { ids: string[] }): React.ReactElement | null => { const { ref, overflowCount, isOverflowing } = useOverflowMenu(); if (!isOverflowing) { @@ -76,7 +76,7 @@ const OverflowMenu = ({ ids }: { ids: string[] }): React.ReactNode => { * `OverflowDivider` registers a divider with a `groupId` so its width is included in the overflow * calculation. Group dividers are hidden (and rendered in the menu) once their group overflows. */ -export const LargerDividers = (): React.ReactNode => ( +export const LargerDividers = (): React.ReactElement => (
diff --git a/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowPinned.stories.tsx b/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowPinned.stories.tsx index 936cf1462334ab..1e028be9da980f 100644 --- a/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowPinned.stories.tsx +++ b/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowPinned.stories.tsx @@ -11,7 +11,7 @@ import styles from './overflow.module.css'; const itemIds = Array.from({ length: 10 }, (_, i) => (i + 1).toString()); -const OverflowMenuItem = ({ id, onClick }: { id: string; onClick: () => void }): React.ReactNode => { +const OverflowMenuItem = ({ id, onClick }: { id: string; onClick: () => void }): React.ReactElement | null => { // Only the overflowed (hidden) items are listed in the menu. const isVisible = useIsOverflowItemVisible(id); return isVisible ? null : ( @@ -26,7 +26,13 @@ const OverflowMenuItem = ({ id, onClick }: { id: string; onClick: () => void }): * through a portal but preserves React context, so the overflow hooks inside still read the * `Overflow` root's context. */ -const OverflowMenu = ({ ids, onItemClick }: { ids: string[]; onItemClick: (id: string) => void }): React.ReactNode => { +const OverflowMenu = ({ + ids, + onItemClick, +}: { + ids: string[]; + onItemClick: (id: string) => void; +}): React.ReactElement | null => { const { ref, overflowCount, isOverflowing } = useOverflowMenu(); if (!isOverflowing) { @@ -55,7 +61,7 @@ const OverflowMenu = ({ ids, onItemClick }: { ids: string[]; onItemClick: (id: s * An item can be pinned (always visible, never overflows) via the `pinned` prop on `OverflowItem` — * useful for selection scenarios. Click items (or menu entries) to toggle their pinned state. */ -export const Pinned = (): React.ReactNode => { +export const Pinned = (): React.ReactElement => { const [selected, setSelected] = React.useState>(() => new Set(['6'])); const toggle = (id: string) => diff --git a/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowPriorityWithDividers.stories.tsx b/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowPriorityWithDividers.stories.tsx index 48ae09a47cfe5b..089b6e706f3b9b 100644 --- a/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowPriorityWithDividers.stories.tsx +++ b/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowPriorityWithDividers.stories.tsx @@ -22,14 +22,14 @@ const GROUPS = { ONE: 1, TWO: 2, THREE: 3, FOUR: 4, FIVE: 5 }; const menuIds = ['6', 'divider-1', '7', 'divider-2', '4', '5', 'divider-3', '1', '2', '3', 'divider-4', '8']; -const OverflowMenuItem = ({ id }: { id: string }): React.ReactNode => { +const OverflowMenuItem = ({ id }: { id: string }): React.ReactElement | null => { // Only the overflowed (hidden) items are listed in the menu. const isVisible = useIsOverflowItemVisible(id); return isVisible ? null : Item {id}; }; /** In-container divider — hidden once its whole group has overflowed. */ -const ContainerGroupDivider = ({ groupId }: { groupId: number }): React.ReactNode => { +const ContainerGroupDivider = ({ groupId }: { groupId: number }): React.ReactElement | null => { const groupVisibility = useIsOverflowGroupVisible(groupId.toString()); if (groupVisibility === 'hidden') { @@ -48,7 +48,7 @@ const ContainerGroupDivider = ({ groupId }: { groupId: number }): React.ReactNod * calling `useIsOverflowGroupVisible` once per group — the latter would call a hook inside a loop * and violate the rules of hooks. */ -const MenuGroupDivider = ({ groupId }: { groupId: number }): React.ReactNode => { +const MenuGroupDivider = ({ groupId }: { groupId: number }): React.ReactElement | null => { const { groupVisibility } = useOverflowVisibility(); const groups = Object.values(GROUPS); @@ -64,7 +64,7 @@ const MenuGroupDivider = ({ groupId }: { groupId: number }): React.ReactNode => return ; }; -const PriorityOverflowMenu = ({ ids }: { ids: string[] }): React.ReactNode => { +const PriorityOverflowMenu = ({ ids }: { ids: string[] }): React.ReactElement | null => { const { ref, overflowCount, isOverflowing } = useOverflowMenu(); if (!isOverflowing) { @@ -97,7 +97,7 @@ const PriorityOverflowMenu = ({ ids }: { ids: string[] }): React.ReactNode => { * Overflow groups respect item `priority`. Managing divider visibility here is non-trivial because * dividers can appear both in the container and the menu — read the code carefully before adopting. */ -export const PriorityWithDividers = (): React.ReactNode => ( +export const PriorityWithDividers = (): React.ReactElement => (
diff --git a/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowReverseDomOrder.stories.tsx b/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowReverseDomOrder.stories.tsx index 8303fcfeb75de4..063e2e4d6bf7be 100644 --- a/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowReverseDomOrder.stories.tsx +++ b/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowReverseDomOrder.stories.tsx @@ -11,7 +11,7 @@ import styles from './overflow.module.css'; const itemIds = Array.from({ length: 10 }, (_, i) => (i + 1).toString()); -const OverflowMenuItem = ({ id }: { id: string }): React.ReactNode => { +const OverflowMenuItem = ({ id }: { id: string }): React.ReactElement | null => { // Only the overflowed (hidden) items are listed in the menu. const isVisible = useIsOverflowItemVisible(id); return isVisible ? null : Item {id}; @@ -22,7 +22,7 @@ const OverflowMenuItem = ({ id }: { id: string }): React.ReactNode => { * through a portal but preserves React context, so the overflow hooks inside still read the * `Overflow` root's context. */ -const OverflowMenu = ({ ids }: { ids: string[] }): React.ReactNode => { +const OverflowMenu = ({ ids }: { ids: string[] }): React.ReactElement | null => { const { ref, overflowCount, isOverflowing } = useOverflowMenu(); if (!isOverflowing) { @@ -51,7 +51,7 @@ const OverflowMenu = ({ ids }: { ids: string[] }): React.ReactNode => { * Overflow can happen in reverse DOM order via `overflowDirection="start"` — here the menu is the * first child and items overflow from the start of the container. */ -export const ReverseDomOrder = (): React.ReactNode => ( +export const ReverseDomOrder = (): React.ReactElement => (
diff --git a/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowVertical.stories.tsx b/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowVertical.stories.tsx index 34ddc1d8f4ce7d..57ee5f9c949a44 100644 --- a/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowVertical.stories.tsx +++ b/packages/react-components/react-headless-components-preview/stories/src/Overflow/OverflowVertical.stories.tsx @@ -11,7 +11,7 @@ import styles from './overflow.module.css'; const itemIds = Array.from({ length: 10 }, (_, i) => (i + 1).toString()); -const OverflowMenuItem = ({ id }: { id: string }): React.ReactNode => { +const OverflowMenuItem = ({ id }: { id: string }): React.ReactElement | null => { // Only the overflowed (hidden) items are listed in the menu. const isVisible = useIsOverflowItemVisible(id); return isVisible ? null : Item {id}; @@ -22,7 +22,7 @@ const OverflowMenuItem = ({ id }: { id: string }): React.ReactNode => { * through a portal but preserves React context, so the overflow hooks inside still read the * `Overflow` root's context. */ -const OverflowMenu = ({ ids }: { ids: string[] }): React.ReactNode => { +const OverflowMenu = ({ ids }: { ids: string[] }): React.ReactElement | null => { const { ref, overflowCount, isOverflowing } = useOverflowMenu(); if (!isOverflowing) { @@ -50,7 +50,7 @@ const OverflowMenu = ({ ids }: { ids: string[] }): React.ReactNode => { /** * Use the `overflowAxis` prop to switch orientation. Drag the dashed box's bottom edge to resize. */ -export const Vertical = (): React.ReactNode => ( +export const Vertical = (): React.ReactElement => (