@nimblebrain/synapse 0.12.1 → 0.12.2

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/README.md CHANGED
@@ -82,8 +82,10 @@ These are the durable decisions behind the library; they rarely change.
82
82
  - **Responsive to the pane, not the device.** Layouts observe their own width
83
83
  (`ResizeObserver` via `useBreakpoint`), because an app's iframe may be
84
84
  fullscreen, split, or a narrow rail regardless of screen size.
85
- - **Lean on the platform.** `Drawer` is built on the native `<dialog>` element
86
- (focus-trap, Escape, scroll behavior for free) rather than re-implementing them.
85
+ - **Sandbox-safe overlays.** `Drawer` is a plain `<div>` overlay, not a native
86
+ `<dialog>`: the app iframe withholds `allow-modals`, so `<dialog>.showModal()`
87
+ throws there. The scrim, Tab focus trap, focus-in/restore, scroll-lock, and
88
+ Escape are hand-rolled.
87
89
 
88
90
  ## Quick Start
89
91
 
package/dist/ui/index.cjs CHANGED
@@ -333,6 +333,18 @@ var RULES3 = `
333
333
  .nb-drawer-iconbtn { min-width: 44px; min-height: 44px; }
334
334
  }
335
335
  `;
336
+ function inClosedDetails(el) {
337
+ const details = el.closest("details:not([open])");
338
+ if (!details) return false;
339
+ const summary = details.querySelector("summary");
340
+ return !summary || !summary.contains(el);
341
+ }
342
+ var CANDIDATES = "a[href], button, input, select, textarea, summary, [tabindex]";
343
+ function tabbables(panel) {
344
+ return Array.from(panel.querySelectorAll(CANDIDATES)).filter(
345
+ (el) => el.tabIndex >= 0 && !el.matches(":disabled") && !el.closest("[hidden]") && !inClosedDetails(el) && el.type !== "hidden"
346
+ );
347
+ }
336
348
  var DrawerContext = react.createContext(null);
337
349
  function DrawerRoot({
338
350
  open,
@@ -361,12 +373,33 @@ function DrawerRoot({
361
373
  }, [open, onEscape, onClose]);
362
374
  react.useEffect(() => {
363
375
  if (!open) return;
376
+ const panel = panelRef.current;
364
377
  const previouslyFocused = document.activeElement;
365
- panelRef.current?.focus();
378
+ panel?.focus();
366
379
  const { body } = document;
367
380
  const prevOverflow = body.style.overflow;
368
381
  body.style.overflow = "hidden";
382
+ const onKeyDown = (e) => {
383
+ if (e.key !== "Tab" || !panel) return;
384
+ const focusables = tabbables(panel);
385
+ if (focusables.length === 0) {
386
+ e.preventDefault();
387
+ return;
388
+ }
389
+ const first = focusables[0];
390
+ const last = focusables[focusables.length - 1];
391
+ const active = document.activeElement;
392
+ if (e.shiftKey && (active === first || active === panel)) {
393
+ e.preventDefault();
394
+ last.focus();
395
+ } else if (!e.shiftKey && active === last) {
396
+ e.preventDefault();
397
+ first.focus();
398
+ }
399
+ };
400
+ panel?.addEventListener("keydown", onKeyDown);
369
401
  return () => {
402
+ panel?.removeEventListener("keydown", onKeyDown);
370
403
  body.style.overflow = prevOverflow;
371
404
  previouslyFocused?.focus?.();
372
405
  };