@astralui/core 0.1.10 → 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.cjs CHANGED
@@ -173,48 +173,139 @@ function AstralThemeProvider({ colors, children }) {
173
173
  children
174
174
  ] });
175
175
  }
176
- function useDismiss(opened, onClose) {
176
+ var dialogSeq = 0;
177
+ var dialogStack = [];
178
+ var FOCUSABLE = [
179
+ "a[href]",
180
+ "area[href]",
181
+ "button:not([disabled])",
182
+ 'input:not([disabled]):not([type="hidden"])',
183
+ "select:not([disabled])",
184
+ "textarea:not([disabled])",
185
+ "iframe",
186
+ "object",
187
+ "embed",
188
+ "[contenteditable]",
189
+ '[tabindex]:not([tabindex="-1"])'
190
+ ].join(",");
191
+ function useDialog(opened, onClose) {
192
+ const panelRef = react.useRef(null);
193
+ const idRef = react.useRef("");
194
+ if (!idRef.current) idRef.current = "au-dialog-title-" + String(++dialogSeq);
195
+ const onCloseRef = react.useRef(onClose);
196
+ onCloseRef.current = onClose;
177
197
  react.useEffect(() => {
178
198
  if (!opened) return;
199
+ const panel = panelRef.current;
200
+ if (!panel) return;
201
+ const restoreTo = document.activeElement;
202
+ dialogStack.push(panel);
203
+ const isTop = () => dialogStack[dialogStack.length - 1] === panel;
204
+ const focusables = () => Array.from(panel.querySelectorAll(FOCUSABLE)).filter(
205
+ (el) => el.offsetWidth > 0 || el.offsetHeight > 0 || el === document.activeElement
206
+ );
207
+ const raf = requestAnimationFrame(() => {
208
+ if (panel.contains(document.activeElement)) return;
209
+ const list = focusables();
210
+ const target = list.find((el) => !el.classList.contains("au-modal-x")) ?? list[0] ?? panel;
211
+ target.focus();
212
+ });
179
213
  const onKey = (e) => {
180
- if (e.key === "Escape") onClose();
214
+ if (!isTop()) return;
215
+ if (e.key === "Escape") {
216
+ onCloseRef.current();
217
+ return;
218
+ }
219
+ if (e.key !== "Tab") return;
220
+ const list = focusables();
221
+ if (!list.length) {
222
+ e.preventDefault();
223
+ panel.focus();
224
+ return;
225
+ }
226
+ const first = list[0];
227
+ const last = list[list.length - 1];
228
+ const active = document.activeElement;
229
+ if (!panel.contains(active)) {
230
+ e.preventDefault();
231
+ (e.shiftKey ? last : first).focus();
232
+ return;
233
+ }
234
+ if (e.shiftKey && active === first) {
235
+ e.preventDefault();
236
+ last.focus();
237
+ } else if (!e.shiftKey && active === last) {
238
+ e.preventDefault();
239
+ first.focus();
240
+ }
181
241
  };
182
- document.addEventListener("keydown", onKey);
183
- const prev = document.body.style.overflow;
242
+ document.addEventListener("keydown", onKey, true);
243
+ const prevOverflow = document.body.style.overflow;
184
244
  document.body.style.overflow = "hidden";
185
245
  return () => {
186
- document.removeEventListener("keydown", onKey);
187
- document.body.style.overflow = prev;
246
+ cancelAnimationFrame(raf);
247
+ document.removeEventListener("keydown", onKey, true);
248
+ const at = dialogStack.indexOf(panel);
249
+ if (at !== -1) dialogStack.splice(at, 1);
250
+ document.body.style.overflow = prevOverflow;
251
+ if (restoreTo && document.contains(restoreTo)) restoreTo.focus();
188
252
  };
189
- }, [opened, onClose]);
253
+ }, [opened]);
254
+ return { panelRef, titleId: idRef.current };
190
255
  }
