@astralui/core 0.1.9 → 0.1.11

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/index.js CHANGED
@@ -167,48 +167,139 @@ function AstralThemeProvider({ colors, children }) {
167
167
  children
168
168
  ] });
169
169
  }
170
- function useDismiss(opened, onClose) {
170
+ var dialogSeq = 0;
171
+ var dialogStack = [];
172
+ var FOCUSABLE = [
173
+ "a[href]",
174
+ "area[href]",
175
+ "button:not([disabled])",
176
+ 'input:not([disabled]):not([type="hidden"])',
177
+ "select:not([disabled])",
178
+ "textarea:not([disabled])",
179
+ "iframe",
180
+ "object",
181
+ "embed",
182
+ "[contenteditable]",
183
+ '[tabindex]:not([tabindex="-1"])'
184
+ ].join(",");
185
+ function useDialog(opened, onClose) {
186
+ const panelRef = useRef(null);
187
+ const idRef = useRef("");
188
+ if (!idRef.current) idRef.current = "au-dialog-title-" + String(++dialogSeq);
189
+ const onCloseRef = useRef(onClose);
190
+ onCloseRef.current = onClose;
171
191
  useEffect(() => {
172
192
  if (!opened) return;
193
+ const panel = panelRef.current;
194
+ if (!panel) return;
195
+ const restoreTo = document.activeElement;
196
+ dialogStack.push(panel);
197
+ const isTop = () => dialogStack[dialogStack.length - 1] === panel;
198
+ const focusables = () => Array.from(panel.querySelectorAll(FOCUSABLE)).filter(
199
+ (el) => el.offsetWidth > 0 || el.offsetHeight > 0 || el === document.activeElement
200
+ );
201
+ const raf = requestAnimationFrame(() => {
202
+ if (panel.contains(document.activeElement)) return;
203
+ const list = focusables();
204
+ const target = list.find((el) => !el.classList.contains("au-modal-x")) ?? list[0] ?? panel;
205
+ target.focus();
206
+ });
173
207
  const onKey = (e) => {
174
- if (e.key === "Escape") onClose();
208
+ if (!isTop()) return;
209
+ if (e.key === "Escape") {
210
+ onCloseRef.current();
211
+ return;
212
+ }
213
+ if (e.key !== "Tab") return;
214
+ const list = focusables();
215
+ if (!list.length) {
216
+ e.preventDefault();
217
+ panel.focus();
218
+ return;
219
+ }
220
+ const first = list[0];
221
+ const last = list[list.length - 1];
222
+ const active = document.activeElement;
223
+ if (!panel.contains(active)) {
224
+ e.preventDefault();
225
+ (e.shiftKey ? last : first).focus();
226
+ return;
227
+ }
228
+ if (e.shiftKey && active === first) {
229
+ e.preventDefault();
230
+ last.focus();
231
+ } else if (!e.shiftKey && active === last) {
232
+ e.preventDefault();
233
+ first.focus();
234
+ }
175
235
  };
176
- document.addEventListener("keydown", onKey);
177
- const prev = document.body.style.overflow;
236
+ document.addEventListener("keydown", onKey, true);
237
+ const prevOverflow = document.body.style.overflow;
178
238
  document.body.style.overflow = "hidden";
179
239
  return () => {
180
- document.removeEventListener("keydown", onKey);
181
- document.body.style.overflow = prev;
240
+ cancelAnimationFrame(raf);
241
+ document.removeEventListener("keydown", onKey, true);
242
+ const at = dialogStack.indexOf(panel);
243
+ if (at !== -1) dialogStack.splice(at, 1);
244
+ document.body.style.overflow = prevOverflow;
245
+ if (restoreTo && document.contains(restoreTo)) restoreTo.focus();
182
246
  };
183
- }, [opened, onClose]);
247
+ }, [opened]);
248
+ return { panelRef, titleId: idRef.current };
184
249
  }
