@lilydesignsystem/web-components-theme-picker 0.1.0 → 0.2.0

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/index.d.ts CHANGED
@@ -7,7 +7,14 @@
7
7
  *
8
8
  * The control is an icon button that opens a dropdown listbox
9
9
  * (WAI-ARIA APG listbox pattern). It is not a native `<select>`.
10
+ *
11
+ * Depends on `@lilydesignsystem/web-components-headless`'s
12
+ * `<lily-icon-button>` for the trigger button, composed in `#render()`
13
+ * rather than hand-rolled. Imported here (not only from `index.ts`) so
14
+ * `lily-icon-button` is registered regardless of which module a
15
+ * consumer (or this package's own tests) imports first.
10
16
  */
17
+
11
18
  /**
12
19
  * Default button icon: a bundled SVG (contrast/half-circle), not a
13
20
  * Unicode character. Reversed 2026-09-16 from the font-dependent-glyph
@@ -70,6 +77,8 @@ declare function nextThemePickerId(): string;
70
77
  declare class ThemePicker extends HTMLElement {
71
78
  #private;
72
79
  static get observedAttributes(): string[];
80
+ /** id of the rendered `<div class="theme-picker-tooltip">`. */
81
+ get tooltipId(): string;
73
82
  get label(): string;
74
83
  set label(v: string);
75
84
  get themesUrl(): string;
package/dist/index.js CHANGED
@@ -1,4 +1,8 @@
1
+ // lily-design-system-web-components-theme-picker/index.ts
2
+ import "@lilydesignsystem/web-components-headless";
3
+
1
4
  // lily-design-system-web-components-theme-picker/theme-picker.ts
5
+ import "@lilydesignsystem/web-components-headless";
2
6
  var SVG_NS = "http://www.w3.org/2000/svg";
3
7
  function themeName(theme) {
4
8
  return theme.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
@@ -45,6 +49,8 @@ var ThemePicker = class extends HTMLElement {
45
49
  // Rendered-DOM references. Null until #render() has run.
46
50
  #rootEl = null;
47
51
  #inputEl = null;
52
+ // The real <button>, nested one level inside the composed
53
+ // <lily-icon-button> host (see #render()) — not #rootEl's direct child.
48
54
  #buttonEl = null;
49
55
  #listEl = null;
50
56
  #optionEls = [];
@@ -53,6 +59,59 @@ var ThemePicker = class extends HTMLElement {
53
59
  #activeIndex = -1;
54
60
  // Stable ids for the button/listbox aria wiring.
55
61
  #baseId = nextThemePickerId();
62
+ // Tooltip: shown while the pointer is over the button or the tooltip
63
+ // itself (hoverable, WCAG 1.4.13) or while the button has keyboard
64
+ // focus; Escape dismisses it without moving focus; never shown while
65
+ // the popup is open, since the popup then explains the control.
66
+ #tooltipEl = null;
67
+ #hoverButton = false;
68
+ #hoverTooltip = false;
69
+ #focusButton = false;
70
+ #tipDismissed = false;
71
+ // Escape anywhere dismisses a visible tooltip (WCAG 1.4.13: dismissable
72
+ // without moving pointer or focus), so a hover-only tooltip (focus
73
+ // elsewhere) still closes. The document listener exists only while
74
+ // the tooltip is visible; it neither prevents default nor stops
75
+ // propagation, and never moves focus.
76
+ #tipDocListening = false;
77
+ #onTipDocKeydown = (event) => {
78
+ if (event.key === "Escape" && this.#tooltipVisible()) {
79
+ this.#tipDismissed = true;
80
+ this.#syncTooltip();
81
+ }
82
+ };
83
+ #setTipDocListener(on) {
84
+ if (on === this.#tipDocListening || typeof document === "undefined") return;
85
+ this.#tipDocListening = on;
86
+ if (on) document.addEventListener("keydown", this.#onTipDocKeydown);
87
+ else document.removeEventListener("keydown", this.#onTipDocKeydown);
88
+ }
89
+ #tooltipVisible() {
90
+ return !this.#open && !this.#tipDismissed && (this.#hoverButton || this.#hoverTooltip || this.#focusButton);
91
+ }
92
+ /** Write the tooltip's text and `hidden` in place; idempotent. */
93
+ #syncTooltip() {
94
+ const el = this.#tooltipEl;
95
+ if (!el) return;
96
+ if (el.textContent !== this.label) el.textContent = this.label;
97
+ const hide = !this.#tooltipVisible();
98
+ this.#setTipDocListener(!hide);
99
+ if (el.hasAttribute("hidden") !== hide) {
100
+ if (hide) el.setAttribute("hidden", "");
101
+ else el.removeAttribute("hidden");
102
+ }
103
+ }
104
+ #resetTooltip() {
105
+ this.#setTipDocListener(false);
106
+ this.#hoverButton = false;
107
+ this.#hoverTooltip = false;
108
+ this.#focusButton = false;
109
+ this.#tipDismissed = false;
110
+ }
111
+ /** id of the rendered `<div class="theme-picker-tooltip">`. */
112
+ get tooltipId() {
113
+ return `${this.#baseId}-tooltip`;
114
+ }
56
115
  // Typeahead buffer: APG listbox behaviour. Reset after a pause.
57
116
  #typeahead = "";
58
117
  #typeaheadTimer;
@@ -250,6 +309,7 @@ var ThemePicker = class extends HTMLElement {
250
309
  }
251
310
  }
252
311
  disconnectedCallback() {
312
+ this.#resetTooltip();
253
313
  document.removeEventListener("click", this.#onDocumentClick);
254
314
  clearTimeout(this.#typeaheadTimer);
255
315
  this.#appliedValue = "";
@@ -459,6 +519,7 @@ var ThemePicker = class extends HTMLElement {
459
519
  */
460
520
  #syncState() {
461
521
  if (!this.#rootEl) return;
522
+ this.#syncTooltip();
462
523
  const value = this.value;
463
524
  if (this.#inputEl) this.#inputEl.value = value;
464
525
  if (this.#buttonEl) {
@@ -496,20 +557,68 @@ var ThemePicker = class extends HTMLElement {
496
557
  input.name = this.name;
497
558
  input.value = this.value;
498
559
  root.appendChild(input);
499
- const button = document.createElement("button");
500
- button.type = "button";
501
- button.className = "theme-picker-button";
502
- button.setAttribute("aria-label", this.label);
503
- button.setAttribute("aria-haspopup", "listbox");
504
- button.setAttribute("aria-expanded", "false");
505
- button.setAttribute("aria-controls", this.listId);
506
- button.appendChild(this.renderButtonContent());
507
- button.addEventListener("click", () => {
560
+ const iconButtonHost = document.createElement("lily-icon-button");
561
+ iconButtonHost.setAttribute("base-class", "theme-picker-button");
562
+ iconButtonHost.setAttribute("label", this.label);
563
+ iconButtonHost.setAttribute("aria-haspopup", "listbox");
564
+ iconButtonHost.setAttribute("aria-expanded", "false");
565
+ iconButtonHost.setAttribute("aria-controls", this.listId);
566
+ iconButtonHost.appendChild(this.renderButtonContent());
567
+ iconButtonHost.addEventListener("click", () => {
508
568
  if (this.#open) this.closeList();
509
569
  else this.openList();
510
570
  });
511
- button.addEventListener("keydown", this.#onButtonKeydown);
512
- root.appendChild(button);
571
+ iconButtonHost.addEventListener("keydown", this.#onButtonKeydown);
572
+ root.appendChild(iconButtonHost);
573
+ this.#resetTooltip();
574
+ const tooltip = document.createElement("div");
575
+ tooltip.className = "theme-picker-tooltip";
576
+ tooltip.setAttribute("role", "tooltip");
577
+ tooltip.id = this.tooltipId;
578
+ tooltip.setAttribute("hidden", "");
579
+ tooltip.textContent = this.label;
580
+ tooltip.addEventListener("mouseenter", () => {
581
+ this.#hoverTooltip = true;
582
+ this.#syncTooltip();
583
+ });
584
+ tooltip.addEventListener("mouseleave", () => {
585
+ this.#hoverTooltip = false;
586
+ this.#syncTooltip();
587
+ });
588
+ this.#tooltipEl = tooltip;
589
+ iconButtonHost.addEventListener("mouseenter", () => {
590
+ this.#hoverButton = true;
591
+ this.#tipDismissed = false;
592
+ this.#syncTooltip();
593
+ });
594
+ iconButtonHost.addEventListener("mouseleave", () => {
595
+ this.#hoverButton = false;
596
+ this.#syncTooltip();
597
+ });
598
+ iconButtonHost.addEventListener("click", () => {
599
+ this.#hoverButton = false;
600
+ this.#syncTooltip();
601
+ });
602
+ iconButtonHost.addEventListener("focusin", () => {
603
+ try {
604
+ this.#focusButton = this.#buttonEl?.matches(":focus-visible") ?? false;
605
+ } catch {
606
+ this.#focusButton = true;
607
+ }
608
+ this.#syncTooltip();
609
+ });
610
+ iconButtonHost.addEventListener("focusout", () => {
611
+ this.#focusButton = false;
612
+ this.#tipDismissed = false;
613
+ this.#syncTooltip();
614
+ });
615
+ iconButtonHost.addEventListener("keydown", (event) => {
616
+ if (event.key === "Escape" && this.#tooltipVisible()) {
617
+ this.#tipDismissed = true;
618
+ this.#syncTooltip();
619
+ }
620
+ });
621
+ root.appendChild(tooltip);
513
622
  const list = document.createElement("ul");
514
623
  list.className = "theme-picker-list";
515
624
  list.id = this.listId;
@@ -533,10 +642,10 @@ var ThemePicker = class extends HTMLElement {
533
642
  root.appendChild(list);
534
643
  this.#rootEl = root;
535
644
  this.#inputEl = input;
536
- this.#buttonEl = button;
537
645
  this.#listEl = list;
538
646
  this.#optionEls = optionEls;
539
647
  this.replaceChildren(root);
648
+ this.#buttonEl = iconButtonHost.querySelector("button");
540
649
  }
541
650
  };
542
651
  function parseCsv(s) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lilydesignsystem/web-components-theme-picker",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "engines": {
5
5
  "node": "=26"
6
6
  },
@@ -23,6 +23,9 @@
23
23
  "scripts": {
24
24
  "prepublishOnly": "cd .. && npm run build"
25
25
  },
26
+ "dependencies": {
27
+ "@lilydesignsystem/web-components-headless": "^0.1.0"
28
+ },
26
29
  "keywords": [
27
30
  "lily",
28
31
  "design",