diff --git a/change/@fluentui-priority-overflow-2091bf95-b608-423e-9966-6e5b821c8cc6.json b/change/@fluentui-priority-overflow-2091bf95-b608-423e-9966-6e5b821c8cc6.json new file mode 100644 index 00000000000000..fd61650f8a975c --- /dev/null +++ b/change/@fluentui-priority-overflow-2091bf95-b608-423e-9966-6e5b821c8cc6.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "fix: synchronously measure overflowing managers when the overflow menu attaches", + "packageName": "@fluentui/priority-overflow", + "email": "bernardo.sunderhus@gmail.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-overflow-02cd3dce-4bab-4722-ba1a-fd36e195d8af.json b/change/@fluentui-react-overflow-02cd3dce-4bab-4722-ba1a-fd36e195d8af.json new file mode 100644 index 00000000000000..304b14350e2816 --- /dev/null +++ b/change/@fluentui-react-overflow-02cd3dce-4bab-4722-ba1a-fd36e195d8af.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "fix: avoid redundant overflow work during menu teardown and unchanged visibility updates", + "packageName": "@fluentui/react-overflow", + "email": "bernardo.sunderhus@gmail.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-web-components-5ceb07c6-c107-4b38-9f71-d5a4923fe23e.json b/change/@fluentui-web-components-5ceb07c6-c107-4b38-9f71-d5a4923fe23e.json deleted file mode 100644 index e2ac9f3b43f1de..00000000000000 --- a/change/@fluentui-web-components-5ceb07c6-c107-4b38-9f71-d5a4923fe23e.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "type": "patch", - "comment": "fix: remove padding-block-start in multiline dismiss slot to fix alignment", - "packageName": "@fluentui/web-components", - "email": "jibinjose884@gmail.com", - "dependentChangeType": "patch" -} diff --git a/packages/charts/chart-web-components/CHANGELOG.json b/packages/charts/chart-web-components/CHANGELOG.json index b61564830263ff..f6e19f2846fb7e 100644 --- a/packages/charts/chart-web-components/CHANGELOG.json +++ b/packages/charts/chart-web-components/CHANGELOG.json @@ -1,6 +1,20 @@ { "name": "@fluentui/chart-web-components", "entries": [ + { + "date": "Wed, 19 Aug 2026 04:08:27 GMT", + "version": "0.0.95", + "tag": "@fluentui/chart-web-components_v0.0.95", + "comments": { + "patch": [ + { + "author": "beachball", + "package": "@fluentui/chart-web-components", + "comment": "Bump @fluentui/web-components to v3.1.1" + } + ] + } + }, { "date": "Thu, 13 Aug 2026 04:10:22 GMT", "version": "0.0.94", diff --git a/packages/charts/chart-web-components/CHANGELOG.md b/packages/charts/chart-web-components/CHANGELOG.md index cd0283ca564b65..42d4081ef40cc6 100644 --- a/packages/charts/chart-web-components/CHANGELOG.md +++ b/packages/charts/chart-web-components/CHANGELOG.md @@ -1,9 +1,18 @@ # Change Log - @fluentui/chart-web-components - + +## [0.0.95](https://github.com/microsoft/fluentui/tree/@fluentui/chart-web-components_v0.0.95) + +Wed, 19 Aug 2026 04:08:27 GMT +[Compare changes](https://github.com/microsoft/fluentui/compare/@fluentui/chart-web-components_v0.0.94..@fluentui/chart-web-components_v0.0.95) + +### Patches + +- Bump @fluentui/web-components to v3.1.1 ([commit](https://github.com/microsoft/fluentui/commit/undefined) by beachball) + ## [0.0.94](https://github.com/microsoft/fluentui/tree/@fluentui/chart-web-components_v0.0.94) Thu, 13 Aug 2026 04:10:22 GMT diff --git a/packages/charts/chart-web-components/package.json b/packages/charts/chart-web-components/package.json index 7a08daf4e3cae4..a0efb8876a1bdb 100644 --- a/packages/charts/chart-web-components/package.json +++ b/packages/charts/chart-web-components/package.json @@ -1,7 +1,7 @@ { "name": "@fluentui/chart-web-components", "description": "A library of Fluent Chart Web Components", - "version": "0.0.94", + "version": "0.0.95", "author": { "name": "Microsoft" }, @@ -68,7 +68,7 @@ }, "dependencies": { "@fluentui/tokens": "^1.0.0-alpha.24", - "@fluentui/web-components": "^3.1.0", + "@fluentui/web-components": "^3.1.1", "@microsoft/fast-web-utilities": "^6.0.0", "@types/d3-selection": "^3.0.0", "@types/d3-shape": "^3.0.0", diff --git a/packages/react-components/priority-overflow/src/debounce.ts b/packages/react-components/priority-overflow/src/debounce.ts index 7a0438a524ae32..2d059c0eec3f78 100644 --- a/packages/react-components/priority-overflow/src/debounce.ts +++ b/packages/react-components/priority-overflow/src/debounce.ts @@ -4,13 +4,13 @@ * @param fn - Function to debounce * @returns debounced function */ -export function debounce(fn: Function): () => void { +export function debounce(fn: () => void): () => void { let pending: boolean; // React testing platforms will often output errors when state updates happen outside `act` // Since there is nothing obvious to wait for we just avoid debouncing in unit test environments if (process.env.NODE_ENV === 'test') { - return fn as () => void; + return fn; } return () => { diff --git a/packages/react-components/priority-overflow/src/overflowManager.test.ts b/packages/react-components/priority-overflow/src/overflowManager.test.ts index d6936206bec85d..6fdc7bcb2a0731 100644 --- a/packages/react-components/priority-overflow/src/overflowManager.test.ts +++ b/packages/react-components/priority-overflow/src/overflowManager.test.ts @@ -177,6 +177,53 @@ describe('overflowManager', () => { expect(onUpdateOverflow).toHaveBeenCalled(); }); + it('should synchronously update once when the overflow menu attaches to an overflowing manager', async () => { + const previousNodeEnv = process.env.NODE_ENV; + process.env.NODE_ENV = 'production'; + const manager = createOverflowManager(createObserveOptions()); + process.env.NODE_ENV = previousNodeEnv; + + const container = createContainer(100); + const getClientWidth = jest.fn(() => 100); + Object.defineProperty(container, 'clientWidth', { configurable: true, get: getClientWidth }); + + manager.addItem({ element: createElementWithSize('button', 60), id: 'a', priority: 1 }); + manager.addItem({ element: createElementWithSize('button', 60), id: 'b', priority: 0 }); + manager.observe(container); + manager.forceUpdate(); + getClientWidth.mockClear(); + + manager.addOverflowMenu(createElementWithSize('button', 30)); + expect(getClientWidth).toHaveBeenCalledTimes(1); + + await Promise.resolve(); + + expect(getClientWidth).toHaveBeenCalledTimes(1); + }); + + it('should batch the update when the overflow menu attaches without hidden items', async () => { + const previousNodeEnv = process.env.NODE_ENV; + process.env.NODE_ENV = 'production'; + const manager = createOverflowManager(createObserveOptions()); + process.env.NODE_ENV = previousNodeEnv; + + const container = createContainer(100); + const getClientWidth = jest.fn(() => 100); + Object.defineProperty(container, 'clientWidth', { configurable: true, get: getClientWidth }); + + manager.addItem({ element: createElementWithSize('button', 40), id: 'a', priority: 1 }); + manager.observe(container); + manager.forceUpdate(); + getClientWidth.mockClear(); + + manager.addOverflowMenu(createElementWithSize('button', 30)); + expect(getClientWidth).not.toHaveBeenCalled(); + + await Promise.resolve(); + + expect(getClientWidth).toHaveBeenCalledTimes(1); + }); + it('should remove items through removeItem', () => { const manager = createOverflowManager(createObserveOptions()); const container = createContainer(100); diff --git a/packages/react-components/priority-overflow/src/overflowManager.ts b/packages/react-components/priority-overflow/src/overflowManager.ts index ac384706f50f68..22498f92a082eb 100644 --- a/packages/react-components/priority-overflow/src/overflowManager.ts +++ b/packages/react-components/priority-overflow/src/overflowManager.ts @@ -339,7 +339,11 @@ export function createOverflowManager(initialOptions: Partial = if (observing) { forceDispatch = true; - update(); + if (invisibleItemQueue.size() > 0) { + forceUpdate(); + } else { + update(); + } } }; diff --git a/packages/react-components/react-overflow/library/src/useOverflowContainer.test.tsx b/packages/react-components/react-overflow/library/src/useOverflowContainer.test.tsx index 81aa49d69a29f7..c1ddb8a333aef9 100644 --- a/packages/react-components/react-overflow/library/src/useOverflowContainer.test.tsx +++ b/packages/react-components/react-overflow/library/src/useOverflowContainer.test.tsx @@ -3,6 +3,7 @@ import type { OverflowAxis, OverflowManager } from '@fluentui/priority-overflow' import { createOverflowManager } from '@fluentui/priority-overflow'; import { render } from '@testing-library/react'; import { renderHook } from '@testing-library/react-hooks'; +import { DATA_OVERFLOW_MENU } from './constants'; import { useOverflowContainer } from './useOverflowContainer'; jest.mock('@fluentui/priority-overflow'); @@ -77,6 +78,21 @@ describe('useOverflowContainer', () => { expect(removeItemMock).toHaveBeenCalledWith(overflowItem.id); }); + it('should mark the overflow menu before registering it with the manager', () => { + const addOverflowMenuMock = jest.fn((element: HTMLElement) => { + expect(element.hasAttribute(DATA_OVERFLOW_MENU)).toBe(true); + }); + mockOverflowManager({ addOverflowMenu: addOverflowMenuMock }); + const { result } = renderHook(() => + useOverflowContainer(() => undefined, { onUpdateItemVisibility: () => undefined }), + ); + const menu = document.createElement('button'); + + result.current.registerOverflowMenu(menu); + + expect(addOverflowMenuMock).toHaveBeenCalledWith(menu); + }); + it('should call observe with the container element', () => { const observeMock = jest.fn(); mockOverflowManager({ observe: observeMock }); diff --git a/packages/react-components/react-overflow/library/src/useOverflowContainer.ts b/packages/react-components/react-overflow/library/src/useOverflowContainer.ts index 77166cb4af8ee0..c53fa1f98a1067 100644 --- a/packages/react-components/react-overflow/library/src/useOverflowContainer.ts +++ b/packages/react-components/react-overflow/library/src/useOverflowContainer.ts @@ -96,8 +96,8 @@ export const useOverflowContainer = ( }, []); const registerOverflowMenu = React.useCallback((el: HTMLElement) => { - managerRef.current?.addOverflowMenu(el); el.setAttribute(DATA_OVERFLOW_MENU, ''); + managerRef.current?.addOverflowMenu(el); return () => { managerRef.current?.removeOverflowMenu(); diff --git a/packages/react-components/react-overflow/library/src/useOverflowMenu.test.tsx b/packages/react-components/react-overflow/library/src/useOverflowMenu.test.tsx index 9c81111aaf0567..550b95d9779b31 100644 --- a/packages/react-components/react-overflow/library/src/useOverflowMenu.test.tsx +++ b/packages/react-components/react-overflow/library/src/useOverflowMenu.test.tsx @@ -1,5 +1,7 @@ import * as React from 'react'; import { render } from '@testing-library/react'; +import type { OverflowContextValue } from './overflowContext'; +import { OverflowProvider } from './overflowContext'; import { Overflow } from './components/Overflow'; import { OverflowItem } from './components/OverflowItem/OverflowItem'; import { useOverflowMenu } from './useOverflowMenu'; @@ -71,4 +73,36 @@ describe('useOverflowMenu', () => { ), ).not.toThrow(); }); + + it('delegates menu updates to registration without forcing another update', () => { + const unregisterOverflowMenu = jest.fn(); + const registerOverflowMenu = jest.fn(() => unregisterOverflowMenu); + const forceUpdateOverflow = jest.fn(); + const contextValue: OverflowContextValue = { + hasOverflow: true, + itemVisibility: { item: false }, + groupVisibility: {}, + registerItem: () => jest.fn(), + registerOverflowMenu, + registerDivider: () => jest.fn(), + updateOverflow: jest.fn(), + forceUpdateOverflow, + getSnapshot: () => ({ itemVisibility: { item: false }, groupVisibility: {}, invisibleItemCount: 1 }), + subscribe: () => jest.fn(), + }; + + const { unmount } = render( + + + , + ); + + expect(registerOverflowMenu).toHaveBeenCalledTimes(1); + expect(forceUpdateOverflow).not.toHaveBeenCalled(); + + unmount(); + + expect(unregisterOverflowMenu).toHaveBeenCalledTimes(1); + expect(forceUpdateOverflow).not.toHaveBeenCalled(); + }); }); diff --git a/packages/react-components/react-overflow/library/src/useOverflowMenu.ts b/packages/react-components/react-overflow/library/src/useOverflowMenu.ts index 6be020a1c11570..f4cc295f7c0053 100644 --- a/packages/react-components/react-overflow/library/src/useOverflowMenu.ts +++ b/packages/react-components/react-overflow/library/src/useOverflowMenu.ts @@ -11,22 +11,15 @@ export function useOverflowMenu( ): { ref: React.MutableRefObject; overflowCount: number; isOverflowing: boolean } { const elementId = useId('overflow-menu', id); const overflowCount = useOverflowCount(); - const { registerOverflowMenu, forceUpdateOverflow } = useOverflowContext(); + const { registerOverflowMenu } = useOverflowContext(); const ref = React.useRef(null); const isOverflowing = overflowCount > 0; useIsomorphicLayoutEffect(() => { if (ref.current) { - const unregister = registerOverflowMenu(ref.current); - if (isOverflowing) { - forceUpdateOverflow(); - } - return () => { - unregister(); - forceUpdateOverflow(); - }; + return registerOverflowMenu(ref.current); } - }, [registerOverflowMenu, forceUpdateOverflow, isOverflowing, elementId]); + }, [registerOverflowMenu, isOverflowing, elementId]); return { ref, overflowCount, isOverflowing }; } diff --git a/packages/react-components/react-overflow/library/src/useOverflowVisibility.test.tsx b/packages/react-components/react-overflow/library/src/useOverflowVisibility.test.tsx index c0f190b477ed2c..bada379ac21d5d 100644 --- a/packages/react-components/react-overflow/library/src/useOverflowVisibility.test.tsx +++ b/packages/react-components/react-overflow/library/src/useOverflowVisibility.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { renderHook } from '@testing-library/react-hooks'; +import { act, renderHook } from '@testing-library/react-hooks'; import { useOverflowVisibility } from './useOverflowVisibility'; import type { OverflowContextValue } from './overflowContext'; import { OverflowContext } from './overflowContext'; @@ -35,4 +35,38 @@ describe('useOverflowVisibility', () => { expect(result.current.groupVisibility).toEqual(groupVisibility); expect(result.current.itemVisibility).toEqual({ foo: true, bar: true, baz: false }); }); + + it('does not render again when the snapshot is unchanged during subscription', () => { + const snapshot = { + itemVisibility: { foo: true }, + groupVisibility: {}, + invisibleItemCount: 0, + }; + let notify: () => void = () => undefined; + const contextValue = { + getSnapshot: () => snapshot, + subscribe: (listener: () => void) => { + notify = listener; + return () => null; + }, + } as unknown as OverflowContextValue; + const Wrapper = (props: { children?: React.ReactNode }) => ( + + ); + let renderCount = 0; + + renderHook( + () => { + renderCount++; + return useOverflowVisibility(); + }, + { wrapper: Wrapper }, + ); + + expect(renderCount).toBe(1); + + act(() => notify()); + + expect(renderCount).toBe(1); + }); }); diff --git a/packages/react-components/react-overflow/library/src/useOverflowVisibility.ts b/packages/react-components/react-overflow/library/src/useOverflowVisibility.ts index fef6a5f9b59d2b..4b95a65d054322 100644 --- a/packages/react-components/react-overflow/library/src/useOverflowVisibility.ts +++ b/packages/react-components/react-overflow/library/src/useOverflowVisibility.ts @@ -16,15 +16,12 @@ export function useOverflowVisibility(): { itemVisibility: Record; groupVisibility: Record; } { - return useOverflowSnapshot(selectVisibility); + const snapshot = useOverflowSnapshot(selectSnapshot); + + return { + itemVisibility: snapshot.itemVisibility, + groupVisibility: snapshot.groupVisibility, + }; } -const selectVisibility = ( - snapshot: OverflowSnapshot, -): { - itemVisibility: Record; - groupVisibility: Record; -} => ({ - itemVisibility: snapshot.itemVisibility, - groupVisibility: snapshot.groupVisibility, -}); +const selectSnapshot = (snapshot: OverflowSnapshot): OverflowSnapshot => snapshot; diff --git a/packages/web-components/CHANGELOG.json b/packages/web-components/CHANGELOG.json index f7fba73df2a748..47e57ad069762e 100644 --- a/packages/web-components/CHANGELOG.json +++ b/packages/web-components/CHANGELOG.json @@ -1,6 +1,21 @@ { "name": "@fluentui/web-components", "entries": [ + { + "date": "Wed, 19 Aug 2026 04:08:27 GMT", + "version": "3.1.1", + "tag": "@fluentui/web-components_v3.1.1", + "comments": { + "patch": [ + { + "author": "jibinjose884@gmail.com", + "package": "@fluentui/web-components", + "commit": "1964e7ce05f25771f4a476ec6b2fdbfb3d5e4bf7", + "comment": "fix: remove padding-block-start in multiline dismiss slot to fix alignment" + } + ] + } + }, { "date": "Thu, 13 Aug 2026 04:10:22 GMT", "version": "3.1.0", diff --git a/packages/web-components/CHANGELOG.md b/packages/web-components/CHANGELOG.md index a2165203c2abcd..f47d3cf65076e3 100644 --- a/packages/web-components/CHANGELOG.md +++ b/packages/web-components/CHANGELOG.md @@ -1,9 +1,18 @@ # Change Log - @fluentui/web-components - + +## [3.1.1](https://github.com/microsoft/fluentui/tree/@fluentui/web-components_v3.1.1) + +Wed, 19 Aug 2026 04:08:27 GMT +[Compare changes](https://github.com/microsoft/fluentui/compare/@fluentui/web-components_v3.1.0..@fluentui/web-components_v3.1.1) + +### Patches + +- fix: remove padding-block-start in multiline dismiss slot to fix alignment ([PR #36547](https://github.com/microsoft/fluentui/pull/36547) by jibinjose884@gmail.com) + ## [3.1.0](https://github.com/microsoft/fluentui/tree/@fluentui/web-components_v3.1.0) Thu, 13 Aug 2026 04:10:22 GMT diff --git a/packages/web-components/package.json b/packages/web-components/package.json index 43b2ed72047e89..b057227d46470e 100644 --- a/packages/web-components/package.json +++ b/packages/web-components/package.json @@ -1,7 +1,7 @@ { "name": "@fluentui/web-components", "description": "A library of Fluent Web Components", - "version": "3.1.0", + "version": "3.1.1", "author": { "name": "Microsoft", "url": "https://discord.gg/FcSNfg4" diff --git a/yarn.lock b/yarn.lock index d6f2b004f9975c..29a79676267e69 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2619,7 +2619,7 @@ __metadata: resolution: "@fluentui/chart-web-components@workspace:packages/charts/chart-web-components" dependencies: "@fluentui/tokens": "npm:^1.0.0-alpha.24" - "@fluentui/web-components": "npm:^3.1.0" + "@fluentui/web-components": "npm:^3.1.1" "@microsoft/fast-web-utilities": "npm:^6.0.0" "@storybook/html": "npm:9.1.17" "@storybook/html-vite": "npm:9.1.17" @@ -6631,7 +6631,7 @@ __metadata: languageName: unknown linkType: soft -"@fluentui/web-components@npm:*, @fluentui/web-components@npm:^3.1.0, @fluentui/web-components@workspace:packages/web-components": +"@fluentui/web-components@npm:*, @fluentui/web-components@npm:^3.1.1, @fluentui/web-components@workspace:packages/web-components": version: 0.0.0-use.local resolution: "@fluentui/web-components@workspace:packages/web-components" dependencies: