@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.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
  }