@nimblebrain/synapse 0.12.0 → 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.
@@ -1,5 +1,5 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { HTMLAttributes, ReactNode, ButtonHTMLAttributes, DialogHTMLAttributes, InputHTMLAttributes, RefObject, CSSProperties, ElementType } from 'react';
2
+ import { HTMLAttributes, ReactNode, ButtonHTMLAttributes, InputHTMLAttributes, RefObject, CSSProperties, ElementType } from 'react';
3
3
 
4
4
  interface AvatarProps extends Omit<HTMLAttributes<HTMLSpanElement>, "children"> {
5
5
  /** Name used for initials and the image alt text. */
@@ -44,9 +44,9 @@ interface CardProps extends HTMLAttributes<HTMLDivElement> {
44
44
  declare function Card({ padding, interactive, style, className, children, ...rest }: CardProps): react_jsx_runtime.JSX.Element;
45
45
 
46
46
  type Side$1 = "left" | "right" | "bottom";
47
- interface DrawerProps extends Omit<DialogHTMLAttributes<HTMLDialogElement>, "title" | "onCancel" | "onClick"> {
47
+ interface DrawerProps extends Omit<HTMLAttributes<HTMLDivElement>, "onClick"> {
48
48
  open: boolean;
49
- /** Called by the backdrop and the Header close button. */
49
+ /** Called by the scrim and the Header close button. */
50
50
  onClose: () => void;
51
51
  /** Called on Escape. Defaults to `onClose` — override to do something else
52
52
  * (e.g. pop one level of a panel stack before closing). */
@@ -56,7 +56,7 @@ interface DrawerProps extends Omit<DialogHTMLAttributes<HTMLDialogElement>, "tit
56
56
  width?: number | string;
57
57
  children?: ReactNode;
58
58
  }
59
- declare function DrawerRoot({ open, onClose, onEscape, side, width, style, children, ...rest }: DrawerProps): react_jsx_runtime.JSX.Element;
59
+ declare function DrawerRoot({ open, onClose, onEscape, side, width, style, children, ...rest }: DrawerProps): react_jsx_runtime.JSX.Element | null;
60
60
  interface HeaderProps extends Omit<HTMLAttributes<HTMLElement>, "title"> {
61
61
  /**
62
62
  * Heading text — rendered as an `<h2>` and wired as the drawer's accessible
@@ -1,5 +1,5 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { HTMLAttributes, ReactNode, ButtonHTMLAttributes, DialogHTMLAttributes, InputHTMLAttributes, RefObject, CSSProperties, ElementType } from 'react';
2
+ import { HTMLAttributes, ReactNode, ButtonHTMLAttributes, InputHTMLAttributes, RefObject, CSSProperties, ElementType } from 'react';
3
3
 
4
4
  interface AvatarProps extends Omit<HTMLAttributes<HTMLSpanElement>, "children"> {
5
5
  /** Name used for initials and the image alt text. */
@@ -44,9 +44,9 @@ interface CardProps extends HTMLAttributes<HTMLDivElement> {
44
44
  declare function Card({ padding, interactive, style, className, children, ...rest }: CardProps): react_jsx_runtime.JSX.Element;
45
45
 
46
46
  type Side$1 = "left" | "right" | "bottom";
47
- interface DrawerProps extends Omit<DialogHTMLAttributes<HTMLDialogElement>, "title" | "onCancel" | "onClick"> {
47
+ interface DrawerProps extends Omit<HTMLAttributes<HTMLDivElement>, "onClick"> {
48
48
  open: boolean;
49
- /** Called by the backdrop and the Header close button. */
49
+ /** Called by the scrim and the Header close button. */
50
50
  onClose: () => void;
51
51
  /** Called on Escape. Defaults to `onClose` — override to do something else
52
52
  * (e.g. pop one level of a panel stack before closing). */
@@ -56,7 +56,7 @@ interface DrawerProps extends Omit<DialogHTMLAttributes<HTMLDialogElement>, "tit
56
56
  width?: number | string;
57
57
  children?: ReactNode;
58
58
  }
59
- declare function DrawerRoot({ open, onClose, onEscape, side, width, style, children, ...rest }: DrawerProps): react_jsx_runtime.JSX.Element;
59
+ declare function DrawerRoot({ open, onClose, onEscape, side, width, style, children, ...rest }: DrawerProps): react_jsx_runtime.JSX.Element | null;
60
60
  interface HeaderProps extends Omit<HTMLAttributes<HTMLElement>, "title"> {
61
61
  /**
62
62
  * Heading text — rendered as an `<h2>` and wired as the drawer's accessible
package/dist/ui/index.js CHANGED
@@ -305,21 +305,23 @@ function Card({
305
305
  }
306
306
  var STYLE_ID3 = "nb-synapse-drawer";
307
307
  var RULES3 = `
308
+ .nb-drawer-scrim {
309
+ position: fixed; inset: 0; z-index: 1000;
310
+ display: flex; background: rgba(0, 0, 0, 0.32);
311
+ animation: nb-drawer-fade 200ms ease;
312
+ }
313
+ .nb-drawer-scrim--right { justify-content: flex-end; }
314
+ .nb-drawer-scrim--left { justify-content: flex-start; }
315
+ .nb-drawer-scrim--bottom { align-items: flex-end; }
308
316
  .nb-drawer {
309
317
  margin: 0; padding: 0; border: none; max-width: 92%;
310
318
  height: 100%; max-height: 100%; display: flex; flex-direction: column;
319
+ outline: none;
311
320
  }
312
- .nb-drawer--right { margin-left: auto; }
313
- .nb-drawer--left { margin-right: auto; }
314
- .nb-drawer--bottom {
315
- margin-top: auto; width: 100%; max-width: 100%;
316
- height: auto; max-height: 92%;
317
- }
318
- .nb-drawer--right[open] { animation: nb-drawer-in-right 240ms cubic-bezier(0.2, 0, 0, 1); }
319
- .nb-drawer--left[open] { animation: nb-drawer-in-left 240ms cubic-bezier(0.2, 0, 0, 1); }
320
- .nb-drawer--bottom[open] { animation: nb-drawer-in-bottom 240ms cubic-bezier(0.2, 0, 0, 1); }
321
- .nb-drawer::backdrop { background: rgba(0, 0, 0, 0.32); }
322
- .nb-drawer[open]::backdrop { animation: nb-drawer-fade 200ms ease; }
321
+ .nb-drawer--bottom { width: 100%; max-width: 100%; height: auto; max-height: 92%; }
322
+ .nb-drawer--right { animation: nb-drawer-in-right 240ms cubic-bezier(0.2, 0, 0, 1); }
323
+ .nb-drawer--left { animation: nb-drawer-in-left 240ms cubic-bezier(0.2, 0, 0, 1); }
324
+ .nb-drawer--bottom { animation: nb-drawer-in-bottom 240ms cubic-bezier(0.2, 0, 0, 1); }
323
325
  @keyframes nb-drawer-in-right { from { transform: translateX(100%); } }
324
326
  @keyframes nb-drawer-in-left { from { transform: translateX(-100%); } }
325
327
  @keyframes nb-drawer-in-bottom { from { transform: translateY(100%); } }
@@ -329,6 +331,18 @@ var RULES3 = `
329
331
  .nb-drawer-iconbtn { min-width: 44px; min-height: 44px; }
330
332
  }
331
333
  `;
334
+ function inClosedDetails(el) {
335
+ const details = el.closest("details:not([open])");
336
+ if (!details) return false;
337
+ const summary = details.querySelector("summary");
338
+ return !summary || !summary.contains(el);
339
+ }
340
+ var CANDIDATES = "a[href], button, input, select, textarea, summary, [tabindex]";
341
+ function tabbables(panel) {
342
+ return Array.from(panel.querySelectorAll(CANDIDATES)).filter(
343
+ (el) => el.tabIndex >= 0 && !el.matches(":disabled") && !el.closest("[hidden]") && !inClosedDetails(el) && el.type !== "hidden"
344
+ );
345
+ }
332
346
  var DrawerContext = createContext(null);
333
347
  function DrawerRoot({
334
348
  open,
@@ -341,41 +355,86 @@ function DrawerRoot({
341
355
  ...rest
342
356
  }) {
343
357
  ensureStyle(STYLE_ID3, RULES3);
344
- const dialogRef = useRef(null);
358
+ const panelRef = useRef(null);
345
359
  const labelId = useId();
346
360
  const [hasTitle, setHasTitle] = useState(false);
347
361
  useEffect(() => {
348
- const dialog = dialogRef.current;
349
- if (!dialog || typeof dialog.showModal !== "function") return;
350
- if (open && !dialog.open) dialog.showModal();
351
- else if (!open && dialog.open) dialog.close();
362
+ if (!open) return;
363
+ const onKey = (e) => {
364
+ if (e.key === "Escape") {
365
+ e.preventDefault();
366
+ (onEscape ?? onClose)();
367
+ }
368
+ };
369
+ window.addEventListener("keydown", onKey);
370
+ return () => window.removeEventListener("keydown", onKey);
371
+ }, [open, onEscape, onClose]);
372
+ useEffect(() => {
373
+ if (!open) return;
374
+ const panel = panelRef.current;
375
+ const previouslyFocused = document.activeElement;
376
+ panel?.focus();
377
+ const { body } = document;
378
+ const prevOverflow = body.style.overflow;
379
+ body.style.overflow = "hidden";
380
+ const onKeyDown = (e) => {
381
+ if (e.key !== "Tab" || !panel) return;
382
+ const focusables = tabbables(panel);
383
+ if (focusables.length === 0) {
384
+ e.preventDefault();
385
+ return;
386
+ }
387
+ const first = focusables[0];
388
+ const last = focusables[focusables.length - 1];
389
+ const active = document.activeElement;
390
+ if (e.shiftKey && (active === first || active === panel)) {
391
+ e.preventDefault();
392
+ last.focus();
393
+ } else if (!e.shiftKey && active === last) {
394
+ e.preventDefault();
395
+ first.focus();
396
+ }
397
+ };
398
+ panel?.addEventListener("keydown", onKeyDown);
399
+ return () => {
400
+ panel?.removeEventListener("keydown", onKeyDown);
401
+ body.style.overflow = prevOverflow;
402
+ previouslyFocused?.focus?.();
403
+ };
352
404
  }, [open]);
405
+ const ctxValue = useMemo(() => ({ labelId, setHasTitle }), [labelId]);
406
+ if (!open) return null;
353
407
  const isBottom = side === "bottom";
354
408
  const panelStyle = {
355
409
  ...isBottom ? { width: "100%", maxWidth: "100%" } : { width },
356
410
  background: tokens.bg,
357
411
  color: tokens.fg,
358
412
  boxShadow: tokens.shadowLg,
413
+ outline: "none",
359
414
  ...side === "right" ? { borderLeft: `${tokens.borderWidth} solid ${tokens.border}` } : side === "left" ? { borderRight: `${tokens.borderWidth} solid ${tokens.border}` } : { borderTop: `${tokens.borderWidth} solid ${tokens.border}` },
360
415
  ...style
361
416
  };
362
- const ctxValue = useMemo(() => ({ labelId, setHasTitle }), [labelId]);
363
417
  return /* @__PURE__ */ jsx(DrawerContext.Provider, { value: ctxValue, children: /* @__PURE__ */ jsx(
364
- "dialog",
418
+ "div",
365
419
  {
366
- ref: dialogRef,
367
- className: `nb-drawer nb-drawer--${side}`,
368
- style: panelStyle,
369
- "aria-labelledby": hasTitle ? labelId : void 0,
370
- onCancel: (e) => {
371
- e.preventDefault();
372
- (onEscape ?? onClose)();
373
- },
420
+ className: `nb-drawer-scrim nb-drawer-scrim--${side}`,
374
421
  onClick: (e) => {
375
- if (e.target === dialogRef.current) onClose();
422
+ if (e.target === e.currentTarget) onClose();
376
423
  },
377
- ...rest,
378
- children
424
+ children: /* @__PURE__ */ jsx(
425
+ "div",
426
+ {
427
+ ref: panelRef,
428
+ role: "dialog",
429
+ "aria-modal": "true",
430
+ "aria-labelledby": hasTitle ? labelId : void 0,
431
+ tabIndex: -1,
432
+ className: `nb-drawer nb-drawer--${side}`,
433
+ style: panelStyle,
434
+ ...rest,
435
+ children
436
+ }
437
+ )
379
438
  }
380
439
  ) });
381
440
  }