@antadesign/anta 0.3.2 → 0.3.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/dist/anta_helpers.d.ts +101 -0
- package/dist/anta_helpers.js +77 -0
- package/dist/calendar-core.d.ts +126 -0
- package/dist/calendar-core.js +289 -0
- package/dist/components/Button.d.ts +9 -1
- package/dist/components/Button.js +5 -2
- package/dist/components/Calendar.d.ts +80 -0
- package/dist/components/Calendar.js +220 -0
- package/dist/components/Checkbox.d.ts +16 -11
- package/dist/components/Checkbox.js +13 -7
- package/dist/components/Expander.d.ts +7 -1
- package/dist/components/Expander.js +4 -2
- package/dist/components/Input.d.ts +13 -5
- package/dist/components/Input.js +14 -6
- package/dist/components/InputDate.d.ts +87 -0
- package/dist/components/InputDate.js +278 -0
- package/dist/components/InputDate.module.css +1 -0
- package/dist/components/Menu.d.ts +5 -1
- package/dist/components/Menu.js +6 -2
- package/dist/components/MenuItem.d.ts +48 -7
- package/dist/components/MenuItem.js +71 -7
- package/dist/components/MenuSeparator.d.ts +13 -2
- package/dist/components/MenuSeparator.js +12 -2
- package/dist/components/Progress.d.ts +4 -1
- package/dist/components/Progress.js +4 -3
- package/dist/components/RadioGroup.d.ts +12 -10
- package/dist/components/RadioGroup.js +5 -5
- package/dist/components/Select.d.ts +255 -0
- package/dist/components/Select.js +290 -0
- package/dist/components/Select.module.css +1 -0
- package/dist/components/Tab.d.ts +3 -0
- package/dist/components/Tab.js +2 -1
- package/dist/components/TabPanel.js +2 -1
- package/dist/components/Tabs.d.ts +18 -1
- package/dist/components/Tabs.js +8 -6
- package/dist/components/Tag.d.ts +10 -9
- package/dist/components/Tag.js +2 -2
- package/dist/components/Text.d.ts +6 -5
- package/dist/components/Title.d.ts +4 -2
- package/dist/components/Tooltip.d.ts +4 -1
- package/dist/components/Tooltip.js +5 -0
- package/dist/elements/a-button.css +1 -1
- package/dist/elements/a-calendar.css +1 -0
- package/dist/elements/a-calendar.d.ts +76 -0
- package/dist/elements/a-calendar.js +190 -0
- package/dist/elements/a-checkbox.css +1 -1
- package/dist/elements/a-checkbox.d.ts +1 -2
- package/dist/elements/a-checkbox.js +5 -5
- package/dist/elements/a-expander.css +1 -1
- package/dist/elements/a-expander.d.ts +16 -5
- package/dist/elements/a-expander.js +59 -8
- package/dist/elements/a-icon.shapes.css +1 -1
- package/dist/elements/a-icon.shapes.d.ts +6 -1
- package/dist/elements/a-icon.shapes.js +9 -0
- package/dist/elements/a-input.css +1 -1
- package/dist/elements/a-input.d.ts +6 -0
- package/dist/elements/a-input.js +27 -8
- package/dist/elements/a-menu-group.css +1 -1
- package/dist/elements/a-menu-item.css +1 -1
- package/dist/elements/a-menu-item.d.ts +14 -4
- package/dist/elements/a-menu-item.js +17 -0
- package/dist/elements/a-menu-separator.css +1 -1
- package/dist/elements/a-menu.css +1 -1
- package/dist/elements/a-menu.d.ts +51 -8
- package/dist/elements/a-menu.js +285 -42
- package/dist/elements/a-progress.css +1 -1
- package/dist/elements/a-radio-group.d.ts +1 -3
- package/dist/elements/a-radio-group.js +13 -13
- package/dist/elements/a-radio.css +1 -1
- package/dist/elements/a-radio.d.ts +3 -11
- package/dist/elements/a-radio.js +3 -34
- package/dist/elements/a-tab.css +1 -1
- package/dist/elements/a-tab.d.ts +3 -11
- package/dist/elements/a-tab.js +3 -34
- package/dist/elements/a-tabs.css +1 -1
- package/dist/elements/a-tabs.d.ts +1 -4
- package/dist/elements/a-tabs.js +14 -14
- package/dist/elements/a-tag.css +1 -1
- package/dist/elements/a-text.d.ts +14 -13
- package/dist/elements/a-text.js +53 -23
- package/dist/elements/a-tooltip.css +1 -1
- package/dist/elements/a-tooltip.d.ts +1 -11
- package/dist/elements/a-tooltip.js +20 -19
- package/dist/elements/index.d.ts +1 -0
- package/dist/elements/index.js +3 -0
- package/dist/general_types.d.ts +127 -31
- package/dist/index.d.ts +9 -1
- package/dist/index.js +37 -1
- package/dist/jsx-runtime.d.ts +16 -7
- package/dist/jsx-runtime.js +6 -0
- package/dist/reset.css +1 -1
- package/package.json +4 -2
package/dist/elements/a-menu.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { HTMLElementBase } from "../anta_helpers";
|
|
1
|
+
import { HTMLElementBase, anchorRect, setMenuPresence } from "../anta_helpers";
|
|
2
2
|
import { AMenuItemElement } from "./a-menu-item";
|
|
3
3
|
import "./a-menu.css";
|
|
4
4
|
const MARGIN = 4;
|
|
@@ -42,11 +42,13 @@ function trackPosition(el, onEscape) {
|
|
|
42
42
|
const vw = doc.documentElement.clientWidth;
|
|
43
43
|
const vh = doc.documentElement.clientHeight;
|
|
44
44
|
const rootMargin = `${-rect.top}px ${-(vw - rect.right)}px ${-(vh - rect.bottom)}px ${-rect.left}px`;
|
|
45
|
+
const win = doc.defaultView;
|
|
46
|
+
const root = win && win !== win.top ? doc.documentElement : null;
|
|
45
47
|
const io = new IntersectionObserver(
|
|
46
48
|
([entry]) => {
|
|
47
49
|
if (!entry.isIntersecting) onEscape();
|
|
48
50
|
},
|
|
49
|
-
{ root
|
|
51
|
+
{ root, rootMargin, threshold: ANCHOR_VISIBLE_RATIO }
|
|
50
52
|
);
|
|
51
53
|
io.observe(el);
|
|
52
54
|
return () => io.disconnect();
|
|
@@ -62,6 +64,18 @@ function pathHitsMenus(e, primaryClick = false) {
|
|
|
62
64
|
}
|
|
63
65
|
return false;
|
|
64
66
|
}
|
|
67
|
+
function nodeHitsMenus(node) {
|
|
68
|
+
for (const m of openStack) {
|
|
69
|
+
if (m.contains(node) || m.surface.contains(node)) return true;
|
|
70
|
+
const anchor = m.triggerAnchor;
|
|
71
|
+
if (anchor && anchor.contains(node)) return true;
|
|
72
|
+
}
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
setMenuPresence({
|
|
76
|
+
isOpen: () => openStack.length > 0,
|
|
77
|
+
contains: nodeHitsMenus
|
|
78
|
+
});
|
|
65
79
|
function onDocPointerDown(e) {
|
|
66
80
|
if (!openStack.length) return;
|
|
67
81
|
if (!pathHitsMenus(e, e.button === 0)) dismiss(e);
|
|
@@ -111,6 +125,8 @@ class AMenuElement extends HTMLElementBase {
|
|
|
111
125
|
static observedAttributes = ["placement", "context", "coord", "offset", "nohover", "state"];
|
|
112
126
|
/** Shadow-internal popover surface — the only thing we ever mutate. */
|
|
113
127
|
surface;
|
|
128
|
+
/** The scrolling body inside the surface (holds the items). */
|
|
129
|
+
scrollEl;
|
|
114
130
|
listening = false;
|
|
115
131
|
_shown = false;
|
|
116
132
|
teardown;
|
|
@@ -124,6 +140,15 @@ class AMenuElement extends HTMLElementBase {
|
|
|
124
140
|
// Typeahead state (root navigation).
|
|
125
141
|
typeBuffer = "";
|
|
126
142
|
typeTimer;
|
|
143
|
+
// Combobox (filter) state — engaged when a `[data-menu-search]` field is slotted
|
|
144
|
+
// in (e.g. `Select` with `filter`). Focus stays in that field; ArrowUp/Down move
|
|
145
|
+
// `activeItem` (a cursor, not DOM focus) and reflect `aria-activedescendant`.
|
|
146
|
+
activeItem = null;
|
|
147
|
+
comboObserver;
|
|
148
|
+
// The vertical side chosen at open (true = flipped above the anchor). A re-anchor
|
|
149
|
+
// (filtering changes height) keeps this side rather than re-deciding — a shrunk
|
|
150
|
+
// menu shouldn't hop back under the trigger.
|
|
151
|
+
_flippedTop = null;
|
|
127
152
|
constructor() {
|
|
128
153
|
super();
|
|
129
154
|
const shadow = this.attachShadow({ mode: "open" });
|
|
@@ -136,16 +161,13 @@ class AMenuElement extends HTMLElementBase {
|
|
|
136
161
|
left: 0;
|
|
137
162
|
top: 0;
|
|
138
163
|
margin: 0;
|
|
164
|
+
padding: 0;
|
|
139
165
|
box-sizing: border-box;
|
|
140
166
|
flex-direction: column;
|
|
141
|
-
|
|
142
|
-
min-width: var(--menu-min-width, 88px);
|
|
167
|
+
min-width: max(var(--menu-min-width, 88px), var(--_anchor-width, 0px));
|
|
143
168
|
max-width: calc(100vw - ${2 * MARGIN}px);
|
|
144
169
|
max-height: calc(100dvh - ${2 * MARGIN}px);
|
|
145
|
-
overflow
|
|
146
|
-
overscroll-behavior: contain;
|
|
147
|
-
scrollbar-width: thin;
|
|
148
|
-
padding: var(--menu-padding, 4px);
|
|
170
|
+
overflow: hidden;
|
|
149
171
|
background: var(--menu-bg, Canvas);
|
|
150
172
|
color: var(--text-2, CanvasText);
|
|
151
173
|
border: var(--menu-border, 1px solid);
|
|
@@ -184,19 +206,58 @@ class AMenuElement extends HTMLElementBase {
|
|
|
184
206
|
@starting-style {
|
|
185
207
|
.container:popover-open { opacity: 0; translate: 0 -4px; }
|
|
186
208
|
}
|
|
209
|
+
/* The scrolling body \u2014 only the items scroll; the header / footer slots stay
|
|
210
|
+
fixed above and below it. min-height:0 lets it shrink so its own scrollbar
|
|
211
|
+
engages instead of the flex item overflowing. The 1px inter-item gap that
|
|
212
|
+
used to live on the container lives here now. */
|
|
213
|
+
.scroll {
|
|
214
|
+
flex: 1 1 auto;
|
|
215
|
+
min-height: 0;
|
|
216
|
+
display: flex;
|
|
217
|
+
flex-direction: column;
|
|
218
|
+
gap: 1px;
|
|
219
|
+
overflow-y: auto;
|
|
220
|
+
overscroll-behavior: contain;
|
|
221
|
+
scrollbar-width: thin;
|
|
222
|
+
/* The menu's padding lives here (not on the clipping container) so an item's
|
|
223
|
+
focus outline has room inside the scroll box instead of being clipped at
|
|
224
|
+
the edge; scroll-padding keeps a focused row off the edge when scrolled. */
|
|
225
|
+
padding: var(--menu-padding, 4px);
|
|
226
|
+
scroll-padding-block: var(--menu-padding, 4px);
|
|
227
|
+
/* Soft scroll edges: fade the content into the top / bottom only on the side
|
|
228
|
+
with more to scroll (the --fade-* vars, set from JS, are 0 otherwise). The
|
|
229
|
+
top also clears a hard --gap-top band (fully transparent, ahead of the ramp)
|
|
230
|
+
so a row scrolling up disappears cleanly under a pinned header / filter field
|
|
231
|
+
before the fade begins, instead of bleeding right against it. */
|
|
232
|
+
--fade-top: 0px;
|
|
233
|
+
--fade-bottom: 0px;
|
|
234
|
+
--gap-top: 0px;
|
|
235
|
+
-webkit-mask-image: linear-gradient(to bottom, transparent 0, transparent var(--gap-top), #000 var(--fade-top), #000 calc(100% - var(--fade-bottom)), transparent 100%);
|
|
236
|
+
mask-image: linear-gradient(to bottom, transparent 0, transparent var(--gap-top), #000 var(--fade-top), #000 calc(100% - var(--fade-bottom)), transparent 100%);
|
|
237
|
+
}
|
|
187
238
|
`;
|
|
188
239
|
this.surface = document.createElement("div");
|
|
189
240
|
this.surface.className = "container";
|
|
190
241
|
this.surface.setAttribute("part", "menu");
|
|
191
242
|
this.surface.setAttribute("popover", "manual");
|
|
192
|
-
|
|
243
|
+
const header = document.createElement("slot");
|
|
244
|
+
header.setAttribute("name", "header");
|
|
245
|
+
const scroll = document.createElement("div");
|
|
246
|
+
scroll.className = "scroll";
|
|
247
|
+
scroll.setAttribute("part", "scroll");
|
|
248
|
+
scroll.append(document.createElement("slot"));
|
|
249
|
+
scroll.addEventListener("scroll", () => this.updateScrollFade(), { passive: true });
|
|
250
|
+
this.scrollEl = scroll;
|
|
251
|
+
const footer = document.createElement("slot");
|
|
252
|
+
footer.setAttribute("name", "footer");
|
|
253
|
+
this.surface.append(header, scroll, footer);
|
|
193
254
|
this.surface.addEventListener("pointerenter", (e) => {
|
|
194
255
|
if (e.pointerType !== "mouse") return;
|
|
195
256
|
if (this.isSubmenu) this.cancelCloseTimer();
|
|
196
257
|
});
|
|
197
258
|
this.surface.addEventListener("pointerleave", (e) => {
|
|
198
259
|
if (e.pointerType !== "mouse") return;
|
|
199
|
-
if (this.isSubmenu && this
|
|
260
|
+
if (this.isSubmenu && this.#isHover) this.scheduleClose();
|
|
200
261
|
});
|
|
201
262
|
this.surface.addEventListener("click", this.onSurfaceClick);
|
|
202
263
|
shadow.append(style, this.surface);
|
|
@@ -248,22 +309,22 @@ class AMenuElement extends HTMLElementBase {
|
|
|
248
309
|
get isSubmenu() {
|
|
249
310
|
return !!this.closest("a-menu-item");
|
|
250
311
|
}
|
|
251
|
-
get isContext() {
|
|
312
|
+
get #isContext() {
|
|
252
313
|
return this.hasAttribute("context");
|
|
253
314
|
}
|
|
254
|
-
get isCoord() {
|
|
315
|
+
get #isCoord() {
|
|
255
316
|
return this.hasAttribute("coord");
|
|
256
317
|
}
|
|
257
318
|
// Submenus open on hover by default; `nohover` opts out (click-only). Root
|
|
258
319
|
// menus never consult this — it's read only on the submenu paths.
|
|
259
|
-
get isHover() {
|
|
320
|
+
get #isHover() {
|
|
260
321
|
return !this.hasAttribute("nohover");
|
|
261
322
|
}
|
|
262
|
-
get offset() {
|
|
323
|
+
get #offset() {
|
|
263
324
|
const n = parseInt(this.getAttribute("offset") ?? "", 10);
|
|
264
325
|
return Number.isFinite(n) ? n : MARGIN;
|
|
265
326
|
}
|
|
266
|
-
get placement() {
|
|
327
|
+
get #placement() {
|
|
267
328
|
const p = this.getAttribute("placement");
|
|
268
329
|
if (p === "bottom-end" || p === "top-start" || p === "top-end" || p === "bottom" || p === "top")
|
|
269
330
|
return p;
|
|
@@ -274,8 +335,32 @@ class AMenuElement extends HTMLElementBase {
|
|
|
274
335
|
get triggerAnchor() {
|
|
275
336
|
return this.closest("a-menu-item") ?? this.previousElementSibling;
|
|
276
337
|
}
|
|
338
|
+
/** The focusable element to hand focus back to — the anchor itself if it's
|
|
339
|
+
* focusable, else the first focusable within it. Positioning still uses the
|
|
340
|
+
* anchor's box; this only picks the focus target. A custom `renderTrigger`
|
|
341
|
+
* that returns a single focusable element (the documented contract) resolves
|
|
342
|
+
* to that element; if it wrongly returns a non-focusable node with none
|
|
343
|
+
* inside, this is null and `focusTrigger` warns. */
|
|
344
|
+
get #triggerFocusable() {
|
|
345
|
+
const a = this.triggerAnchor;
|
|
346
|
+
if (!a) return null;
|
|
347
|
+
const sel = 'a-input, a-button, button, [href], input:not([type="hidden"]), select, textarea, [tabindex]:not([tabindex="-1"])';
|
|
348
|
+
return a.matches(sel) ? a : a.querySelector(sel);
|
|
349
|
+
}
|
|
350
|
+
/** Return focus to the trigger. Focuses the first focusable in the anchor; if
|
|
351
|
+
* there is none (a mis-authored trigger — e.g. `renderTrigger` returning a
|
|
352
|
+
* fragment or a non-focusable node), warns instead of silently focusing air. */
|
|
353
|
+
focusTrigger() {
|
|
354
|
+
const f = this.#triggerFocusable;
|
|
355
|
+
if (f) f.focus();
|
|
356
|
+
else if (this.triggerAnchor) {
|
|
357
|
+
console.warn(
|
|
358
|
+
"[a-menu] trigger has no focusable element \u2014 a menu trigger (or `Select` renderTrigger) must be a single focusable element."
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
277
362
|
/** For a submenu: the menu that contains its anchor item. */
|
|
278
|
-
get ownerMenu() {
|
|
363
|
+
get #ownerMenu() {
|
|
279
364
|
if (!this.isSubmenu) return null;
|
|
280
365
|
return this.triggerAnchor?.closest("a-menu") ?? null;
|
|
281
366
|
}
|
|
@@ -308,13 +393,120 @@ class AMenuElement extends HTMLElementBase {
|
|
|
308
393
|
* used to trap Tab within the open menu. Submenu contents are excluded
|
|
309
394
|
* (their nearest `a-menu` is the submenu). */
|
|
310
395
|
focusables() {
|
|
311
|
-
const sel = 'a-menu-item, a[href], button:not([disabled]), input:not([disabled]):not([type="hidden"]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
396
|
+
const sel = 'a-menu-item, a[href], button:not([disabled]), input:not([disabled]):not([type="hidden"]), a-input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"]), [data-menu-search]';
|
|
312
397
|
return Array.from(this.querySelectorAll(sel)).filter(
|
|
313
398
|
(el) => el.closest("a-menu") === this && !el.hasAttribute("disabled") && this.isVisible(el)
|
|
314
399
|
);
|
|
315
400
|
}
|
|
316
|
-
|
|
317
|
-
|
|
401
|
+
/** On open, seat initial focus like a native `<select>` / macOS menu: a menu
|
|
402
|
+
* carrying a current value opens *at* that value, not at the top. Brings the
|
|
403
|
+
* first selected row into view and, when opened via keyboard, focuses it;
|
|
404
|
+
* with no selected row it focuses the first item (keyboard) and does nothing
|
|
405
|
+
* otherwise. "Selected" = a `selected` row (tint) or `aria-checked` /
|
|
406
|
+
* `aria-selected="true"` (checkable rows) — the first such visible item of
|
|
407
|
+
* THIS menu, so a leaf buried in a closed submenu (not visible) is skipped and
|
|
408
|
+
* a multi-select lands on its topmost checked row. */
|
|
409
|
+
seatInitialFocus(viaKeyboard) {
|
|
410
|
+
const items = this.focusableItems();
|
|
411
|
+
const selected = items.find(
|
|
412
|
+
(el) => el.hasAttribute("selected") || el.getAttribute("aria-checked") === "true" || el.getAttribute("aria-selected") === "true"
|
|
413
|
+
);
|
|
414
|
+
if (selected) this.scrollItemIntoView(selected);
|
|
415
|
+
if (viaKeyboard) (selected ?? items[0])?.focus({ preventScroll: true });
|
|
416
|
+
}
|
|
417
|
+
/** Scroll THIS menu's body so `item` sits inside the visible scroll viewport,
|
|
418
|
+
* touching only the internal `.scroll` container — never the document, whose
|
|
419
|
+
* scroll would trip the anchor-scrolled-out dismiss. No-op for a menu short
|
|
420
|
+
* enough not to scroll. */
|
|
421
|
+
scrollItemIntoView(item) {
|
|
422
|
+
const c = this.scrollEl;
|
|
423
|
+
if (!c) return;
|
|
424
|
+
const pad = 4;
|
|
425
|
+
const cr = c.getBoundingClientRect();
|
|
426
|
+
const ir = item.getBoundingClientRect();
|
|
427
|
+
if (ir.top < cr.top + pad) c.scrollTop -= cr.top + pad - ir.top;
|
|
428
|
+
else if (ir.bottom > cr.bottom - pad) c.scrollTop += ir.bottom - cr.bottom + pad;
|
|
429
|
+
}
|
|
430
|
+
/** Fade the scrolling body's content into the top / bottom edges — but only on the
|
|
431
|
+
* side that actually has more to scroll, so a short (non-scrolling) menu and the
|
|
432
|
+
* true top / bottom stay crisp. Drives the `--fade-*` / `--gap-top` vars the
|
|
433
|
+
* `.scroll` mask reads; runs on scroll and after every (re)position.
|
|
434
|
+
* Shadow-internal only. */
|
|
435
|
+
updateScrollFade() {
|
|
436
|
+
const el = this.scrollEl;
|
|
437
|
+
if (!el) return;
|
|
438
|
+
const top = el.scrollTop > 1;
|
|
439
|
+
const bottom = el.scrollTop + el.clientHeight < el.scrollHeight - 1;
|
|
440
|
+
el.style.setProperty("--fade-top", top ? "16px" : "0px");
|
|
441
|
+
el.style.setProperty("--gap-top", top ? "3px" : "0px");
|
|
442
|
+
el.style.setProperty("--fade-bottom", bottom ? "14px" : "0px");
|
|
443
|
+
}
|
|
444
|
+
/* ------------------------------ combobox mode ------------------------------ */
|
|
445
|
+
/** The filter field slotted into THIS menu (never a submenu's), or null. Its
|
|
446
|
+
* presence switches the menu to the combobox keyboard. */
|
|
447
|
+
get #searchField() {
|
|
448
|
+
const el = this.querySelector("[data-menu-search]");
|
|
449
|
+
return el && el.closest("a-menu") === this ? el : null;
|
|
450
|
+
}
|
|
451
|
+
/** Move the combobox cursor. Sets the item's `active` **property** (off-DOM
|
|
452
|
+
* `:state(active)`, no attribute churn) and reflects `aria-activedescendant`
|
|
453
|
+
* onto the search field — the same ARIA-state-reflection latitude as
|
|
454
|
+
* `aria-expanded` on the anchor. `null` clears the cursor. */
|
|
455
|
+
setActive(item) {
|
|
456
|
+
if (this.activeItem && this.activeItem !== item) this.activeItem.active = false;
|
|
457
|
+
this.activeItem = item;
|
|
458
|
+
if (item) {
|
|
459
|
+
item.active = true;
|
|
460
|
+
item.scrollIntoView?.({ block: "nearest" });
|
|
461
|
+
}
|
|
462
|
+
const s = this.#searchField;
|
|
463
|
+
if (!s) return;
|
|
464
|
+
if (item?.id) s.setAttribute("aria-activedescendant", item.id);
|
|
465
|
+
else s.removeAttribute("aria-activedescendant");
|
|
466
|
+
}
|
|
467
|
+
/** Re-seat the cursor on the first option — but only once the filter has input.
|
|
468
|
+
* An empty filter (e.g. right after opening) shows NO active row, so the first
|
|
469
|
+
* ArrowDown is what steps onto the list; typing then keeps the top match active.
|
|
470
|
+
* Rows marked `data-menu-skip-active` (e.g. a Select-all action) are skipped as
|
|
471
|
+
* the seat target — the cursor lands on the first real option — but they stay
|
|
472
|
+
* arrow-reachable. */
|
|
473
|
+
resetActive() {
|
|
474
|
+
const q = this.#searchField?.value;
|
|
475
|
+
if (!q || !q.trim()) return this.setActive(null);
|
|
476
|
+
const items = this.focusableItems();
|
|
477
|
+
this.setActive(items.find((it) => !it.hasAttribute("data-menu-skip-active")) ?? items[0] ?? null);
|
|
478
|
+
}
|
|
479
|
+
/** Combobox arrow / Home / End / Enter handling; returns true if it consumed the
|
|
480
|
+
* key (so `handleKey` stops). Any other key falls through to the input (typing). */
|
|
481
|
+
handleComboKey(e) {
|
|
482
|
+
const items = this.focusableItems();
|
|
483
|
+
const idx = this.activeItem ? items.indexOf(this.activeItem) : -1;
|
|
484
|
+
switch (e.key) {
|
|
485
|
+
case "ArrowDown":
|
|
486
|
+
e.preventDefault();
|
|
487
|
+
this.setActive(items[idx < 0 ? 0 : (idx + 1) % items.length] ?? null);
|
|
488
|
+
return true;
|
|
489
|
+
case "ArrowUp":
|
|
490
|
+
e.preventDefault();
|
|
491
|
+
this.setActive(items[idx <= 0 ? items.length - 1 : idx - 1] ?? null);
|
|
492
|
+
return true;
|
|
493
|
+
case "Home":
|
|
494
|
+
e.preventDefault();
|
|
495
|
+
this.setActive(items[0] ?? null);
|
|
496
|
+
return true;
|
|
497
|
+
case "End":
|
|
498
|
+
e.preventDefault();
|
|
499
|
+
this.setActive(items[items.length - 1] ?? null);
|
|
500
|
+
return true;
|
|
501
|
+
case "Enter":
|
|
502
|
+
if (this.activeItem) {
|
|
503
|
+
e.preventDefault();
|
|
504
|
+
this.activeItem.click();
|
|
505
|
+
}
|
|
506
|
+
return true;
|
|
507
|
+
default:
|
|
508
|
+
return false;
|
|
509
|
+
}
|
|
318
510
|
}
|
|
319
511
|
/* ============================ open / close ============================ */
|
|
320
512
|
/** Public imperative API. Routes through the same intent path as the
|
|
@@ -372,7 +564,7 @@ class AMenuElement extends HTMLElementBase {
|
|
|
372
564
|
* controlled `state` sync. */
|
|
373
565
|
show(coord, viaKeyboard = false, _originEvent) {
|
|
374
566
|
if (this.isSubmenu) {
|
|
375
|
-
const parent = this
|
|
567
|
+
const parent = this.#ownerMenu;
|
|
376
568
|
if (parent) {
|
|
377
569
|
const pidx = openStack.indexOf(parent);
|
|
378
570
|
if (pidx !== -1) {
|
|
@@ -381,7 +573,19 @@ class AMenuElement extends HTMLElementBase {
|
|
|
381
573
|
}
|
|
382
574
|
}
|
|
383
575
|
} else if (!openStack.includes(this)) {
|
|
384
|
-
|
|
576
|
+
const anchor = this.triggerAnchor;
|
|
577
|
+
const container = anchor ? openStack.find((m) => m !== this && m.contains(anchor)) : void 0;
|
|
578
|
+
if (container) {
|
|
579
|
+
const idx = openStack.indexOf(container);
|
|
580
|
+
for (let i = openStack.length - 1; i > idx; i--) {
|
|
581
|
+
const m = openStack[i];
|
|
582
|
+
if (m.isOpen && !m._dismissNotified) m.emitChange("closed");
|
|
583
|
+
m._doHide();
|
|
584
|
+
}
|
|
585
|
+
openStack.length = idx + 1;
|
|
586
|
+
} else {
|
|
587
|
+
closeAll();
|
|
588
|
+
}
|
|
385
589
|
}
|
|
386
590
|
if (openStack.includes(this)) {
|
|
387
591
|
this.position(coord);
|
|
@@ -394,7 +598,29 @@ class AMenuElement extends HTMLElementBase {
|
|
|
394
598
|
bindDocListeners(this.doc, this.view);
|
|
395
599
|
this.armPositionTracker();
|
|
396
600
|
}
|
|
397
|
-
|
|
601
|
+
const search = this.#searchField;
|
|
602
|
+
if (search) {
|
|
603
|
+
;
|
|
604
|
+
search.focus({ preventScroll: true });
|
|
605
|
+
this.resetActive();
|
|
606
|
+
this.startComboObserver();
|
|
607
|
+
} else {
|
|
608
|
+
this.seatInitialFocus(viaKeyboard);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
/** While a filter field is open, the visible option list changes as the user
|
|
612
|
+
* types (the consumer re-renders the matches); re-seat the cursor on the first
|
|
613
|
+
* match. `childList` only — an item toggling its own `selected` (multi-select)
|
|
614
|
+
* mutates deep in its subtree, not this menu's direct children, so it won't
|
|
615
|
+
* spuriously reset the cursor. */
|
|
616
|
+
startComboObserver() {
|
|
617
|
+
this.comboObserver?.disconnect();
|
|
618
|
+
this.comboObserver = new this.view.MutationObserver(() => {
|
|
619
|
+
if (!this._shown || !this.#searchField) return;
|
|
620
|
+
this.resetActive();
|
|
621
|
+
this.position(void 0, false, true);
|
|
622
|
+
});
|
|
623
|
+
this.comboObserver.observe(this, { childList: true });
|
|
398
624
|
}
|
|
399
625
|
/** Watch the root trigger and dismiss the system once it scrolls out of the
|
|
400
626
|
* spot it held at open (see trackPosition). Deferred a frame so the trigger's
|
|
@@ -413,6 +639,10 @@ class AMenuElement extends HTMLElementBase {
|
|
|
413
639
|
/** Apply CLOSE to the DOM (no event). Closes this menu and everything stacked
|
|
414
640
|
* above it (its submenus). */
|
|
415
641
|
hide() {
|
|
642
|
+
this.comboObserver?.disconnect();
|
|
643
|
+
this.comboObserver = void 0;
|
|
644
|
+
this.setActive(null);
|
|
645
|
+
this._flippedTop = null;
|
|
416
646
|
const idx = openStack.indexOf(this);
|
|
417
647
|
if (idx === -1) {
|
|
418
648
|
if (this._shown) this._doHide();
|
|
@@ -467,7 +697,7 @@ class AMenuElement extends HTMLElementBase {
|
|
|
467
697
|
this.triggerAnchor?.setAttribute("aria-expanded", open ? "true" : "false");
|
|
468
698
|
}
|
|
469
699
|
/* ============================ positioning ============================ */
|
|
470
|
-
position(coord, sync = false) {
|
|
700
|
+
position(coord, sync = false, reanchor = false) {
|
|
471
701
|
const run = () => {
|
|
472
702
|
if (!this._shown) return;
|
|
473
703
|
const view = this.view;
|
|
@@ -486,30 +716,38 @@ class AMenuElement extends HTMLElementBase {
|
|
|
486
716
|
if (top + box.height > vh - MARGIN) top = top - box.height;
|
|
487
717
|
top = Math.max(MARGIN, top);
|
|
488
718
|
} else if (this.isSubmenu) {
|
|
489
|
-
const it = this.triggerAnchor
|
|
719
|
+
const it = this.triggerAnchor ? anchorRect(this.triggerAnchor) : null;
|
|
490
720
|
if (!it) return;
|
|
491
721
|
surface.style.maxHeight = `${Math.max(MIN_HEIGHT, vh - 2 * MARGIN)}px`;
|
|
492
722
|
const box = surface.getBoundingClientRect();
|
|
493
|
-
left = it.right + this
|
|
723
|
+
left = it.right + this.#offset;
|
|
494
724
|
if (left + box.width > vw - MARGIN) {
|
|
495
|
-
left = it.left - box.width - this
|
|
725
|
+
left = it.left - box.width - this.#offset;
|
|
496
726
|
}
|
|
497
727
|
left = Math.max(MARGIN, left);
|
|
498
728
|
const cs = view.getComputedStyle(surface);
|
|
499
|
-
const
|
|
729
|
+
const scs = view.getComputedStyle(this.scrollEl);
|
|
730
|
+
const insetTop = parseFloat(cs.borderTopWidth) + parseFloat(cs.paddingTop) + parseFloat(scs.paddingTop);
|
|
500
731
|
top = it.top - insetTop;
|
|
501
732
|
if (top + box.height > vh - MARGIN) top = vh - box.height - MARGIN;
|
|
502
733
|
top = Math.max(MARGIN, top);
|
|
503
734
|
} else {
|
|
504
|
-
const a = this.triggerAnchor
|
|
735
|
+
const a = this.triggerAnchor ? anchorRect(this.triggerAnchor) : null;
|
|
505
736
|
if (!a) return;
|
|
506
|
-
|
|
737
|
+
surface.style.setProperty("--_anchor-width", `${Math.ceil(a.width)}px`);
|
|
738
|
+
const p = this.#placement;
|
|
507
739
|
const spaceBelow = vh - a.bottom - 2 * MARGIN;
|
|
508
740
|
const spaceAbove = a.top - 2 * MARGIN;
|
|
509
741
|
let onTop = p.startsWith("top");
|
|
742
|
+
surface.style.maxHeight = "";
|
|
510
743
|
const natural = surface.scrollHeight;
|
|
511
|
-
if (
|
|
512
|
-
|
|
744
|
+
if (reanchor && this._flippedTop !== null) {
|
|
745
|
+
onTop = this._flippedTop;
|
|
746
|
+
} else {
|
|
747
|
+
if (onTop && spaceAbove < natural && spaceBelow > spaceAbove) onTop = false;
|
|
748
|
+
else if (!onTop && spaceBelow < natural && spaceAbove > spaceBelow) onTop = true;
|
|
749
|
+
}
|
|
750
|
+
this._flippedTop = onTop;
|
|
513
751
|
const space = onTop ? spaceAbove : spaceBelow;
|
|
514
752
|
surface.style.maxHeight = `${Math.max(MIN_HEIGHT, Math.floor(space))}px`;
|
|
515
753
|
const box = surface.getBoundingClientRect();
|
|
@@ -517,10 +755,11 @@ class AMenuElement extends HTMLElementBase {
|
|
|
517
755
|
left = align === "center" ? a.left + a.width / 2 - box.width / 2 : align === "end" ? a.right - box.width : a.left;
|
|
518
756
|
if (left + box.width > vw - MARGIN) left = vw - box.width - MARGIN;
|
|
519
757
|
left = Math.max(MARGIN, left);
|
|
520
|
-
top = onTop ? a.top - box.height - this
|
|
758
|
+
top = onTop ? a.top - box.height - this.#offset : a.bottom + this.#offset;
|
|
521
759
|
top = Math.max(MARGIN, top);
|
|
522
760
|
}
|
|
523
761
|
surface.style.transform = `translate(${Math.round(left)}px, ${Math.round(top)}px)`;
|
|
762
|
+
this.updateScrollFade();
|
|
524
763
|
};
|
|
525
764
|
if (sync) run();
|
|
526
765
|
else requestAnimationFrame(run);
|
|
@@ -569,9 +808,8 @@ class AMenuElement extends HTMLElementBase {
|
|
|
569
808
|
if (e.key === "Escape") {
|
|
570
809
|
e.preventDefault();
|
|
571
810
|
e.stopPropagation();
|
|
572
|
-
const anchor = this.triggerAnchor;
|
|
573
811
|
this.requestClose(e);
|
|
574
|
-
|
|
812
|
+
this.focusTrigger();
|
|
575
813
|
return;
|
|
576
814
|
}
|
|
577
815
|
if (e.key === "Tab") {
|
|
@@ -587,6 +825,11 @@ class AMenuElement extends HTMLElementBase {
|
|
|
587
825
|
f[next]?.focus();
|
|
588
826
|
return;
|
|
589
827
|
}
|
|
828
|
+
const search = this.#searchField;
|
|
829
|
+
if (search && (active === search || search.contains(active))) {
|
|
830
|
+
this.handleComboKey(e);
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
590
833
|
const within = active?.closest("a-menu") === this;
|
|
591
834
|
if (within && !(active instanceof AMenuItemElement)) return;
|
|
592
835
|
const items = this.focusableItems();
|
|
@@ -594,19 +837,19 @@ class AMenuElement extends HTMLElementBase {
|
|
|
594
837
|
switch (e.key) {
|
|
595
838
|
case "ArrowDown":
|
|
596
839
|
e.preventDefault();
|
|
597
|
-
items[idx < 0 ? 0 : (idx + 1) % items.length]?.focus();
|
|
840
|
+
items[idx < 0 ? 0 : (idx + 1) % items.length]?.focus({ preventScroll: true });
|
|
598
841
|
break;
|
|
599
842
|
case "ArrowUp":
|
|
600
843
|
e.preventDefault();
|
|
601
|
-
items[idx <= 0 ? items.length - 1 : idx - 1]?.focus();
|
|
844
|
+
items[idx <= 0 ? items.length - 1 : idx - 1]?.focus({ preventScroll: true });
|
|
602
845
|
break;
|
|
603
846
|
case "Home":
|
|
604
847
|
e.preventDefault();
|
|
605
|
-
items[0]?.focus();
|
|
848
|
+
items[0]?.focus({ preventScroll: true });
|
|
606
849
|
break;
|
|
607
850
|
case "End":
|
|
608
851
|
e.preventDefault();
|
|
609
|
-
items[items.length - 1]?.focus();
|
|
852
|
+
items[items.length - 1]?.focus({ preventScroll: true });
|
|
610
853
|
break;
|
|
611
854
|
case "ArrowRight": {
|
|
612
855
|
const sub = this.submenuOf(active);
|
|
@@ -665,7 +908,7 @@ class AMenuElement extends HTMLElementBase {
|
|
|
665
908
|
anchor.addEventListener("click", onClick);
|
|
666
909
|
let onEnter;
|
|
667
910
|
let onLeave;
|
|
668
|
-
if (this
|
|
911
|
+
if (this.#isHover) {
|
|
669
912
|
onEnter = (e) => {
|
|
670
913
|
if (e.pointerType === "mouse") this.scheduleOpen();
|
|
671
914
|
};
|
|
@@ -681,7 +924,7 @@ class AMenuElement extends HTMLElementBase {
|
|
|
681
924
|
if (onLeave) anchor.removeEventListener("pointerleave", onLeave);
|
|
682
925
|
this.listening = false;
|
|
683
926
|
};
|
|
684
|
-
} else if (this
|
|
927
|
+
} else if (this.#isContext) {
|
|
685
928
|
const onContext = (e) => {
|
|
686
929
|
e.preventDefault();
|
|
687
930
|
this.requestOpen({ coord: [e.clientX, e.clientY], originEvent: e });
|
|
@@ -695,9 +938,9 @@ class AMenuElement extends HTMLElementBase {
|
|
|
695
938
|
const onClick = (e) => {
|
|
696
939
|
const viaKeyboard = e.detail === 0;
|
|
697
940
|
let coord;
|
|
698
|
-
if (this
|
|
941
|
+
if (this.#isCoord) {
|
|
699
942
|
if (viaKeyboard) {
|
|
700
|
-
const r = anchor
|
|
943
|
+
const r = anchorRect(anchor);
|
|
701
944
|
coord = [r.left, r.bottom];
|
|
702
945
|
} else {
|
|
703
946
|
coord = [e.clientX, e.clientY];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
@layer anta{a-progress{--progress-bg: var(--bg-4);--progress-border-color: var(--border-4);--progress-indicator-bg: var(--bg-5);--progress-indicator-edge: linear-gradient( 90deg, var(--progress-indicator-bg) 0%, var(--progress-border-color) 100% );--progress-text-color: var(--text-2);--progress-hint-color: var(--text-3);display:block;container-type:inline-size;padding:4px 8px;min-height:8px;background:var(--progress-bg);border:0px solid var(--progress-border-color);border-radius:0}a-progress[tone=brand]{--progress-bg: var(--bg-4-brand);--progress-border-color: var(--border-4-brand);--progress-indicator-bg: var(--bg-5-brand);--progress-text-color: var(--text-2-brand);--progress-hint-color: var(--text-3-brand)}a-progress[tone=info]{--progress-bg: var(--bg-4-info);--progress-border-color: var(--border-4-info);--progress-indicator-bg: var(--bg-5-info);--progress-text-color: var(--text-2-info);--progress-hint-color: var(--text-3-info)}a-progress[tone=success]{--progress-bg: var(--bg-4-success);--progress-border-color: var(--border-4-success);--progress-indicator-bg: var(--bg-5-success);--progress-text-color: var(--text-2-success);--progress-hint-color: var(--text-3-success)}a-progress[tone=warning]{--progress-bg: var(--bg-4-warning);--progress-border-color: var(--border-4-warning);--progress-indicator-bg: var(--bg-5-warning);--progress-text-color: var(--text-2-warning);--progress-hint-color: var(--text-3-warning)}a-progress[tone=critical]{--progress-bg: var(--bg-4-critical);--progress-border-color: var(--border-4-critical);--progress-indicator-bg: var(--bg-5-critical);--progress-text-color: var(--text-2-critical);--progress-hint-color: var(--text-3-critical)}a-progress[tone]:not([tone=""],[tone=neutral],[tone=brand],[tone=info],[tone=success],[tone=warning],[tone=critical]){--progress-tone-source: attr(tone type(<color>), transparent);--progress-bg: oklch(from var(--progress-tone-source) .95 calc(c*.4) h);--progress-border-color: oklch(from var(--progress-tone-source) .85 calc(c*.6) h);--progress-indicator-bg: oklch(from var(--progress-tone-source) .6 c h);--progress-text-color: oklch(from var(--progress-tone-source) .45 c h);--progress-hint-color: oklch(from var(--progress-tone-source) .55 c h);.dark &{--progress-bg: oklch(from var(--progress-tone-source) .28 calc(c/2) h);--progress-border-color: oklch(from var(--progress-tone-source) .4 calc(c*.6) h);--progress-indicator-bg: oklch(from var(--progress-tone-source) .6 c h);--progress-text-color: oklch(from var(--progress-tone-source) .85 c h);--progress-hint-color: oklch(from var(--progress-tone-source) .75 c h)}}a-progress-label{display:flex;align-items:flex-start;gap:.5ch;font-variant-numeric:tabular-nums;font-feature-settings:"ss02","ss05";letter-spacing:.03em;font-size:13px;line-height:16px}a-progress-number{flex-shrink:0;color:var(--progress-text-color)}a-progress-text{flex:1 1 auto;min-width:0;color:var(--progress-text-color)}a-progress-hint{flex-shrink:0;margin-left:auto;color:var(--progress-hint-color)}}
|
|
1
|
+
@layer anta{a-progress{--progress-bg: var(--bg-4);--progress-border-color: var(--border-4);--progress-indicator-bg: var(--bg-5);--progress-indicator-edge: linear-gradient( 90deg, var(--progress-indicator-bg) 0%, var(--progress-border-color) 100% );--progress-text-color: var(--text-2);--progress-hint-color: var(--text-3);display:block;container-type:inline-size;padding:4px 8px;min-height:8px;background:var(--progress-bg);border:0px solid var(--progress-border-color);border-radius:0}a-progress[round]{--progress-round: attr(round type(<length>), 999px);border-radius:var(--progress-round, 999px)}a-progress[tone=brand]{--progress-bg: var(--bg-4-brand);--progress-border-color: var(--border-4-brand);--progress-indicator-bg: var(--bg-5-brand);--progress-text-color: var(--text-2-brand);--progress-hint-color: var(--text-3-brand)}a-progress[tone=info]{--progress-bg: var(--bg-4-info);--progress-border-color: var(--border-4-info);--progress-indicator-bg: var(--bg-5-info);--progress-text-color: var(--text-2-info);--progress-hint-color: var(--text-3-info)}a-progress[tone=success]{--progress-bg: var(--bg-4-success);--progress-border-color: var(--border-4-success);--progress-indicator-bg: var(--bg-5-success);--progress-text-color: var(--text-2-success);--progress-hint-color: var(--text-3-success)}a-progress[tone=warning]{--progress-bg: var(--bg-4-warning);--progress-border-color: var(--border-4-warning);--progress-indicator-bg: var(--bg-5-warning);--progress-text-color: var(--text-2-warning);--progress-hint-color: var(--text-3-warning)}a-progress[tone=critical]{--progress-bg: var(--bg-4-critical);--progress-border-color: var(--border-4-critical);--progress-indicator-bg: var(--bg-5-critical);--progress-text-color: var(--text-2-critical);--progress-hint-color: var(--text-3-critical)}a-progress[tone]:not([tone=""],[tone=neutral],[tone=brand],[tone=info],[tone=success],[tone=warning],[tone=critical]){--progress-tone-source: attr(tone type(<color>), transparent);--progress-bg: oklch(from var(--progress-tone-source) .95 calc(c*.4) h);--progress-border-color: oklch(from var(--progress-tone-source) .85 calc(c*.6) h);--progress-indicator-bg: oklch(from var(--progress-tone-source) .6 c h);--progress-text-color: oklch(from var(--progress-tone-source) .45 c h);--progress-hint-color: oklch(from var(--progress-tone-source) .55 c h);.dark &{--progress-bg: oklch(from var(--progress-tone-source) .28 calc(c/2) h);--progress-border-color: oklch(from var(--progress-tone-source) .4 calc(c*.6) h);--progress-indicator-bg: oklch(from var(--progress-tone-source) .6 c h);--progress-text-color: oklch(from var(--progress-tone-source) .85 c h);--progress-hint-color: oklch(from var(--progress-tone-source) .75 c h)}}a-progress-label{display:flex;align-items:flex-start;gap:.5ch;font-variant-numeric:lining-nums tabular-nums;font-feature-settings:"ss02","ss05";letter-spacing:.03em;font-size:13px;line-height:16px}a-progress-number{flex-shrink:0;color:var(--progress-text-color)}a-progress-text{flex:1 1 auto;min-width:0;color:var(--progress-text-color)}a-progress-hint{flex-shrink:0;margin-left:auto;color:var(--progress-hint-color)}}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { HTMLElementBase } from "../anta_helpers";
|
|
2
2
|
import "./a-radio-group.css";
|
|
3
3
|
export declare class ARadioGroupElement extends HTMLElementBase {
|
|
4
|
+
#private;
|
|
4
5
|
static formAssociated: boolean;
|
|
5
6
|
static observedAttributes: string[];
|
|
6
7
|
private internals?;
|
|
@@ -17,9 +18,6 @@ export declare class ARadioGroupElement extends HTMLElementBase {
|
|
|
17
18
|
formDisabledCallback(disabled: boolean): void;
|
|
18
19
|
formResetCallback(): void;
|
|
19
20
|
formStateRestoreCallback(state: string): void;
|
|
20
|
-
private get currentValue();
|
|
21
|
-
private get isDisabled();
|
|
22
|
-
private get radios();
|
|
23
21
|
private sync;
|
|
24
22
|
private requestSelect;
|
|
25
23
|
private emitChange;
|
|
@@ -12,7 +12,7 @@ class ARadioGroupElement extends HTMLElementBase {
|
|
|
12
12
|
alive = false;
|
|
13
13
|
/** The selected option's value, or `null` when nothing is selected. */
|
|
14
14
|
get value() {
|
|
15
|
-
return this
|
|
15
|
+
return this.#currentValue;
|
|
16
16
|
}
|
|
17
17
|
constructor() {
|
|
18
18
|
super();
|
|
@@ -51,28 +51,28 @@ class ARadioGroupElement extends HTMLElementBase {
|
|
|
51
51
|
}
|
|
52
52
|
formResetCallback() {
|
|
53
53
|
const next = this.getAttribute("default-state");
|
|
54
|
-
this.emitStateChange(next, this
|
|
54
|
+
this.emitStateChange(next, this.#currentValue, "reset", false);
|
|
55
55
|
this.uncontrolledValue = next;
|
|
56
56
|
this.sync();
|
|
57
57
|
}
|
|
58
58
|
formStateRestoreCallback(state) {
|
|
59
|
-
this.emitStateChange(state, this
|
|
59
|
+
this.emitStateChange(state, this.#currentValue, "restore", false);
|
|
60
60
|
this.uncontrolledValue = state;
|
|
61
61
|
this.sync();
|
|
62
62
|
}
|
|
63
63
|
// Controlled when `state` is present; otherwise the in-memory uncontrolled value.
|
|
64
|
-
get currentValue() {
|
|
64
|
+
get #currentValue() {
|
|
65
65
|
return this.hasAttribute("state") ? this.getAttribute("state") : this.uncontrolledValue;
|
|
66
66
|
}
|
|
67
|
-
get isDisabled() {
|
|
67
|
+
get #isDisabled() {
|
|
68
68
|
return this.hasAttribute("disabled") || (this.internals?.states.has("disabled") ?? false);
|
|
69
69
|
}
|
|
70
|
-
get radios() {
|
|
70
|
+
get #radios() {
|
|
71
71
|
return Array.from(this.querySelectorAll("a-radio"));
|
|
72
72
|
}
|
|
73
73
|
sync = () => {
|
|
74
|
-
const value = this
|
|
75
|
-
const radios = this
|
|
74
|
+
const value = this.#currentValue;
|
|
75
|
+
const radios = this.#radios;
|
|
76
76
|
const selectedEl = radios.find((r) => r.value === value && value != null) ?? null;
|
|
77
77
|
const submitted = selectedEl ? selectedEl.value : null;
|
|
78
78
|
this.internals?.setFormValue(submitted, submitted);
|
|
@@ -84,7 +84,7 @@ class ARadioGroupElement extends HTMLElementBase {
|
|
|
84
84
|
// The shared state algorithm: fire the cancelable `statechange` *before*
|
|
85
85
|
// applying. Controlled never self-applies; uncontrolled applies unless vetoed.
|
|
86
86
|
requestSelect(next) {
|
|
87
|
-
const prev = this
|
|
87
|
+
const prev = this.#currentValue;
|
|
88
88
|
if (next === prev) return;
|
|
89
89
|
const ok = this.emitStateChange(next, prev, "user", true);
|
|
90
90
|
if (this.hasAttribute("state")) return;
|
|
@@ -114,7 +114,7 @@ class ARadioGroupElement extends HTMLElementBase {
|
|
|
114
114
|
);
|
|
115
115
|
}
|
|
116
116
|
onClick = (e) => {
|
|
117
|
-
if (this
|
|
117
|
+
if (this.#isDisabled) return;
|
|
118
118
|
const radio = e.target?.closest(
|
|
119
119
|
"a-radio"
|
|
120
120
|
);
|
|
@@ -123,8 +123,8 @@ class ARadioGroupElement extends HTMLElementBase {
|
|
|
123
123
|
this.requestSelect(radio.value);
|
|
124
124
|
};
|
|
125
125
|
onKeyDown = (e) => {
|
|
126
|
-
if (this
|
|
127
|
-
const enabled = this
|
|
126
|
+
if (this.#isDisabled) return;
|
|
127
|
+
const enabled = this.#radios.filter((r) => !r.hasAttribute("disabled"));
|
|
128
128
|
if (enabled.length === 0) return;
|
|
129
129
|
const focused = e.target?.closest(
|
|
130
130
|
"a-radio"
|
|
@@ -141,7 +141,7 @@ class ARadioGroupElement extends HTMLElementBase {
|
|
|
141
141
|
if (!forward && !back) return;
|
|
142
142
|
e.preventDefault();
|
|
143
143
|
let i = focused ? enabled.indexOf(focused) : -1;
|
|
144
|
-
if (i === -1) i = enabled.findIndex((r) => r.value === this
|
|
144
|
+
if (i === -1) i = enabled.findIndex((r) => r.value === this.#currentValue);
|
|
145
145
|
if (i === -1) i = 0;
|
|
146
146
|
const next = enabled[(i + (forward ? 1 : -1) + enabled.length) % enabled.length];
|
|
147
147
|
next.focus();
|