@antadesign/anta 0.3.1 → 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.
Files changed (98) hide show
  1. package/dist/anta_helpers.d.ts +111 -0
  2. package/dist/anta_helpers.js +90 -1
  3. package/dist/calendar-core.d.ts +126 -0
  4. package/dist/calendar-core.js +289 -0
  5. package/dist/components/Button.d.ts +9 -1
  6. package/dist/components/Button.js +12 -18
  7. package/dist/components/Calendar.d.ts +80 -0
  8. package/dist/components/Calendar.js +220 -0
  9. package/dist/components/Checkbox.d.ts +21 -15
  10. package/dist/components/Checkbox.js +17 -7
  11. package/dist/components/Expander.d.ts +7 -1
  12. package/dist/components/Expander.js +4 -2
  13. package/dist/components/Input.d.ts +16 -8
  14. package/dist/components/Input.js +19 -11
  15. package/dist/components/InputDate.d.ts +87 -0
  16. package/dist/components/InputDate.js +278 -0
  17. package/dist/components/InputDate.module.css +1 -0
  18. package/dist/components/Menu.d.ts +5 -1
  19. package/dist/components/Menu.js +6 -2
  20. package/dist/components/MenuItem.d.ts +48 -7
  21. package/dist/components/MenuItem.js +71 -7
  22. package/dist/components/MenuSeparator.d.ts +13 -2
  23. package/dist/components/MenuSeparator.js +12 -2
  24. package/dist/components/Progress.d.ts +4 -1
  25. package/dist/components/Progress.js +4 -3
  26. package/dist/components/RadioGroup.d.ts +17 -15
  27. package/dist/components/RadioGroup.js +8 -8
  28. package/dist/components/Select.d.ts +255 -0
  29. package/dist/components/Select.js +290 -0
  30. package/dist/components/Select.module.css +1 -0
  31. package/dist/components/Tab.d.ts +37 -0
  32. package/dist/components/Tab.js +5 -0
  33. package/dist/components/TabPanel.d.ts +22 -0
  34. package/dist/components/TabPanel.js +5 -0
  35. package/dist/components/Tabs.d.ts +129 -0
  36. package/dist/components/Tabs.js +176 -0
  37. package/dist/components/Tabs.module.css +1 -0
  38. package/dist/components/Tag.d.ts +10 -9
  39. package/dist/components/Tag.js +2 -2
  40. package/dist/components/Text.d.ts +6 -5
  41. package/dist/components/Title.d.ts +4 -2
  42. package/dist/components/Tooltip.d.ts +13 -1
  43. package/dist/components/Tooltip.js +9 -0
  44. package/dist/elements/a-button.css +1 -1
  45. package/dist/elements/a-calendar.css +1 -0
  46. package/dist/elements/a-calendar.d.ts +76 -0
  47. package/dist/elements/a-calendar.js +190 -0
  48. package/dist/elements/a-checkbox.css +1 -1
  49. package/dist/elements/a-checkbox.d.ts +1 -2
  50. package/dist/elements/a-checkbox.js +5 -5
  51. package/dist/elements/a-expander.css +1 -1
  52. package/dist/elements/a-expander.d.ts +16 -5
  53. package/dist/elements/a-expander.js +65 -9
  54. package/dist/elements/a-icon.shapes.css +1 -1
  55. package/dist/elements/a-icon.shapes.d.ts +6 -1
  56. package/dist/elements/a-icon.shapes.js +9 -0
  57. package/dist/elements/a-input.css +1 -1
  58. package/dist/elements/a-input.d.ts +6 -0
  59. package/dist/elements/a-input.js +28 -9
  60. package/dist/elements/a-menu-group.css +1 -1
  61. package/dist/elements/a-menu-item.css +1 -1
  62. package/dist/elements/a-menu-item.d.ts +14 -4
  63. package/dist/elements/a-menu-item.js +17 -0
  64. package/dist/elements/a-menu-separator.css +1 -1
  65. package/dist/elements/a-menu.css +1 -1
  66. package/dist/elements/a-menu.d.ts +51 -8
  67. package/dist/elements/a-menu.js +285 -42
  68. package/dist/elements/a-progress.css +1 -1
  69. package/dist/elements/a-radio-group.d.ts +1 -3
  70. package/dist/elements/a-radio-group.js +13 -13
  71. package/dist/elements/a-radio.css +1 -1
  72. package/dist/elements/a-radio.d.ts +3 -11
  73. package/dist/elements/a-radio.js +3 -34
  74. package/dist/elements/a-tab.css +1 -0
  75. package/dist/elements/a-tab.d.ts +6 -0
  76. package/dist/elements/a-tab.js +15 -0
  77. package/dist/elements/a-tabpanel.css +1 -0
  78. package/dist/elements/a-tabs.css +1 -0
  79. package/dist/elements/a-tabs.d.ts +28 -0
  80. package/dist/elements/a-tabs.js +157 -0
  81. package/dist/elements/a-tag.css +1 -1
  82. package/dist/elements/a-text.css +1 -1
  83. package/dist/elements/a-text.d.ts +14 -13
  84. package/dist/elements/a-text.js +53 -23
  85. package/dist/elements/a-title.css +1 -1
  86. package/dist/elements/a-tooltip.css +1 -1
  87. package/dist/elements/a-tooltip.d.ts +16 -8
  88. package/dist/elements/a-tooltip.js +62 -16
  89. package/dist/elements/index.d.ts +4 -0
  90. package/dist/elements/index.js +10 -0
  91. package/dist/general_types.d.ts +252 -30
  92. package/dist/index.d.ts +15 -1
  93. package/dist/index.js +43 -1
  94. package/dist/jsx-runtime.d.ts +20 -7
  95. package/dist/jsx-runtime.js +6 -0
  96. package/dist/reset.css +1 -1
  97. package/dist/tokens.css +1 -1
  98. package/package.json +4 -2
@@ -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: null, rootMargin, threshold: ANCHOR_VISIBLE_RATIO }
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
- gap: 1px;
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-y: auto;
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
- this.surface.append(document.createElement("slot"));
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.isHover) this.scheduleClose();
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
- focusFirstItem() {
317
- this.focusableItems()[0]?.focus({ preventScroll: true });
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.ownerMenu;
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
- closeAll();
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
- if (viaKeyboard) this.focusFirstItem();
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?.getBoundingClientRect();
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.offset;
723
+ left = it.right + this.#offset;
494
724
  if (left + box.width > vw - MARGIN) {
495
- left = it.left - box.width - this.offset;
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 insetTop = parseFloat(cs.borderTopWidth) + parseFloat(cs.paddingTop);
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?.getBoundingClientRect();
735
+ const a = this.triggerAnchor ? anchorRect(this.triggerAnchor) : null;
505
736
  if (!a) return;
506
- const p = this.placement;
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 (onTop && spaceAbove < natural && spaceBelow > spaceAbove) onTop = false;
512
- else if (!onTop && spaceBelow < natural && spaceAbove > spaceBelow) onTop = true;
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.offset : a.bottom + this.offset;
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
- anchor?.focus();
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.isHover) {
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.isContext) {
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.isCoord) {
941
+ if (this.#isCoord) {
699
942
  if (viaKeyboard) {
700
- const r = anchor.getBoundingClientRect();
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.currentValue;
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.currentValue, "reset", false);
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.currentValue, "restore", false);
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.currentValue;
75
- const radios = this.radios;
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.currentValue;
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.isDisabled) return;
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.isDisabled) return;
127
- const enabled = this.radios.filter((r) => !r.hasAttribute("disabled"));
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.currentValue);
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();