@lilydesignsystem/angular-text-size-picker 0.1.2 → 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.
@@ -1,283 +1,336 @@
1
- import { NgTemplateOutlet } from '@angular/common';
2
- import * as i0 from '@angular/core';
3
- import { Directive, input, model, output, contentChild, TemplateRef, viewChild, signal, computed, inject, ChangeDetectorRef, effect, ChangeDetectionStrategy, Component } from '@angular/core';
4
- import { IconButton, Listbox } from '@lilydesignsystem/angular-headless';
5
-
6
- /**
7
- * Resolve a size slug to its display label: each hyphen-separated word
8
- * title-cased, so a slug like "x-large" renders as "X Large". Mirrors
9
- * `themeName` in theme-picker and `localeName` in locale-picker.
10
- */
1
+ import { NgTemplateOutlet } from "@angular/common";
2
+ import * as i0 from "@angular/core";
3
+ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Directive, TemplateRef, computed, contentChild, effect, inject, input, model, output, signal, viewChild } from "@angular/core";
4
+ import { IconButton, Listbox } from "@lilydesignsystem/angular-headless";
11
5
  function sizeName(size) {
12
- return size
13
- .split("-")
14
- .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
15
- .join(" ");
6
+ return size.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
16
7
  }
17
8
  let uid = 0;
18
- /** Stable per-instance id prefix; SSR-safe (no Math.random / Date.now). */
19
9
  function nextTextSizePickerId() {
20
- uid += 1;
21
- return `text-size-picker-${uid}`;
22
- }
23
- /**
24
- * Optional marker for the projected icon template. Gives consumers typed
25
- * `let-` variables:
26
- *
27
- * ```html
28
- * <lily-text-size-picker ...>
29
- * <ng-template lilyTextSizePickerIcon let-args>{{ args.labelFor(args.value) }}</ng-template>
30
- * </lily-text-size-picker>
31
- * ```
32
- *
33
- * The component queries any projected `<ng-template>`, so the marker is
34
- * for type-checking and readability, not for matching.
35
- */
36
- class TextSizePickerIcon {
37
- static ngTemplateContextGuard(_dir, _ctx) {
38
- return true;
39
- }
40
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: TextSizePickerIcon, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
41
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.7", type: TextSizePickerIcon, isStandalone: true, selector: "ng-template[lilyTextSizePickerIcon]", ngImport: i0 }); }
10
+ uid += 1;
11
+ return `text-size-picker-${uid}`;
42
12
  }
