@rebasepro/ui 0.21.1 → 0.21.2-canary.g1ea48be
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.
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Finish a focus move that a modal's focus trap interrupted.
|
|
3
|
+
*
|
|
4
|
+
* Radix's `FocusScope` watches a modal for removed nodes, and when one goes
|
|
5
|
+
* while `document.activeElement` is `body` it concludes the focused element was
|
|
6
|
+
* deleted and focuses the modal itself. But `body` is also what `activeElement`
|
|
7
|
+
* reads *during* an ordinary focus move: the browser fires `focusout` before it
|
|
8
|
+
* lands on the next element. Anything that unmounts in response — a tooltip that
|
|
9
|
+
* opened on focus closing on blur, above all — is removed inside that window.
|
|
10
|
+
* The scope focuses the container, the browser abandons the move it was making,
|
|
11
|
+
* and Tab from one field lands on the dialog, so the next Tab starts again from
|
|
12
|
+
* the header. Shift+Tab the same.
|
|
13
|
+
*
|
|
14
|
+
* So remember where each move inside the container was going, and if the
|
|
15
|
+
* container takes focus before the move lands, send it on. A move that nothing
|
|
16
|
+
* interrupts lands on its target and clears the note; a note nobody used is
|
|
17
|
+
* dropped by the next task.
|
|
18
|
+
*
|
|
19
|
+
* The note is taken on `window`, in the capture phase — before any other
|
|
20
|
+
* listener for the event runs. The scope's correction arrives in the microtask
|
|
21
|
+
* checkpoint after whichever listener removed the node, so a note taken any
|
|
22
|
+
* later (on the container, bubbling) is taken after focus has already been
|
|
23
|
+
* moved: a field that drops something from its own `focusout` handler beat it.
|
|
24
|
+
*
|
|
25
|
+
* Internal to the kit's modals — not exported.
|
|
26
|
+
*/
|
|
27
|
+
export declare function useRestoreInterruptedFocus(container: HTMLElement | null): void;
|
package/dist/index.es.js
CHANGED
|
@@ -4175,6 +4175,62 @@ var inputStyles = `
|
|
|
4175
4175
|
}
|
|
4176
4176
|
`;
|
|
4177
4177
|
//#endregion
|
|
4178
|
+
//#region src/hooks/useRestoreInterruptedFocus.ts
|
|
4179
|
+
/**
|
|
4180
|
+
* Finish a focus move that a modal's focus trap interrupted.
|
|
4181
|
+
*
|
|
4182
|
+
* Radix's `FocusScope` watches a modal for removed nodes, and when one goes
|
|
4183
|
+
* while `document.activeElement` is `body` it concludes the focused element was
|
|
4184
|
+
* deleted and focuses the modal itself. But `body` is also what `activeElement`
|
|
4185
|
+
* reads *during* an ordinary focus move: the browser fires `focusout` before it
|
|
4186
|
+
* lands on the next element. Anything that unmounts in response — a tooltip that
|
|
4187
|
+
* opened on focus closing on blur, above all — is removed inside that window.
|
|
4188
|
+
* The scope focuses the container, the browser abandons the move it was making,
|
|
4189
|
+
* and Tab from one field lands on the dialog, so the next Tab starts again from
|
|
4190
|
+
* the header. Shift+Tab the same.
|
|
4191
|
+
*
|
|
4192
|
+
* So remember where each move inside the container was going, and if the
|
|
4193
|
+
* container takes focus before the move lands, send it on. A move that nothing
|
|
4194
|
+
* interrupts lands on its target and clears the note; a note nobody used is
|
|
4195
|
+
* dropped by the next task.
|
|
4196
|
+
*
|
|
4197
|
+
* The note is taken on `window`, in the capture phase — before any other
|
|
4198
|
+
* listener for the event runs. The scope's correction arrives in the microtask
|
|
4199
|
+
* checkpoint after whichever listener removed the node, so a note taken any
|
|
4200
|
+
* later (on the container, bubbling) is taken after focus has already been
|
|
4201
|
+
* moved: a field that drops something from its own `focusout` handler beat it.
|
|
4202
|
+
*
|
|
4203
|
+
* Internal to the kit's modals — not exported.
|
|
4204
|
+
*/
|
|
4205
|
+
function useRestoreInterruptedFocus(container) {
|
|
4206
|
+
useEffect(() => {
|
|
4207
|
+
if (!container) return;
|
|
4208
|
+
let intended = null;
|
|
4209
|
+
let expiry;
|
|
4210
|
+
const onFocusOut = (event) => {
|
|
4211
|
+
if (!(event.target instanceof Node) || !container.contains(event.target)) return;
|
|
4212
|
+
const next = event.relatedTarget;
|
|
4213
|
+
intended = next instanceof HTMLElement && next !== container && container.contains(next) ? next : null;
|
|
4214
|
+
clearTimeout(expiry);
|
|
4215
|
+
if (intended) expiry = setTimeout(() => {
|
|
4216
|
+
intended = null;
|
|
4217
|
+
}, 0);
|
|
4218
|
+
};
|
|
4219
|
+
const onFocusIn = (event) => {
|
|
4220
|
+
const target = intended;
|
|
4221
|
+
intended = null;
|
|
4222
|
+
if (event.target === container && target?.isConnected && container.contains(target)) target.focus();
|
|
4223
|
+
};
|
|
4224
|
+
window.addEventListener("focusout", onFocusOut, true);
|
|
4225
|
+
container.addEventListener("focusin", onFocusIn);
|
|
4226
|
+
return () => {
|
|
4227
|
+
clearTimeout(expiry);
|
|
4228
|
+
window.removeEventListener("focusout", onFocusOut, true);
|
|
4229
|
+
container.removeEventListener("focusin", onFocusIn);
|
|
4230
|
+
};
|
|
4231
|
+
}, [container]);
|
|
4232
|
+
}
|
|
4233
|
+
//#endregion
|
|
4178
4234
|
//#region src/components/Dialog.tsx
|
|
4179
4235
|
var widthClasses = {
|
|
4180
4236
|
xs: "max-w-xs w-xs",
|
|
@@ -4193,6 +4249,8 @@ var widthClasses = {
|
|
|
4193
4249
|
var Dialog = ({ open, onOpenChange, children, className, containerClassName, fullWidth = true, fullHeight, fullScreen, scrollable = true, maxWidth = "lg", modal = true, onOpenAutoFocus, onEscapeKeyDown, onPointerDownOutside, onInteractOutside, disableInitialFocus = true, portalContainer, "aria-describedby": ariaDescribedby }) => {
|
|
4194
4250
|
const [displayed, setDisplayed] = useState(false);
|
|
4195
4251
|
const [popupHost, setPopupHost] = useState(null);
|
|
4252
|
+
const [contentEl, setContentEl] = useState(null);
|
|
4253
|
+
useRestoreInterruptedFocus(contentEl);
|
|
4196
4254
|
const contextContainer = usePortalContainer();
|
|
4197
4255
|
const finalContainer = portalContainer ?? contextContainer ?? void 0;
|
|
4198
4256
|
useEffect(() => {
|
|
@@ -4218,6 +4276,7 @@ var Dialog = ({ open, onOpenChange, children, className, containerClassName, ful
|
|
|
4218
4276
|
className: cls("fixed inset-0 transition-opacity ease-in-out duration-200 bg-black/50 dark:bg-black/60 backdrop-blur-sm", displayed && open ? "opacity-100" : "opacity-0", "z-50 fixed top-0 left-0 w-full h-full flex justify-center items-center"),
|
|
4219
4277
|
style: { pointerEvents: displayed ? "auto" : "none" }
|
|
4220
4278
|
}), /* @__PURE__ */ jsxs(DialogPrimitive.Content, {
|
|
4279
|
+
ref: setContentEl,
|
|
4221
4280
|
onEscapeKeyDown,
|
|
4222
4281
|
onOpenAutoFocus: (e) => {
|
|
4223
4282
|
if (disableInitialFocus) e.preventDefault();
|
|
@@ -5327,6 +5386,7 @@ Slider.displayName = "Slider";
|
|
|
5327
5386
|
var Sheet = ({ children, side = "right", title, modal = true, includeBackgroundOverlay = true, open, onOpenChange, onPointerDownOutside, onInteractOutside, transparent, className, style, overlayClassName, overlayZIndex = "z-50", overlayStyle, portalContainer, ...props }) => {
|
|
5328
5387
|
const [displayed, setDisplayed] = useState(false);
|
|
5329
5388
|
const [contentEl, setContentEl] = useState(null);
|
|
5389
|
+
useRestoreInterruptedFocus(contentEl);
|
|
5330
5390
|
const contextContainer = usePortalContainer();
|
|
5331
5391
|
const finalContainer = portalContainer ?? contextContainer ?? void 0;
|
|
5332
5392
|
useEffect(() => {
|