@lilydesignsystem/angular-text-size-picker 0.1.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/README.md +180 -0
- package/fesm2022/lilydesignsystem-angular-text-size-picker.mjs +509 -0
- package/fesm2022/lilydesignsystem-angular-text-size-picker.mjs.map +1 -0
- package/package.json +47 -0
- package/types/lilydesignsystem-angular-text-size-picker.d.ts +124 -0
- package/types/lilydesignsystem-angular-text-size-picker.d.ts.map +1 -0
package/README.md
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# TextSizePicker (Angular helper)
|
|
2
|
+
|
|
3
|
+
A reusable Angular 20 headless **text-size picker**. Renders an icon
|
|
4
|
+
button that opens a [WAI-ARIA APG listbox](https://www.w3.org/WAI/ARIA/apg/patterns/listbox/)
|
|
5
|
+
of text-size slugs and, on every change, sets
|
|
6
|
+
`data-text-size="{slug}"` on a target element (default
|
|
7
|
+
`document.documentElement`), optionally persisting the choice to
|
|
8
|
+
`localStorage`. Same shape as the sibling `theme-picker` and
|
|
9
|
+
`locale-picker` helpers. Ships no CSS — the consumer supplies the
|
|
10
|
+
listbox positioning and maps each size slug to real typography, e.g.:
|
|
11
|
+
|
|
12
|
+
```css
|
|
13
|
+
:root[data-text-size="small"] {
|
|
14
|
+
font-size: 87.5%;
|
|
15
|
+
}
|
|
16
|
+
:root[data-text-size="medium"] {
|
|
17
|
+
font-size: 100%;
|
|
18
|
+
}
|
|
19
|
+
:root[data-text-size="large"] {
|
|
20
|
+
font-size: 112.5%;
|
|
21
|
+
}
|
|
22
|
+
:root[data-text-size="x-large"] {
|
|
23
|
+
font-size: 125%;
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
This supports WCAG 2.2 — 1.4.4 (Resize Text) and 1.4.12 (Text
|
|
28
|
+
Spacing) — by letting users pick a comfortable reading size that the
|
|
29
|
+
app remembers.
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import { Component, signal } from "@angular/core";
|
|
35
|
+
import { TextSizePicker } from "@lilydesignsystem/angular-text-size-picker";
|
|
36
|
+
|
|
37
|
+
@Component({
|
|
38
|
+
standalone: true,
|
|
39
|
+
imports: [TextSizePicker],
|
|
40
|
+
template: `
|
|
41
|
+
<lily-text-size-picker
|
|
42
|
+
label="Text size"
|
|
43
|
+
[sizes]="['small', 'medium', 'large', 'x-large']"
|
|
44
|
+
[(value)]="size"
|
|
45
|
+
storageKey="lily-text-size"
|
|
46
|
+
/>
|
|
47
|
+
`,
|
|
48
|
+
})
|
|
49
|
+
export class SettingsPage {
|
|
50
|
+
size = signal("");
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Inputs / outputs
|
|
55
|
+
|
|
56
|
+
| Input / output | Type | Required | Description |
|
|
57
|
+
| -------------- | ----------------------- | -------- | ---------------------------------------------------------- |
|
|
58
|
+
| `label` | `string` | yes | Accessible name (`aria-label`) for the button and listbox. |
|
|
59
|
+
| `sizes` | `string[]` | yes | Available size slugs. |
|
|
60
|
+
| `value` | `string` | no | Selected slug. Two-way bindable via `[(value)]`. |
|
|
61
|
+
| `defaultValue` | `string` | no | Initial slug when nothing else is supplied. |
|
|
62
|
+
| `storageKey` | `string` | no | If set, persist the slug to `localStorage`. |
|
|
63
|
+
| `name` | `string` | no | `name` of the hidden input (default `"text-size"`). |
|
|
64
|
+
| `target` | `HTMLElement \| null` | no | Element to receive `data-text-size`. Default `<html>`. |
|
|
65
|
+
| `sizeLabels` | `Record<string,string>` | no | Pretty labels per slug. |
|
|
66
|
+
| `className` | `string` | no | Extra CSS class on the root `<div>`. |
|
|
67
|
+
| `sizeChange` | `output<string>` | no | Emits after a new size is applied. |
|
|
68
|
+
|
|
69
|
+
## Markup
|
|
70
|
+
|
|
71
|
+
```html
|
|
72
|
+
<div class="text-size-picker">
|
|
73
|
+
<input type="hidden" name="text-size" value="medium" />
|
|
74
|
+
<button
|
|
75
|
+
type="button"
|
|
76
|
+
class="text-size-picker-button"
|
|
77
|
+
aria-label="Text size"
|
|
78
|
+
aria-haspopup="listbox"
|
|
79
|
+
aria-expanded="false"
|
|
80
|
+
aria-controls="…-list"
|
|
81
|
+
>
|
|
82
|
+
<svg class="text-size-picker-icon" viewBox="0 0 16 16" aria-hidden="true">…</svg>
|
|
83
|
+
</button>
|
|
84
|
+
<ul
|
|
85
|
+
class="text-size-picker-list"
|
|
86
|
+
id="…-list"
|
|
87
|
+
role="listbox"
|
|
88
|
+
aria-label="Text size"
|
|
89
|
+
tabindex="-1"
|
|
90
|
+
hidden
|
|
91
|
+
>
|
|
92
|
+
<li
|
|
93
|
+
class="text-size-picker-option"
|
|
94
|
+
role="option"
|
|
95
|
+
aria-selected="true"
|
|
96
|
+
data-active
|
|
97
|
+
>
|
|
98
|
+
Medium
|
|
99
|
+
</li>
|
|
100
|
+
</ul>
|
|
101
|
+
</div>
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The icon is a bundled outline "A" SVG, not a Unicode character
|
|
105
|
+
(reversed 2026-09-16) — it renders identically on every platform
|
|
106
|
+
regardless of the consumer's installed fonts. `data-active` marks the
|
|
107
|
+
keyboard cursor; `aria-selected` marks the size in effect. Style both,
|
|
108
|
+
differently.
|
|
109
|
+
|
|
110
|
+
## Custom icon
|
|
111
|
+
|
|
112
|
+
Project an `<ng-template>` to replace the icon inside the button. It
|
|
113
|
+
receives `{ value, open, labelFor }`; it does **not** render options.
|
|
114
|
+
|
|
115
|
+
```html
|
|
116
|
+
<lily-text-size-picker label="Text size" [sizes]="sizes">
|
|
117
|
+
<ng-template lilyTextSizePickerIcon let-args>
|
|
118
|
+
<svg width="16" height="16" aria-hidden="true" focusable="false">…</svg>
|
|
119
|
+
</ng-template>
|
|
120
|
+
</lily-text-size-picker>
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Behaviour
|
|
124
|
+
|
|
125
|
+
Initial value resolves from `value` > storage > `defaultValue` >
|
|
126
|
+
`"medium"` (if present) > `sizes[0]`. All DOM writes happen inside an
|
|
127
|
+
`effect()` guarded on `typeof document`, so the component is SSR-safe.
|
|
128
|
+
|
|
129
|
+
`labelFor(slug)` returns `sizeLabels[slug]` if present, else delegates
|
|
130
|
+
to the exported `sizeName(slug)`, which title-cases the slug per
|
|
131
|
+
hyphen-word (`x-large` → `X Large`).
|
|
132
|
+
|
|
133
|
+
There is no OS-preference detection: unlike `prefers-color-scheme`
|
|
134
|
+
for themes or `navigator.languages` for locales, no "preferred text
|
|
135
|
+
size" media query exists.
|
|
136
|
+
|
|
137
|
+
## Keyboard
|
|
138
|
+
|
|
139
|
+
`ArrowDown` / `Enter` / `Space` open the listbox on the selected
|
|
140
|
+
option; `ArrowUp` opens on the last. Inside the listbox: arrows move
|
|
141
|
+
the cursor and clamp (no wrap), `Home` / `End` jump, `PageUp` /
|
|
142
|
+
`PageDown` move by ten (clamped), printable characters typeahead over
|
|
143
|
+
the labels with a 500 ms buffer — a single character advances to the
|
|
144
|
+
next match and repeats cycle; differing characters refine — `Enter` /
|
|
145
|
+
`Space` select and return focus to the button, `Escape` closes
|
|
146
|
+
unchanged, `Tab` closes via the button so the default Tab proceeds
|
|
147
|
+
from the picker's position. Clicking an option selects it; clicking
|
|
148
|
+
outside closes.
|
|
149
|
+
|
|
150
|
+
## Accessibility
|
|
151
|
+
|
|
152
|
+
- WCAG 2.2 AAA target; directly supports 1.4.4 (Resize Text) and
|
|
153
|
+
1.4.12 (Text Spacing).
|
|
154
|
+
- The button is icon-only, so `aria-label` is its **entire**
|
|
155
|
+
accessible name. Name the setting ("Text size"), not the widget.
|
|
156
|
+
- This is a hand-rolled listbox, not a native `<select>` — it has
|
|
157
|
+
weaker assistive-technology support. See
|
|
158
|
+
[docs/accessibility.md](./docs/accessibility.md) for the full
|
|
159
|
+
tradeoffs.
|
|
160
|
+
- Default labels title-case the slug.
|
|
161
|
+
|
|
162
|
+
## Conventions this package follows
|
|
163
|
+
|
|
164
|
+
- Angular 20 standalone component with `input<T>()` /
|
|
165
|
+
`input.required<T>()`, `model<string>()`, `output<T>()`.
|
|
166
|
+
- `ChangeDetectionStrategy.OnPush`.
|
|
167
|
+
- `@for` control flow.
|
|
168
|
+
- Strict TypeScript on the public surface.
|
|
169
|
+
- No runtime dependency beyond `@angular/core` / `@angular/common`.
|
|
170
|
+
- No bundled CSS, fonts, icons, or images.
|
|
171
|
+
- All user-facing strings come from inputs.
|
|
172
|
+
|
|
173
|
+
## License
|
|
174
|
+
|
|
175
|
+
Dual-licensed under MIT or Apache-2.0 or GPL-2.0 or GPL-3.0 or
|
|
176
|
+
BSD-3-Clause. Contact joel@joelparkerhenderson.com for other terms.
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
Lily™ and Lily Design System™ are trademarks.
|
|
@@ -0,0 +1,509 @@
|
|
|
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, DestroyRef, effect, ChangeDetectionStrategy, Component } from '@angular/core';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Resolve a size slug to its display label: each hyphen-separated word
|
|
7
|
+
* title-cased, so a slug like "x-large" renders as "X Large". Mirrors
|
|
8
|
+
* `themeName` in theme-picker and `localeName` in locale-picker.
|
|
9
|
+
*/
|
|
10
|
+
function sizeName(size) {
|
|
11
|
+
return size
|
|
12
|
+
.split("-")
|
|
13
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
14
|
+
.join(" ");
|
|
15
|
+
}
|
|
16
|
+
let uid = 0;
|
|
17
|
+
/** Stable per-instance id prefix; SSR-safe (no Math.random / Date.now). */
|
|
18
|
+
function nextTextSizePickerId() {
|
|
19
|
+
uid += 1;
|
|
20
|
+
return `text-size-picker-${uid}`;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Optional marker for the projected icon template. Gives consumers typed
|
|
24
|
+
* `let-` variables:
|
|
25
|
+
*
|
|
26
|
+
* ```html
|
|
27
|
+
* <lily-text-size-picker ...>
|
|
28
|
+
* <ng-template lilyTextSizePickerIcon let-args>{{ args.labelFor(args.value) }}</ng-template>
|
|
29
|
+
* </lily-text-size-picker>
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* The component queries any projected `<ng-template>`, so the marker is
|
|
33
|
+
* for type-checking and readability, not for matching.
|
|
34
|
+
*/
|
|
35
|
+
class TextSizePickerIcon {
|
|
36
|
+
static ngTemplateContextGuard(_dir, _ctx) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: TextSizePickerIcon, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
40
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.4", type: TextSizePickerIcon, isStandalone: true, selector: "ng-template[lilyTextSizePickerIcon]", ngImport: i0 }); }
|
|
41
|
+
}
|
|
42
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: TextSizePickerIcon, decorators: [{
|
|
43
|
+
type: Directive,
|
|
44
|
+
args: [{
|
|
45
|
+
selector: "ng-template[lilyTextSizePickerIcon]",
|
|
46
|
+
standalone: true,
|
|
47
|
+
}]
|
|
48
|
+
}] });
|
|
49
|
+
/**
|
|
50
|
+
* TextSizePicker — `data-text-size` text-size picker.
|
|
51
|
+
*
|
|
52
|
+
* Renders an icon button that opens a WAI-ARIA APG listbox of sizes. On
|
|
53
|
+
* every size change the component sets `data-text-size="{slug}"` on the
|
|
54
|
+
* document root (or on a consumer-supplied target), with optional
|
|
55
|
+
* `localStorage` persistence. Ships no CSS; the consumer maps each
|
|
56
|
+
* `[data-text-size="…"]` slug to real typography. See `spec/index.md`
|
|
57
|
+
* for the full contract.
|
|
58
|
+
*/
|
|
59
|
+
class TextSizePicker {
|
|
60
|
+
constructor() {
|
|
61
|
+
this.label = input.required(/* @ts-ignore */
|
|
62
|
+
...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
|
|
63
|
+
this.sizes = input.required(/* @ts-ignore */
|
|
64
|
+
...(ngDevMode ? [{ debugName: "sizes" }] : /* istanbul ignore next */ []));
|
|
65
|
+
this.value = model("", /* @ts-ignore */
|
|
66
|
+
...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
|
|
67
|
+
this.defaultValue = input("", /* @ts-ignore */
|
|
68
|
+
...(ngDevMode ? [{ debugName: "defaultValue" }] : /* istanbul ignore next */ []));
|
|
69
|
+
this.storageKey = input("", /* @ts-ignore */
|
|
70
|
+
...(ngDevMode ? [{ debugName: "storageKey" }] : /* istanbul ignore next */ []));
|
|
71
|
+
this.name = input("text-size", /* @ts-ignore */
|
|
72
|
+
...(ngDevMode ? [{ debugName: "name" }] : /* istanbul ignore next */ []));
|
|
73
|
+
this.target = input(null, /* @ts-ignore */
|
|
74
|
+
...(ngDevMode ? [{ debugName: "target" }] : /* istanbul ignore next */ []));
|
|
75
|
+
this.sizeLabels = input({}, /* @ts-ignore */
|
|
76
|
+
...(ngDevMode ? [{ debugName: "sizeLabels" }] : /* istanbul ignore next */ []));
|
|
77
|
+
this.className = input("", /* @ts-ignore */
|
|
78
|
+
...(ngDevMode ? [{ debugName: "className" }] : /* istanbul ignore next */ []));
|
|
79
|
+
this.sizeChange = output();
|
|
80
|
+
/** Projected icon template; replaces the default icon when supplied. */
|
|
81
|
+
this.iconTemplate = contentChild(TemplateRef, /* @ts-ignore */
|
|
82
|
+
...(ngDevMode ? [{ debugName: "iconTemplate" }] : /* istanbul ignore next */ []));
|
|
83
|
+
this.rootRef = viewChild.required("rootEl", /* @ts-ignore */
|
|
84
|
+
...(ngDevMode ? [{ debugName: "rootRef" }] : /* istanbul ignore next */ []));
|
|
85
|
+
this.buttonRef = viewChild.required("buttonEl", /* @ts-ignore */
|
|
86
|
+
...(ngDevMode ? [{ debugName: "buttonRef" }] : /* istanbul ignore next */ []));
|
|
87
|
+
this.listRef = viewChild.required("listEl", /* @ts-ignore */
|
|
88
|
+
...(ngDevMode ? [{ debugName: "listRef" }] : /* istanbul ignore next */ []));
|
|
89
|
+
this.baseId = nextTextSizePickerId();
|
|
90
|
+
this.listId = `${this.baseId}-list`;
|
|
91
|
+
this.open = signal(false, /* @ts-ignore */
|
|
92
|
+
...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
|
|
93
|
+
this.activeIndex = signal(-1, /* @ts-ignore */
|
|
94
|
+
...(ngDevMode ? [{ debugName: "activeIndex" }] : /* istanbul ignore next */ []));
|
|
95
|
+
/** `aria-activedescendant` is only meaningful while the listbox is open. */
|
|
96
|
+
this.activeDescendant = computed(() => {
|
|
97
|
+
const i = this.activeIndex();
|
|
98
|
+
return this.open() && i >= 0 ? this.optionId(i) : null;
|
|
99
|
+
}, /* @ts-ignore */
|
|
100
|
+
...(ngDevMode ? [{ debugName: "activeDescendant" }] : /* istanbul ignore next */ []));
|
|
101
|
+
this.childContext = computed(() => {
|
|
102
|
+
const args = {
|
|
103
|
+
value: this.value(),
|
|
104
|
+
open: this.open(),
|
|
105
|
+
labelFor: (size) => this.labelFor(size),
|
|
106
|
+
};
|
|
107
|
+
return { $implicit: args, ...args };
|
|
108
|
+
}, /* @ts-ignore */
|
|
109
|
+
...(ngDevMode ? [{ debugName: "childContext" }] : /* istanbul ignore next */ []));
|
|
110
|
+
// Typeahead buffer: APG listbox behaviour. Reset after a pause.
|
|
111
|
+
this.typeahead = "";
|
|
112
|
+
this.initialised = false;
|
|
113
|
+
// ---------------------------------------------------------------
|
|
114
|
+
// Open / close
|
|
115
|
+
// ---------------------------------------------------------------
|
|
116
|
+
/** Under zoneless change detection the signal write that un-hides the
|
|
117
|
+
* list has not reached the DOM when a microtask runs, so focusing the
|
|
118
|
+
* still-hidden list fails silently and Escape lands on the button.
|
|
119
|
+
* Flushing detection first is the Angular equivalent of Svelte's
|
|
120
|
+
* synchronous update (same fix the date-time-picker port recorded). */
|
|
121
|
+
this.cdr = inject(ChangeDetectorRef);
|
|
122
|
+
inject(DestroyRef).onDestroy(() => clearTimeout(this.typeaheadTimer));
|
|
123
|
+
effect(() => {
|
|
124
|
+
const current = this.value();
|
|
125
|
+
if (!this.initialised) {
|
|
126
|
+
this.initialised = true;
|
|
127
|
+
let initial = current;
|
|
128
|
+
const sk = this.storageKey();
|
|
129
|
+
if (!initial && sk) {
|
|
130
|
+
try {
|
|
131
|
+
initial =
|
|
132
|
+
(typeof localStorage !== "undefined"
|
|
133
|
+
? localStorage.getItem(sk)
|
|
134
|
+
: null) ?? "";
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
// ignore privacy errors
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (!initial) {
|
|
141
|
+
const sizes = this.sizes();
|
|
142
|
+
const dv = this.defaultValue();
|
|
143
|
+
initial =
|
|
144
|
+
dv || (sizes.includes("medium") ? "medium" : sizes[0]) || "";
|
|
145
|
+
}
|
|
146
|
+
if (initial && initial !== current) {
|
|
147
|
+
this.value.set(initial);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (current)
|
|
152
|
+
this.applySize(current);
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
optionId(index) {
|
|
156
|
+
return `${this.baseId}-option-${index}`;
|
|
157
|
+
}
|
|
158
|
+
labelFor(size) {
|
|
159
|
+
const labels = this.sizeLabels();
|
|
160
|
+
if (size in labels)
|
|
161
|
+
return labels[size];
|
|
162
|
+
return sizeName(size);
|
|
163
|
+
}
|
|
164
|
+
toggle() {
|
|
165
|
+
if (this.open())
|
|
166
|
+
this.closeList();
|
|
167
|
+
else
|
|
168
|
+
this.openList();
|
|
169
|
+
}
|
|
170
|
+
/** Open the listbox, activating `startIndex` (default: the selection). */
|
|
171
|
+
openList(startIndex) {
|
|
172
|
+
const selected = this.sizes().indexOf(this.value());
|
|
173
|
+
// An empty list has no option to activate; -1 keeps
|
|
174
|
+
// aria-activedescendant off rather than pointing at an id that
|
|
175
|
+
// does not exist.
|
|
176
|
+
this.activeIndex.set(this.sizes().length === 0
|
|
177
|
+
? -1
|
|
178
|
+
: (startIndex ?? (selected >= 0 ? selected : 0)));
|
|
179
|
+
this.open.set(true);
|
|
180
|
+
// Focus moves to the listbox; the active option is conveyed via
|
|
181
|
+
// aria-activedescendant, per the APG listbox pattern.
|
|
182
|
+
this.cdr.detectChanges();
|
|
183
|
+
queueMicrotask(() => {
|
|
184
|
+
this.listRef().nativeElement.focus({ preventScroll: true });
|
|
185
|
+
this.scrollActiveIntoView();
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
/** Close the listbox; `refocus` returns focus to the button. */
|
|
189
|
+
closeList(refocus = true) {
|
|
190
|
+
if (!this.open())
|
|
191
|
+
return;
|
|
192
|
+
this.open.set(false);
|
|
193
|
+
this.activeIndex.set(-1);
|
|
194
|
+
if (refocus)
|
|
195
|
+
queueMicrotask(() => this.buttonRef().nativeElement.focus({ preventScroll: true }));
|
|
196
|
+
}
|
|
197
|
+
choose(index) {
|
|
198
|
+
const slug = this.sizes()[index];
|
|
199
|
+
if (slug)
|
|
200
|
+
this.value.set(slug);
|
|
201
|
+
this.closeList();
|
|
202
|
+
}
|
|
203
|
+
scrollActiveIntoView() {
|
|
204
|
+
const i = this.activeIndex();
|
|
205
|
+
if (i < 0)
|
|
206
|
+
return;
|
|
207
|
+
const el = this.listRef().nativeElement.children[i];
|
|
208
|
+
// jsdom does not implement scrollIntoView; call it only if present.
|
|
209
|
+
el?.scrollIntoView?.({ block: "nearest" });
|
|
210
|
+
}
|
|
211
|
+
moveActive(delta) {
|
|
212
|
+
const count = this.sizes().length;
|
|
213
|
+
if (count === 0)
|
|
214
|
+
return;
|
|
215
|
+
// Clamp rather than wrap, per the APG listbox pattern.
|
|
216
|
+
this.activeIndex.set(Math.min(Math.max(this.activeIndex() + delta, 0), count - 1));
|
|
217
|
+
this.scrollActiveIntoView();
|
|
218
|
+
}
|
|
219
|
+
runTypeahead(char) {
|
|
220
|
+
const lower = char.toLowerCase();
|
|
221
|
+
// APG listbox typeahead: a single character moves to the NEXT
|
|
222
|
+
// option starting with it, and repeating that character keeps
|
|
223
|
+
// cycling. Only a buffer of differing characters refines the
|
|
224
|
+
// match, and that buffer stays anchored on the active option.
|
|
225
|
+
const sameCharRun = this.typeahead === "" || [...this.typeahead].every((c) => c === lower);
|
|
226
|
+
this.typeahead += lower;
|
|
227
|
+
clearTimeout(this.typeaheadTimer);
|
|
228
|
+
this.typeaheadTimer = setTimeout(() => (this.typeahead = ""), 500);
|
|
229
|
+
const query = sameCharRun ? lower : this.typeahead;
|
|
230
|
+
const sizes = this.sizes();
|
|
231
|
+
const anchor = this.activeIndex() < 0 ? 0 : this.activeIndex();
|
|
232
|
+
const start = sameCharRun ? anchor + 1 : anchor;
|
|
233
|
+
// Search forward, wrapping once — typeahead wraps even though the
|
|
234
|
+
// arrows clamp, or options above the cursor would be untypable.
|
|
235
|
+
for (let n = 0; n < sizes.length; n++) {
|
|
236
|
+
const i = (start + n) % sizes.length;
|
|
237
|
+
if (this.labelFor(sizes[i]).toLowerCase().startsWith(query)) {
|
|
238
|
+
this.activeIndex.set(i);
|
|
239
|
+
this.scrollActiveIntoView();
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
// ---------------------------------------------------------------
|
|
245
|
+
// Keyboard
|
|
246
|
+
// ---------------------------------------------------------------
|
|
247
|
+
onButtonKeydown(event) {
|
|
248
|
+
switch (event.key) {
|
|
249
|
+
case "ArrowDown":
|
|
250
|
+
case "Enter":
|
|
251
|
+
case " ":
|
|
252
|
+
event.preventDefault();
|
|
253
|
+
this.openList();
|
|
254
|
+
break;
|
|
255
|
+
case "ArrowUp":
|
|
256
|
+
event.preventDefault();
|
|
257
|
+
this.openList(this.sizes().length - 1);
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
onListKeydown(event) {
|
|
262
|
+
switch (event.key) {
|
|
263
|
+
case "ArrowDown":
|
|
264
|
+
event.preventDefault();
|
|
265
|
+
this.moveActive(1);
|
|
266
|
+
break;
|
|
267
|
+
case "ArrowUp":
|
|
268
|
+
event.preventDefault();
|
|
269
|
+
this.moveActive(-1);
|
|
270
|
+
break;
|
|
271
|
+
case "Home":
|
|
272
|
+
event.preventDefault();
|
|
273
|
+
this.activeIndex.set(0);
|
|
274
|
+
this.scrollActiveIntoView();
|
|
275
|
+
break;
|
|
276
|
+
case "End":
|
|
277
|
+
event.preventDefault();
|
|
278
|
+
this.activeIndex.set(this.sizes().length - 1);
|
|
279
|
+
this.scrollActiveIntoView();
|
|
280
|
+
break;
|
|
281
|
+
case "Enter":
|
|
282
|
+
case " ":
|
|
283
|
+
event.preventDefault();
|
|
284
|
+
if (this.activeIndex() >= 0)
|
|
285
|
+
this.choose(this.activeIndex());
|
|
286
|
+
break;
|
|
287
|
+
case "Escape":
|
|
288
|
+
event.preventDefault();
|
|
289
|
+
this.closeList();
|
|
290
|
+
break;
|
|
291
|
+
case "PageUp":
|
|
292
|
+
event.preventDefault();
|
|
293
|
+
this.moveActive(-10);
|
|
294
|
+
break;
|
|
295
|
+
case "PageDown":
|
|
296
|
+
// ±10, clamped: an APG-optional key for long size lists.
|
|
297
|
+
event.preventDefault();
|
|
298
|
+
this.moveActive(10);
|
|
299
|
+
break;
|
|
300
|
+
case "Tab":
|
|
301
|
+
// Tab moves on — but focus goes to the button FIRST, without
|
|
302
|
+
// cancelling the key. Hiding the focused list drops focus to
|
|
303
|
+
// <body>, and the browser then computes the default Tab move
|
|
304
|
+
// from the top of the document, so tabbing out of an open
|
|
305
|
+
// picker teleported the user to the page's first tab stop.
|
|
306
|
+
// From the button, the default Tab lands exactly where leaving
|
|
307
|
+
// the picker should. The button always exists, so no
|
|
308
|
+
// detectChanges is needed before the focus move; guard the
|
|
309
|
+
// METHOD because jsdom-shaped hosts may not implement it.
|
|
310
|
+
this.buttonRef().nativeElement.focus?.({ preventScroll: true });
|
|
311
|
+
this.closeList(false);
|
|
312
|
+
break;
|
|
313
|
+
default:
|
|
314
|
+
if (event.key.length === 1 &&
|
|
315
|
+
!event.ctrlKey &&
|
|
316
|
+
!event.metaKey &&
|
|
317
|
+
!event.altKey) {
|
|
318
|
+
this.runTypeahead(event.key);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
onRootFocusOut(event) {
|
|
323
|
+
const next = event.relatedTarget;
|
|
324
|
+
if (next && this.rootRef().nativeElement.contains(next))
|
|
325
|
+
return;
|
|
326
|
+
this.closeList(false);
|
|
327
|
+
}
|
|
328
|
+
onDocumentClick(event) {
|
|
329
|
+
if (!this.open())
|
|
330
|
+
return;
|
|
331
|
+
const t = event.target;
|
|
332
|
+
if (t && !this.rootRef().nativeElement.contains(t))
|
|
333
|
+
this.closeList(false);
|
|
334
|
+
}
|
|
335
|
+
// ---------------------------------------------------------------
|
|
336
|
+
// Apply
|
|
337
|
+
// ---------------------------------------------------------------
|
|
338
|
+
applySize(slug) {
|
|
339
|
+
if (typeof document === "undefined" || !slug)
|
|
340
|
+
return;
|
|
341
|
+
(this.target() ?? document.documentElement).setAttribute("data-text-size", slug);
|
|
342
|
+
const sk = this.storageKey();
|
|
343
|
+
if (sk) {
|
|
344
|
+
try {
|
|
345
|
+
localStorage.setItem(sk, slug);
|
|
346
|
+
}
|
|
347
|
+
catch {
|
|
348
|
+
// ignore quota / privacy errors
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
this.sizeChange.emit(slug);
|
|
352
|
+
}
|
|
353
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: TextSizePicker, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
354
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.4", 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: `
|
|
355
|
+
<div
|
|
356
|
+
#rootEl
|
|
357
|
+
class="text-size-picker {{ className() }}"
|
|
358
|
+
(focusout)="onRootFocusOut($event)"
|
|
359
|
+
>
|
|
360
|
+
<input type="hidden" [name]="name()" [value]="value()" />
|
|
361
|
+
|
|
362
|
+
<button
|
|
363
|
+
#buttonEl
|
|
364
|
+
type="button"
|
|
365
|
+
class="text-size-picker-button"
|
|
366
|
+
[attr.aria-label]="label() || null"
|
|
367
|
+
aria-haspopup="listbox"
|
|
368
|
+
[attr.aria-expanded]="open()"
|
|
369
|
+
[attr.aria-controls]="listId"
|
|
370
|
+
(click)="toggle()"
|
|
371
|
+
(keydown)="onButtonKeydown($event)"
|
|
372
|
+
>
|
|
373
|
+
@if (iconTemplate(); as tpl) {
|
|
374
|
+
<ng-container
|
|
375
|
+
[ngTemplateOutlet]="tpl"
|
|
376
|
+
[ngTemplateOutletContext]="childContext()"
|
|
377
|
+
/>
|
|
378
|
+
} @else {
|
|
379
|
+
<svg
|
|
380
|
+
class="text-size-picker-icon"
|
|
381
|
+
viewBox="0 0 16 16"
|
|
382
|
+
width="1.05rem"
|
|
383
|
+
height="1.05rem"
|
|
384
|
+
aria-hidden="true"
|
|
385
|
+
fill="none"
|
|
386
|
+
stroke="currentColor"
|
|
387
|
+
stroke-width="1.6"
|
|
388
|
+
stroke-linecap="round"
|
|
389
|
+
stroke-linejoin="round"
|
|
390
|
+
>
|
|
391
|
+
<path d="M4 13 7.2 3h1.6L12 13M5.4 9.5h5.2" />
|
|
392
|
+
</svg>
|
|
393
|
+
}
|
|
394
|
+
</button>
|
|
395
|
+
|
|
396
|
+
<ul
|
|
397
|
+
#listEl
|
|
398
|
+
class="text-size-picker-list"
|
|
399
|
+
[id]="listId"
|
|
400
|
+
role="listbox"
|
|
401
|
+
[attr.aria-label]="label() || null"
|
|
402
|
+
[attr.aria-activedescendant]="activeDescendant()"
|
|
403
|
+
tabindex="-1"
|
|
404
|
+
[attr.hidden]="open() ? null : ''"
|
|
405
|
+
(keydown)="onListKeydown($event)"
|
|
406
|
+
>
|
|
407
|
+
@for (size of sizes(); track size; let i = $index) {
|
|
408
|
+
<li
|
|
409
|
+
class="text-size-picker-option"
|
|
410
|
+
[id]="optionId(i)"
|
|
411
|
+
role="option"
|
|
412
|
+
[attr.aria-selected]="size === value()"
|
|
413
|
+
[attr.data-active]="i === activeIndex() ? '' : null"
|
|
414
|
+
(click)="choose(i)"
|
|
415
|
+
>
|
|
416
|
+
{{ labelFor(size) }}
|
|
417
|
+
</li>
|
|
418
|
+
}
|
|
419
|
+
</ul>
|
|
420
|
+
</div>
|
|
421
|
+
`, isInline: true, dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
422
|
+
}
|
|
423
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: TextSizePicker, decorators: [{
|
|
424
|
+
type: Component,
|
|
425
|
+
args: [{
|
|
426
|
+
selector: "lily-text-size-picker",
|
|
427
|
+
standalone: true,
|
|
428
|
+
imports: [NgTemplateOutlet],
|
|
429
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
430
|
+
host: {
|
|
431
|
+
"(document:click)": "onDocumentClick($event)",
|
|
432
|
+
},
|
|
433
|
+
template: `
|
|
434
|
+
<div
|
|
435
|
+
#rootEl
|
|
436
|
+
class="text-size-picker {{ className() }}"
|
|
437
|
+
(focusout)="onRootFocusOut($event)"
|
|
438
|
+
>
|
|
439
|
+
<input type="hidden" [name]="name()" [value]="value()" />
|
|
440
|
+
|
|
441
|
+
<button
|
|
442
|
+
#buttonEl
|
|
443
|
+
type="button"
|
|
444
|
+
class="text-size-picker-button"
|
|
445
|
+
[attr.aria-label]="label() || null"
|
|
446
|
+
aria-haspopup="listbox"
|
|
447
|
+
[attr.aria-expanded]="open()"
|
|
448
|
+
[attr.aria-controls]="listId"
|
|
449
|
+
(click)="toggle()"
|
|
450
|
+
(keydown)="onButtonKeydown($event)"
|
|
451
|
+
>
|
|
452
|
+
@if (iconTemplate(); as tpl) {
|
|
453
|
+
<ng-container
|
|
454
|
+
[ngTemplateOutlet]="tpl"
|
|
455
|
+
[ngTemplateOutletContext]="childContext()"
|
|
456
|
+
/>
|
|
457
|
+
} @else {
|
|
458
|
+
<svg
|
|
459
|
+
class="text-size-picker-icon"
|
|
460
|
+
viewBox="0 0 16 16"
|
|
461
|
+
width="1.05rem"
|
|
462
|
+
height="1.05rem"
|
|
463
|
+
aria-hidden="true"
|
|
464
|
+
fill="none"
|
|
465
|
+
stroke="currentColor"
|
|
466
|
+
stroke-width="1.6"
|
|
467
|
+
stroke-linecap="round"
|
|
468
|
+
stroke-linejoin="round"
|
|
469
|
+
>
|
|
470
|
+
<path d="M4 13 7.2 3h1.6L12 13M5.4 9.5h5.2" />
|
|
471
|
+
</svg>
|
|
472
|
+
}
|
|
473
|
+
</button>
|
|
474
|
+
|
|
475
|
+
<ul
|
|
476
|
+
#listEl
|
|
477
|
+
class="text-size-picker-list"
|
|
478
|
+
[id]="listId"
|
|
479
|
+
role="listbox"
|
|
480
|
+
[attr.aria-label]="label() || null"
|
|
481
|
+
[attr.aria-activedescendant]="activeDescendant()"
|
|
482
|
+
tabindex="-1"
|
|
483
|
+
[attr.hidden]="open() ? null : ''"
|
|
484
|
+
(keydown)="onListKeydown($event)"
|
|
485
|
+
>
|
|
486
|
+
@for (size of sizes(); track size; let i = $index) {
|
|
487
|
+
<li
|
|
488
|
+
class="text-size-picker-option"
|
|
489
|
+
[id]="optionId(i)"
|
|
490
|
+
role="option"
|
|
491
|
+
[attr.aria-selected]="size === value()"
|
|
492
|
+
[attr.data-active]="i === activeIndex() ? '' : null"
|
|
493
|
+
(click)="choose(i)"
|
|
494
|
+
>
|
|
495
|
+
{{ labelFor(size) }}
|
|
496
|
+
</li>
|
|
497
|
+
}
|
|
498
|
+
</ul>
|
|
499
|
+
</div>
|
|
500
|
+
`,
|
|
501
|
+
}]
|
|
502
|
+
}], 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 }] }] } });
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Generated bundle index. Do not edit.
|
|
506
|
+
*/
|
|
507
|
+
|
|
508
|
+
export { TextSizePicker, TextSizePickerIcon, nextTextSizePickerId, sizeName };
|
|
509
|
+
//# sourceMappingURL=lilydesignsystem-angular-text-size-picker.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lilydesignsystem-angular-text-size-picker.mjs","sources":["../../text-size-picker.component.ts","../../lilydesignsystem-angular-text-size-picker.ts"],"sourcesContent":["import { NgTemplateOutlet } from \"@angular/common\";\nimport {\n ChangeDetectionStrategy,\n ChangeDetectorRef,\n Component,\n DestroyRef,\n Directive,\n ElementRef,\n TemplateRef,\n computed,\n contentChild,\n effect,\n inject,\n input,\n model,\n output,\n signal,\n viewChild,\n} from \"@angular/core\";\n\n/**\n * Default button icon: a bundled SVG (a stroke-drawn \"A\"), not a\n * Unicode character. Reversed 2026-09-16 from the font-dependent-glyph\n * convention (was the plain letter U+0041, exported as\n * `LATIN_CAPITAL_LETTER_A` — removed, not renamed). \"A\" itself needed\n * no escaping and had no font-fallback risk, but it still varied in\n * weight and proportions across font stacks; a bundled outline SVG\n * matches the other four picker icons as one consistent visual family\n * regardless of the consumer's fonts.\n */\n\n/** Context passed to a custom icon `<ng-template>` (the button icon). */\nexport type ChildArgs = {\n /** Currently selected size slug. */\n value: string;\n /** Is the listbox open? */\n open: boolean;\n /** Resolve a slug to its display label. */\n labelFor: (size: string) => string;\n};\n\n/**\n * Resolve a size slug to its display label: each hyphen-separated word\n * title-cased, so a slug like \"x-large\" renders as \"X Large\". Mirrors\n * `themeName` in theme-picker and `localeName` in locale-picker.\n */\nexport function sizeName(size: string): string {\n return size\n .split(\"-\")\n .map((word) => word.charAt(0).toUpperCase() + word.slice(1))\n .join(\" \");\n}\n\nlet uid = 0;\n/** Stable per-instance id prefix; SSR-safe (no Math.random / Date.now). */\nexport function nextTextSizePickerId(): string {\n uid += 1;\n return `text-size-picker-${uid}`;\n}\n\n/**\n * Optional marker for the projected icon template. Gives consumers typed\n * `let-` variables:\n *\n * ```html\n * <lily-text-size-picker ...>\n * <ng-template lilyTextSizePickerIcon let-args>{{ args.labelFor(args.value) }}</ng-template>\n * </lily-text-size-picker>\n * ```\n *\n * The component queries any projected `<ng-template>`, so the marker is\n * for type-checking and readability, not for matching.\n */\n@Directive({\n selector: \"ng-template[lilyTextSizePickerIcon]\",\n standalone: true,\n})\nexport class TextSizePickerIcon {\n static ngTemplateContextGuard(\n _dir: TextSizePickerIcon,\n _ctx: unknown,\n ): _ctx is ChildArgs & { $implicit: ChildArgs } {\n return true;\n }\n}\n\n/**\n * TextSizePicker — `data-text-size` text-size picker.\n *\n * Renders an icon button that opens a WAI-ARIA APG listbox of sizes. On\n * every size change the component sets `data-text-size=\"{slug}\"` on the\n * document root (or on a consumer-supplied target), with optional\n * `localStorage` persistence. Ships no CSS; the consumer maps each\n * `[data-text-size=\"…\"]` slug to real typography. See `spec/index.md`\n * for the full contract.\n */\n@Component({\n selector: \"lily-text-size-picker\",\n standalone: true,\n imports: [NgTemplateOutlet],\n changeDetection: ChangeDetectionStrategy.OnPush,\n host: {\n \"(document:click)\": \"onDocumentClick($event)\",\n },\n template: `\n <div\n #rootEl\n class=\"text-size-picker {{ className() }}\"\n (focusout)=\"onRootFocusOut($event)\"\n >\n <input type=\"hidden\" [name]=\"name()\" [value]=\"value()\" />\n\n <button\n #buttonEl\n type=\"button\"\n class=\"text-size-picker-button\"\n [attr.aria-label]=\"label() || null\"\n aria-haspopup=\"listbox\"\n [attr.aria-expanded]=\"open()\"\n [attr.aria-controls]=\"listId\"\n (click)=\"toggle()\"\n (keydown)=\"onButtonKeydown($event)\"\n >\n @if (iconTemplate(); as tpl) {\n <ng-container\n [ngTemplateOutlet]=\"tpl\"\n [ngTemplateOutletContext]=\"childContext()\"\n />\n } @else {\n <svg\n class=\"text-size-picker-icon\"\n viewBox=\"0 0 16 16\"\n width=\"1.05rem\"\n height=\"1.05rem\"\n aria-hidden=\"true\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"1.6\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n >\n <path d=\"M4 13 7.2 3h1.6L12 13M5.4 9.5h5.2\" />\n </svg>\n }\n </button>\n\n <ul\n #listEl\n class=\"text-size-picker-list\"\n [id]=\"listId\"\n role=\"listbox\"\n [attr.aria-label]=\"label() || null\"\n [attr.aria-activedescendant]=\"activeDescendant()\"\n tabindex=\"-1\"\n [attr.hidden]=\"open() ? null : ''\"\n (keydown)=\"onListKeydown($event)\"\n >\n @for (size of sizes(); track size; let i = $index) {\n <li\n class=\"text-size-picker-option\"\n [id]=\"optionId(i)\"\n role=\"option\"\n [attr.aria-selected]=\"size === value()\"\n [attr.data-active]=\"i === activeIndex() ? '' : null\"\n (click)=\"choose(i)\"\n >\n {{ labelFor(size) }}\n </li>\n }\n </ul>\n </div>\n `,\n})\nexport class TextSizePicker {\n readonly label = input.required<string>();\n readonly sizes = input.required<string[]>();\n readonly value = model<string>(\"\");\n readonly defaultValue = input<string>(\"\");\n readonly storageKey = input<string>(\"\");\n readonly name = input<string>(\"text-size\");\n readonly target = input<HTMLElement | null>(null);\n readonly sizeLabels = input<Record<string, string>>({});\n readonly className = input<string>(\"\");\n readonly sizeChange = output<string>();\n\n /** Projected icon template; replaces the default icon when supplied. */\n protected readonly iconTemplate = contentChild(TemplateRef);\n\n private readonly rootRef =\n viewChild.required<ElementRef<HTMLDivElement>>(\"rootEl\");\n private readonly buttonRef =\n viewChild.required<ElementRef<HTMLButtonElement>>(\"buttonEl\");\n private readonly listRef =\n viewChild.required<ElementRef<HTMLUListElement>>(\"listEl\");\n\n private readonly baseId = nextTextSizePickerId();\n protected readonly listId = `${this.baseId}-list`;\n\n protected readonly open = signal(false);\n protected readonly activeIndex = signal(-1);\n\n /** `aria-activedescendant` is only meaningful while the listbox is open. */\n protected readonly activeDescendant = computed(() => {\n const i = this.activeIndex();\n return this.open() && i >= 0 ? this.optionId(i) : null;\n });\n\n protected readonly childContext = computed(() => {\n const args: ChildArgs = {\n value: this.value(),\n open: this.open(),\n labelFor: (size: string) => this.labelFor(size),\n };\n return { $implicit: args, ...args };\n });\n\n // Typeahead buffer: APG listbox behaviour. Reset after a pause.\n private typeahead = \"\";\n private typeaheadTimer: ReturnType<typeof setTimeout> | undefined;\n\n private initialised = false;\n\n constructor() {\n inject(DestroyRef).onDestroy(() => clearTimeout(this.typeaheadTimer));\n\n effect(() => {\n const current = this.value();\n\n if (!this.initialised) {\n this.initialised = true;\n let initial = current;\n\n const sk = this.storageKey();\n if (!initial && sk) {\n try {\n initial =\n (typeof localStorage !== \"undefined\"\n ? localStorage.getItem(sk)\n : null) ?? \"\";\n } catch {\n // ignore privacy errors\n }\n }\n\n if (!initial) {\n const sizes = this.sizes();\n const dv = this.defaultValue();\n initial =\n dv || (sizes.includes(\"medium\") ? \"medium\" : sizes[0]) || \"\";\n }\n\n if (initial && initial !== current) {\n this.value.set(initial);\n return;\n }\n }\n\n if (current) this.applySize(current);\n });\n }\n\n protected optionId(index: number): string {\n return `${this.baseId}-option-${index}`;\n }\n\n labelFor(size: string): string {\n const labels = this.sizeLabels();\n if (size in labels) return labels[size];\n return sizeName(size);\n }\n\n // ---------------------------------------------------------------\n // Open / close\n // ---------------------------------------------------------------\n\n /** Under zoneless change detection the signal write that un-hides the\n * list has not reached the DOM when a microtask runs, so focusing the\n * still-hidden list fails silently and Escape lands on the button.\n * Flushing detection first is the Angular equivalent of Svelte's\n * synchronous update (same fix the date-time-picker port recorded). */\n private readonly cdr = inject(ChangeDetectorRef);\n\n protected toggle(): void {\n if (this.open()) this.closeList();\n else this.openList();\n }\n\n /** Open the listbox, activating `startIndex` (default: the selection). */\n openList(startIndex?: number): void {\n const selected = this.sizes().indexOf(this.value());\n // An empty list has no option to activate; -1 keeps\n // aria-activedescendant off rather than pointing at an id that\n // does not exist.\n this.activeIndex.set(\n this.sizes().length === 0\n ? -1\n : (startIndex ?? (selected >= 0 ? selected : 0)),\n );\n this.open.set(true);\n // Focus moves to the listbox; the active option is conveyed via\n // aria-activedescendant, per the APG listbox pattern.\n this.cdr.detectChanges();\n queueMicrotask(() => {\n this.listRef().nativeElement.focus({ preventScroll: true });\n this.scrollActiveIntoView();\n });\n }\n\n /** Close the listbox; `refocus` returns focus to the button. */\n closeList(refocus = true): void {\n if (!this.open()) return;\n this.open.set(false);\n this.activeIndex.set(-1);\n if (refocus) queueMicrotask(() => this.buttonRef().nativeElement.focus({ preventScroll: true }));\n }\n\n protected choose(index: number): void {\n const slug = this.sizes()[index];\n if (slug) this.value.set(slug);\n this.closeList();\n }\n\n private scrollActiveIntoView(): void {\n const i = this.activeIndex();\n if (i < 0) return;\n const el = this.listRef().nativeElement.children[i] as\n HTMLElement | undefined;\n // jsdom does not implement scrollIntoView; call it only if present.\n el?.scrollIntoView?.({ block: \"nearest\" });\n }\n\n private moveActive(delta: number): void {\n const count = this.sizes().length;\n if (count === 0) return;\n // Clamp rather than wrap, per the APG listbox pattern.\n this.activeIndex.set(\n Math.min(Math.max(this.activeIndex() + delta, 0), count - 1),\n );\n this.scrollActiveIntoView();\n }\n\n private runTypeahead(char: string): void {\n const lower = char.toLowerCase();\n // APG listbox typeahead: a single character moves to the NEXT\n // option starting with it, and repeating that character keeps\n // cycling. Only a buffer of differing characters refines the\n // match, and that buffer stays anchored on the active option.\n const sameCharRun =\n this.typeahead === \"\" || [...this.typeahead].every((c) => c === lower);\n this.typeahead += lower;\n clearTimeout(this.typeaheadTimer);\n this.typeaheadTimer = setTimeout(() => (this.typeahead = \"\"), 500);\n const query = sameCharRun ? lower : this.typeahead;\n const sizes = this.sizes();\n const anchor = this.activeIndex() < 0 ? 0 : this.activeIndex();\n const start = sameCharRun ? anchor + 1 : anchor;\n // Search forward, wrapping once — typeahead wraps even though the\n // arrows clamp, or options above the cursor would be untypable.\n for (let n = 0; n < sizes.length; n++) {\n const i = (start + n) % sizes.length;\n if (this.labelFor(sizes[i]).toLowerCase().startsWith(query)) {\n this.activeIndex.set(i);\n this.scrollActiveIntoView();\n return;\n }\n }\n }\n\n // ---------------------------------------------------------------\n // Keyboard\n // ---------------------------------------------------------------\n\n protected onButtonKeydown(event: KeyboardEvent): void {\n switch (event.key) {\n case \"ArrowDown\":\n case \"Enter\":\n case \" \":\n event.preventDefault();\n this.openList();\n break;\n case \"ArrowUp\":\n event.preventDefault();\n this.openList(this.sizes().length - 1);\n break;\n }\n }\n\n protected onListKeydown(event: KeyboardEvent): void {\n switch (event.key) {\n case \"ArrowDown\":\n event.preventDefault();\n this.moveActive(1);\n break;\n case \"ArrowUp\":\n event.preventDefault();\n this.moveActive(-1);\n break;\n case \"Home\":\n event.preventDefault();\n this.activeIndex.set(0);\n this.scrollActiveIntoView();\n break;\n case \"End\":\n event.preventDefault();\n this.activeIndex.set(this.sizes().length - 1);\n this.scrollActiveIntoView();\n break;\n case \"Enter\":\n case \" \":\n event.preventDefault();\n if (this.activeIndex() >= 0) this.choose(this.activeIndex());\n break;\n case \"Escape\":\n event.preventDefault();\n this.closeList();\n break;\n case \"PageUp\":\n event.preventDefault();\n this.moveActive(-10);\n break;\n case \"PageDown\":\n // ±10, clamped: an APG-optional key for long size lists.\n event.preventDefault();\n this.moveActive(10);\n break;\n case \"Tab\":\n // Tab moves on — but focus goes to the button FIRST, without\n // cancelling the key. Hiding the focused list drops focus to\n // <body>, and the browser then computes the default Tab move\n // from the top of the document, so tabbing out of an open\n // picker teleported the user to the page's first tab stop.\n // From the button, the default Tab lands exactly where leaving\n // the picker should. The button always exists, so no\n // detectChanges is needed before the focus move; guard the\n // METHOD because jsdom-shaped hosts may not implement it.\n this.buttonRef().nativeElement.focus?.({ preventScroll: true });\n this.closeList(false);\n break;\n default:\n if (\n event.key.length === 1 &&\n !event.ctrlKey &&\n !event.metaKey &&\n !event.altKey\n ) {\n this.runTypeahead(event.key);\n }\n }\n }\n\n protected onRootFocusOut(event: FocusEvent): void {\n const next = event.relatedTarget as Node | null;\n if (next && this.rootRef().nativeElement.contains(next)) return;\n this.closeList(false);\n }\n\n protected onDocumentClick(event: Event): void {\n if (!this.open()) return;\n const t = event.target as Node | null;\n if (t && !this.rootRef().nativeElement.contains(t)) this.closeList(false);\n }\n\n // ---------------------------------------------------------------\n // Apply\n // ---------------------------------------------------------------\n\n private applySize(slug: string): void {\n if (typeof document === \"undefined\" || !slug) return;\n (this.target() ?? document.documentElement).setAttribute(\n \"data-text-size\",\n slug,\n );\n\n const sk = this.storageKey();\n if (sk) {\n try {\n localStorage.setItem(sk, slug);\n } catch {\n // ignore quota / privacy errors\n }\n }\n this.sizeChange.emit(slug);\n }\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lilydesignsystem/angular-text-size-picker",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"engines": {
|
|
5
|
+
"node": "=26"
|
|
6
|
+
},
|
|
7
|
+
"description": "Lily Design System Angular 20 text size picker: an icon button opening an APG listbox that sets data-text-size on the document. Headless, SSR-safe, no CSS.",
|
|
8
|
+
"peerDependencies": {
|
|
9
|
+
"@angular/core": ">=20.0.0 <23.0.0",
|
|
10
|
+
"@angular/common": ">=20.0.0 <23.0.0"
|
|
11
|
+
},
|
|
12
|
+
"sideEffects": false,
|
|
13
|
+
"keywords": [
|
|
14
|
+
"lily",
|
|
15
|
+
"design",
|
|
16
|
+
"system",
|
|
17
|
+
"angular",
|
|
18
|
+
"text-size",
|
|
19
|
+
"picker"
|
|
20
|
+
],
|
|
21
|
+
"author": "Joel Parker Henderson <joel@joelparkerhenderson.com>",
|
|
22
|
+
"license": "MIT OR Apache-2.0 OR GPL-2.0-only OR GPL-3.0-only OR BSD-3-Clause",
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/LilyDesignSystem/lily-design-system-angular-helpers.git",
|
|
26
|
+
"directory": "lily-design-system-angular-text-size-picker"
|
|
27
|
+
},
|
|
28
|
+
"homepage": "https://lilydesignsystem.com/",
|
|
29
|
+
"bugs": {
|
|
30
|
+
"url": "https://github.com/LilyDesignSystem/lily-design-system/issues"
|
|
31
|
+
},
|
|
32
|
+
"module": "fesm2022/lilydesignsystem-angular-text-size-picker.mjs",
|
|
33
|
+
"typings": "types/lilydesignsystem-angular-text-size-picker.d.ts",
|
|
34
|
+
"exports": {
|
|
35
|
+
"./package.json": {
|
|
36
|
+
"default": "./package.json"
|
|
37
|
+
},
|
|
38
|
+
".": {
|
|
39
|
+
"types": "./types/lilydesignsystem-angular-text-size-picker.d.ts",
|
|
40
|
+
"default": "./fesm2022/lilydesignsystem-angular-text-size-picker.mjs"
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"type": "module",
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"tslib": "^2.3.0"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import * as _angular_core from '@angular/core';
|
|
2
|
+
import { TemplateRef } from '@angular/core';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Default button icon: a bundled SVG (a stroke-drawn "A"), not a
|
|
6
|
+
* Unicode character. Reversed 2026-09-16 from the font-dependent-glyph
|
|
7
|
+
* convention (was the plain letter U+0041, exported as
|
|
8
|
+
* `LATIN_CAPITAL_LETTER_A` — removed, not renamed). "A" itself needed
|
|
9
|
+
* no escaping and had no font-fallback risk, but it still varied in
|
|
10
|
+
* weight and proportions across font stacks; a bundled outline SVG
|
|
11
|
+
* matches the other four picker icons as one consistent visual family
|
|
12
|
+
* regardless of the consumer's fonts.
|
|
13
|
+
*/
|
|
14
|
+
/** Context passed to a custom icon `<ng-template>` (the button icon). */
|
|
15
|
+
type ChildArgs = {
|
|
16
|
+
/** Currently selected size slug. */
|
|
17
|
+
value: string;
|
|
18
|
+
/** Is the listbox open? */
|
|
19
|
+
open: boolean;
|
|
20
|
+
/** Resolve a slug to its display label. */
|
|
21
|
+
labelFor: (size: string) => string;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Resolve a size slug to its display label: each hyphen-separated word
|
|
25
|
+
* title-cased, so a slug like "x-large" renders as "X Large". Mirrors
|
|
26
|
+
* `themeName` in theme-picker and `localeName` in locale-picker.
|
|
27
|
+
*/
|
|
28
|
+
declare function sizeName(size: string): string;
|
|
29
|
+
/** Stable per-instance id prefix; SSR-safe (no Math.random / Date.now). */
|
|
30
|
+
declare function nextTextSizePickerId(): string;
|
|
31
|
+
/**
|
|
32
|
+
* Optional marker for the projected icon template. Gives consumers typed
|
|
33
|
+
* `let-` variables:
|
|
34
|
+
*
|
|
35
|
+
* ```html
|
|
36
|
+
* <lily-text-size-picker ...>
|
|
37
|
+
* <ng-template lilyTextSizePickerIcon let-args>{{ args.labelFor(args.value) }}</ng-template>
|
|
38
|
+
* </lily-text-size-picker>
|
|
39
|
+
* ```
|
|
40
|
+
*
|
|
41
|
+
* The component queries any projected `<ng-template>`, so the marker is
|
|
42
|
+
* for type-checking and readability, not for matching.
|
|
43
|
+
*/
|
|
44
|
+
declare class TextSizePickerIcon {
|
|
45
|
+
static ngTemplateContextGuard(_dir: TextSizePickerIcon, _ctx: unknown): _ctx is ChildArgs & {
|
|
46
|
+
$implicit: ChildArgs;
|
|
47
|
+
};
|
|
48
|
+
static ɵfac: _angular_core.ɵɵFactoryDeclaration<TextSizePickerIcon, never>;
|
|
49
|
+
static ɵdir: _angular_core.ɵɵDirectiveDeclaration<TextSizePickerIcon, "ng-template[lilyTextSizePickerIcon]", never, {}, {}, never, never, true, never>;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* TextSizePicker — `data-text-size` text-size picker.
|
|
53
|
+
*
|
|
54
|
+
* Renders an icon button that opens a WAI-ARIA APG listbox of sizes. On
|
|
55
|
+
* every size change the component sets `data-text-size="{slug}"` on the
|
|
56
|
+
* document root (or on a consumer-supplied target), with optional
|
|
57
|
+
* `localStorage` persistence. Ships no CSS; the consumer maps each
|
|
58
|
+
* `[data-text-size="…"]` slug to real typography. See `spec/index.md`
|
|
59
|
+
* for the full contract.
|
|
60
|
+
*/
|
|
61
|
+
declare class TextSizePicker {
|
|
62
|
+
readonly label: _angular_core.InputSignal<string>;
|
|
63
|
+
readonly sizes: _angular_core.InputSignal<string[]>;
|
|
64
|
+
readonly value: _angular_core.ModelSignal<string>;
|
|
65
|
+
readonly defaultValue: _angular_core.InputSignal<string>;
|
|
66
|
+
readonly storageKey: _angular_core.InputSignal<string>;
|
|
67
|
+
readonly name: _angular_core.InputSignal<string>;
|
|
68
|
+
readonly target: _angular_core.InputSignal<HTMLElement | null>;
|
|
69
|
+
readonly sizeLabels: _angular_core.InputSignal<Record<string, string>>;
|
|
70
|
+
readonly className: _angular_core.InputSignal<string>;
|
|
71
|
+
readonly sizeChange: _angular_core.OutputEmitterRef<string>;
|
|
72
|
+
/** Projected icon template; replaces the default icon when supplied. */
|
|
73
|
+
protected readonly iconTemplate: _angular_core.Signal<TemplateRef<any> | undefined>;
|
|
74
|
+
private readonly rootRef;
|
|
75
|
+
private readonly buttonRef;
|
|
76
|
+
private readonly listRef;
|
|
77
|
+
private readonly baseId;
|
|
78
|
+
protected readonly listId: string;
|
|
79
|
+
protected readonly open: _angular_core.WritableSignal<boolean>;
|
|
80
|
+
protected readonly activeIndex: _angular_core.WritableSignal<number>;
|
|
81
|
+
/** `aria-activedescendant` is only meaningful while the listbox is open. */
|
|
82
|
+
protected readonly activeDescendant: _angular_core.Signal<string | null>;
|
|
83
|
+
protected readonly childContext: _angular_core.Signal<{
|
|
84
|
+
/** Currently selected size slug. */
|
|
85
|
+
value: string;
|
|
86
|
+
/** Is the listbox open? */
|
|
87
|
+
open: boolean;
|
|
88
|
+
/** Resolve a slug to its display label. */
|
|
89
|
+
labelFor: (size: string) => string;
|
|
90
|
+
$implicit: ChildArgs;
|
|
91
|
+
}>;
|
|
92
|
+
private typeahead;
|
|
93
|
+
private typeaheadTimer;
|
|
94
|
+
private initialised;
|
|
95
|
+
constructor();
|
|
96
|
+
protected optionId(index: number): string;
|
|
97
|
+
labelFor(size: string): string;
|
|
98
|
+
/** Under zoneless change detection the signal write that un-hides the
|
|
99
|
+
* list has not reached the DOM when a microtask runs, so focusing the
|
|
100
|
+
* still-hidden list fails silently and Escape lands on the button.
|
|
101
|
+
* Flushing detection first is the Angular equivalent of Svelte's
|
|
102
|
+
* synchronous update (same fix the date-time-picker port recorded). */
|
|
103
|
+
private readonly cdr;
|
|
104
|
+
protected toggle(): void;
|
|
105
|
+
/** Open the listbox, activating `startIndex` (default: the selection). */
|
|
106
|
+
openList(startIndex?: number): void;
|
|
107
|
+
/** Close the listbox; `refocus` returns focus to the button. */
|
|
108
|
+
closeList(refocus?: boolean): void;
|
|
109
|
+
protected choose(index: number): void;
|
|
110
|
+
private scrollActiveIntoView;
|
|
111
|
+
private moveActive;
|
|
112
|
+
private runTypeahead;
|
|
113
|
+
protected onButtonKeydown(event: KeyboardEvent): void;
|
|
114
|
+
protected onListKeydown(event: KeyboardEvent): void;
|
|
115
|
+
protected onRootFocusOut(event: FocusEvent): void;
|
|
116
|
+
protected onDocumentClick(event: Event): void;
|
|
117
|
+
private applySize;
|
|
118
|
+
static ɵfac: _angular_core.ɵɵFactoryDeclaration<TextSizePicker, never>;
|
|
119
|
+
static ɵcmp: _angular_core.ɵɵComponentDeclaration<TextSizePicker, "lily-text-size-picker", never, { "label": { "alias": "label"; "required": true; "isSignal": true; }; "sizes": { "alias": "sizes"; "required": true; "isSignal": true; }; "value": { "alias": "value"; "required": false; "isSignal": true; }; "defaultValue": { "alias": "defaultValue"; "required": false; "isSignal": true; }; "storageKey": { "alias": "storageKey"; "required": false; "isSignal": true; }; "name": { "alias": "name"; "required": false; "isSignal": true; }; "target": { "alias": "target"; "required": false; "isSignal": true; }; "sizeLabels": { "alias": "sizeLabels"; "required": false; "isSignal": true; }; "className": { "alias": "className"; "required": false; "isSignal": true; }; }, { "value": "valueChange"; "sizeChange": "sizeChange"; }, ["iconTemplate"], never, true, never>;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export { TextSizePicker, TextSizePickerIcon, nextTextSizePickerId, sizeName };
|
|
123
|
+
export type { ChildArgs };
|
|
124
|
+
//# sourceMappingURL=lilydesignsystem-angular-text-size-picker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lilydesignsystem-angular-text-size-picker.d.ts","sources":["../../text-size-picker.component.ts"],"mappings":";;;AAoBA;;;;;;;;;AASG;AAEH;AACM,KAAM,SAAS;;;;;;AAMnB;;AAGF;;;;AAIG;AACH,iBAAgB,QAAQ;AAQxB;AACA,iBAAgB,oBAAoB;AAKpC;;;;;;;;;;;;AAYG;AACH,cAIa,kBAAkB;AAC7B,wCACQ,kBAAkB,0BAEf,SAAS;mBAAgB,SAAS;AAAE;oDAJpC,kBAAkB;sDAAlB,kBAAkB;AAO9B;AAED;;;;;;;;;AASG;AACH,cA6Ea,cAAc;oBACX,aAAA,CAAA,WAAA;oBACA,aAAA,CAAA,WAAA;oBACA,aAAA,CAAA,WAAA;2BACO,aAAA,CAAA,WAAA;yBACF,aAAA,CAAA,WAAA;mBACN,aAAA,CAAA,WAAA;qBACE,aAAA,CAAA,WAAA,CAAA,WAAA;yBACI,aAAA,CAAA,WAAA,CAAA,MAAA;wBACD,aAAA,CAAA,WAAA;yBACC,aAAA,CAAA,gBAAA;;AAGnB,qCAA+B,aAAA,CAAA,MAAA,CAAA,WAAA;AAE/B;AAEA;AAEA;AAGA;AACA;AAEA,6BAAuB,aAAA,CAAA,cAAA;AACvB,oCAA8B,aAAA,CAAA,cAAA;;AAG9B,yCAAmC,aAAA,CAAA,MAAA;qCAKJ,aAAA,CAAA,MAAA;;;;;;AAzKrB;;AAgLP;;;;;AA+CH;AAIA;AAUA;;;;AAIuE;AACvE;;;AAQA;;AAqBA;AAOA;AAMA;AASA;AAUA;AA+BA,qCAAiC,aAAa;AAe9C,mCAA+B,aAAa;AA+D5C,oCAAgC,UAAU;AAM1C,qCAAiC,KAAK;AAUtC;oDArSW,cAAc;sDAAd,cAAc;AAsT1B;;","names":[]}
|