43
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: TextSizePickerIcon, decorators: [{
44
- type: Directive,
45
- args: [{
46
- selector: "ng-template[lilyTextSizePickerIcon]",
47
- standalone: true,
48
- }]
49
- }] });
50
- /**
51
- * TextSizePicker — `data-text-size` text-size picker.
52
- *
53
- * Renders an icon button that opens a WAI-ARIA APG listbox of sizes. On
54
- * every size change the component sets `data-text-size="{slug}"` on the
55
- * document root (or on a consumer-supplied target), with optional
56
- * `localStorage` persistence. Ships no CSS; the consumer maps each
57
- * `[data-text-size="…"]` slug to real typography. See `spec/index.md`
58
- * for the full contract.
59
- */
60
- class TextSizePicker {
61
- constructor() {
62
- this.label = input.required(/* @ts-ignore */
63
- ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
64
- this.sizes = input.required(/* @ts-ignore */
65
- ...(ngDevMode ? [{ debugName: "sizes" }] : /* istanbul ignore next */ []));
66
- this.value = model("", /* @ts-ignore */
67
- ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
68
- this.defaultValue = input("", /* @ts-ignore */
69
- ...(ngDevMode ? [{ debugName: "defaultValue" }] : /* istanbul ignore next */ []));
70
- this.storageKey = input("", /* @ts-ignore */
71
- ...(ngDevMode ? [{ debugName: "storageKey" }] : /* istanbul ignore next */ []));
72
- this.name = input("text-size", /* @ts-ignore */
73
- ...(ngDevMode ? [{ debugName: "name" }] : /* istanbul ignore next */ []));
74
- this.target = input(null, /* @ts-ignore */
75
- ...(ngDevMode ? [{ debugName: "target" }] : /* istanbul ignore next */ []));
76
- this.sizeLabels = input({}, /* @ts-ignore */
77
- ...(ngDevMode ? [{ debugName: "sizeLabels" }] : /* istanbul ignore next */ []));
78
- this.className = input("", /* @ts-ignore */
79
- ...(ngDevMode ? [{ debugName: "className" }] : /* istanbul ignore next */ []));
80
- this.sizeChange = output();
81
- /** Projected icon template; replaces the default icon when supplied. */
82
- this.iconTemplate = contentChild(TemplateRef, /* @ts-ignore */
83
- ...(ngDevMode ? [{ debugName: "iconTemplate" }] : /* istanbul ignore next */ []));
84
- this.rootRef = viewChild.required("rootEl", /* @ts-ignore */
85
- ...(ngDevMode ? [{ debugName: "rootRef" }] : /* istanbul ignore next */ []));
86
- // Angular resolves a template-ref-variable on a component tag to the
87
- // component INSTANCE by default (not its ElementRef) — exactly what's
88
- // needed to call the headless components' own public `focus()` methods.
89
- this.buttonRef = viewChild.required("buttonEl", /* @ts-ignore */
90
- ...(ngDevMode ? [{ debugName: "buttonRef" }] : /* istanbul ignore next */ []));
91
- this.listRef = viewChild.required("listEl", /* @ts-ignore */
92
- ...(ngDevMode ? [{ debugName: "listRef" }] : /* istanbul ignore next */ []));
93
- this.baseId = nextTextSizePickerId();
94
- this.listId = `${this.baseId}-list`;
95
- this.open = signal(false, /* @ts-ignore */
96
- ...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
97
- this.activeIndex = signal(-1, /* @ts-ignore */
98
- ...(ngDevMode ? [{ debugName: "activeIndex" }] : /* istanbul ignore next */ []));
99
- this.childContext = computed(() => {
100
- const args = {
101
- value: this.value(),
102
- open: this.open(),
103
- labelFor: (size) => this.labelFor(size),
104
- };
105
- return { $implicit: args, ...args };
106
- }, /* @ts-ignore */
107
- ...(ngDevMode ? [{ debugName: "childContext" }] : /* istanbul ignore next */ []));
108
- this.initialised = false;
109
- // ---------------------------------------------------------------
110
- // Open / close
111
- // ---------------------------------------------------------------
112
- /** Under zoneless change detection the signal write that un-hides the
113
- * list has not reached the DOM when a microtask runs, so focusing the
114
- * still-hidden list fails silently and Escape lands on the button.
115
- * Flushing detection first is the Angular equivalent of Svelte's
116
- * synchronous update (same fix the date-time-picker port recorded). */
117
- this.cdr = inject(ChangeDetectorRef);
118
- // Arrow/Home/End/PageUp/PageDown/typeahead/Escape/Tab keyboard handling
119
- // inside the open list is owned by the composed `lily-listbox`'s
120
- // `navigation="active-descendant"` mode (see
121
- // @lilydesignsystem/angular-headless); this component only decides
122
- // what open/close/choose/scroll mean, and keeps the highlighted option
123
- // in view whenever activeIndex changes.
124
- this.scrollOnActiveIndexChange = effect(() => {
125
- this.activeIndex();
126
- this.scrollActiveIntoView();
127
- }, /* @ts-ignore */
128
- ...(ngDevMode ? [{ debugName: "scrollOnActiveIndexChange" }] : /* istanbul ignore next */ []));
129
- effect(() => {
130
- const current = this.value();
131
- if (!this.initialised) {
132
- this.initialised = true;
133
- let initial = current;
134
- const sk = this.storageKey();
135
- if (!initial && sk) {
136
- try {
137
- initial =
138
- (typeof localStorage !== "undefined"
139
- ? localStorage.getItem(sk)
140
- : null) ?? "";
141
- }
142
- catch {
143
- // ignore privacy errors
144
- }
145
- }
146
- if (!initial) {
147
- const sizes = this.sizes();
148
- const dv = this.defaultValue();
149
- initial =
150
- dv || (sizes.includes("medium") ? "medium" : sizes[0]) || "";
151
- }
152
- if (initial && initial !== current) {
153
- this.value.set(initial);
154
- return;
155
- }
156
- }
157
- if (current)
158
- this.applySize(current);
159
- });
160
- }
161
- optionId(index) {
162
- return `${this.baseId}-option-${index}`;
163
- }
164
- labelFor(size) {
165
- const labels = this.sizeLabels();
166
- if (size in labels)
167
- return labels[size];
168
- return sizeName(size);
169
- }
170
- toggle() {
171
- if (this.open())
172
- this.closeList();
173
- else
174
- this.openList();
175
- }
176
- /** Open the listbox, activating `startIndex` (default: the selection). */
177
- openList(startIndex) {
178
- const selected = this.sizes().indexOf(this.value());
179
- // An empty list has no option to activate; -1 keeps
180
- // aria-activedescendant off rather than pointing at an id that
181
- // does not exist.
182
- this.activeIndex.set(this.sizes().length === 0
183
- ? -1
184
- : (startIndex ?? (selected >= 0 ? selected : 0)));
185
- this.open.set(true);
186
- // Focus moves to the listbox; the active option is conveyed via
187
- // aria-activedescendant, per the APG listbox pattern.
188
- this.cdr.detectChanges();
189
- queueMicrotask(() => {
190
- this.listRef().focus({ preventScroll: true });
191
- this.scrollActiveIntoView();
192
- });
193
- }
194
- /** Close the listbox; `refocus` returns focus to the button. */
195
- closeList(refocus = true) {
196
- if (!this.open())
197
- return;
198
- this.open.set(false);
199
- this.activeIndex.set(-1);
200
- if (refocus)
201
- queueMicrotask(() => this.buttonRef().focus({ preventScroll: true }));
202
- }
203
- choose(index) {
204
- const slug = this.sizes()[index];
205
- if (slug)
206
- this.value.set(slug);
207
- this.closeList();
208
- }
209
- scrollActiveIntoView() {
210
- const i = this.activeIndex();
211
- if (i < 0)
212
- return;
213
- // getElementById rather than reaching into the composed Listbox's
214
- // DOM: Listbox exposes behaviour (focus()) and state (activeIndex),
215
- // not its rendered children, the same "consumer owns the option
216
- // elements" division of responsibility the headless component
217
- // documents for itself.
218
- const el = document.getElementById(this.optionId(i));
219
- // jsdom does not implement scrollIntoView; call it only if present.
220
- el?.scrollIntoView?.({ block: "nearest" });
221
- }
222
- // ---------------------------------------------------------------
223
- // Keyboard
224
- // ---------------------------------------------------------------
225
- onButtonKeydown(event) {
226
- switch (event.key) {
227
- case "ArrowDown":
228
- case "Enter":
229
- case " ":
230
- event.preventDefault();
231
- this.openList();
232
- break;
233
- case "ArrowUp":
234
- event.preventDefault();
235
- this.openList(this.sizes().length - 1);
236
- break;
237
- }
238
- }
239
- /** `lily-listbox` Tab handling never prevents the key — focus goes to
240
- * the button FIRST so the browser's default Tab move computes from
241
- * the picker's position rather than from <body> (hiding the focused
242
- * list drops focus there, and the browser would otherwise compute the
243
- * default Tab move from the top of the document). */
244
- onListTabOut() {
245
- this.buttonRef().focus({ preventScroll: true });
246
- this.closeList(false);
247
- }
248
- onRootFocusOut(event) {
249
- const next = event.relatedTarget;
250
- if (next && this.rootRef().nativeElement.contains(next))
251
- return;
252
- this.closeList(false);
253
- }
254
- onDocumentClick(event) {
255
- if (!this.open())
256
- return;
257
- const t = event.target;
258
- if (t && !this.rootRef().nativeElement.contains(t))
259
- this.closeList(false);
260
- }
261
- // ---------------------------------------------------------------
262
- // Apply
263
- // ---------------------------------------------------------------
264
- applySize(slug) {
265
- if (typeof document === "undefined" || !slug)
266
- return;
267
- (this.target() ?? document.documentElement).setAttribute("data-text-size", slug);
268
- const sk = this.storageKey();
269
- if (sk) {
270
- try {
271
- localStorage.setItem(sk, slug);
272
- }
273
- catch {
274
- // ignore quota / privacy errors
275
- }
276
- }
277
- this.sizeChange.emit(slug);
278
- }
279
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: TextSizePicker, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
280
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", type: TextSizePicker, isStandalone: true, selector: "lily-text-size-picker", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, sizes: { classPropertyName: "sizes", publicName: "sizes", isSignal: true, isRequired: true, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, defaultValue: { classPropertyName: "defaultValue", publicName: "defaultValue", isSignal: true, isRequired: false, transformFunction: null }, storageKey: { classPropertyName: "storageKey", publicName: "storageKey", isSignal: true, isRequired: false, transformFunction: null }, name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, target: { classPropertyName: "target", publicName: "target", isSignal: true, isRequired: false, transformFunction: null }, sizeLabels: { classPropertyName: "sizeLabels", publicName: "sizeLabels", isSignal: true, isRequired: false, transformFunction: null }, className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", sizeChange: "sizeChange" }, host: { listeners: { "document:click": "onDocumentClick($event)" } }, queries: [{ propertyName: "iconTemplate", first: true, predicate: TemplateRef, descendants: true, isSignal: true }], viewQueries: [{ propertyName: "rootRef", first: true, predicate: ["rootEl"], descendants: true, isSignal: true }, { propertyName: "buttonRef", first: true, predicate: ["buttonEl"], descendants: true, isSignal: true }, { propertyName: "listRef", first: true, predicate: ["listEl"], descendants: true, isSignal: true }], ngImport: i0, template: `
13
+ var TextSizePickerIcon = class TextSizePickerIcon {
14
+ static ngTemplateContextGuard(_dir, _ctx) {
15
+ return true;
16
+ }
17
+ static {
18
+ this.ɵfac = i0.ɵɵngDeclareFactory({
19
+ minVersion: "12.0.0",
20
+ version: "22.2.1",
21
+ ngImport: i0,
22
+ type: TextSizePickerIcon,
23
+ deps: [],
24
+ target: i0.ɵɵFactoryTarget.Directive
25
+ });
26
+ }
27
+ static {
28
+ this.ɵdir = i0.ɵɵngDeclareDirective({
29
+ minVersion: "14.0.0",
30
+ version: "22.2.1",
31
+ type: TextSizePickerIcon,
32
+ isStandalone: true,
33
+ selector: "ng-template[lilyTextSizePickerIcon]",
34
+ ngImport: i0
35
+ });
36
+ }
37
+ };
38
+ i0.ɵɵngDeclareClassMetadata({
39
+ minVersion: "12.0.0",
40
+ version: "22.2.1",
41
+ ngImport: i0,
42
+ type: TextSizePickerIcon,
43
+ decorators: [{
44
+ type: Directive,
45
+ args: [{
46
+ selector: "ng-template[lilyTextSizePickerIcon]",
47
+ standalone: true
48
+ }]
49
+ }]
50
+ });
51
+ var TextSizePicker = class TextSizePicker {
52
+ onTooltipButtonEnter() {
53
+ this.hoverButton.set(true);
54
+ this.dismissed.set(false);
55
+ }
56
+ onTooltipButtonFocusIn() {
57
+ try {
58
+ this.focusButton.set(this.buttonRef().element?.matches(":focus-visible") ?? false);
59
+ } catch {
60
+ this.focusButton.set(true);
61
+ }
62
+ }
63
+ onTooltipButtonFocusOut() {
64
+ this.focusButton.set(false);
65
+ this.dismissed.set(false);
66
+ }
67
+ onTooltipKeydown(event) {
68
+ if (event.key === "Escape" && this.tooltipVisible()) this.dismissed.set(true);
69
+ }
70
+ constructor() {
71
+ this.label = input.required(...ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []);
72
+ this.sizes = input.required(...ngDevMode ? [{ debugName: "sizes" }] : /* istanbul ignore next */ []);
73
+ this.value = model("", ...ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []);
74
+ this.defaultValue = input("", ...ngDevMode ? [{ debugName: "defaultValue" }] : /* istanbul ignore next */ []);
75
+ this.storageKey = input("", ...ngDevMode ? [{ debugName: "storageKey" }] : /* istanbul ignore next */ []);
76
+ this.name = input("text-size", ...ngDevMode ? [{ debugName: "name" }] : /* istanbul ignore next */ []);
77
+ this.target = input(null, ...ngDevMode ? [{ debugName: "target" }] : /* istanbul ignore next */ []);
78
+ this.sizeLabels = input({}, ...ngDevMode ? [{ debugName: "sizeLabels" }] : /* istanbul ignore next */ []);
79
+ this.className = input("", ...ngDevMode ? [{ debugName: "className" }] : /* istanbul ignore next */ []);
80
+ this.sizeChange = output();
81
+ this.iconTemplate = contentChild(TemplateRef, ...ngDevMode ? [{ debugName: "iconTemplate" }] : /* istanbul ignore next */ []);
82
+ this.rootRef = viewChild.required("rootEl", ...ngDevMode ? [{ debugName: "rootRef" }] : /* istanbul ignore next */ []);
83
+ this.buttonRef = viewChild.required("buttonEl", ...ngDevMode ? [{ debugName: "buttonRef" }] : /* istanbul ignore next */ []);
84
+ this.listRef = viewChild.required("listEl", ...ngDevMode ? [{ debugName: "listRef" }] : /* istanbul ignore next */ []);
85
+ this.baseId = nextTextSizePickerId();
86
+ this.listId = `${this.baseId}-list`;
87
+ this.open = signal(false, ...ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []);
88
+ this.tooltipId = `${this.baseId}-tooltip`;
89
+ this.hoverButton = signal(false, ...ngDevMode ? [{ debugName: "hoverButton" }] : /* istanbul ignore next */ []);
90
+ this.hoverTooltip = signal(false, ...ngDevMode ? [{ debugName: "hoverTooltip" }] : /* istanbul ignore next */ []);
91
+ this.focusButton = signal(false, ...ngDevMode ? [{ debugName: "focusButton" }] : /* istanbul ignore next */ []);
92
+ this.dismissed = signal(false, ...ngDevMode ? [{ debugName: "dismissed" }] : /* istanbul ignore next */ []);
93
+ this.tooltipVisible = computed(() => !this.open() && !this.dismissed() && (this.hoverButton() || this.hoverTooltip() || this.focusButton()), ...ngDevMode ? [{ debugName: "tooltipVisible" }] : /* istanbul ignore next */ []);
94
+ this.tooltipEscapeListener = effect((onCleanup) => {
95
+ if (!this.tooltipVisible() || typeof document === "undefined") return;
96
+ const onKey = (event) => {
97
+ if (event.key === "Escape") this.dismissed.set(true);
98
+ };
99
+ document.addEventListener("keydown", onKey);
100
+ onCleanup(() => document.removeEventListener("keydown", onKey));
101
+ }, ...ngDevMode ? [{ debugName: "tooltipEscapeListener" }] : /* istanbul ignore next */ []);
102
+ this.activeIndex = signal(-1, ...ngDevMode ? [{ debugName: "activeIndex" }] : /* istanbul ignore next */ []);
103
+ this.childContext = computed(() => {
104
+ const args = {
105
+ value: this.value(),
106
+ open: this.open(),
107
+ labelFor: (size) => this.labelFor(size)
108
+ };
109
+ return {
110
+ $implicit: args,
111
+ ...args
112
+ };
113
+ }, ...ngDevMode ? [{ debugName: "childContext" }] : /* istanbul ignore next */ []);
114
+ this.initialised = false;
115
+ this.cdr = inject(ChangeDetectorRef);
116
+ this.scrollOnActiveIndexChange = effect(() => {
117
+ this.activeIndex();
118
+ this.scrollActiveIntoView();
119
+ }, ...ngDevMode ? [{ debugName: "scrollOnActiveIndexChange" }] : /* istanbul ignore next */ []);
120
+ effect(() => {
121
+ const current = this.value();
122
+ if (!this.initialised) {
123
+ this.initialised = true;
124
+ let initial = current;
125
+ const sk = this.storageKey();
126
+ if (!initial && sk) try {
127
+ initial = (typeof localStorage !== "undefined" ? localStorage.getItem(sk) : null) ?? "";
128
+ } catch {}
129
+ if (!initial) {
130
+ const sizes = this.sizes();
131
+ initial = this.defaultValue() || (sizes.includes("medium") ? "medium" : sizes[0]) || "";
132
+ }
133
+ if (initial && initial !== current) {
134
+ this.value.set(initial);
135
+ return;
136
+ }
137
+ }
138
+ if (current) this.applySize(current);
139
+ });
140
+ }
141
+ optionId(index) {
142
+ return `${this.baseId}-option-${index}`;
143
+ }
144
+ labelFor(size) {
145
+ const labels = this.sizeLabels();
146
+ if (size in labels) return labels[size];
147
+ return sizeName(size);
148
+ }
149
+ toggle() {
150
+ if (this.open()) this.closeList();
151
+ else this.openList();
152
+ }
153
+ openList(startIndex) {
154
+ const selected = this.sizes().indexOf(this.value());
155
+ this.activeIndex.set(this.sizes().length === 0 ? -1 : startIndex ?? (selected >= 0 ? selected : 0));
156
+ this.open.set(true);
157
+ this.cdr.detectChanges();
158
+ queueMicrotask(() => {
159
+ this.listRef().focus({ preventScroll: true });
160
+ this.scrollActiveIntoView();
161
+ });
162
+ }
163
+ closeList(refocus = true) {
164
+ if (!this.open()) return;
165
+ this.open.set(false);
166
+ this.activeIndex.set(-1);
167
+ if (refocus) queueMicrotask(() => this.buttonRef().focus({ preventScroll: true }));
168
+ }
169
+ choose(index) {
170
+ const slug = this.sizes()[index];
171
+ if (slug) this.value.set(slug);
172
+ this.closeList();
173
+ }
174
+ scrollActiveIntoView() {
175
+ const i = this.activeIndex();
176
+ if (i < 0) return;
177
+ document.getElementById(this.optionId(i))?.scrollIntoView?.({ block: "nearest" });
178
+ }
179
+ onButtonKeydown(event) {
180
+ switch (event.key) {
181
+ case "ArrowDown":
182
+ case "Enter":
183
+ case " ":
184
+ event.preventDefault();
185
+ this.openList();
186
+ break;
187
+ case "ArrowUp":
188
+ event.preventDefault();
189
+ this.openList(this.sizes().length - 1);
190
+ }
191
+ }
192
+ onListTabOut() {
193
+ this.buttonRef().focus({ preventScroll: true });
194
+ this.closeList(false);
195
+ }
196
+ onRootFocusOut(event) {
197
+ const next = event.relatedTarget;
198
+ if (next && this.rootRef().nativeElement.contains(next)) return;
199
+ this.closeList(false);
200
+ }
201
+ onDocumentClick(event) {
202
+ if (!this.open()) return;
203
+ const t = event.target;
204
+ if (t && !this.rootRef().nativeElement.contains(t)) this.closeList(false);
205
+ }
206
+ applySize(slug) {
207
+ if (typeof document === "undefined" || !slug) return;
208
+ (this.target() ?? document.documentElement).setAttribute("data-text-size", slug);
209
+ const sk = this.storageKey();
210
+ if (sk) try {
211
+ localStorage.setItem(sk, slug);
212
+ } catch {}
213
+ this.sizeChange.emit(slug);
214
+ }
215
+ static {
216
+ this.ɵfac = i0.ɵɵngDeclareFactory({
217
+ minVersion: "12.0.0",
218
+ version: "22.2.1",
219
+ ngImport: i0,
220
+ type: TextSizePicker,
221
+ deps: [],
222
+ target: i0.ɵɵFactoryTarget.Component
223
+ });
224
+ }
225
+ static {
226
+ this.ɵcmp = i0.ɵɵngDeclareComponent({
227
+ minVersion: "17.0.0",
228
+ version: "22.2.1",
229
+ type: TextSizePicker,
230
+ isStandalone: true,
231
+ selector: "lily-text-size-picker",
232
+ inputs: {
233
+ label: {
234
+ classPropertyName: "label",
235
+ publicName: "label",
236
+ isSignal: true,
237
+ isRequired: true,
238
+ transformFunction: null
239
+ },
240
+ sizes: {
241
+ classPropertyName: "sizes",
242
+ publicName: "sizes",
243
+ isSignal: true,
244
+ isRequired: true,
245
+ transformFunction: null
246
+ },
247
+ value: {
248
+ classPropertyName: "value",
249
+ publicName: "value",
250
+ isSignal: true,
251
+ isRequired: false,
252
+ transformFunction: null
253
+ },
254
+ defaultValue: {
255
+ classPropertyName: "defaultValue",
256
+ publicName: "defaultValue",
257
+ isSignal: true,
258
+ isRequired: false,
259
+ transformFunction: null
260
+ },
261
+ storageKey: {
262
+ classPropertyName: "storageKey",
263
+ publicName: "storageKey",
264
+ isSignal: true,
265
+ isRequired: false,
266
+ transformFunction: null
267
+ },
268
+ name: {
269
+ classPropertyName: "name",
270
+ publicName: "name",
271
+ isSignal: true,
272
+ isRequired: false,
273
+ transformFunction: null
274
+ },
275
+ target: {
276
+ classPropertyName: "target",
277
+ publicName: "target",
278
+ isSignal: true,
279
+ isRequired: false,
280
+ transformFunction: null
281
+ },
282
+ sizeLabels: {
283
+ classPropertyName: "sizeLabels",
284
+ publicName: "sizeLabels",
285
+ isSignal: true,
286
+ isRequired: false,
287
+ transformFunction: null
288
+ },
289
+ className: {
290
+ classPropertyName: "className",
291
+ publicName: "className",
292
+ isSignal: true,
293
+ isRequired: false,
294
+ transformFunction: null
295
+ }
296
+ },
297
+ outputs: {
298
+ value: "valueChange",
299
+ sizeChange: "sizeChange"
300
+ },
301
+ host: { listeners: { "document:click": "onDocumentClick($event)" } },
302
+ queries: [{
303
+ propertyName: "iconTemplate",
304
+ first: true,
305
+ predicate: TemplateRef,
306
+ descendants: true,
307
+ isSignal: true
308
+ }],
309
+ viewQueries: [
310
+ {
311
+ propertyName: "rootRef",
312
+ first: true,
313
+ predicate: ["rootEl"],
314
+ descendants: true,
315
+ isSignal: true
316
+ },
317
+ {
318
+ propertyName: "buttonRef",
319
+ first: true,
320
+ predicate: ["buttonEl"],
321
+ descendants: true,
322
+ isSignal: true
323
+ },
324
+ {
325
+ propertyName: "listRef",
326
+ first: true,
327
+ predicate: ["listEl"],
328
+ descendants: true,
329
+ isSignal: true
330
+ }
331
+ ],
332
+ ngImport: i0,
333
+ template: `
281
334
  <div
282
335
  #rootEl
283
336
  class="text-size-picker {{ className() }}"
@@ -292,8 +345,12 @@ class TextSizePicker {
292
345
  ariaHaspopup="listbox"
293
346
  [ariaExpanded]="open()"
294
347
  [ariaControls]="listId"
295
- (click)="toggle()"
296
- (keydown)="onButtonKeydown($event)"
348
+ (click)="toggle(); hoverButton.set(false)"
349
+ (mouseenter)="onTooltipButtonEnter()"
350
+ (mouseleave)="hoverButton.set(false)"
351
+ (focusin)="onTooltipButtonFocusIn()"
352
+ (focusout)="onTooltipButtonFocusOut()"
353
+ (keydown)="onButtonKeydown($event); onTooltipKeydown($event)"
297
354
  >
298
355
  @if (iconTemplate(); as tpl) {
299
356
  <ng-container
@@ -318,6 +375,18 @@ class TextSizePicker {
318
375
  }
319
376
  </lily-icon-button>
320
377
 
378
+ <!-- Purely visual: the same text is already the button's aria-label,
379
+ so it is not wired with aria-describedby (that would announce
380
+ the name twice). -->
381
+ <div
382
+ class="text-size-picker-tooltip"
383
+ role="tooltip"
384
+ [id]="tooltipId"
385
+ [attr.hidden]="tooltipVisible() ? null : ''"
386
+ (mouseenter)="hoverTooltip.set(true)"
387
+ (mouseleave)="hoverTooltip.set(false)"
388
+ >{{ label() }}</div>
389
+
321
390
  <lily-listbox
322
391
  #listEl
323
392
  [label]="label()"
@@ -347,19 +416,80 @@ class TextSizePicker {
347
416
  }
348
417
  </lily-listbox>
349
418
  </div>
350
- `, isInline: true, dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: IconButton, selector: "lily-icon-button", inputs: ["label", "className", "baseClass", "ariaHaspopup", "ariaExpanded", "ariaControls", "tabIndex", "disabled"] }, { kind: "component", type: Listbox, selector: "lily-listbox", inputs: ["label", "className", "baseClass", "elementId", "hidden", "navigation", "clamp", "typeahead", "pageSize", "activeIndex"], outputs: ["activeIndexChange", "activate", "escape", "tabOut"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
351
- }
352
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: TextSizePicker, decorators: [{
353
- type: Component,
354
- args: [{
355
- selector: "lily-text-size-picker",
356
- standalone: true,
357
- imports: [NgTemplateOutlet, IconButton, Listbox],
358
- changeDetection: ChangeDetectionStrategy.OnPush,
359
- host: {
360
- "(document:click)": "onDocumentClick($event)",
361
- },
362
- template: `
419
+ `,
420
+ isInline: true,
421
+ dependencies: [
422
+ {
423
+ kind: "directive",
424
+ type: NgTemplateOutlet,
425
+ selector: "[ngTemplateOutlet]",
426
+ inputs: [
427
+ "ngTemplateOutletContext",
428
+ "ngTemplateOutlet",
429
+ "ngTemplateOutletInjector"
430
+ ]
431
+ },
432
+ {
433
+ kind: "component",
434
+ type: IconButton,
435
+ selector: "lily-icon-button",
436
+ inputs: [
437
+ "label",
438
+ "className",
439
+ "baseClass",
440
+ "ariaHaspopup",
441
+ "ariaExpanded",
442
+ "ariaControls",
443
+ "tabIndex",
444
+ "disabled"
445
+ ]
446
+ },
447
+ {
448
+ kind: "component",
449
+ type: Listbox,
450
+ selector: "lily-listbox",
451
+ inputs: [
452
+ "label",
453
+ "className",
454
+ "baseClass",
455
+ "elementId",
456
+ "hidden",
457
+ "navigation",
458
+ "clamp",
459
+ "typeahead",
460
+ "pageSize",
461
+ "activeIndex"
462
+ ],
463
+ outputs: [
464
+ "activeIndexChange",
465
+ "activate",
466
+ "escape",
467
+ "tabOut"
468
+ ]
469
+ }
470
+ ],
471
+ changeDetection: i0.ChangeDetectionStrategy.OnPush
472
+ });
473
+ }
474
+ };
475
+ i0.ɵɵngDeclareClassMetadata({
476
+ minVersion: "12.0.0",
477
+ version: "22.2.1",
478
+ ngImport: i0,
479
+ type: TextSizePicker,
480
+ decorators: [{
481
+ type: Component,
482
+ args: [{
483
+ selector: "lily-text-size-picker",
484
+ standalone: true,
485
+ imports: [
486
+ NgTemplateOutlet,
487
+ IconButton,
488
+ Listbox
489
+ ],
490
+ changeDetection: ChangeDetectionStrategy.OnPush,
491
+ host: { "(document:click)": "onDocumentClick($event)" },
492
+ template: `
363
493
  <div
364
494
  #rootEl
365
495
  class="text-size-picker {{ className() }}"
@@ -374,8 +504,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
374
504
  ariaHaspopup="listbox"
375
505
  [ariaExpanded]="open()"
376
506
  [ariaControls]="listId"
377
- (click)="toggle()"
378
- (keydown)="onButtonKeydown($event)"
507
+ (click)="toggle(); hoverButton.set(false)"
508
+ (mouseenter)="onTooltipButtonEnter()"
509
+ (mouseleave)="hoverButton.set(false)"
510
+ (focusin)="onTooltipButtonFocusIn()"
511
+ (focusout)="onTooltipButtonFocusOut()"
512
+ (keydown)="onButtonKeydown($event); onTooltipKeydown($event)"
379
513
  >
380
514
  @if (iconTemplate(); as tpl) {
381
515
  <ng-container
@@ -400,6 +534,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
400
534
  }
401
535
  </lily-icon-button>
402
536
 
537
+ <!-- Purely visual: the same text is already the button's aria-label,
538
+ so it is not wired with aria-describedby (that would announce
539
+ the name twice). -->
540
+ <div
541
+ class="text-size-picker-tooltip"
542
+ role="tooltip"
543
+ [id]="tooltipId"
544
+ [attr.hidden]="tooltipVisible() ? null : ''"
545
+ (mouseenter)="hoverTooltip.set(true)"
546
+ (mouseleave)="hoverTooltip.set(false)"
547
+ >{{ label() }}</div>
548
+
403
549
  <lily-listbox
404
550
  #listEl
405
551
  [label]="label()"
@@ -429,13 +575,108 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
429
575
  }
430
576
  </lily-listbox>
431
577
  </div>
432
- `,
433
- }]
434
- }], ctorParameters: () => [], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], sizes: [{ type: i0.Input, args: [{ isSignal: true, alias: "sizes", required: true }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], defaultValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "defaultValue", required: false }] }], storageKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "storageKey", required: false }] }], name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: false }] }], target: [{ type: i0.Input, args: [{ isSignal: true, alias: "target", required: false }] }], sizeLabels: [{ type: i0.Input, args: [{ isSignal: true, alias: "sizeLabels", required: false }] }], className: [{ type: i0.Input, args: [{ isSignal: true, alias: "className", required: false }] }], sizeChange: [{ type: i0.Output, args: ["sizeChange"] }], iconTemplate: [{ type: i0.ContentChild, args: [i0.forwardRef(() => TemplateRef), { isSignal: true }] }], rootRef: [{ type: i0.ViewChild, args: ["rootEl", { isSignal: true }] }], buttonRef: [{ type: i0.ViewChild, args: ["buttonEl", { isSignal: true }] }], listRef: [{ type: i0.ViewChild, args: ["listEl", { isSignal: true }] }] } });
435
-
436
- /**
437
- * Generated bundle index. Do not edit.
438
- */
439
-
578
+ `
579
+ }]
580
+ }],
581
+ ctorParameters: () => [],
582
+ propDecorators: {
583
+ label: [{
584
+ type: i0.Input,
585
+ args: [{
586
+ isSignal: true,
587
+ alias: "label",
588
+ required: true
589
+ }]
590
+ }],
591
+ sizes: [{
592
+ type: i0.Input,
593
+ args: [{
594
+ isSignal: true,
595
+ alias: "sizes",
596
+ required: true
597
+ }]
598
+ }],
599
+ value: [{
600
+ type: i0.Input,
601
+ args: [{
602
+ isSignal: true,
603
+ alias: "value",
604
+ required: false
605
+ }]
606
+ }, {
607
+ type: i0.Output,
608
+ args: ["valueChange"]
609
+ }],
610
+ defaultValue: [{
611
+ type: i0.Input,
612
+ args: [{
613
+ isSignal: true,
614
+ alias: "defaultValue",
615
+ required: false
616
+ }]
617
+ }],
618
+ storageKey: [{
619
+ type: i0.Input,
620
+ args: [{
621
+ isSignal: true,
622
+ alias: "storageKey",
623
+ required: false
624
+ }]
625
+ }],
626
+ name: [{
627
+ type: i0.Input,
628
+ args: [{
629
+ isSignal: true,
630
+ alias: "name",
631
+ required: false
632
+ }]
633
+ }],
634
+ target: [{
635
+ type: i0.Input,
636
+ args: [{
637
+ isSignal: true,
638
+ alias: "target",
639
+ required: false
640
+ }]
641
+ }],
642
+ sizeLabels: [{
643
+ type: i0.Input,
644
+ args: [{
645
+ isSignal: true,
646
+ alias: "sizeLabels",
647
+ required: false
648
+ }]
649
+ }],
650
+ className: [{
651
+ type: i0.Input,
652
+ args: [{
653
+ isSignal: true,
654
+ alias: "className",
655
+ required: false
656
+ }]
657
+ }],
658
+ sizeChange: [{
659
+ type: i0.Output,
660
+ args: ["sizeChange"]
661
+ }],
662
+ iconTemplate: [{
663
+ type: i0.ContentChild,
664
+ args: [i0.forwardRef(() => TemplateRef), { isSignal: true }]
665
+ }],
666
+ rootRef: [{
667
+ type: i0.ViewChild,
668
+ args: ["rootEl", { isSignal: true }]
669
+ }],
670
+ buttonRef: [{
671
+ type: i0.ViewChild,
672
+ args: ["buttonEl", { isSignal: true }]
673
+ }],
674
+ listRef: [{
675
+ type: i0.ViewChild,
676
+ args: ["listEl", { isSignal: true }]
677
+ }]
678
+ }
679
+ });
440
680
  export { TextSizePicker, TextSizePickerIcon, nextTextSizePickerId, sizeName };
441
- //# sourceMappingURL=lilydesignsystem-angular-text-size-picker.mjs.map
681
+
682
+ //# sourceMappingURL=lilydesignsystem-angular-text-size-picker.mjs.map