185
250
  function AstralModal({ opened, onClose, title, brand, width = 460, zIndex, children }) {
186
- useDismiss(opened, onClose);
251
+ const { panelRef, titleId } = useDialog(opened, onClose);
187
252
  if (!opened) return null;
188
253
  const style = { maxWidth: width, ...brand ? { "--au-ic": brand } : {} };
189
254
  return createPortal(
190
- /* @__PURE__ */ jsx("div", { className: "au-modal-overlay", onMouseDown: onClose, style: zIndex != null ? { zIndex } : void 0, children: /* @__PURE__ */ jsxs("div", { className: "au-modal-panel", style, onMouseDown: (e) => e.stopPropagation(), children: [
191
- /* @__PURE__ */ jsxs("div", { className: "au-modal-head", children: [
192
- /* @__PURE__ */ jsx("span", { className: "au-modal-title", children: title }),
193
- /* @__PURE__ */ jsx("button", { type: "button", className: "au-modal-x", onClick: onClose, "aria-label": "Close", children: /* @__PURE__ */ jsx(IconX, {}) })
194
- ] }),
195
- /* @__PURE__ */ jsx("div", { className: "au-modal-body", children })
196
- ] }) }),
255
+ /* @__PURE__ */ jsx("div", { className: "au-modal-overlay", onMouseDown: onClose, style: zIndex != null ? { zIndex } : void 0, children: /* @__PURE__ */ jsxs(
256
+ "div",
257
+ {
258
+ ref: panelRef,
259
+ className: "au-modal-panel",
260
+ style,
261
+ onMouseDown: (e) => e.stopPropagation(),
262
+ role: "dialog",
263
+ "aria-modal": "true",
264
+ "aria-labelledby": title ? titleId : void 0,
265
+ tabIndex: -1,
266
+ children: [
267
+ /* @__PURE__ */ jsxs("div", { className: "au-modal-head", children: [
268
+ /* @__PURE__ */ jsx("span", { className: "au-modal-title", id: titleId, children: title }),
269
+ /* @__PURE__ */ jsx("button", { type: "button", className: "au-modal-x", onClick: onClose, "aria-label": "Close", children: /* @__PURE__ */ jsx(IconX, {}) })
270
+ ] }),
271
+ /* @__PURE__ */ jsx("div", { className: "au-modal-body", children })
272
+ ]
273
+ }
274
+ ) }),
197
275
  document.body
198
276
  );
199
277
  }
200
278
  function AstralDrawer({ opened, onClose, title, brand, width = 440, zIndex, children }) {
201
- useDismiss(opened, onClose);
279
+ const { panelRef, titleId } = useDialog(opened, onClose);
202
280
  if (!opened) return null;
203
281
  const style = { width, ...brand ? { "--au-ic": brand } : {} };
204
282
  return createPortal(
205
- /* @__PURE__ */ jsx("div", { className: "au-drawer-overlay", onMouseDown: onClose, style: zIndex != null ? { zIndex } : void 0, children: /* @__PURE__ */ jsxs("div", { className: "au-drawer-panel", style, onMouseDown: (e) => e.stopPropagation(), children: [
206
- /* @__PURE__ */ jsxs("div", { className: "au-drawer-head", children: [
207
- /* @__PURE__ */ jsx("span", { className: "au-drawer-title", children: title }),
208
- /* @__PURE__ */ jsx("button", { type: "button", className: "au-modal-x", onClick: onClose, "aria-label": "Close", children: /* @__PURE__ */ jsx(IconX, {}) })
209
- ] }),
210
- /* @__PURE__ */ jsx("div", { className: "au-drawer-body", children })
211
- ] }) }),
283
+ /* @__PURE__ */ jsx("div", { className: "au-drawer-overlay", onMouseDown: onClose, style: zIndex != null ? { zIndex } : void 0, children: /* @__PURE__ */ jsxs(
284
+ "div",
285
+ {
286
+ ref: panelRef,
287
+ className: "au-drawer-panel",
288
+ style,
289
+ onMouseDown: (e) => e.stopPropagation(),
290
+ role: "dialog",
291
+ "aria-modal": "true",
292
+ "aria-labelledby": title ? titleId : void 0,
293
+ tabIndex: -1,
294
+ children: [
295
+ /* @__PURE__ */ jsxs("div", { className: "au-drawer-head", children: [
296
+ /* @__PURE__ */ jsx("span", { className: "au-drawer-title", id: titleId, children: title }),
297
+ /* @__PURE__ */ jsx("button", { type: "button", className: "au-modal-x", onClick: onClose, "aria-label": "Close", children: /* @__PURE__ */ jsx(IconX, {}) })
298
+ ] }),
299
+ /* @__PURE__ */ jsx("div", { className: "au-drawer-body", children })
300
+ ]
301
+ }
302
+ ) }),
212
303
  document.body
213
304
  );
214
305
  }
@@ -448,6 +539,8 @@ function AstralMenu({ trigger, items, align = "end", width = 220 }) {
448
539
  )
449
540
  ] });
450
541
  }
542
+ var DEFAULT_LIMIT = 4;
543
+ var EXIT_MS = 220;
451
544
  var toasts = [];
452
545
  var listeners = /* @__PURE__ */ new Set();
453
546
  var timers = /* @__PURE__ */ new Map();
