@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 +1 -1
- package/react/README.md +8 -0
- package/react/dist/index.js +24 -9
package/package.json
CHANGED
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:
|
package/react/dist/index.js
CHANGED
|
@@ -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 =
|
|
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
|
|
137
|
-
|
|
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: (
|
|
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 })
|