diff --git a/change/@fluentui-priority-overflow-06e37a2f-4190-4541-9eba-5d513a0200da.json b/change/@fluentui-priority-overflow-06e37a2f-4190-4541-9eba-5d513a0200da.json new file mode 100644 index 0000000000000..0e4eb034956e7 --- /dev/null +++ b/change/@fluentui-priority-overflow-06e37a2f-4190-4541-9eba-5d513a0200da.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "fix: New overflow items all always dispatch updates to subscriber", + "packageName": "@fluentui/priority-overflow", + "email": "lingfangao@hotmail.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-overflow-8b8469a8-30b4-4a4d-bc67-d168fd9e2362.json b/change/@fluentui-react-overflow-8b8469a8-30b4-4a4d-bc67-d168fd9e2362.json new file mode 100644 index 0000000000000..341e2d425ee83 --- /dev/null +++ b/change/@fluentui-react-overflow-8b8469a8-30b4-4a4d-bc67-d168fd9e2362.json @@ -0,0 +1,7 @@ +{ + "type": "none", + "comment": "Export useOverflowContext as @internal for testing purposes", + "packageName": "@fluentui/react-overflow", + "email": "lingfangao@hotmail.com", + "dependentChangeType": "none" +} diff --git a/packages/react-components/priority-overflow/src/overflowManager.ts b/packages/react-components/priority-overflow/src/overflowManager.ts index 0b710e42ccd93..15c24ded2c02a 100644 --- a/packages/react-components/priority-overflow/src/overflowManager.ts +++ b/packages/react-components/priority-overflow/src/overflowManager.ts @@ -9,7 +9,10 @@ import type { OverflowGroupState, OverflowItemEntry, OverflowManager, ObserveOpt export function createOverflowManager(): OverflowManager { let container: HTMLElement | undefined; let overflowMenu: HTMLElement | undefined; + // Set as true when resize observer is observing let observing = false; + // If true, next update will dispatch to onUpdateOverflow even if queue top states don't change + let forceDispatch = false; const options: Required = { padding: 10, overflowAxis: 'horizontal', @@ -118,11 +121,12 @@ export function createOverflowManager(): OverflowManager { options.onUpdateOverflow({ visibleItems, invisibleItems, groupVisibility }); }; - const processOverflowItems = (availableSize: number) => { + const processOverflowItems = (): boolean => { if (!container) { - return; + return false; } + const availableSize = getOffsetSize(container) - options.padding; const overflowMenuOffset = overflowMenu ? getOffsetSize(overflowMenu) : 0; // Snapshot of the visible/invisible state to compare for updates @@ -159,17 +163,17 @@ export function createOverflowManager(): OverflowManager { // only update when the state of visible/invisible items has changed if (visibleItemQueue.peek() !== visibleTop || invisibleItemQueue.peek() !== invisibleTop) { - dispatchOverflowUpdate(); + return true; } + + return false; }; const forceUpdate: OverflowManager['forceUpdate'] = () => { - if (!container) { - return; + if (processOverflowItems() || forceDispatch) { + forceDispatch = false; + dispatchOverflowUpdate(); } - - const availableSize = getOffsetSize(container) - options.padding; - processOverflowItems(availableSize); }; const update: OverflowManager['update'] = debounce(forceUpdate); @@ -197,6 +201,10 @@ export function createOverflowManager(): OverflowManager { // some options can affect priority which are only set on `observe` if (observing) { + // Updates to elements might not change the queue tops + // i.e. new element is enqueued but the top of the queue stays the same + // force a dispatch on the next batched update + forceDispatch = true; visibleItemQueue.enqueue(item.id); } diff --git a/packages/react-components/react-overflow/etc/react-overflow.api.md b/packages/react-components/react-overflow/etc/react-overflow.api.md index 37adf79a6dd5a..23ccaca79cc5a 100644 --- a/packages/react-components/react-overflow/etc/react-overflow.api.md +++ b/packages/react-components/react-overflow/etc/react-overflow.api.md @@ -4,6 +4,7 @@ ```ts +import { ContextSelector } from '@fluentui/react-context-selector'; import type { ObserveOptions } from '@fluentui/priority-overflow'; import type { OnUpdateOverflow } from '@fluentui/priority-overflow'; import { OverflowGroupState } from '@fluentui/priority-overflow'; @@ -54,6 +55,9 @@ export interface UseOverflowContainerReturn extend containerRef: React_2.RefObject; } +// @internal (undocumented) +export const useOverflowContext: (selector: ContextSelector) => SelectedValue; + // @public (undocumented) export const useOverflowCount: () => number; diff --git a/packages/react-components/react-overflow/src/Overflow.cy.tsx b/packages/react-components/react-overflow/src/Overflow.cy.tsx index 2aca539bdb9b6..c2ae2986de316 100644 --- a/packages/react-components/react-overflow/src/Overflow.cy.tsx +++ b/packages/react-components/react-overflow/src/Overflow.cy.tsx @@ -7,7 +7,9 @@ import { OverflowProps, useIsOverflowGroupVisible, useOverflowMenu, + useOverflowContext, } from '@fluentui/react-overflow'; +import { Portal } from '@fluentui/react-portal'; const selectors = { container: 'data-test-container', @@ -72,6 +74,7 @@ const Item: React.FC<{ children?: React.ReactNode; width?: number } & Omit = ({ width }) => { const { isOverflowing, ref, overflowCount } = useOverflowMenu(); + const itemVisibility = useOverflowContext(ctx => ctx.itemVisibility); const selector = { [selectors.menu]: '', }; @@ -82,9 +85,21 @@ const Menu: React.FC<{ width?: number }> = ({ width }) => { // No need to actually render a menu, we're testing state return ( - + <> + + +
+ {Object.entries(itemVisibility).map(([id, visible]) => ( + <> +
{id}
+
{visible ? 'visible' : 'invisible'}
+ + ))} +
+
+ ); }; @@ -452,4 +467,51 @@ describe('Overflow', () => { cy.get(`[${selectors.menu}]`).should('not.be.visible'); cy.get(`[${selectors.item}="4"]`).should('not.be.visible'); }); + + // Container will fit 4 items with width 100 and the overflow menu + // Once the priority of the foo item is updated it should have more + // priority than Item 5 (width 100 item), so the foo item should be visible + // once this foo item is visible, make sure that the subscriber is in sync. + it('should dispatch update when updating priority of an item', () => { + const Example = () => { + const [priority, setPriority] = React.useState(); + + return ( + <> + + + Item 1 + + + Item 2 + + + Item 3 + + + Item 4 + + + Item 5 + + + Foo + + + Item + + + Item + + + + + + ); + }; + mount(); + + setContainerSize(500); + cy.contains('Update priority').click().get('#foo-visibility').should('have.text', 'visible'); + }); }); diff --git a/packages/react-components/react-overflow/src/index.ts b/packages/react-components/react-overflow/src/index.ts index 46b1aee8a9904..5466d7c6a93a3 100644 --- a/packages/react-components/react-overflow/src/index.ts +++ b/packages/react-components/react-overflow/src/index.ts @@ -9,5 +9,7 @@ export { useOverflowCount } from './useOverflowCount'; export { useOverflowItem } from './useOverflowItem'; export { useOverflowMenu } from './useOverflowMenu'; +export { useOverflowContext } from './overflowContext'; + export type { OverflowItemProps } from './components/OverflowItem/OverflowItem.types'; export { OverflowItem } from './components/OverflowItem/OverflowItem'; diff --git a/packages/react-components/react-overflow/src/overflowContext.ts b/packages/react-components/react-overflow/src/overflowContext.ts index 9ced465a74fc0..87a53f791f9c2 100644 --- a/packages/react-components/react-overflow/src/overflowContext.ts +++ b/packages/react-components/react-overflow/src/overflowContext.ts @@ -1,6 +1,9 @@ import type { OverflowGroupState, OverflowItemEntry } from '@fluentui/priority-overflow'; import { ContextSelector, createContext, useContextSelector, Context } from '@fluentui/react-context-selector'; +/** + * @internal + */ export interface OverflowContextValue { itemVisibility: Record; groupVisibility: Record; @@ -23,5 +26,8 @@ const overflowContextDefaultValue: OverflowContextValue = { registerOverflowMenu: () => () => null, }; +/** + * @internal + */ export const useOverflowContext = (selector: ContextSelector) => useContextSelector(OverflowContext, (ctx = overflowContextDefaultValue) => selector(ctx));