Skip to content

Commit 36b5ff2

Browse files
committed
chore: remove manager state from Overflow container
1 parent 94e7a92 commit 36b5ff2

13 files changed

Lines changed: 126 additions & 128 deletions

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
```ts
66

77
// @internal (undocumented)
8-
export function createOverflowManager(): OverflowManager;
8+
export function createOverflowManager(initialOptions: Required<ObserveOptions>): OverflowManager;
99

1010
// @public (undocumented)
1111
export interface ObserveOptions {

packages/react-components/priority-overflow/src/overflowManager.ts

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import type {
1616
* @internal
1717
* @returns overflow manager instance
1818
*/
19-
export function createOverflowManager(): OverflowManager {
19+
export function createOverflowManager(initialOptions: Required<ObserveOptions>): OverflowManager {
2020
// calls to `offsetWidth or offsetHeight` can happen multiple times in an update
2121
// Use a cache to avoid causing too many recalcs and avoid scripting time to meausure sizes
2222
const sizeCache = new Map<HTMLElement, number>();
@@ -27,16 +27,7 @@ export function createOverflowManager(): OverflowManager {
2727
// If true, next update will dispatch to onUpdateOverflow even if queue top states don't change
2828
// Initially true to force dispatch on first mount
2929
let forceDispatch = true;
30-
const options: Required<ObserveOptions> = {
31-
padding: 10,
32-
overflowAxis: 'horizontal',
33-
overflowDirection: 'end',
34-
minimumVisible: 0,
35-
onUpdateItemVisibility: () => undefined,
36-
onUpdateOverflow: () => undefined,
37-
hasHiddenItems: false,
38-
};
39-
30+
const options: Required<ObserveOptions> = initialOptions;
4031
const overflowItems: Record<string, OverflowItemEntry> = {};
4132
const overflowDividers: Record<string, OverflowDividerEntry> = {};
4233
const listeners = new Set<() => void>();
@@ -255,6 +246,7 @@ export function createOverflowManager(): OverflowManager {
255246
};
256247

257248
const setOptions: OverflowManager['setOptions'] = nextOptions => {
249+
if (options === nextOptions) return;
258250
const previousAxis = options.overflowAxis;
259251
const previousDirection = options.overflowDirection;
260252
const previousPadding = options.padding;

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

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

7-
import type { ContextSelector } from '@fluentui/react-context-selector';
87
import type { ObserveOptions } from '@fluentui/priority-overflow';
98
import type { OnUpdateOverflow } from '@fluentui/priority-overflow';
109
import type { OverflowDividerEntry } from '@fluentui/priority-overflow';
11-
import { OverflowGroupState } from '@fluentui/priority-overflow';
10+
import type { OverflowGroupState } from '@fluentui/priority-overflow';
1211
import type { OverflowItemEntry } from '@fluentui/priority-overflow';
1312
import type { OverflowManager } from '@fluentui/priority-overflow';
13+
import type { OverflowSnapshot } from '@fluentui/priority-overflow';
1414
import * as React_2 from 'react';
1515

1616
// @public (undocumented)
@@ -79,7 +79,10 @@ export interface UseOverflowContainerReturn<TElement extends HTMLElement> extend
7979
}
8080

8181
// @internal (undocumented)
82-
export const useOverflowContext: <SelectedValue>(selector: ContextSelector<OverflowContextValue, SelectedValue>) => SelectedValue;
82+
export const useOverflowContext: {
83+
<SelectedValue>(selector: ContextSelector<OverflowContextValue, SelectedValue>): SelectedValue;
84+
(): OverflowContextValue;
85+
};
8386

8487
// @public (undocumented)
8588
export const useOverflowCount: () => number;

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

Lines changed: 34 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@
22

33
import * as React from 'react';
44
import { mergeClasses } from '@griffel/react';
5-
import type { ObserveOptions, OverflowGroupState, OverflowSnapshot } from '@fluentui/priority-overflow';
5+
import type { ObserveOptions, OnUpdateOverflow, OverflowGroupState } from '@fluentui/priority-overflow';
66
import {
77
applyTriggerPropsToChildren,
88
getTriggerChild,
99
getReactElementRef,
1010
useMergedRefs,
1111
} from '@fluentui/react-utilities';
1212

13-
import { defaultOverflowManager, OverflowContext } from '../overflowContext';
13+
import { OverflowContext, type OverflowContextValue } from '../overflowContext';
1414
import { updateVisibilityAttribute, useOverflowContainer } from '../useOverflowContainer';
1515
import { useOverflowStyles } from './useOverflowStyles.styles';
1616

@@ -20,12 +20,6 @@ interface OverflowState {
2020
groupVisibility: Record<string, OverflowGroupState>;
2121
}
2222

23-
const toOverflowState = (snapshot: OverflowSnapshot): OverflowState => ({
24-
hasOverflow: snapshot.hasOverflow,
25-
itemVisibility: snapshot.itemVisibility,
26-
groupVisibility: snapshot.groupVisibility,
27-
});
28-
2923
export interface OnOverflowChangeData extends OverflowState {}
3024

3125
/**
@@ -57,8 +51,24 @@ export const Overflow = React.forwardRef((props: OverflowProps, ref) => {
5751
hasHiddenItems,
5852
} = props;
5953

54+
const update: OnUpdateOverflow = data => {
55+
const { visibleItems, invisibleItems, groupVisibility } = data;
56+
57+
const itemVisibility: Record<string, boolean> = {};
58+
visibleItems.forEach(item => {
59+
itemVisibility[item.id] = true;
60+
});
61+
invisibleItems.forEach(x => (itemVisibility[x.id] = false));
62+
const newState = {
63+
hasOverflow: data.invisibleItems.length > 0,
64+
itemVisibility,
65+
groupVisibility,
66+
};
67+
onOverflowChange?.(null, { ...newState });
68+
};
69+
6070
const { containerRef, manager, registerItem, updateOverflow, registerOverflowMenu, registerDivider } =
61-
useOverflowContainer(() => undefined, {
71+
useOverflowContainer(update, {
6272
overflowDirection,
6373
overflowAxis,
6474
padding,
@@ -67,46 +77,26 @@ export const Overflow = React.forwardRef((props: OverflowProps, ref) => {
6777
onUpdateItemVisibility: updateVisibilityAttribute,
6878
});
6979

70-
const lastReportedState = React.useRef<OverflowState | null>(null);
71-
72-
React.useEffect(() => {
73-
if (!manager || !onOverflowChange) {
74-
return;
75-
}
76-
77-
lastReportedState.current = null;
78-
79-
return manager.subscribe(() => {
80-
const overflowState = toOverflowState(manager.getSnapshot());
81-
82-
if (lastReportedState.current === null) {
83-
lastReportedState.current = overflowState;
84-
return;
85-
}
86-
87-
onOverflowChange(null, overflowState);
88-
lastReportedState.current = overflowState;
89-
});
90-
}, [manager, onOverflowChange]);
91-
9280
const child = getTriggerChild<HTMLElement>(children);
9381
const clonedChild = applyTriggerPropsToChildren(children, {
9482
ref: useMergedRefs(containerRef, ref, getReactElementRef(child)),
9583
className: mergeClasses('fui-Overflow', styles.overflowMenu, styles.overflowingItems, child?.props.className),
9684
});
9785

98-
return (
99-
<OverflowContext.Provider
100-
value={{
101-
manager: manager ?? defaultOverflowManager,
102-
registerItem,
103-
updateOverflow,
104-
registerOverflowMenu,
105-
registerDivider,
106-
containerRef,
107-
}}
108-
>
109-
{clonedChild}
110-
</OverflowContext.Provider>
86+
const ctx: OverflowContextValue = React.useMemo(
87+
() => ({
88+
groupVisibility: {},
89+
itemVisibility: {},
90+
hasOverflow: false,
91+
registerItem,
92+
updateOverflow,
93+
registerOverflowMenu,
94+
registerDivider,
95+
getSnapshot: manager!.getSnapshot,
96+
subscribe: manager!.subscribe,
97+
}),
98+
[manager, registerItem, updateOverflow, registerOverflowMenu, registerDivider],
11199
);
100+
101+
return <OverflowContext.Provider value={ctx}>{clonedChild}</OverflowContext.Provider>;
112102
});
Lines changed: 42 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,73 @@
11
'use client';
22

3-
import type * as React from 'react';
43
import type {
54
OverflowItemEntry,
65
OverflowDividerEntry,
7-
OverflowManager,
6+
OverflowGroupState,
87
OverflowSnapshot,
98
} from '@fluentui/priority-overflow';
10-
import type { ContextSelector, Context } from '@fluentui/react-context-selector';
11-
import { createContext, useContextSelector } from '@fluentui/react-context-selector';
12-
13-
const defaultSnapshot: OverflowSnapshot = {
14-
hasOverflow: false,
15-
overflowCount: 0,
16-
itemVisibility: {},
17-
groupVisibility: {},
18-
};
19-
20-
export const defaultOverflowManager: OverflowManager = {
21-
setOptions: () => undefined,
22-
observe: () => () => undefined,
23-
registerItem: () => () => undefined,
24-
removeItem: () => undefined,
25-
update: () => undefined,
26-
forceUpdate: () => undefined,
27-
attachOverflowMenu: () => () => undefined,
28-
registerDivider: () => () => undefined,
29-
getSnapshot: () => defaultSnapshot,
30-
subscribe: () => () => undefined,
31-
};
9+
import * as React from 'react';
3210

3311
/**
3412
* @internal
3513
*/
3614
export interface OverflowContextValue {
37-
manager: OverflowManager;
15+
/**
16+
* @deprecated This value is not guaranteed to be up to date and should not be used directly. Use getSnapshot or the provided hooks instead
17+
*/
18+
itemVisibility: Record<string, boolean>;
19+
/**
20+
* @deprecated This value is not guaranteed to be up to date and should not be used directly. Use getSnapshot or the provided hooks instead
21+
*/
22+
groupVisibility: Record<string, OverflowGroupState>;
23+
/**
24+
* @deprecated This value is not guaranteed to be up to date and should not be used directly. Use getSnapshot or the provided hooks instead
25+
*/
26+
hasOverflow: boolean;
3827
registerItem: (item: OverflowItemEntry) => () => void;
3928
registerOverflowMenu: (el: HTMLElement) => () => void;
4029
registerDivider: (divider: OverflowDividerEntry) => () => void;
4130
updateOverflow: (padding?: number) => void;
4231
containerRef?: React.RefObject<HTMLElement | null>;
32+
getSnapshot: () => OverflowSnapshot;
33+
subscribe: (listener: () => void) => () => void;
4334
}
4435

45-
export const OverflowContext = createContext<OverflowContextValue | undefined>(
36+
export const OverflowContext = React.createContext<OverflowContextValue | undefined>(
4637
undefined,
47-
) as Context<OverflowContextValue>;
38+
) as React.Context<OverflowContextValue>;
4839

4940
const overflowContextDefaultValue: OverflowContextValue = {
50-
manager: defaultOverflowManager,
41+
hasOverflow: false,
42+
itemVisibility: {},
43+
groupVisibility: {},
5144
registerItem: () => () => null,
5245
updateOverflow: () => null,
5346
registerOverflowMenu: () => () => null,
5447
registerDivider: () => () => null,
48+
getSnapshot: () => ({
49+
hasOverflow: false,
50+
overflowCount: 0,
51+
itemVisibility: {},
52+
groupVisibility: {},
53+
}),
54+
subscribe: () => () => null,
5555
};
5656

57+
type ContextSelector<TContext, TSelected> = (context: TContext) => TSelected;
58+
5759
/**
5860
* @internal
5961
*/
60-
export const useOverflowContext = <SelectedValue>(
61-
selector: ContextSelector<OverflowContextValue, SelectedValue>,
62-
): SelectedValue => useContextSelector(OverflowContext, (ctx = overflowContextDefaultValue) => selector(ctx));
62+
export const useOverflowContext: {
63+
<SelectedValue>(selector: ContextSelector<OverflowContextValue, SelectedValue>): SelectedValue;
64+
(): OverflowContextValue;
65+
} =
66+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
67+
(selector?: ContextSelector<OverflowContextValue, any>): any => {
68+
const context = React.useContext(OverflowContext) ?? overflowContextDefaultValue;
69+
if (selector) {
70+
return selector(context);
71+
}
72+
return context;
73+
};
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
'use client';
22

33
import type { OverflowGroupState } from '@fluentui/priority-overflow';
4-
import { useOverflowSelector } from './useOverflowSelector';
4+
import { useOverflowSnapshot } from './useOverflowSnapshot';
55

66
/**
77
* @param id - unique identifier for a group of overflow items
88
* @returns visibility state of the group
99
*/
1010
export function useIsOverflowGroupVisible(id: string): OverflowGroupState {
11-
return useOverflowSelector(snapshot => snapshot.groupVisibility[id]);
11+
return useOverflowSnapshot().groupVisibility[id];
1212
}
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
'use client';
22

3-
import { useOverflowSelector } from './useOverflowSelector';
3+
import { useOverflowSnapshot } from './useOverflowSnapshot';
44

55
/**
66
* @param id - unique identifier for the item used by the overflow manager
77
* @returns visibility state of an overflow item
88
*/
99
export function useIsOverflowItemVisible(id: string): boolean {
10-
return !!useOverflowSelector(snapshot => snapshot.itemVisibility[id]);
10+
return !!useOverflowSnapshot().itemVisibility[id];
1111
}

packages/react-components/react-overflow/library/src/useOverflowContainer.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import type {
1414
OverflowManager,
1515
ObserveOptions,
1616
} from '@fluentui/priority-overflow';
17-
import { canUseDOM, useEventCallback, useIsomorphicLayoutEffect } from '@fluentui/react-utilities';
17+
import { canUseDOM, useEventCallback } from '@fluentui/react-utilities';
1818
import type { UseOverflowContainerReturn } from './types';
1919
import { DATA_OVERFLOWING, DATA_OVERFLOW_DIVIDER, DATA_OVERFLOW_ITEM, DATA_OVERFLOW_MENU } from './constants';
2020

@@ -44,7 +44,7 @@ export const useOverflowContainer = <TElement extends HTMLElement>(
4444
const onUpdateOverflow = useEventCallback(update);
4545
const onUpdateItemVisibilityCallback = useEventCallback(onUpdateItemVisibility);
4646

47-
const observeOptions = React.useMemo(
47+
const observeOptions: Required<ObserveOptions> = React.useMemo(
4848
() => ({
4949
overflowAxis,
5050
overflowDirection,
@@ -69,8 +69,9 @@ export const useOverflowContainer = <TElement extends HTMLElement>(
6969
const cleanupObservationRef = React.useRef<(() => void) | null>(null);
7070
const observedContainerRef = React.useRef<TElement | null>(null);
7171

72+
// eslint-disable-next-line react-hooks/refs
7273
if (!overflowManagerRef.current && canUseDOM()) {
73-
overflowManagerRef.current = createOverflowManager();
74+
overflowManagerRef.current = createOverflowManager(observeOptions);
7475
}
7576

7677
const containerRef = React.useCallback<React.RefCallback<TElement>>(node => {
@@ -87,7 +88,7 @@ export const useOverflowContainer = <TElement extends HTMLElement>(
8788
}
8889
}, []);
8990

90-
useIsomorphicLayoutEffect(() => {
91+
React.useEffect(() => {
9192
overflowManagerRef.current?.setOptions(observeOptions);
9293
}, [observeOptions]);
9394

@@ -133,6 +134,7 @@ export const useOverflowContainer = <TElement extends HTMLElement>(
133134
registerOverflowMenu,
134135
updateOverflow,
135136
containerRef,
137+
// eslint-disable-next-line react-hooks/refs
136138
manager: overflowManagerRef.current,
137139
};
138140
};
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
'use client';
22

3-
import { useOverflowSelector } from './useOverflowSelector';
3+
import { useOverflowSnapshot } from './useOverflowSnapshot';
44

55
/**
66
* @returns Number of items that are overflowing
77
*/
8-
export const useOverflowCount = (): number => useOverflowSelector(snapshot => snapshot.overflowCount);
8+
export const useOverflowCount = (): number => useOverflowSnapshot().overflowCount;

0 commit comments

Comments
 (0)