@apliteni/apliteni-ui 0.25.2 → 0.25.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apliteni/apliteni-ui",
3
- "version": "0.25.2",
3
+ "version": "0.25.3",
4
4
  "workspaces": [
5
5
  "react"
6
6
  ],
package/react/README.md CHANGED
@@ -31,6 +31,14 @@ import { DataTable, Modal, Button } from '@apliteni/apliteni-ui/react';
31
31
 
32
32
  Components: `DataTable`, `Modal`, `Button`, `Badge`, `Card`, `Icon`.
33
33
 
34
+ ## What the Modal does with focus
35
+
36
+ Opening moves focus to the first eligible control in the body, in DOM order, or to
37
+ the dialog itself if none exists. Links and disclosure summaries are eligible; hidden
38
+ controls, disabled controls, controls inside a closed disclosure and elements with a
39
+ negative tabindex are skipped. Tab and Shift+Tab wrap at the ends of the same list.
40
+ Escape and a click on the scrim dismiss the dialog and return focus to its opener.
41
+
34
42
  ## Work on them
35
43
 
36
44
  From the repo root — one `npm install` covers the workspace:
@@ -84,7 +84,25 @@ function Card({ title, sub, children }) {
84
84
  import { useEffect, useRef } from "react";
85
85
  import { createPortal } from "react-dom";
86
86
  import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
87
- var FOCUSABLE = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
87
+ var FOCUSABLE = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"]), details > summary:first-of-type';
88
+ function tabbable(el) {
89
+ const tabindex = el.getAttribute("tabindex");
90
+ if (tabindex !== null && Number(tabindex) < 0) return false;
91
+ if (el.matches(":disabled")) return false;
92
+ for (let node = el; node; node = node.parentElement) {
93
+ if (node.inert || node.hasAttribute("inert") || node.hasAttribute("hidden")) return false;
94
+ const holder = node.parentElement;
95
+ const folded = holder?.tagName === "DETAILS" && !holder.open;
96
+ if (folded && node !== holder.querySelector(":scope > summary")) return false;
97
+ }
98
+ return typeof el.checkVisibility !== "function" || el.checkVisibility({ visibilityProperty: true });
99
+ }
100
+ var tabbablesIn = (root) => Array.from(root.querySelectorAll(FOCUSABLE)).filter(tabbable);
101
+ var dismissOnScrim = (onClose) => (e) => {
102
+ if (e.target !== e.currentTarget) return;
103
+ e.preventDefault();
104
+ onClose();
105
+ };
88
106
  function Modal({ open, title, onClose, footer, children }) {
89
107
  const panel = useRef(null);
90
108
  useEffect(() => {
@@ -95,7 +113,7 @@ function Modal({ open, title, onClose, footer, children }) {
95
113
  return;
96
114
  }
97
115
  if (e.key !== "Tab" || !panel.current) return;
98
- const items = Array.from(panel.current.querySelectorAll(FOCUSABLE));
116
+ const items = tabbablesIn(panel.current);
99
117
  if (items.length === 0) {
100
118
  e.preventDefault();
101
119
  panel.current.focus();
@@ -112,7 +130,7 @@ function Modal({ open, title, onClose, footer, children }) {
112
130
  if (!e.shiftKey && active === last) {
113
131
  e.preventDefault();
114
132
  first.focus();
115
- } else if (e.shiftKey && active === first) {
133
+ } else if (e.shiftKey && (active === first || active === panel.current)) {
116
134
  e.preventDefault();
117
135
  last.focus();
118
136
  }
@@ -133,16 +151,13 @@ function Modal({ open, title, onClose, footer, children }) {
133
151
  }, [open]);
134
152
  useEffect(() => {
135
153
  if (!open) return;
136
- const target = panel.current?.querySelector(
137
- ".rx-modal__body input, .rx-modal__body select, .rx-modal__body textarea, .rx-modal__body button"
138
- ) || panel.current;
154
+ const body = panel.current?.querySelector(".rx-modal__body");
155
+ const target = (body ? tabbablesIn(body) : [])[0] || panel.current;
139
156
  target?.focus();
140
157
  }, [open]);
141
158
  if (!open) return null;
142
159
  return createPortal(
143
- /* @__PURE__ */ jsx5("div", { className: "rx-scrim", onMouseDown: (e) => {
144
- if (e.target === e.currentTarget) onClose();
145
- }, children: /* @__PURE__ */ jsxs3("div", { className: "rx-modal", role: "dialog", "aria-modal": "true", "aria-label": title, tabIndex: -1, ref: panel, children: [
160
+ /* @__PURE__ */ jsx5("div", { className: "rx-scrim", onMouseDown: dismissOnScrim(onClose), children: /* @__PURE__ */ jsxs3("div", { className: "rx-modal", role: "dialog", "aria-modal": "true", "aria-label": title, tabIndex: -1, ref: panel, children: [
146
161
  /* @__PURE__ */ jsxs3("div", { className: "rx-modal__head", children: [
147
162
  /* @__PURE__ */ jsx5("div", { className: "rx-modal__title", children: title }),
148
163
  /* @__PURE__ */ jsx5(Button, { variant: "ghost", size: "sm", iconOnly: true, icon: "x", "aria-label": "Close", onClick: onClose })