@@ -469,7 +562,7 @@ function scheduleAutoClose(t) {
469
562
  function show(opts) {
470
563
  const id = opts.id ?? `toast-${++counter}`;
471
564
  const existing = toasts.find((t) => t.id === id);
472
- const toast = { withCloseButton: true, ...existing, ...opts, id };
565
+ const toast = { withCloseButton: true, ...existing, ...opts, id, exiting: false };
473
566
  toasts = existing ? toasts.map((t) => t.id === id ? toast : t) : [...toasts, toast];
474
567
  emit();
475
568
  scheduleAutoClose(toast);
@@ -479,7 +572,7 @@ function update(opts) {
479
572
  if (!opts.id) return show(opts);
480
573
  const existing = toasts.find((t) => t.id === opts.id);
481
574
  if (!existing) return show(opts);
482
- const toast = { ...existing, ...opts, id: opts.id };
575
+ const toast = { ...existing, ...opts, id: opts.id, exiting: false };
483
576
  toasts = toasts.map((t) => t.id === opts.id ? toast : t);
484
577
  emit();
485
578
  scheduleAutoClose(toast);
@@ -487,10 +580,16 @@ function update(opts) {
487
580
  }
488
581
  function hide(id) {
489
582
  const t = toasts.find((x) => x.id === id);
583
+ if (!t || t.exiting) return;
490
584
  clearTimer(id);
491
- toasts = toasts.filter((x) => x.id !== id);
585
+ toasts = toasts.map((x) => x.id === id ? { ...x, exiting: true } : x);
492
586
  emit();
493
- t?.onClose?.();
587
+ timers.set(id, setTimeout(() => {
588
+ toasts = toasts.filter((x) => x.id !== id);
589
+ timers.delete(id);
590
+ emit();
591
+ t.onClose?.();
592
+ }, EXIT_MS));
494
593
  }
495
594
  function clean() {
496
595
  toasts.forEach((t) => clearTimer(t.id));
@@ -518,20 +617,29 @@ function Spinner() {
518
617
  function ToastCard({ t }) {
519
618
  const accent = t.color ? `var(--astral-color-${t.color}-6)` : "var(--astral-color-default-border)";
520
619
  const lead = t.loading ? /* @__PURE__ */ jsx(Spinner, {}) : t.icon ? t.icon : t.color && DEFAULT_ICON[t.color] || /* @__PURE__ */ jsx(IconInfoCircle, { size: 16 });
521
- return /* @__PURE__ */ jsxs("div", { className: "au-toast", style: { ["--au-toast-accent"]: accent }, role: "status", children: [
522
- /* @__PURE__ */ jsx("span", { className: "au-toast-lead", children: lead }),
523
- /* @__PURE__ */ jsxs("div", { className: "au-toast-body", children: [
524
- t.title && /* @__PURE__ */ jsx("div", { className: "au-toast-title", children: t.title }),
525
- t.message != null && /* @__PURE__ */ jsx("div", { className: "au-toast-msg", children: t.message })
526
- ] }),
527
- t.withCloseButton !== false && /* @__PURE__ */ jsx("button", { type: "button", className: "au-toast-x", onClick: () => hide(t.id), "aria-label": "Close", children: /* @__PURE__ */ jsx(IconX, { size: 14 }) })
528
- ] });
620
+ return /* @__PURE__ */ jsxs(
621
+ "div",
622
+ {
623
+ className: t.exiting ? "au-toast au-toast-out" : "au-toast",
624
+ style: { ["--au-toast-accent"]: accent },
625
+ role: "status",
626
+ children: [
627
+ /* @__PURE__ */ jsx("span", { className: "au-toast-lead", children: lead }),
628
+ /* @__PURE__ */ jsxs("div", { className: "au-toast-body", children: [
629
+ t.title && /* @__PURE__ */ jsx("div", { className: "au-toast-title", children: t.title }),
630
+ t.message != null && /* @__PURE__ */ jsx("div", { className: "au-toast-msg", children: t.message })
631
+ ] }),
632
+ t.withCloseButton !== false && /* @__PURE__ */ jsx("button", { type: "button", className: "au-toast-x", onClick: () => hide(t.id), "aria-label": "Close", children: /* @__PURE__ */ jsx(IconX, { size: 14 }) })
633
+ ]
634
+ }
635
+ );
529
636
  }
530
- function AstralToaster() {
637
+ function AstralToaster({ limit = DEFAULT_LIMIT } = {}) {
531
638
  const items = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
532
639
  if (typeof document === "undefined") return null;
640
+ const visible = limit > 0 ? items.filter((t, i) => t.exiting || i >= items.length - limit) : items;
533
641
  return createPortal(
534
- /* @__PURE__ */ jsx("div", { className: "au-toast-host", role: "region", "aria-live": "polite", "aria-label": "Notifications", children: items.map((t) => /* @__PURE__ */ jsx(ToastCard, { t }, t.id)) }),
642
+ /* @__PURE__ */ jsx("div", { className: "au-toast-host", role: "region", "aria-live": "polite", "aria-label": "Notifications", children: visible.map((t) => /* @__PURE__ */ jsx(ToastCard, { t }, t.id)) }),
535
643
  document.body
536
644
  );
537
645
  }