@rebasepro/ui 0.21.1 → 0.22.0

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(() => {