@apliteni/apliteni-ui 0.25.0 → 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.0",
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 })
@@ -83,7 +83,7 @@
83
83
  .ui-toast__action { flex: none; align-self: center; background: none; border: 0; padding: 4px 9px; border-radius: var(--radius-xs); font: inherit; font-weight: var(--weight-medium); color: var(--toast-action-ink); cursor: pointer; }
84
84
  .ui-toast__action:hover { background: color-mix(in srgb, var(--bg) 55%, transparent); }
85
85
 
86
- .ui-toast__close { flex: none; align-self: flex-start; background: none; border: 0; color: var(--muted); cursor: pointer; padding: 2px; display: grid; place-items: center; position: relative; }
86
+ .ui-toast__close { flex: none; align-self: flex-start; background: none; border: 0; color: var(--muted); cursor: pointer; padding: 2px; display: grid; place-items: center; position: relative; font: inherit; }
87
87
  .ui-toast__close svg { width: 15px; height: 15px; stroke: currentColor; fill: none; stroke-width: 2.5; }
88
88
  /* The target, not the ink. WCAG 2.5.8 measures what a pointer can land on, and
89
89
  the drawn button is 19x19 — a 15px glyph in 2px of padding. Growing the box
@@ -43,6 +43,13 @@
43
43
  /* Interactive card (link / button semantics) */
44
44
  .ui-card--interactive {
45
45
  cursor: pointer;
46
+ /* Written as a <button>, the card would take the browser's frame, centred text
47
+ and `font: 400 13.3333px Arial` — a shorthand, so `font: inherit` and not the
48
+ family longhand. .ui-card already states its own background, and the light
49
+ theme's border rule is (0,3,0) so `border: 0` here cannot reach it. #251 */
50
+ border: 0;
51
+ text-align: left;
52
+ font: inherit;
46
53
  transition: transform var(--dur-fast) var(--ease), box-shadow var(--dur-med) var(--ease);
47
54
  }
48
55
  .ui-card--interactive:hover {
@@ -142,6 +142,7 @@
142
142
  background: transparent;
143
143
  border: 1px solid transparent;
144
144
  border-radius: var(--radius-sm);
145
+ font: inherit;
145
146
  transition:
146
147
  color var(--dur-fast) var(--ease),
147
148
  background var(--dur-fast) var(--ease),
@@ -102,15 +102,27 @@
102
102
  }
103
103
  .ui-dropdown__panel--portal.is-open { opacity: 1; visibility: visible; transform: translateY(0); }
104
104
 
105
- /* Item row — [icon] [main: label + desc] [badge] [tick] */
105
+ /* Item row — [icon] [main: label + desc] [badge] [tick]. A row is a <div>, an
106
+ <a href> or a <button>: `is-selected` and __tick below mean a row gets
107
+ chosen, and choosing is a button's job. So the tag's own styling is reset
108
+ here, the way .ui-nav__item resets it, and the three render identically.
109
+ why: docs/specification.md#a-dropdown-row-is-a-div-a-link-or-a-button */
106
110
  .ui-dropdown__item {
107
111
  display: flex;
108
112
  gap: 11px;
109
113
  align-items: center;
114
+ width: 100%;
110
115
  padding: 9px 12px;
116
+ border: 0;
111
117
  border-radius: var(--radius-sm);
118
+ background: none;
112
119
  cursor: pointer;
120
+ text-align: left;
113
121
  text-decoration: none;
122
+ /* `font`, not `font-family`: the UA writes one shorthand, `font: 400 13.3333px
123
+ Arial`, so answering its family alone leaves the size and the line-height
124
+ standing. The same answer .ui-toast__action gives. */
125
+ font: inherit;
114
126
  color: var(--text);
115
127
  transition: background var(--dur-fast) var(--ease);
116
128
  }
@@ -22,6 +22,16 @@
22
22
  transform: translate(-50%, -100%) translateY(-4px) scale(.82); transform-origin: bottom center;
23
23
  display: inline-flex; align-items: center; gap: 9px;
