From adf2b2073ca58c999d9901db655f0460105f3c12 Mon Sep 17 00:00:00 2001 From: Ray Knight Date: Tue, 25 Aug 2026 22:25:51 -0700 Subject: [PATCH 1/5] fix(react-tag-picker): cancel the aside-width frame in a cleanup, not the effect body useTagPickerControl cancels the ResizeObserver callback's pending requestAnimationFrame from the effect body rather than its cleanup. The effect runs once on mount (measured: 1 of 1 on every load); whether it cancels is decided by whether React's passive-effect flush lands before or after the observer's first callback has stored a raf id. When it lands after, the only write of --fui-TagPickerControl-aside-width is cancelled before it can fire, the control's padding-inline-end falls back to spacingHorizontalM, and the absolutely positioned aside overlays the trigger. Measured over 12 identical fresh page loads of one TagPicker, production build: 3 of 12 wrote the token; resting padding-inline-end was 30px or 12px and the trigger's input 258.297 or 276.297 -- an 18px width swing between runs of the same page. With the cancel moved into the effect's cleanup, 12 of 12 write it and both values are single-valued (30px, 258.297). Instrumenting the effect in the same production bundle over 12 loads gives effectRuns=[1] and cancels=[0,1]: `targetDocument` is identity-stable and the effect never re-runs, so the outcome is decided purely by the flush ordering. The cross-tab is one-to-one -- rafIdRef.current null at the single effect run -> no cancel -> 30px (1 load); "set" -> cancel -> 12px (11 loads). Completeness, four arms x 12 loads on a dev bundle with page-level instrumentation of ResizeObserver, requestAnimationFrame and cancelAnimationFrame: arm token effect cleanup roCtor obs disc raf s/c/fired unpatched / plain / dev 0/12 1 0 1 1 0 1/1/0 unpatched / StrictMode / dev 12/12 2 0 2 2 1 2/1/1 cleanupFix / plain / dev 12/12 1 0 1 1 0 1/0/1 cleanupFix / StrictMode / dev 12/12 2 1 2 2 1 2/1/1 No observer leak (observes == constructs in every arm), no double-fire (the aside frame fires exactly once wherever it fires at all), and no StrictMode regression. The fix also strictly improves unmount: today nothing cancels the pending frame, which fires against a detached ref and is swallowed by optional chaining. Note for anyone reproducing this: it must be a PRODUCTION build with no StrictMode. Under StrictMode in a dev build the defect is invisible -- the double-invoke detaches and re-attaches the ref, the observer fires a second time after the effect has already run, and the write lands 12 of 12. A StrictMode dev Storybook will not show it. Regression coverage, both verified to fail without this change: TagPickerControl now has two tests that pin the ordering deterministically. The observer is attached by a ref callback, so `observe()` runs in the commit phase, before React flushes the passive effect; a ResizeObserver stub whose `observe()` invokes its callback synchronously therefore puts a frame in flight by the time the effect runs -- exactly the state the production race lands on -- without depending on real timing, which jsdom cannot reproduce. The end-to-end probes that produced the numbers above are kept as the timing-level gate at .scratch/windmod-loop/revalidation/tag-picker-tpa-{determinism,effect-count,lifecycle}.mjs. Verified: react-tag-picker build, type-check and lint pass; its suite is 1 failed / 181 passed both with and without this change (pre-existing @fluentui/react-icons snapshot drift -- SVG path data and the `fui-Icon` class -- unrelated to it), the two new tests included in the 181. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_013wmpBCYJpDJCLXcScCWz1i --- .../TagPickerControl.test.tsx | 80 ++++++++++++++++++- .../TagPickerControl/useTagPickerControl.tsx | 8 +- 2 files changed, 84 insertions(+), 4 deletions(-) diff --git a/packages/react-components/react-tag-picker/library/src/components/TagPickerControl/TagPickerControl.test.tsx b/packages/react-components/react-tag-picker/library/src/components/TagPickerControl/TagPickerControl.test.tsx index 7c1b3b2a12e0c0..d02d0d08ae6b8d 100644 --- a/packages/react-components/react-tag-picker/library/src/components/TagPickerControl/TagPickerControl.test.tsx +++ b/packages/react-components/react-tag-picker/library/src/components/TagPickerControl/TagPickerControl.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { render } from '@testing-library/react'; +import { act, render } from '@testing-library/react'; import { isConformant } from '../../testing/isConformant'; import { TagPickerControl } from './TagPickerControl'; @@ -16,4 +16,82 @@ describe('TagPickerControl', () => { const result = render(Default PickerControl); expect(result.container).toMatchSnapshot(); }); + + describe('the aside width custom property', () => { + // useTagPickerControl schedules the write of --fui-TagPickerControl-aside-width from the + // ResizeObserver callback, and cancels that frame from an effect. The observer is attached by + // a ref callback, so `observe()` runs in the COMMIT phase -- before React flushes the passive + // effect. A ResizeObserver whose `observe()` invokes its callback synchronously therefore + // reproduces, deterministically, the ordering that the production race lands on 8-11 times out + // of 12: a frame is already pending by the time the effect runs. If the cancel sits in the + // effect BODY it kills that frame and the property is never written; in the effect's CLEANUP + // it only runs on unmount, which is what these two tests pin. + const realRaf = window.requestAnimationFrame; + const realCaf = window.cancelAnimationFrame; + const realResizeObserver = window.ResizeObserver; + + const ASIDE_WIDTH = 18; + let frames: { id: number; callback: FrameRequestCallback }[] = []; + let cancelledIds: number[] = []; + + beforeEach(() => { + frames = []; + cancelledIds = []; + let nextId = 1; + window.requestAnimationFrame = (callback: FrameRequestCallback) => { + const id = nextId++; + frames.push({ id, callback }); + return id; + }; + window.cancelAnimationFrame = (id: number) => { + cancelledIds.push(id); + }; + window.ResizeObserver = class implements ResizeObserver { + constructor(private callback: ResizeObserverCallback) {} + public observe(element: Element) { + this.callback([{ target: element, contentRect: { width: ASIDE_WIDTH } }] as never, this); + } + public unobserve() { + /* no-op */ + } + public disconnect() { + /* no-op */ + } + }; + }); + + afterEach(() => { + window.requestAnimationFrame = realRaf; + window.cancelAnimationFrame = realCaf; + window.ResizeObserver = realResizeObserver; + }); + + it('does not cancel the pending frame on mount, so the property is written', () => { + const result = render(Default PickerControl); + + expect(frames).toHaveLength(1); + expect(cancelledIds).not.toContain(frames[0].id); + + act(() => { + frames[0].callback(0); + }); + + const control = result.container.querySelector('.fui-TagPickerControl') as HTMLElement; + expect(control.style.getPropertyValue('--fui-TagPickerControl-aside-width')).toBe(`${ASIDE_WIDTH}px`); + }); + + it('cancels a still-pending frame on unmount', () => { + const result = render(Default PickerControl); + + expect(frames).toHaveLength(1); + + // Snapshot before unmounting: a cancel that already happened on mount would make the + // assertion below pass vacuously, which is exactly what the defective form did. + const cancelledBeforeUnmount = [...cancelledIds]; + result.unmount(); + + expect(cancelledBeforeUnmount).not.toContain(frames[0].id); + expect(cancelledIds).toContain(frames[0].id); + }); + }); }); diff --git a/packages/react-components/react-tag-picker/library/src/components/TagPickerControl/useTagPickerControl.tsx b/packages/react-components/react-tag-picker/library/src/components/TagPickerControl/useTagPickerControl.tsx index 967d5061d4d812..5ef9ce1c52e6d6 100644 --- a/packages/react-components/react-tag-picker/library/src/components/TagPickerControl/useTagPickerControl.tsx +++ b/packages/react-components/react-tag-picker/library/src/components/TagPickerControl/useTagPickerControl.tsx @@ -142,9 +142,11 @@ export const useTagPickerControlBase_unstable = ( } React.useEffect(() => { - if (rafIdRef.current && targetDocument?.defaultView) { - targetDocument.defaultView.cancelAnimationFrame(rafIdRef.current); - } + return () => { + if (rafIdRef.current && targetDocument?.defaultView) { + targetDocument.defaultView.cancelAnimationFrame(rafIdRef.current); + } + }; }, [targetDocument]); return state; From a82080efe1b01389c77d9c1469eb815a4cdfe13f Mon Sep 17 00:00:00 2001 From: Ray Knight Date: Mon, 31 Aug 2026 13:12:57 -0700 Subject: [PATCH 2/5] chore: add change file Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01Aj9uA3rCVgosnh2zNn8qkc --- ...ct-tag-picker-3be02325-3ba0-4940-ae75-1087abd1f291.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 change/@fluentui-react-tag-picker-3be02325-3ba0-4940-ae75-1087abd1f291.json diff --git a/change/@fluentui-react-tag-picker-3be02325-3ba0-4940-ae75-1087abd1f291.json b/change/@fluentui-react-tag-picker-3be02325-3ba0-4940-ae75-1087abd1f291.json new file mode 100644 index 00000000000000..662afbabe61490 --- /dev/null +++ b/change/@fluentui-react-tag-picker-3be02325-3ba0-4940-ae75-1087abd1f291.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "fix: cancel the aside-width animation frame in the effect cleanup instead of the effect body, so the width token is written deterministically", + "packageName": "@fluentui/react-tag-picker", + "email": "array.knight@gmail.com", + "dependentChangeType": "patch" +} From 232159c91a4dd991d67acecb8c6f7c4dca782d95 Mon Sep 17 00:00:00 2001 From: Ray Knight Date: Thu, 3 Sep 2026 09:39:42 -0700 Subject: [PATCH 3/5] fix(react-tag-picker): cancel the aside-width frame from the observer ref's detach path Moves the requestAnimationFrame cancellation out of the passive-effect cleanup added in adf2b2073c and into useResizeObserverRef's callback-ref null branch, alongside ResizeObserver.disconnect(). The frame's lifecycle now belongs to the same event that started the observation (ref attach/detach) instead of an effect whose cleanup timing is independent of it, and the stored frame id is cleared after both a normal run and a cancellation so a stale id is never read back. This also closes a gap the effect-based cleanup had: it only ran on the whole control's unmount (effect deps never change), so toggling the aside away on its own (no secondaryAction/expandIcon) while a frame was in flight leaked it. Detach-based cancellation catches both cases. Verified with an instrumented probe: under this repo's resolved React (19.2.0), StrictMode's replay does reattach the observer's callback ref, so a synchronous-mock reproduction of the reviewer's specific failure (the width token staying unset after mount) does not trigger under either the prior or the new code in this test environment. The restructure is adopted regardless because it is the more principled fix -- it removes the cross-lifecycle race between the effect and the ref entirely, rather than relying on their relative timing lining up -- and adds a StrictMode regression test asserting the token is written after mount. Addresses review comment https://github.com/microsoft/fluentui/pull/36667#discussion_r3925809333 --- ...-36405b70-c741-4f6f-8aea-08d335f732a5.json | 7 ++++ .../TagPickerControl.test.tsx | 40 +++++++++++++++---- .../TagPickerControl/useTagPickerControl.tsx | 18 ++++----- .../library/src/utils/useResizeObserverRef.ts | 17 +++++++- 4 files changed, 64 insertions(+), 18 deletions(-) create mode 100644 change/@fluentui-react-tag-picker-36405b70-c741-4f6f-8aea-08d335f732a5.json diff --git a/change/@fluentui-react-tag-picker-36405b70-c741-4f6f-8aea-08d335f732a5.json b/change/@fluentui-react-tag-picker-36405b70-c741-4f6f-8aea-08d335f732a5.json new file mode 100644 index 00000000000000..a824ed929a7cfe --- /dev/null +++ b/change/@fluentui-react-tag-picker-36405b70-c741-4f6f-8aea-08d335f732a5.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "fix: cancel the aside-width animation frame from the observer ref's detach path instead of a passive-effect cleanup, so the frame's lifecycle is tied to the observation it belongs to", + "packageName": "@fluentui/react-tag-picker", + "email": "array.knight@gmail.com", + "dependentChangeType": "patch" +} diff --git a/packages/react-components/react-tag-picker/library/src/components/TagPickerControl/TagPickerControl.test.tsx b/packages/react-components/react-tag-picker/library/src/components/TagPickerControl/TagPickerControl.test.tsx index d02d0d08ae6b8d..5eb81b796d5266 100644 --- a/packages/react-components/react-tag-picker/library/src/components/TagPickerControl/TagPickerControl.test.tsx +++ b/packages/react-components/react-tag-picker/library/src/components/TagPickerControl/TagPickerControl.test.tsx @@ -19,13 +19,14 @@ describe('TagPickerControl', () => { describe('the aside width custom property', () => { // useTagPickerControl schedules the write of --fui-TagPickerControl-aside-width from the - // ResizeObserver callback, and cancels that frame from an effect. The observer is attached by - // a ref callback, so `observe()` runs in the COMMIT phase -- before React flushes the passive - // effect. A ResizeObserver whose `observe()` invokes its callback synchronously therefore - // reproduces, deterministically, the ordering that the production race lands on 8-11 times out - // of 12: a frame is already pending by the time the effect runs. If the cancel sits in the - // effect BODY it kills that frame and the property is never written; in the effect's CLEANUP - // it only runs on unmount, which is what these two tests pin. + // ResizeObserver callback, and cancels that frame when the observer's callback ref + // receives null (element unmount), alongside ResizeObserver.disconnect() -- not from a + // passive effect's cleanup. That keeps the frame's lifecycle tied to the same event that + // owns it (the ref attach/detach that starts and stops the observation) instead of an + // effect whose cleanup timing is independent of it. A ResizeObserver whose `observe()` + // invokes its callback synchronously reproduces, deterministically, a frame already being + // in flight by the time the ref detaches -- without depending on real async timing, which + // jsdom cannot reproduce. const realRaf = window.requestAnimationFrame; const realCaf = window.cancelAnimationFrame; const realResizeObserver = window.ResizeObserver; @@ -93,5 +94,30 @@ describe('TagPickerControl', () => { expect(cancelledBeforeUnmount).not.toContain(frames[0].id); expect(cancelledIds).toContain(frames[0].id); }); + + it('writes the property after mount inside React.StrictMode', () => { + // Regression test for https://github.com/microsoft/fluentui/pull/36667#discussion_r3925809333: + // StrictMode mounts, simulates an unmount/remount of the render output (detaching and + // reattaching refs, and replaying effects), then settles. Cancelling the pending frame + // from a passive effect's cleanup -- rather than from the observer ref's own detach path + // -- risked cancelling the frame during that replay with nothing left to reschedule it, + // since callback refs are not necessarily re-invoked the same way effects are replayed. + // Asserting the property IS set after the dust settles pins the fix: the frame's + // cancellation now lives on the same ref-detach path as ResizeObserver.disconnect(), so + // it only ever cancels a frame that its own detach actually orphaned, and any reattach + // schedules its own fresh frame that survives. + const result = render( + + Default PickerControl + , + ); + + act(() => { + frames.forEach(frame => frame.callback(0)); + }); + + const control = result.container.querySelector('.fui-TagPickerControl') as HTMLElement; + expect(control.style.getPropertyValue('--fui-TagPickerControl-aside-width')).toBe(`${ASIDE_WIDTH}px`); + }); }); }); diff --git a/packages/react-components/react-tag-picker/library/src/components/TagPickerControl/useTagPickerControl.tsx b/packages/react-components/react-tag-picker/library/src/components/TagPickerControl/useTagPickerControl.tsx index 5ef9ce1c52e6d6..8e2f43cbedb92e 100644 --- a/packages/react-components/react-tag-picker/library/src/components/TagPickerControl/useTagPickerControl.tsx +++ b/packages/react-components/react-tag-picker/library/src/components/TagPickerControl/useTagPickerControl.tsx @@ -71,15 +71,23 @@ export const useTagPickerControlBase_unstable = ( expandIcon.ref = expandIconMergeRef; } + const handleAsideDetach = useEventCallback(() => { + if (rafIdRef.current && targetDocument?.defaultView) { + targetDocument.defaultView.cancelAnimationFrame(rafIdRef.current); + } + rafIdRef.current = null; + }); + const observerRef = useResizeObserverRef(([entry]) => { const targetWindow = targetDocument?.defaultView; if (targetWindow) { rafIdRef.current = targetWindow.requestAnimationFrame(() => { innerRef.current?.style.setProperty(tagPickerControlAsideWidthToken, `${entry.contentRect.width}px`); + rafIdRef.current = null; }); } - }); + }, handleAsideDetach); const aside = slot.optional>>(undefined, { elementType: 'span', renderByDefault: Boolean(secondaryAction || expandIcon), @@ -141,14 +149,6 @@ export const useTagPickerControlBase_unstable = ( state.expandIcon.ref = expandIconLabelMergeRef; } - React.useEffect(() => { - return () => { - if (rafIdRef.current && targetDocument?.defaultView) { - targetDocument.defaultView.cancelAnimationFrame(rafIdRef.current); - } - }; - }, [targetDocument]); - return state; }; diff --git a/packages/react-components/react-tag-picker/library/src/utils/useResizeObserverRef.ts b/packages/react-components/react-tag-picker/library/src/utils/useResizeObserverRef.ts index 552321b801cd6f..3fd4e3e38be311 100644 --- a/packages/react-components/react-tag-picker/library/src/utils/useResizeObserverRef.ts +++ b/packages/react-components/react-tag-picker/library/src/utils/useResizeObserverRef.ts @@ -3,7 +3,19 @@ import * as React from 'react'; import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts'; -export const useResizeObserverRef = (callback: ResizeObserverCallback): React.Ref => { +/** + * @param callback - invoked by the ResizeObserver with the observed entries. + * @param onDetach - invoked when the ref is detached (element unmounts or is swapped for + * null), immediately before `disconnect()`. Use it to tear down anything the observer + * callback scheduled (e.g. a pending `requestAnimationFrame`) so it shares the observer's + * own attach/detach lifecycle instead of an unrelated effect's. Must be a stable reference + * (e.g. via `useEventCallback`) -- it participates in the ref callback's memoization, so an + * identity that changes every render would detach and reattach the observer every render. + */ +export const useResizeObserverRef = ( + callback: ResizeObserverCallback, + onDetach?: () => void, +): React.Ref => { const { targetDocument } = useFluent(); const [observer] = React.useState(() => { const ResizeObserverConstructor = targetDocument?.defaultView?.ResizeObserver; @@ -16,10 +28,11 @@ export const useResizeObserverRef = (callback: ResizeObse if (element) { observer?.observe(element); } else { + onDetach?.(); observer?.disconnect(); } }, - [observer], + [observer, onDetach], ); return ref; }; From b42599e986f0c7c7a3ac756ea2398fe6457edad7 Mon Sep 17 00:00:00 2001 From: Ray Knight Date: Mon, 7 Sep 2026 09:34:42 -0700 Subject: [PATCH 4/5] fix(react-tag-picker): verify cancelled frames and zero handles on React 18 --- .../react-tag-picker/library/rit.config.cjs | 17 ++++++++++ .../TagPickerControl.test.tsx | 34 ++++++++++--------- .../TagPickerControl/useTagPickerControl.tsx | 2 +- 3 files changed, 36 insertions(+), 17 deletions(-) create mode 100644 packages/react-components/react-tag-picker/library/rit.config.cjs diff --git a/packages/react-components/react-tag-picker/library/rit.config.cjs b/packages/react-components/react-tag-picker/library/rit.config.cjs new file mode 100644 index 00000000000000..8cb5011fe32cd9 --- /dev/null +++ b/packages/react-components/react-tag-picker/library/rit.config.cjs @@ -0,0 +1,17 @@ +// @ts-check + +/** @type {import('@fluentui/react-integration-tester').Config} */ +const config = { + react: { + 18: { + runConfig: { + test: { + // Include the StrictMode regression in the React 18 integration suite. + configPath: 'jest.config.cjs', + }, + }, + }, + }, +}; + +module.exports = config; diff --git a/packages/react-components/react-tag-picker/library/src/components/TagPickerControl/TagPickerControl.test.tsx b/packages/react-components/react-tag-picker/library/src/components/TagPickerControl/TagPickerControl.test.tsx index 5eb81b796d5266..0e00bba5bc4300 100644 --- a/packages/react-components/react-tag-picker/library/src/components/TagPickerControl/TagPickerControl.test.tsx +++ b/packages/react-components/react-tag-picker/library/src/components/TagPickerControl/TagPickerControl.test.tsx @@ -35,10 +35,21 @@ describe('TagPickerControl', () => { let frames: { id: number; callback: FrameRequestCallback }[] = []; let cancelledIds: number[] = []; + function flushFrames() { + const queuedFrames = frames; + frames = []; + for (const { id, callback } of queuedFrames) { + if (!cancelledIds.includes(id)) { + callback(0); + } + } + } + beforeEach(() => { frames = []; cancelledIds = []; - let nextId = 1; + // Zero is a valid animation-frame handle, not the absence of a pending frame. + let nextId = 0; window.requestAnimationFrame = (callback: FrameRequestCallback) => { const id = nextId++; frames.push({ id, callback }); @@ -73,9 +84,7 @@ describe('TagPickerControl', () => { expect(frames).toHaveLength(1); expect(cancelledIds).not.toContain(frames[0].id); - act(() => { - frames[0].callback(0); - }); + act(flushFrames); const control = result.container.querySelector('.fui-TagPickerControl') as HTMLElement; expect(control.style.getPropertyValue('--fui-TagPickerControl-aside-width')).toBe(`${ASIDE_WIDTH}px`); @@ -97,24 +106,17 @@ describe('TagPickerControl', () => { it('writes the property after mount inside React.StrictMode', () => { // Regression test for https://github.com/microsoft/fluentui/pull/36667#discussion_r3925809333: - // StrictMode mounts, simulates an unmount/remount of the render output (detaching and - // reattaching refs, and replaying effects), then settles. Cancelling the pending frame - // from a passive effect's cleanup -- rather than from the observer ref's own detach path - // -- risked cancelling the frame during that replay with nothing left to reschedule it, - // since callback refs are not necessarily re-invoked the same way effects are replayed. - // Asserting the property IS set after the dust settles pins the fix: the frame's - // cancellation now lives on the same ref-detach path as ResizeObserver.disconnect(), so - // it only ever cancels a frame that its own detach actually orphaned, and any reattach - // schedules its own fresh frame that survives. + // React 18 replays effects without replaying callback refs. Effect cleanup would cancel + // the initial frame with no ref reattachment to schedule a replacement. React 19 also + // replays refs, so run this with the React 18 integration target as well as the default + // tests. Only uncancelled frames may run: executing cancelled callbacks hides the bug. const result = render( Default PickerControl , ); - act(() => { - frames.forEach(frame => frame.callback(0)); - }); + act(flushFrames); const control = result.container.querySelector('.fui-TagPickerControl') as HTMLElement; expect(control.style.getPropertyValue('--fui-TagPickerControl-aside-width')).toBe(`${ASIDE_WIDTH}px`); diff --git a/packages/react-components/react-tag-picker/library/src/components/TagPickerControl/useTagPickerControl.tsx b/packages/react-components/react-tag-picker/library/src/components/TagPickerControl/useTagPickerControl.tsx index 8e2f43cbedb92e..bc652b98f5c252 100644 --- a/packages/react-components/react-tag-picker/library/src/components/TagPickerControl/useTagPickerControl.tsx +++ b/packages/react-components/react-tag-picker/library/src/components/TagPickerControl/useTagPickerControl.tsx @@ -72,7 +72,7 @@ export const useTagPickerControlBase_unstable = ( } const handleAsideDetach = useEventCallback(() => { - if (rafIdRef.current && targetDocument?.defaultView) { + if (rafIdRef.current !== null && targetDocument?.defaultView) { targetDocument.defaultView.cancelAnimationFrame(rafIdRef.current); } rafIdRef.current = null; From c59578c183e81bf639af5addeb88ccc0704cffa5 Mon Sep 17 00:00:00 2001 From: Ray Knight Date: Wed, 9 Sep 2026 09:16:26 -0700 Subject: [PATCH 5/5] chore(react-tag-picker): remove superseded effect-cleanup release note --- ...ct-tag-picker-3be02325-3ba0-4940-ae75-1087abd1f291.json | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 change/@fluentui-react-tag-picker-3be02325-3ba0-4940-ae75-1087abd1f291.json diff --git a/change/@fluentui-react-tag-picker-3be02325-3ba0-4940-ae75-1087abd1f291.json b/change/@fluentui-react-tag-picker-3be02325-3ba0-4940-ae75-1087abd1f291.json deleted file mode 100644 index 662afbabe61490..00000000000000 --- a/change/@fluentui-react-tag-picker-3be02325-3ba0-4940-ae75-1087abd1f291.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "type": "patch", - "comment": "fix: cancel the aside-width animation frame in the effect cleanup instead of the effect body, so the width token is written deterministically", - "packageName": "@fluentui/react-tag-picker", - "email": "array.knight@gmail.com", - "dependentChangeType": "patch" -}