Skip to content

Commit 7530c0c

Browse files
authored
feat(react-overflow): split and expose hooks for headless reuse (#36304)
1 parent de337cf commit 7530c0c

15 files changed

Lines changed: 374 additions & 145 deletions
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "minor",
3+
"comment": "Expose Overflow use/render/styles hooks, provider and context types for headless consumers",
4+
"packageName": "@fluentui/react-overflow",
5+
"email": "vgenaev@gmail.com",
6+
"dependentChangeType": "patch"
7+
}

packages/react-components/react-overflow/library/etc/react-overflow.api.md

Lines changed: 82 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44
55
```ts
66

7-
import type { OnUpdateOverflow } from '@fluentui/priority-overflow';
7+
import { OnUpdateItemVisibility } from '@fluentui/priority-overflow';
8+
import { OnUpdateOverflow } from '@fluentui/priority-overflow';
89
import type { OverflowDividerEntry } from '@fluentui/priority-overflow';
10+
import { OverflowEventPayload } from '@fluentui/priority-overflow';
911
import type { OverflowGroupState } from '@fluentui/priority-overflow';
1012
import type { OverflowItemEntry } from '@fluentui/priority-overflow';
11-
import type { OverflowOptions } from '@fluentui/priority-overflow';
13+
import { OverflowOptions } from '@fluentui/priority-overflow';
1214
import type { OverflowSnapshot } from '@fluentui/priority-overflow';
1315
import * as React_2 from 'react';
1416

@@ -28,15 +30,65 @@ export const DATA_OVERFLOWING = "data-overflowing";
2830
export interface OnOverflowChangeData extends OverflowState {
2931
}
3032

33+
export { OnUpdateItemVisibility }
34+
35+
export { OnUpdateOverflow }
36+
3137
// @public
32-
export const Overflow: React_2.ForwardRefExoticComponent<Partial<Pick<OverflowOptions, "padding" | "overflowDirection" | "overflowAxis" | "minimumVisible" | "hasHiddenItems">> & {
38+
export const Overflow: React_2.ForwardRefExoticComponent<Partial<Pick<OverflowOptions, "overflowAxis" | "overflowDirection" | "padding" | "minimumVisible" | "hasHiddenItems">> & {
3339
children: React_2.ReactElement;
3440
onOverflowChange?: (ev: null, data: OverflowState) => void;
3541
} & React_2.RefAttributes<unknown>>;
3642

43+
// @public
44+
export type OverflowComponentState = UseOverflowContainerReturn<HTMLElement> & {
45+
ref: React_2.Ref<HTMLElement>;
46+
className?: string;
47+
children: React_2.ReactElement;
48+
};
49+
50+
// @public (undocumented)
51+
export interface OverflowContextValue {
52+
// (undocumented)
53+
containerRef?: React_2.RefObject<HTMLElement | null>;
54+
// (undocumented)
55+
forceUpdateOverflow: () => void;
56+
// (undocumented)
57+
getSnapshot: () => OverflowSnapshot;
58+
// @deprecated (undocumented)
59+
groupVisibility: Record<string, OverflowGroupState>;
60+
// @deprecated (undocumented)
61+
hasOverflow: boolean;
62+
// @deprecated (undocumented)
63+
itemVisibility: Record<string, boolean>;
64+
// (undocumented)
65+
registerDivider: (divider: OverflowDividerEntry) => () => void;
66+
// (undocumented)
67+
registerItem: (item: OverflowItemEntry) => () => void;
68+
// (undocumented)
69+
registerOverflowMenu: (el: HTMLElement) => () => void;
70+
// (undocumented)
71+
subscribe: (listener: () => void) => () => void;
72+
// (undocumented)
73+
updateOverflow: (padding?: number) => void;
74+
}
75+
76+
// @public
77+
export type OverflowContextValues = {
78+
overflow: OverflowContextValue;
79+
};
80+
3781
// @public
3882
export const OverflowDivider: React_2.ForwardRefExoticComponent<OverflowDividerProps & React_2.RefAttributes<unknown>>;
3983

84+
// @public
85+
export type OverflowDividerProps = {
86+
groupId: string;
87+
children: React_2.ReactElement;
88+
};
89+
90+
export { OverflowEventPayload }
91+
4092
// @public
4193
export const OverflowItem: React_2.ForwardRefExoticComponent<OverflowItemProps & React_2.RefAttributes<unknown>>;
4294

@@ -53,25 +105,44 @@ export type OverflowItemProps = {
53105
priority?: number;
54106
});
55107

56-
// @public
108+
// @public (undocumented)
57109
export type OverflowProps = Partial<Pick<OverflowOptions, 'overflowAxis' | 'overflowDirection' | 'padding' | 'minimumVisible' | 'hasHiddenItems'>> & {
58110
children: React_2.ReactElement;
59111
onOverflowChange?: (ev: null, data: OverflowState) => void;
60112
};
61113

114+
// @public
115+
export const OverflowProvider: React_2.Provider<OverflowContextValue>;
116+
62117
// @public
63118
export const OverflowReorderObserver: React_2.FC;
64119

120+
// @public (undocumented)
121+
export interface OverflowState {
122+
// (undocumented)
123+
groupVisibility: Record<string, OverflowGroupState>;
124+
// (undocumented)
125+
hasOverflow: boolean;
126+
// (undocumented)
127+
itemVisibility: Record<string, boolean>;
128+
}
129+
130+
// @public
131+
export const renderOverflow_unstable: (state: OverflowComponentState, contextValues: OverflowContextValues) => React_2.ReactElement;
132+
65133
// @public (undocumented)
66134
export function useIsOverflowGroupVisible(id: string): OverflowGroupState;
67135

68136
// @public (undocumented)
69137
export function useIsOverflowItemVisible(id: string): boolean;
70138

139+
// @public
140+
export const useOverflow_unstable: (props: OverflowProps, ref: React_2.Ref<HTMLElement>) => OverflowComponentState;
141+
71142
// @internal (undocumented)
72143
export const useOverflowContainer: <TElement extends HTMLElement>(update: OnUpdateOverflow, options: Omit<OverflowOptions, "onUpdateOverflow">) => UseOverflowContainerReturn<TElement>;
73144

74-
// @internal (undocumented)
145+
// @public (undocumented)
75146
export interface UseOverflowContainerReturn<TElement extends HTMLElement> extends Pick<OverflowContextValue, 'registerItem' | 'updateOverflow' | 'forceUpdateOverflow' | 'registerOverflowMenu' | 'registerDivider' | 'getSnapshot' | 'subscribe'> {
76147
containerRef: React_2.RefObject<TElement | null>;
77148
}
@@ -82,6 +153,9 @@ export function useOverflowContext(): OverflowContextValue;
82153
// @internal (undocumented)
83154
export function useOverflowContext<SelectedValue>(selector: (context: OverflowContextValue) => SelectedValue): SelectedValue;
84155

156+
// @internal
157+
export const useOverflowContextValues_unstable: (state: OverflowComponentState) => OverflowContextValues;
158+
85159
// @public (undocumented)
86160
export const useOverflowCount: () => number;
87161

@@ -98,6 +172,9 @@ export function useOverflowMenu<TElement extends HTMLElement>(id?: string): {
98172
isOverflowing: boolean;
99173
};
100174

175+
// @public (undocumented)
176+
export const useOverflowStyles_unstable: (state: OverflowComponentState) => OverflowComponentState;
177+
101178
// @public
102179
export function useOverflowVisibility(): {
103180
itemVisibility: Record<string, boolean>;

packages/react-components/react-overflow/library/src/components/Overflow.tsx

Lines changed: 0 additions & 116 deletions
This file was deleted.

packages/react-components/react-overflow/library/src/components/Overflow.test.tsx renamed to packages/react-components/react-overflow/library/src/components/Overflow/Overflow.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as React from 'react';
22
import { render } from '@testing-library/react';
33
import { Overflow } from './Overflow';
4-
import { OverflowItem } from './OverflowItem';
4+
import { OverflowItem } from '../OverflowItem';
55

66
describe('Overflow', () => {
77
beforeAll(() => {
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
'use client';
2+
3+
import * as React from 'react';
4+
import type { OverflowProps } from './Overflow.types';
5+
import { useOverflow_unstable } from './useOverflow';
6+
import { useOverflowContextValues_unstable } from '../../useOverflowContextValues';
7+
import { useOverflowStyles_unstable } from './useOverflowStyles.styles';
8+
import { renderOverflow_unstable } from './renderOverflow';
9+
10+
/**
11+
* Provides an OverflowContext for OverflowItem descendants.
12+
*/
13+
export const Overflow = React.forwardRef((props: OverflowProps, ref) => {
14+
const state = useOverflow_unstable(props, ref as React.Ref<HTMLElement>);
15+
const contextValues = useOverflowContextValues_unstable(state);
16+
17+
useOverflowStyles_unstable(state);
18+
19+
return renderOverflow_unstable(state, contextValues);
20+
});
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import type * as React from 'react';
2+
import type { OverflowOptions, OverflowGroupState } from '@fluentui/priority-overflow';
3+
import type { OverflowContextValue } from '../../overflowContext';
4+
import type { UseOverflowContainerReturn } from '../../types';
5+
6+
export interface OverflowState {
7+
hasOverflow: boolean;
8+
itemVisibility: Record<string, boolean>;
9+
groupVisibility: Record<string, OverflowGroupState>;
10+
}
11+
12+
export interface OnOverflowChangeData extends OverflowState {}
13+
14+
export type OverflowProps = Partial<
15+
Pick<OverflowOptions, 'overflowAxis' | 'overflowDirection' | 'padding' | 'minimumVisible' | 'hasHiddenItems'>
16+
> & {
17+
children: React.ReactElement;
18+
19+
// overflow is not caused by DOM event
20+
// eslint-disable-next-line @nx/workspace-consistent-callback-type
21+
onOverflowChange?: (ev: null, data: OverflowState) => void;
22+
};
23+
24+
/**
25+
* State used in rendering Overflow. Carries the overflow container's registration api so
26+
* `useOverflowContextValues_unstable` can assemble the context values.
27+
*/
28+
export type OverflowComponentState = UseOverflowContainerReturn<HTMLElement> & {
29+
/**
30+
* Merged ref applied to the cloned child's root element.
31+
*/
32+
ref: React.Ref<HTMLElement>;
33+
/**
34+
* Class name applied to the cloned child. Set by `useOverflowStyles_unstable`; left `undefined`
35+
* by headless consumers that apply no styling.
36+
*/
37+
className?: string;
38+
/**
39+
* The single child that overflow behavior is attached to.
40+
*/
41+
children: React.ReactElement;
42+
};
43+
44+
/**
45+
* Context values provided to overflow descendants.
46+
*/
47+
export type OverflowContextValues = {
48+
overflow: OverflowContextValue;
49+
};
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export { Overflow } from './Overflow';
2+
export { useOverflow_unstable } from './useOverflow';
3+
export { renderOverflow_unstable } from './renderOverflow';
4+
export { useOverflowStyles_unstable } from './useOverflowStyles.styles';
5+
export type {
6+
OverflowProps,
7+
OnOverflowChangeData,
8+
OverflowState,
9+
OverflowComponentState,
10+
OverflowContextValues,
11+
} from './Overflow.types';
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import * as React from 'react';
2+
import { applyTriggerPropsToChildren, getTriggerChild } from '@fluentui/react-utilities';
3+
import { OverflowContext } from '../../overflowContext';
4+
import type { OverflowComponentState, OverflowContextValues } from './Overflow.types';
5+
6+
/**
7+
* Render the final JSX of Overflow.
8+
*
9+
* Griffel-free so it can be reused by headless consumers; all class merging happens in
10+
* `useOverflowStyles_unstable`.
11+
*/
12+
export const renderOverflow_unstable = (
13+
state: OverflowComponentState,
14+
contextValues: OverflowContextValues,
15+
): React.ReactElement => {
16+
const child = getTriggerChild<HTMLElement>(state.children);
17+
18+
const clonedChild = applyTriggerPropsToChildren(state.children, {
19+
ref: state.ref,
20+
className: state.className ?? child?.props.className,
21+
});
22+
23+
return <OverflowContext.Provider value={contextValues.overflow}>{clonedChild}</OverflowContext.Provider>;
24+
};

0 commit comments

Comments
 (0)