191
256
  function AstralModal({ opened, onClose, title, brand, width = 460, zIndex, children }) {
192
- useDismiss(opened, onClose);
257
+ const { panelRef, titleId } = useDialog(opened, onClose);
193
258
  if (!opened) return null;
194
259
  const style = { maxWidth: width, ...brand ? { "--au-ic": brand } : {} };
195
260
  return reactDom.createPortal(
196
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "au-modal-overlay", onMouseDown: onClose, style: zIndex != null ? { zIndex } : void 0, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "au-modal-panel", style, onMouseDown: (e) => e.stopPropagation(), children: [
197
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "au-modal-head", children: [
198
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "au-modal-title", children: title }),
199
- /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "au-modal-x", onClick: onClose, "aria-label": "Close", children: /* @__PURE__ */ jsxRuntime.jsx(iconsReact.IconX, {}) })
200
- ] }),
201
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "au-modal-body", children })
202
- ] }) }),
261
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "au-modal-overlay", onMouseDown: onClose, style: zIndex != null ? { zIndex } : void 0, children: /* @__PURE__ */ jsxRuntime.jsxs(
262
+ "div",
263
+ {
264
+ ref: panelRef,
265
+ className: "au-modal-panel",
266
+ style,
267
+ onMouseDown: (e) => e.stopPropagation(),
268
+ role: "dialog",
269
+ "aria-modal": "true",
270
+ "aria-labelledby": title ? titleId : void 0,
271
+ tabIndex: -1,
272
+ children: [
273
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "au-modal-head", children: [
274
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "au-modal-title", id: titleId, children: title }),
275
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "au-modal-x", onClick: onClose, "aria-label": "Close", children: /* @__PURE__ */ jsxRuntime.jsx(iconsReact.IconX, {}) })
276
+ ] }),
277
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "au-modal-body", children })
278
+ ]
279
+ }
280
+ ) }),
203
281
  document.body
204
282
  );
205
283
  }
206
284
  function AstralDrawer({ opened, onClose, title, brand, width = 440, zIndex, children }) {
207
- useDismiss(opened, onClose);
285
+ const { panelRef, titleId } = useDialog(opened, onClose);
208
286
  if (!opened) return null;
209
287
  const style = { width, ...brand ? { "--au-ic": brand } : {} };
210
288
  return reactDom.createPortal(
211
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "au-drawer-overlay", onMouseDown: onClose, style: zIndex != null ? { zIndex } : void 0, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "au-drawer-panel", style, onMouseDown: (e) => e.stopPropagation(), children: [
212
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "au-drawer-head", children: [
213
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "au-drawer-title", children: title }),
214
- /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "au-modal-x", onClick: onClose, "aria-label": "Close", children: /* @__PURE__ */ jsxRuntime.jsx(iconsReact.IconX, {}) })
215
- ] }),
216
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "au-drawer-body", children })
217
- ] }) }),
289
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "au-drawer-overlay", onMouseDown: onClose, style: zIndex != null ? { zIndex } : void 0, children: /* @__PURE__ */ jsxRuntime.jsxs(
290
+ "div",
291
+ {
292
+ ref: panelRef,
293
+ className: "au-drawer-panel",
294
+ style,
295
+ onMouseDown: (e) => e.stopPropagation(),
296
+ role: "dialog",
297
+ "aria-modal": "true",
298
+ "aria-labelledby": title ? titleId : void 0,
299
+ tabIndex: -1,
300
+ children: [
301
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "au-drawer-head", children: [
302
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "au-drawer-title", id: titleId, children: title }),
303
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "au-modal-x", onClick: onClose, "aria-label": "Close", children: /* @__PURE__ */ jsxRuntime.jsx(iconsReact.IconX, {}) })
304
+ ] }),
305
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "au-drawer-body", children })
306
+ ]
307
+ }
308
+ ) }),
218
309
  document.body
219
310
  );
220
311
  }