@multiplatform.one/forms 7.49.1 → 7.49.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/FloatingPanel/useFloatingPanel.cjs +13 -2
- package/dist/cjs/FloatingPanel/useFloatingPanel.native.js +14 -5
- package/dist/cjs/FloatingPanel/useFloatingPanel.native.js.map +1 -1
- package/dist/cjs/shared/focusManagement.cjs +13 -11
- package/dist/cjs/shared/focusManagement.native.js +15 -15
- package/dist/cjs/shared/focusManagement.native.js.map +1 -1
- package/dist/cjs/shared/restoreRetries.cjs +96 -0
- package/dist/cjs/shared/restoreRetries.native.js +101 -0
- package/dist/cjs/shared/restoreRetries.native.js.map +1 -0
- package/dist/esm/FloatingPanel/useFloatingPanel.mjs +13 -2
- package/dist/esm/FloatingPanel/useFloatingPanel.mjs.map +1 -1
- package/dist/esm/FloatingPanel/useFloatingPanel.native.js +14 -5
- package/dist/esm/FloatingPanel/useFloatingPanel.native.js.map +1 -1
- package/dist/esm/shared/focusManagement.mjs +13 -11
- package/dist/esm/shared/focusManagement.mjs.map +1 -1
- package/dist/esm/shared/focusManagement.native.js +15 -15
- package/dist/esm/shared/focusManagement.native.js.map +1 -1
- package/dist/esm/shared/restoreRetries.mjs +71 -0
- package/dist/esm/shared/restoreRetries.mjs.map +1 -0
- package/dist/esm/shared/restoreRetries.native.js +73 -0
- package/dist/esm/shared/restoreRetries.native.js.map +1 -0
- package/dist/jsx/FloatingPanel/useFloatingPanel.mjs +13 -2
- package/dist/jsx/FloatingPanel/useFloatingPanel.mjs.map +1 -1
- package/dist/jsx/FloatingPanel/useFloatingPanel.native.js +14 -5
- package/dist/jsx/FloatingPanel/useFloatingPanel.native.js.map +1 -1
- package/dist/jsx/shared/focusManagement.mjs +13 -11
- package/dist/jsx/shared/focusManagement.mjs.map +1 -1
- package/dist/jsx/shared/focusManagement.native.js +15 -15
- package/dist/jsx/shared/focusManagement.native.js.map +1 -1
- package/dist/jsx/shared/restoreRetries.mjs +71 -0
- package/dist/jsx/shared/restoreRetries.mjs.map +1 -0
- package/dist/jsx/shared/restoreRetries.native.js +101 -0
- package/dist/jsx/shared/restoreRetries.native.js.map +1 -0
- package/package.json +8 -8
- package/src/FloatingPanel/useFloatingPanel.frames.spec.tsx +79 -0
- package/src/FloatingPanel/useFloatingPanel.ts +14 -2
- package/src/shared/animationFrames.spec.ts +47 -0
- package/src/shared/focusManagement.spec.tsx +52 -3
- package/src/shared/focusManagement.ts +20 -25
- package/src/shared/restoreRetries.ts +87 -0
- package/types/FloatingPanel/useFloatingPanel.d.ts.map +1 -1
- package/types/shared/focusManagement.d.ts.map +1 -1
- package/types/shared/restoreRetries.d.ts +14 -0
- package/types/shared/restoreRetries.d.ts.map +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@multiplatform.one/forms",
|
|
3
|
-
"version": "7.49.
|
|
3
|
+
"version": "7.49.3",
|
|
4
4
|
"description": "Cross-platform form components with URL state management",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cross-platform",
|
|
@@ -57,10 +57,10 @@
|
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
59
|
"@floating-ui/react": "^0.27.20",
|
|
60
|
-
"@multiplatform.one/platform": "^7.49.
|
|
61
|
-
"@multiplatform.one/router": "^7.49.
|
|
62
|
-
"@multiplatform.one/table-primitives": "^7.49.
|
|
63
|
-
"@multiplatform.one/theme": "^7.49.
|
|
60
|
+
"@multiplatform.one/platform": "^7.49.3",
|
|
61
|
+
"@multiplatform.one/router": "^7.49.3",
|
|
62
|
+
"@multiplatform.one/table-primitives": "^7.49.3",
|
|
63
|
+
"@multiplatform.one/theme": "^7.49.3",
|
|
64
64
|
"@phosphor-icons/react": "^2.1.10",
|
|
65
65
|
"@tamagui/get-font-sized": "2.7.6",
|
|
66
66
|
"@tamagui/get-token": "2.7.6",
|
|
@@ -79,9 +79,9 @@
|
|
|
79
79
|
"tamagui": "2.7.6"
|
|
80
80
|
},
|
|
81
81
|
"devDependencies": {
|
|
82
|
-
"@multiplatform.one/config": "7.49.
|
|
83
|
-
"@multiplatform.one/storybook": "7.49.
|
|
84
|
-
"@multiplatform.one/test-utils": "7.49.
|
|
82
|
+
"@multiplatform.one/config": "7.49.3",
|
|
83
|
+
"@multiplatform.one/storybook": "7.49.3",
|
|
84
|
+
"@multiplatform.one/test-utils": "7.49.3",
|
|
85
85
|
"@tamagui/build": "2.7.6",
|
|
86
86
|
"@tamagui/config": "2.7.6",
|
|
87
87
|
"@tamagui/vite-plugin": "2.7.6",
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FZ-7904: the open effect's two frames (setVisible(true)) and the scroll
|
|
3
|
+
* lock's frame (window.scrollTo) used to be requested and never cancelled, so
|
|
4
|
+
* a close or unmount inside them still showed the panel or scrolled the page,
|
|
5
|
+
* and after teardown they threw into a deleted window.
|
|
6
|
+
*/
|
|
7
|
+
import { holdAnimationFrames } from "@multiplatform.one/test-utils/frames";
|
|
8
|
+
import { renderWithProviders } from "@multiplatform.one/test-utils";
|
|
9
|
+
import { act, cleanup } from "@testing-library/react";
|
|
10
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
11
|
+
import { useFloatingPanel } from "./useFloatingPanel";
|
|
12
|
+
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
cleanup();
|
|
15
|
+
vi.restoreAllMocks();
|
|
16
|
+
vi.unstubAllGlobals();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
function Harness({ open }: { open: boolean }) {
|
|
20
|
+
const panel = useFloatingPanel({ open, onOpenChange: () => {}, useAutoUpdate: false });
|
|
21
|
+
return (
|
|
22
|
+
<>
|
|
23
|
+
<div ref={panel.setReferenceRef} />
|
|
24
|
+
{panel.mounted && (
|
|
25
|
+
<div
|
|
26
|
+
data-testid="panel"
|
|
27
|
+
data-visible={String(panel.visible)}
|
|
28
|
+
ref={panel.refs.setFloating}
|
|
29
|
+
/>
|
|
30
|
+
)}
|
|
31
|
+
</>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function openPanel() {
|
|
36
|
+
const frames = holdAnimationFrames();
|
|
37
|
+
const result = renderWithProviders(<Harness open={false} />);
|
|
38
|
+
result.rerender(<Harness open />);
|
|
39
|
+
expect(result.getByTestId("panel")).toBeTruthy();
|
|
40
|
+
return { ...result, frames };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function flushFrames(frames: { flush: () => void }) {
|
|
44
|
+
act(() => {
|
|
45
|
+
frames.flush();
|
|
46
|
+
frames.flush();
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe("useFloatingPanel frames (FZ-7904)", () => {
|
|
51
|
+
it("shows the panel two frames after it opens", () => {
|
|
52
|
+
const h = openPanel();
|
|
53
|
+
flushFrames(h.frames);
|
|
54
|
+
expect(h.getByTestId("panel").dataset.visible).toBe("true");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("a close inside the open frames never shows the panel", () => {
|
|
58
|
+
const h = openPanel();
|
|
59
|
+
h.rerender(<Harness open={false} />);
|
|
60
|
+
flushFrames(h.frames);
|
|
61
|
+
expect(h.getByTestId("panel").dataset.visible).toBe("false");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("a close cancels the scroll lock's frame", () => {
|
|
65
|
+
const scrollTo = vi.spyOn(window, "scrollTo").mockImplementation(() => {});
|
|
66
|
+
const h = openPanel();
|
|
67
|
+
h.rerender(<Harness open={false} />);
|
|
68
|
+
scrollTo.mockClear();
|
|
69
|
+
flushFrames(h.frames);
|
|
70
|
+
expect(scrollTo).not.toHaveBeenCalled();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("an unmount leaves no frame behind", () => {
|
|
74
|
+
const h = openPanel();
|
|
75
|
+
expect(h.frames.pending).toBeGreaterThan(0);
|
|
76
|
+
h.unmount();
|
|
77
|
+
expect(h.frames.pending).toBe(0);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
@@ -336,7 +336,18 @@ export function useFloatingPanel({
|
|
|
336
336
|
if (open) {
|
|
337
337
|
if (needsSetupRef.current) setupForOpen();
|
|
338
338
|
needsSetupRef.current = true;
|
|
339
|
-
|
|
339
|
+
let inner = 0;
|
|
340
|
+
const outer = requestAnimationFrame(() => {
|
|
341
|
+
// The outer frame can outlive the document, and with it the global.
|
|
342
|
+
if (typeof requestAnimationFrame === "function") {
|
|
343
|
+
inner = requestAnimationFrame(() => setVisible(true));
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
return () => {
|
|
347
|
+
if (typeof cancelAnimationFrame !== "function") return;
|
|
348
|
+
cancelAnimationFrame(outer);
|
|
349
|
+
cancelAnimationFrame(inner);
|
|
350
|
+
};
|
|
340
351
|
} else {
|
|
341
352
|
setVisible(false);
|
|
342
353
|
setArrowUp(false);
|
|
@@ -473,12 +484,13 @@ export function useFloatingPanel({
|
|
|
473
484
|
};
|
|
474
485
|
|
|
475
486
|
window.scrollTo(0, scrollY);
|
|
476
|
-
requestAnimationFrame(() => window.scrollTo(0, scrollY));
|
|
487
|
+
const restoreScroll = requestAnimationFrame(() => window.scrollTo(0, scrollY));
|
|
477
488
|
|
|
478
489
|
document.addEventListener("wheel", preventScroll, { passive: false, capture: true });
|
|
479
490
|
document.addEventListener("keydown", preventKeyScroll, { capture: true });
|
|
480
491
|
document.addEventListener("touchmove", preventTouchScroll, { passive: false, capture: true });
|
|
481
492
|
return () => {
|
|
493
|
+
if (typeof cancelAnimationFrame === "function") cancelAnimationFrame(restoreScroll);
|
|
482
494
|
document.removeEventListener("wheel", preventScroll, { capture: true });
|
|
483
495
|
document.removeEventListener("keydown", preventKeyScroll, { capture: true });
|
|
484
496
|
document.removeEventListener("touchmove", preventTouchScroll, { capture: true });
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @multiplatform.one/test-utils frames (FZ-7904). test-utils has no runner of
|
|
3
|
+
* its own; forms loads its setup, so the frames are pinned here.
|
|
4
|
+
*/
|
|
5
|
+
import { createAnimationFrames } from "@multiplatform.one/test-utils/frames";
|
|
6
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
7
|
+
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
vi.useRealTimers();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
describe("test-utils animation frames", () => {
|
|
13
|
+
it("runs a frame on the next timer turn with a timestamp", () => {
|
|
14
|
+
vi.useFakeTimers();
|
|
15
|
+
const frames = createAnimationFrames();
|
|
16
|
+
const callback = vi.fn();
|
|
17
|
+
frames.request(callback);
|
|
18
|
+
expect(callback).not.toHaveBeenCalled();
|
|
19
|
+
vi.advanceTimersByTime(0);
|
|
20
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
21
|
+
expect(typeof callback.mock.calls[0][0]).toBe("number");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("cancels a frame by its handle", () => {
|
|
25
|
+
vi.useFakeTimers();
|
|
26
|
+
const frames = createAnimationFrames();
|
|
27
|
+
const callback = vi.fn();
|
|
28
|
+
frames.cancel(frames.request(callback));
|
|
29
|
+
vi.advanceTimersByTime(10);
|
|
30
|
+
expect(callback).not.toHaveBeenCalled();
|
|
31
|
+
expect(vi.getTimerCount()).toBe(0);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("close cancels pending frames and refuses new ones", () => {
|
|
35
|
+
vi.useFakeTimers();
|
|
36
|
+
const frames = createAnimationFrames();
|
|
37
|
+
const pending = vi.fn();
|
|
38
|
+
frames.request(pending);
|
|
39
|
+
frames.close();
|
|
40
|
+
const late = vi.fn();
|
|
41
|
+
frames.request(late);
|
|
42
|
+
expect(vi.getTimerCount()).toBe(0);
|
|
43
|
+
vi.advanceTimersByTime(10);
|
|
44
|
+
expect(pending).not.toHaveBeenCalled();
|
|
45
|
+
expect(late).not.toHaveBeenCalled();
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -1,20 +1,23 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { StrictMode, type ReactNode } from "react";
|
|
2
2
|
import { Preset, type Animation } from "@multiplatform.one/theme";
|
|
3
3
|
import { TestProviders } from "@multiplatform.one/test-utils";
|
|
4
|
+
import { holdAnimationFrames } from "@multiplatform.one/test-utils/frames";
|
|
4
5
|
import { act, cleanup, renderHook } from "@testing-library/react";
|
|
5
6
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
6
7
|
import { useReturnFocusOnClose, useSheetModalFocus } from "./focusManagement";
|
|
7
8
|
|
|
8
9
|
afterEach(() => {
|
|
9
10
|
cleanup();
|
|
11
|
+
vi.unstubAllGlobals();
|
|
10
12
|
vi.useRealTimers();
|
|
11
13
|
document.body.replaceChildren();
|
|
12
14
|
});
|
|
13
15
|
|
|
14
16
|
for (const mode of ["floating", "sheet"] as const) {
|
|
15
17
|
describe(`${mode} close focus`, () => {
|
|
16
|
-
function setup() {
|
|
18
|
+
function setup({ holdFrames = false, strict = false } = {}) {
|
|
17
19
|
vi.useFakeTimers();
|
|
20
|
+
const frames = holdFrames ? holdAnimationFrames() : null;
|
|
18
21
|
const trigger = document.createElement("button");
|
|
19
22
|
const frame = document.createElement("div");
|
|
20
23
|
const item = document.createElement("button");
|
|
@@ -25,11 +28,19 @@ for (const mode of ["floating", "sheet"] as const) {
|
|
|
25
28
|
const skipReturnFocusRef = { current: false };
|
|
26
29
|
const frameRef = { current: frame };
|
|
27
30
|
let animation: Animation = "quick";
|
|
28
|
-
const
|
|
31
|
+
const Providers = ({ children }: { children: ReactNode }) => (
|
|
29
32
|
<TestProviders>
|
|
30
33
|
<Preset overrides={{ animation }}>{children}</Preset>
|
|
31
34
|
</TestProviders>
|
|
32
35
|
);
|
|
36
|
+
const Wrapper = ({ children }: { children: ReactNode }) =>
|
|
37
|
+
strict ? (
|
|
38
|
+
<StrictMode>
|
|
39
|
+
<Providers>{children}</Providers>
|
|
40
|
+
</StrictMode>
|
|
41
|
+
) : (
|
|
42
|
+
<Providers>{children}</Providers>
|
|
43
|
+
);
|
|
33
44
|
const hook = renderHook(
|
|
34
45
|
({ open }) => {
|
|
35
46
|
if (mode === "sheet") useSheetModalFocus(open, frameRef, skipReturnFocusRef);
|
|
@@ -39,6 +50,7 @@ for (const mode of ["floating", "sheet"] as const) {
|
|
|
39
50
|
);
|
|
40
51
|
return {
|
|
41
52
|
...hook,
|
|
53
|
+
frames,
|
|
42
54
|
trigger,
|
|
43
55
|
item,
|
|
44
56
|
outside,
|
|
@@ -111,5 +123,42 @@ for (const mode of ["floating", "sheet"] as const) {
|
|
|
111
123
|
await act(() => vi.advanceTimersByTimeAsync(400));
|
|
112
124
|
expect(document.activeElement).not.toBe(h.trigger);
|
|
113
125
|
});
|
|
126
|
+
|
|
127
|
+
// FZ-7904: the close retries used to drop their frame and timer ids.
|
|
128
|
+
it("cancels a close's retries when the overlay reopens", () => {
|
|
129
|
+
const h = setup({ holdFrames: true });
|
|
130
|
+
const frames = h.frames!.pending;
|
|
131
|
+
const timers = vi.getTimerCount();
|
|
132
|
+
h.rerender({ open: false });
|
|
133
|
+
h.rerender({ open: true });
|
|
134
|
+
expect(h.frames!.pending).toBe(frames);
|
|
135
|
+
expect(vi.getTimerCount()).toBe(timers);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("leaves no frame or timer behind once it unmounts, and still restores", async () => {
|
|
139
|
+
const h = setup({ holdFrames: true });
|
|
140
|
+
h.trigger.blur();
|
|
141
|
+
h.rerender({ open: false });
|
|
142
|
+
h.unmount();
|
|
143
|
+
expect(h.frames!.pending).toBe(0);
|
|
144
|
+
expect(vi.getTimerCount()).toBe(0);
|
|
145
|
+
await Promise.resolve();
|
|
146
|
+
expect(document.activeElement).toBe(h.trigger);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("a StrictMode remount on open does not pull focus back out", async () => {
|
|
150
|
+
const h = setup({ strict: true });
|
|
151
|
+
h.item.focus();
|
|
152
|
+
await Promise.resolve();
|
|
153
|
+
await act(() => vi.advanceTimersByTimeAsync(400));
|
|
154
|
+
expect(document.activeElement).toBe(h.item);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("does not throw when its frame outlives the requestAnimationFrame global", () => {
|
|
158
|
+
const h = setup({ holdFrames: true });
|
|
159
|
+
h.rerender({ open: false });
|
|
160
|
+
vi.stubGlobal("requestAnimationFrame", undefined);
|
|
161
|
+
expect(() => h.frames!.flush()).not.toThrow();
|
|
162
|
+
});
|
|
114
163
|
});
|
|
115
164
|
}
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
import { useEffect, useRef, type RefObject } from "react";
|
|
38
38
|
import { isWeb, useDialogContext } from "tamagui";
|
|
39
39
|
import { wasKeyboardFocus, useResolvedKnobs, motionHideDelayMs } from "@multiplatform.one/theme";
|
|
40
|
+
import { useRestoreRetries } from "./restoreRetries";
|
|
40
41
|
|
|
41
42
|
const FOCUSABLE =
|
|
42
43
|
'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
@@ -67,15 +68,14 @@ function useCaptureOnOpen(open: boolean) {
|
|
|
67
68
|
return { prevRef, generationRef };
|
|
68
69
|
}
|
|
69
70
|
|
|
70
|
-
function
|
|
71
|
+
function focusRestore(
|
|
71
72
|
prevRef: RefObject<HTMLElement | null>,
|
|
72
73
|
generationRef: RefObject<number>,
|
|
73
|
-
delayMs: number,
|
|
74
74
|
insideRef?: RefObject<HTMLElement | null>,
|
|
75
75
|
skipReturnFocusRef?: RefObject<boolean>,
|
|
76
76
|
) {
|
|
77
77
|
const generation = generationRef.current;
|
|
78
|
-
|
|
78
|
+
return () => {
|
|
79
79
|
// Fires from the unmount path, so the document can be gone by now (a test
|
|
80
80
|
// environment torn down inside the retry window). `isConnected` stays
|
|
81
81
|
// stale-true on a detached node, so it does not cover this.
|
|
@@ -90,10 +90,6 @@ function restoreLater(
|
|
|
90
90
|
} as FocusOptions);
|
|
91
91
|
}
|
|
92
92
|
};
|
|
93
|
-
// Exit animations unmount / park the overlay a few frames later; retry
|
|
94
|
-
// after the typical exit window. All calls are idempotent.
|
|
95
|
-
requestAnimationFrame(() => requestAnimationFrame(restore));
|
|
96
|
-
setTimeout(restore, delayMs);
|
|
97
93
|
}
|
|
98
94
|
|
|
99
95
|
/**
|
|
@@ -111,17 +107,18 @@ export function useReturnFocusOnClose(
|
|
|
111
107
|
const { knobProps } = useResolvedKnobs();
|
|
112
108
|
const restoreDelayMsRef = useRef(0);
|
|
113
109
|
restoreDelayMsRef.current = motionHideDelayMs(knobProps.motion.scrim.transition);
|
|
110
|
+
const retries = useRestoreRetries();
|
|
114
111
|
useEffect(() => {
|
|
115
112
|
if (!canUseDom() || !open) return;
|
|
113
|
+
retries.cancel();
|
|
114
|
+
// Exit animations unmount / park the overlay a few frames later; retry
|
|
115
|
+
// after the typical exit window. All calls are idempotent.
|
|
116
116
|
return () =>
|
|
117
|
-
|
|
118
|
-
prevRef,
|
|
119
|
-
generationRef,
|
|
117
|
+
retries.schedule(
|
|
118
|
+
focusRestore(prevRef, generationRef, insideRef, skipReturnFocusRef),
|
|
120
119
|
restoreDelayMsRef.current,
|
|
121
|
-
insideRef,
|
|
122
|
-
skipReturnFocusRef,
|
|
123
120
|
);
|
|
124
|
-
}, [open, prevRef, generationRef, skipReturnFocusRef, insideRef]);
|
|
121
|
+
}, [open, prevRef, generationRef, skipReturnFocusRef, insideRef, retries]);
|
|
125
122
|
}
|
|
126
123
|
|
|
127
124
|
/**
|
|
@@ -139,19 +136,17 @@ export function useSheetModalFocus(
|
|
|
139
136
|
const { knobProps } = useResolvedKnobs();
|
|
140
137
|
const restoreDelayMsRef = useRef(0);
|
|
141
138
|
restoreDelayMsRef.current = motionHideDelayMs(knobProps.motion.scrim.transition);
|
|
139
|
+
const retries = useRestoreRetries();
|
|
142
140
|
useEffect(() => {
|
|
143
141
|
if (!canUseDom() || !open) return;
|
|
142
|
+
retries.cancel();
|
|
143
|
+
const restoreLater = () =>
|
|
144
|
+
retries.schedule(
|
|
145
|
+
focusRestore(prevRef, generationRef, frameRef, skipReturnFocusRef),
|
|
146
|
+
restoreDelayMsRef.current,
|
|
147
|
+
);
|
|
144
148
|
const frame = frameRef.current;
|
|
145
|
-
if (!frame || typeof frame.addEventListener !== "function")
|
|
146
|
-
return () =>
|
|
147
|
-
restoreLater(
|
|
148
|
-
prevRef,
|
|
149
|
-
generationRef,
|
|
150
|
-
restoreDelayMsRef.current,
|
|
151
|
-
frameRef,
|
|
152
|
-
skipReturnFocusRef,
|
|
153
|
-
);
|
|
154
|
-
}
|
|
149
|
+
if (!frame || typeof frame.addEventListener !== "function") return restoreLater;
|
|
155
150
|
|
|
156
151
|
// Focus moves in once the sheet has mounted its frame (spring start).
|
|
157
152
|
const focusIn = () => {
|
|
@@ -193,9 +188,9 @@ export function useSheetModalFocus(
|
|
|
193
188
|
cancelAnimationFrame(raf);
|
|
194
189
|
cancelAnimationFrame(innerRaf);
|
|
195
190
|
frame.removeEventListener("keydown", onKeyDown);
|
|
196
|
-
restoreLater(
|
|
191
|
+
restoreLater();
|
|
197
192
|
};
|
|
198
|
-
}, [open, frameRef, prevRef, generationRef, skipReturnFocusRef]);
|
|
193
|
+
}, [open, frameRef, prevRef, generationRef, skipReturnFocusRef, retries]);
|
|
199
194
|
}
|
|
200
195
|
|
|
201
196
|
const DIALOG_SHOW_FOCUS_EVENT = "mp.dialogShowFocus";
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The retries an overlay runs to return focus after it closes: after two
|
|
3
|
+
* frames, then after the exit window (FZ-7904). The handles are kept, so a
|
|
4
|
+
* new schedule or a reopen cancels the old retries instead of stacking them,
|
|
5
|
+
* and an unmount leaves no frame or timer behind: the restore it would have
|
|
6
|
+
* run happens once, in a microtask, unless the component mounted again
|
|
7
|
+
* (StrictMode's simulated unmount).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { useEffect, useRef } from "react";
|
|
11
|
+
|
|
12
|
+
export interface RestoreRetries {
|
|
13
|
+
schedule: (restore: () => void, delayMs: number) => void;
|
|
14
|
+
cancel: () => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const hasFrames = () => typeof requestAnimationFrame === "function";
|
|
18
|
+
|
|
19
|
+
export function useRestoreRetries(): RestoreRetries {
|
|
20
|
+
const mountedRef = useRef(false);
|
|
21
|
+
const retriesRef = useRef<(RestoreRetries & { unmount: () => void }) | null>(null);
|
|
22
|
+
if (!retriesRef.current) {
|
|
23
|
+
let outer = 0;
|
|
24
|
+
let inner = 0;
|
|
25
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
26
|
+
let pending: (() => void) | null = null;
|
|
27
|
+
const restoreOnceUnmounted = (restore: () => void) => {
|
|
28
|
+
queueMicrotask(() => {
|
|
29
|
+
if (!mountedRef.current) restore();
|
|
30
|
+
});
|
|
31
|
+
};
|
|
32
|
+
const cancel = () => {
|
|
33
|
+
if (typeof cancelAnimationFrame === "function") {
|
|
34
|
+
if (outer) cancelAnimationFrame(outer);
|
|
35
|
+
if (inner) cancelAnimationFrame(inner);
|
|
36
|
+
}
|
|
37
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
38
|
+
outer = 0;
|
|
39
|
+
inner = 0;
|
|
40
|
+
timer = undefined;
|
|
41
|
+
pending = null;
|
|
42
|
+
};
|
|
43
|
+
retriesRef.current = {
|
|
44
|
+
cancel,
|
|
45
|
+
schedule(restore, delayMs) {
|
|
46
|
+
cancel();
|
|
47
|
+
if (!mountedRef.current) {
|
|
48
|
+
restoreOnceUnmounted(restore);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
pending = restore;
|
|
52
|
+
if (hasFrames()) {
|
|
53
|
+
outer = requestAnimationFrame(() => {
|
|
54
|
+
outer = 0;
|
|
55
|
+
// The outer frame can outlive the document (a test window torn
|
|
56
|
+
// down between the two frames), and with it the global.
|
|
57
|
+
if (!hasFrames()) return;
|
|
58
|
+
inner = requestAnimationFrame(() => {
|
|
59
|
+
inner = 0;
|
|
60
|
+
if (timer === undefined) pending = null;
|
|
61
|
+
restore();
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
timer = setTimeout(() => {
|
|
66
|
+
timer = undefined;
|
|
67
|
+
if (!outer && !inner) pending = null;
|
|
68
|
+
restore();
|
|
69
|
+
}, delayMs);
|
|
70
|
+
},
|
|
71
|
+
unmount() {
|
|
72
|
+
const restore = pending;
|
|
73
|
+
cancel();
|
|
74
|
+
if (restore) restoreOnceUnmounted(restore);
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
const retries = retriesRef.current;
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
mountedRef.current = true;
|
|
81
|
+
return () => {
|
|
82
|
+
mountedRef.current = false;
|
|
83
|
+
retries.unmount();
|
|
84
|
+
};
|
|
85
|
+
}, [retries]);
|
|
86
|
+
return retries;
|
|
87
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useFloatingPanel.d.ts","sourceRoot":"","sources":["../../src/FloatingPanel/useFloatingPanel.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"useFloatingPanel.d.ts","sourceRoot":"","sources":["../../src/FloatingPanel/useFloatingPanel.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAChE,OAAO,EAAmC,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAClF,OAAO,EAKL,gBAAgB,EACjB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,KAAK,QAAQ,EACb,KAAK,SAAS,EACd,KAAK,cAAc,EAMpB,MAAM,OAAO,CAAC;AACf,OAAO,EAAiB,KAAK,cAAc,EAAE,MAAM,SAAS,CAAC;AAqB7D,eAAO,MAAM,sBAAsB,QAA0D,CAAC;AAC9F,eAAO,MAAM,iBAAiB,QAAyB,CAAC;AACxD,eAAO,MAAM,oBAAoB,KAAK,CAAC;AACvC;yEACyE;AACzE,eAAO,MAAM,iBAAiB,IAAqB,CAAC;AAEpD;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,eAAe,EAAE,MAAM,GAAG,MAAM,CAEhE;AAED;;;;;;;;;;GAUG;AACH,MAAM,MAAM,cAAc,GAAG,aAAa,GAAG,eAAe,GAAG,kBAAkB,CAAC;AAClF,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,cAAc,GAAG,SAAS,GAAG,MAAM,CAOjF;AAED,eAAO,MAAM,eAAe,QAA4D,CAAC;AAIzF,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,OAAO,CAAC;IACd,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IACtC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,8EAA8E;IAC9E,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IAC1B,uEAAuE;IACvE,aAAa,CAAC,EAAE,SAAS,CAAC,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC;IAC/C;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAClD,wEAAwE;IACxE,UAAU,CAAC,EAAE,UAAU,EAAE,CAAC;IAC1B,oDAAoD;IACpD,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB;;;;OAIG;IACH,YAAY,CAAC,EAAE,SAAS,CAAC,OAAO,CAAC,CAAC;IAClC;;;OAGG;IACH,SAAS,CAAC,EAAE,cAAc,CAAC;IAC3B;;;;;OAKG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;OAIG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED;8EAC8E;AAC9E,wBAAgB,qBAAqB,CACnC,SAAS,EAAE,cAAc,GAAG,SAAS,EACrC,UAAU,EAAE,OAAO,GAAG,SAAS,GAC9B,cAAc,CAGhB;AAID,MAAM,WAAW,sBAAsB;IACrC,OAAO,EAAE,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC,SAAS,CAAC,CAAC;IACnD,IAAI,EAAE,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,CAAC,CAAC;IAC7C,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,QAAQ,EAAE,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC,UAAU,CAAC,CAAC;IACrD,MAAM,EAAE,MAAM,IAAI,CAAC;IAEnB,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,SAAS,CAAC;IACrB,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,OAAO,CAAC;IAEnB,UAAU,EAAE,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAC1C,iBAAiB,EAAE,SAAS,CAAC,IAAI,GAAG,MAAM,GAAG,IAAI,CAAC,CAAC;IACnD,gBAAgB,EAAE,SAAS,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC;IACjD,eAAe,EAAE,SAAS,CAAC,OAAO,CAAC,CAAC;IAEpC,gBAAgB,EAAE,MAAM,IAAI,CAAC;IAC7B,YAAY,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,IAAI,CAAC;IAC7C,eAAe,EAAE,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,KAAK,IAAI,CAAC;IACpD,UAAU,EAAE,QAAQ,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC,CAAC;IAE9C;;;;OAIG;IACH,cAAc,EAAE,MAAM,GAAG,MAAM,CAAC;IAChC,SAAS,EAAE,OAAO,CAAC;IACnB,eAAe,EAAE,MAAM,CAAC;IACxB,SAAS,EAAE,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC,WAAW,CAAC,CAAC;CAC7D;AAwCD;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,WAAW,GAAG,MAAM,GAAG,IAAI,CAQjE;AAED,wBAAgB,gBAAgB,CAAC,EAC/B,IAAI,EACJ,YAAY,EACZ,QAAQ,EACR,YAAY,EACZ,aAAa,EACb,iBAAiB,EACjB,UAAU,EAAE,cAAc,EAC1B,aAAa,EAAE,iBAAwB,EACvC,YAAY,EACZ,SAAS,EAAE,aAAa,EACxB,UAAU,EACV,WAAmB,EACnB,KAAY,GACb,EAAE,uBAAuB,GAAG,sBAAsB,CAsTlD"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"focusManagement.d.ts","sourceRoot":"","sources":["../../src/shared/focusManagement.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH,OAAO,EAAqB,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"focusManagement.d.ts","sourceRoot":"","sources":["../../src/shared/focusManagement.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH,OAAO,EAAqB,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AA0D1D;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,OAAO,EACb,kBAAkB,CAAC,EAAE,SAAS,CAAC,OAAO,CAAC,EACvC,SAAS,CAAC,EAAE,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,QAkB1C;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,OAAO,EACb,QAAQ,EAAE,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,EACvC,kBAAkB,CAAC,EAAE,SAAS,CAAC,OAAO,CAAC,QA6DxC;AAID;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,eAAe,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,QAWzE;AAED,wEAAwE;AACxE,wBAAgB,eAAe,CAAC,EAC9B,eAAe,GAChB,EAAE;IACD,eAAe,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;CACzC,GAAG,IAAI,CAGP"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The retries an overlay runs to return focus after it closes: after two
|
|
3
|
+
* frames, then after the exit window (FZ-7904). The handles are kept, so a
|
|
4
|
+
* new schedule or a reopen cancels the old retries instead of stacking them,
|
|
5
|
+
* and an unmount leaves no frame or timer behind: the restore it would have
|
|
6
|
+
* run happens once, in a microtask, unless the component mounted again
|
|
7
|
+
* (StrictMode's simulated unmount).
|
|
8
|
+
*/
|
|
9
|
+
export interface RestoreRetries {
|
|
10
|
+
schedule: (restore: () => void, delayMs: number) => void;
|
|
11
|
+
cancel: () => void;
|
|
12
|
+
}
|
|
13
|
+
export declare function useRestoreRetries(): RestoreRetries;
|
|
14
|
+
//# sourceMappingURL=restoreRetries.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"restoreRetries.d.ts","sourceRoot":"","sources":["../../src/shared/restoreRetries.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,CAAC,OAAO,EAAE,MAAM,IAAI,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACzD,MAAM,EAAE,MAAM,IAAI,CAAC;CACpB;AAID,wBAAgB,iBAAiB,IAAI,cAAc,CAoElD"}
|