24
24
  background: var(--ui-fb-pill-grad); color: var(--accent-contrast);
25
+ /* The pill is a click target the kit renders as a bare <div>, so the consumer who
26
+ needs it from the keyboard writes it as a <button> — which brings a 2px outset
27
+ frame around the gradient, a centred line and `font: 400 13.3333px Arial`. Stated
28
+ here rather than taken from the element. `font` and not `font-family`, because
29
+ Chrome's rule is the shorthand; the three longhands after it keep what the pill
30
+ wants and only the leading comes from the shorthand, which is the 1.62 the pill
31
+ already inherits as a div. `text-align` paints nothing on an inline-flex box
32
+ either way and is declared because leaving it to the element is the dependency
33
+ this refuses. why: #251 */
34
+ border: 0; text-align: center; font: inherit;
25
35
  font-family: var(--font-sans); font-size: 13px; font-weight: var(--weight-semibold); letter-spacing: .01em;
26
36
  padding: 9px 17px 9px 15px; border-radius: 999px; cursor: pointer; white-space: nowrap; overflow: hidden;
27
37
  box-shadow: 0 10px 26px var(--ui-fb-pill-glow),
@@ -68,7 +78,7 @@
68
78
  .ui-fbc__head { display: flex; align-items: center; gap: 12px; padding: 17px 20px 0; }
69
79
  .ui-fbc__chip { display: inline-flex; align-items: center; gap: 8px; color: var(--accent); font-size: 13px; font-weight: var(--weight-semibold); letter-spacing: .01em; white-space: nowrap; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
70
80
  .ui-fbc__chip svg { width: 14px; height: 14px; flex: none; opacity: .85; stroke-width: 2.7; }
71
- .ui-fbc__x { margin-left: auto; background: var(--surface-2); color: var(--dim); border: 0; width: 28px; height: 28px; border-radius: 8px; cursor: pointer; display: grid; place-items: center; flex: none; }
81
+ .ui-fbc__x { margin-left: auto; background: var(--surface-2); color: var(--dim); border: 0; width: 28px; height: 28px; border-radius: 8px; cursor: pointer; display: grid; place-items: center; flex: none; font: inherit; }
72
82
  .ui-fbc__x:hover { color: var(--strong); }
73
83
  .ui-fbc__x svg { width: 14px; height: 14px; stroke-width: 2.7; }
74
84
  .ui-fbc__quote { margin: 14px 20px 0; background: var(--surface-2); border-radius: 12px; padding: 12px 15px; display: flex; gap: 11px; }
@@ -79,6 +79,7 @@
79
79
  color: var(--text);
80
80
  border: 1px solid var(--border);
81
81
  border-radius: 10px;
82
+ font: inherit;
82
83
  transition:
83
84
  transform var(--dur-fast) var(--ease),
84
85
  background var(--dur-fast) var(--ease),
@@ -146,6 +147,17 @@
146
147
  padding: 10px 13px;
147
148
  border-radius: 10px;
148
149
  cursor: pointer;
150
+ /* What a browser paints on a <button> before any author rule runs. A consumer
151
+ reaches for <button> whenever the row has to work from the keyboard, so the
152
+ row states these rather than taking them from the element. `font` and not
153
+ `font-family`: Chrome's rule is the shorthand `font: 400 13.3333px Arial`,
154
+ and answering the family alone leaves a row 2.25px shorter in type 1.17px
155
+ smaller. Any font longhand a rule keeps goes after it. why: #251 */
156
+ width: 100%;
157
+ background: none;
158
+ border: 0;
159
+ text-align: left;
160
+ font: inherit;
149
161
  transition: background var(--dur-fast) var(--ease);
150
162
  }
151
163
  .vopt:hover { background: var(--surface); }
@@ -178,6 +190,7 @@
178
190
  border: 1px solid var(--border);
179
191
  background: linear-gradient(145deg, var(--accent-strong), var(--purple));
180
192
  color: var(--accent-contrast);
193
+ font: inherit;
181
194
  font-weight: 600;
182
195
  font-size: 12.5px;
183
196
  display: grid;