@lilydesignsystem/angular-motion-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.
|
@@ -0,0 +1,536 @@
|
|
|
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 motion slug to its display label: each hyphen-separated
|
|
7
|
+
* word title-cased, so a slug like "no-preference" renders as
|
|
8
|
+
* "No Preference". Mirrors `sizeName` in text-size-picker and
|
|
9
|
+
* `themeName` in theme-picker.
|
|
10
|
+
*/
|
|
11
|
+
function motionName(motion) {
|
|
12
|
+
return motion
|
|
13
|
+
.split("-")
|
|
14
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
15
|
+
.join(" ");
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* True when the platform reports a preference for reduced motion.
|
|
19
|
+
* SSR-safe: `window`/`matchMedia` are absent on the server, so this
|
|
20
|
+
* resolves to `false` there and the client re-derives it on mount.
|
|
21
|
+
*/
|
|
22
|
+
function prefersReducedMotion() {
|
|
23
|
+
if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
let uid = 0;
|
|
34
|
+
/** Stable per-instance id prefix; SSR-safe (no Math.random / Date.now). */
|
|
35
|
+
function nextMotionPickerId() {
|
|
36
|
+
uid += 1;
|
|
37
|
+
return `motion-picker-${uid}`;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Optional marker for the projected icon template. Gives consumers typed
|
|
41
|
+
* `let-` variables:
|
|
42
|
+
*
|
|
43
|
+
* ```html
|
|
44
|
+
* <lily-motion-picker ...>
|
|
45
|
+
* <ng-template lilyMotionPickerIcon let-args>{{ args.labelFor(args.value) }}</ng-template>
|
|
46
|
+
* </lily-motion-picker>
|
|
47
|
+
* ```
|
|
48
|
+
*
|
|
49
|
+
* The component queries any projected `<ng-template>`, so the marker is
|
|
50
|
+
* for type-checking and readability, not for matching.
|
|
51
|
+
*/
|
|
52
|
+
class MotionPickerIcon {
|
|
53
|
+
static ngTemplateContextGuard(_dir, _ctx) {
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: MotionPickerIcon, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
57
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.4", type: MotionPickerIcon, isStandalone: true, selector: "ng-template[lilyMotionPickerIcon]", ngImport: i0 }); }
|
|
58
|
+
}
|
|
59
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: MotionPickerIcon, decorators: [{
|
|
60
|
+
type: Directive,
|
|
61
|
+
args: [{
|
|
62
|
+
selector: "ng-template[lilyMotionPickerIcon]",
|
|
63
|
+
standalone: true,
|
|
64
|
+
}]
|
|
65
|
+
}] });
|
|
66
|
+
/**
|
|
67
|
+
* MotionPicker — `data-motion` reduced-motion picker.
|
|
68
|
+
*
|
|
69
|
+
* Renders an icon button that opens a WAI-ARIA APG listbox of motion
|
|
70
|
+
* slugs. On every change the component sets `data-motion="{slug}"` on
|
|
71
|
+
* the document root (or on a consumer-supplied target), with optional
|
|
72
|
+
* `localStorage` persistence. Its initial value defers to the
|
|
73
|
+
* platform's `(prefers-reduced-motion: reduce)` media query before
|
|
74
|
+
* falling back to a fixed default — the one behaviour difference from
|
|
75
|
+
* its `theme-picker`/`text-size-picker` siblings. Ships no CSS; the
|
|
76
|
+
* consumer decides what `[data-motion="reduce"]` actually suppresses.
|
|
77
|
+
* See `spec/index.md` for the full contract.
|
|
78
|
+
*/
|
|
79
|
+
class MotionPicker {
|
|
80
|
+
constructor() {
|
|
81
|
+
this.label = input.required(/* @ts-ignore */
|
|
82
|
+
...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
|
|
83
|
+
this.motions = input.required(/* @ts-ignore */
|
|
84
|
+
...(ngDevMode ? [{ debugName: "motions" }] : /* istanbul ignore next */ []));
|
|
85
|
+
this.value = model("", /* @ts-ignore */
|
|
86
|
+
...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
|
|
87
|
+
this.defaultValue = input("", /* @ts-ignore */
|
|
88
|
+
...(ngDevMode ? [{ debugName: "defaultValue" }] : /* istanbul ignore next */ []));
|
|
89
|
+
this.storageKey = input("", /* @ts-ignore */
|
|
90
|
+
...(ngDevMode ? [{ debugName: "storageKey" }] : /* istanbul ignore next */ []));
|
|
91
|
+
this.name = input("motion", /* @ts-ignore */
|
|
92
|
+
...(ngDevMode ? [{ debugName: "name" }] : /* istanbul ignore next */ []));
|
|
93
|
+
this.target = input(null, /* @ts-ignore */
|
|
94
|
+
...(ngDevMode ? [{ debugName: "target" }] : /* istanbul ignore next */ []));
|
|
95
|
+
this.motionLabels = input({}, /* @ts-ignore */
|
|
96
|
+
...(ngDevMode ? [{ debugName: "motionLabels" }] : /* istanbul ignore next */ []));
|
|
97
|
+
this.className = input("", /* @ts-ignore */
|
|
98
|
+
...(ngDevMode ? [{ debugName: "className" }] : /* istanbul ignore next */ []));
|
|
99
|
+
this.motionChange = output();
|
|
100
|
+
/** Projected icon template; replaces the default icon when supplied. */
|
|
101
|
+
this.iconTemplate = contentChild(TemplateRef, /* @ts-ignore */
|
|
102
|
+
...(ngDevMode ? [{ debugName: "iconTemplate" }] : /* istanbul ignore next */ []));
|
|
103
|
+
this.rootRef = viewChild.required("rootEl", /* @ts-ignore */
|
|
104
|
+
...(ngDevMode ? [{ debugName: "rootRef" }] : /* istanbul ignore next */ []));
|
|
105
|
+
this.buttonRef = viewChild.required("buttonEl", /* @ts-ignore */
|
|
106
|
+
...(ngDevMode ? [{ debugName: "buttonRef" }] : /* istanbul ignore next */ []));
|
|
107
|
+
this.listRef = viewChild.required("listEl", /* @ts-ignore */
|
|
108
|
+
...(ngDevMode ? [{ debugName: "listRef" }] : /* istanbul ignore next */ []));
|
|
109
|
+
this.baseId = nextMotionPickerId();
|
|
110
|
+
this.listId = `${this.baseId}-list`;
|
|
111
|
+
this.open = signal(false, /* @ts-ignore */
|
|
112
|
+
...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
|
|
113
|
+
this.activeIndex = signal(-1, /* @ts-ignore */
|
|
114
|
+
...(ngDevMode ? [{ debugName: "activeIndex" }] : /* istanbul ignore next */ []));
|
|
115
|
+
/** `aria-activedescendant` is only meaningful while the listbox is open. */
|
|
116
|
+
this.activeDescendant = computed(() => {
|
|
117
|
+
const i = this.activeIndex();
|
|
118
|
+
return this.open() && i >= 0 ? this.optionId(i) : null;
|
|
119
|
+
}, /* @ts-ignore */
|
|
120
|
+
...(ngDevMode ? [{ debugName: "activeDescendant" }] : /* istanbul ignore next */ []));
|
|
121
|
+
this.childContext = computed(() => {
|
|
122
|
+
const args = {
|
|
123
|
+
value: this.value(),
|
|
124
|
+
open: this.open(),
|
|
125
|
+
labelFor: (motion) => this.labelFor(motion),
|
|
126
|
+
};
|
|
127
|
+
return { $implicit: args, ...args };
|
|
128
|
+
}, /* @ts-ignore */
|
|
129
|
+
...(ngDevMode ? [{ debugName: "childContext" }] : /* istanbul ignore next */ []));
|
|
130
|
+
// Typeahead buffer: APG listbox behaviour. Reset after a pause.
|
|
131
|
+
this.typeahead = "";
|
|
132
|
+
this.initialised = false;
|
|
133
|
+
// ---------------------------------------------------------------
|
|
134
|
+
// Open / close
|
|
135
|
+
// ---------------------------------------------------------------
|
|
136
|
+
/** Under zoneless change detection the signal write that un-hides the
|
|
137
|
+
* list has not reached the DOM when a microtask runs, so focusing the
|
|
138
|
+
* still-hidden list fails silently and Escape lands on the button.
|
|
139
|
+
* Flushing detection first is the Angular equivalent of Svelte's
|
|
140
|
+
* synchronous update (same fix the date-time-picker port recorded). */
|
|
141
|
+
this.cdr = inject(ChangeDetectorRef);
|
|
142
|
+
inject(DestroyRef).onDestroy(() => clearTimeout(this.typeaheadTimer));
|
|
143
|
+
effect(() => {
|
|
144
|
+
const current = this.value();
|
|
145
|
+
if (!this.initialised) {
|
|
146
|
+
this.initialised = true;
|
|
147
|
+
let initial = current;
|
|
148
|
+
const sk = this.storageKey();
|
|
149
|
+
if (!initial && sk) {
|
|
150
|
+
try {
|
|
151
|
+
initial =
|
|
152
|
+
(typeof localStorage !== "undefined"
|
|
153
|
+
? localStorage.getItem(sk)
|
|
154
|
+
: null) ?? "";
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
// ignore privacy errors
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (!initial) {
|
|
161
|
+
// Unlike text-size-picker's "medium" default, motion has a
|
|
162
|
+
// real external signal to defer to: the platform's own
|
|
163
|
+
// (prefers-reduced-motion: reduce) media query.
|
|
164
|
+
const motions = this.motions();
|
|
165
|
+
const dv = this.defaultValue();
|
|
166
|
+
const osPreferred = prefersReducedMotion() ? "reduce" : "no-preference";
|
|
167
|
+
initial =
|
|
168
|
+
dv ||
|
|
169
|
+
(motions.includes(osPreferred) ? osPreferred : undefined) ||
|
|
170
|
+
motions[0] ||
|
|
171
|
+
"";
|
|
172
|
+
}
|
|
173
|
+
if (initial && initial !== current) {
|
|
174
|
+
this.value.set(initial);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (current)
|
|
179
|
+
this.applyMotion(current);
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
optionId(index) {
|
|
183
|
+
return `${this.baseId}-option-${index}`;
|
|
184
|
+
}
|
|
185
|
+
labelFor(motion) {
|
|
186
|
+
const labels = this.motionLabels();
|
|
187
|
+
if (motion in labels)
|
|
188
|
+
return labels[motion];
|
|
189
|
+
return motionName(motion);
|
|
190
|
+
}
|
|
191
|
+
toggle() {
|
|
192
|
+
if (this.open())
|
|
193
|
+
this.closeList();
|
|
194
|
+
else
|
|
195
|
+
this.openList();
|
|
196
|
+
}
|
|
197
|
+
/** Open the listbox, activating `startIndex` (default: the selection). */
|
|
198
|
+
openList(startIndex) {
|
|
199
|
+
const selected = this.motions().indexOf(this.value());
|
|
200
|
+
// An empty list has no option to activate; -1 keeps
|
|
201
|
+
// aria-activedescendant off rather than pointing at an id that
|
|
202
|
+
// does not exist.
|
|
203
|
+
this.activeIndex.set(this.motions().length === 0
|
|
204
|
+
? -1
|
|
205
|
+
: (startIndex ?? (selected >= 0 ? selected : 0)));
|
|
206
|
+
this.open.set(true);
|
|
207
|
+
// Focus moves to the listbox; the active option is conveyed via
|
|
208
|
+
// aria-activedescendant, per the APG listbox pattern.
|
|
209
|
+
this.cdr.detectChanges();
|
|
210
|
+
queueMicrotask(() => {
|
|
211
|
+
this.listRef().nativeElement.focus({ preventScroll: true });
|
|
212
|
+
this.scrollActiveIntoView();
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
/** Close the listbox; `refocus` returns focus to the button. */
|
|
216
|
+
closeList(refocus = true) {
|
|
217
|
+
if (!this.open())
|
|
218
|
+
return;
|
|
219
|
+
this.open.set(false);
|
|
220
|
+
this.activeIndex.set(-1);
|
|
221
|
+
if (refocus)
|
|
222
|
+
queueMicrotask(() => this.buttonRef().nativeElement.focus({ preventScroll: true }));
|
|
223
|
+
}
|
|
224
|
+
choose(index) {
|
|
225
|
+
const slug = this.motions()[index];
|
|
226
|
+
if (slug)
|
|
227
|
+
this.value.set(slug);
|
|
228
|
+
this.closeList();
|
|
229
|
+
}
|
|
230
|
+
scrollActiveIntoView() {
|
|
231
|
+
const i = this.activeIndex();
|
|
232
|
+
if (i < 0)
|
|
233
|
+
return;
|
|
234
|
+
const el = this.listRef().nativeElement.children[i];
|
|
235
|
+
// jsdom does not implement scrollIntoView; call it only if present.
|
|
236
|
+
el?.scrollIntoView?.({ block: "nearest" });
|
|
237
|
+
}
|
|
238
|
+
moveActive(delta) {
|
|
239
|
+
const count = this.motions().length;
|
|
240
|
+
if (count === 0)
|
|
241
|
+
return;
|
|
242
|
+
// Clamp rather than wrap, per the APG listbox pattern.
|
|
243
|
+
this.activeIndex.set(Math.min(Math.max(this.activeIndex() + delta, 0), count - 1));
|
|
244
|
+
this.scrollActiveIntoView();
|
|
245
|
+
}
|
|
246
|
+
runTypeahead(char) {
|
|
247
|
+
const lower = char.toLowerCase();
|
|
248
|
+
// APG listbox typeahead: a single character moves to the NEXT
|
|
249
|
+
// option starting with it, and repeating that character keeps
|
|
250
|
+
// cycling. Only a buffer of differing characters refines the
|
|
251
|
+
// match, and that buffer stays anchored on the active option.
|
|
252
|
+
const sameCharRun = this.typeahead === "" || [...this.typeahead].every((c) => c === lower);
|
|
253
|
+
this.typeahead += lower;
|
|
254
|
+
clearTimeout(this.typeaheadTimer);
|
|
255
|
+
this.typeaheadTimer = setTimeout(() => (this.typeahead = ""), 500);
|
|
256
|
+
const query = sameCharRun ? lower : this.typeahead;
|
|
257
|
+
const motions = this.motions();
|
|
258
|
+
const anchor = this.activeIndex() < 0 ? 0 : this.activeIndex();
|
|
259
|
+
const start = sameCharRun ? anchor + 1 : anchor;
|
|
260
|
+
// Search forward, wrapping once — typeahead wraps even though the
|
|
261
|
+
// arrows clamp, or options above the cursor would be untypable.
|
|
262
|
+
for (let n = 0; n < motions.length; n++) {
|
|
263
|
+
const i = (start + n) % motions.length;
|
|
264
|
+
if (this.labelFor(motions[i]).toLowerCase().startsWith(query)) {
|
|
265
|
+
this.activeIndex.set(i);
|
|
266
|
+
this.scrollActiveIntoView();
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
// ---------------------------------------------------------------
|
|
272
|
+
// Keyboard
|
|
273
|
+
// ---------------------------------------------------------------
|
|
274
|
+
onButtonKeydown(event) {
|
|
275
|
+
switch (event.key) {
|
|
276
|
+
case "ArrowDown":
|
|
277
|
+
case "Enter":
|
|
278
|
+
case " ":
|
|
279
|
+
event.preventDefault();
|
|
280
|
+
this.openList();
|
|
281
|
+
break;
|
|
282
|
+
case "ArrowUp":
|
|
283
|
+
event.preventDefault();
|
|
284
|
+
this.openList(this.motions().length - 1);
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
onListKeydown(event) {
|
|
289
|
+
switch (event.key) {
|
|
290
|
+
case "ArrowDown":
|
|
291
|
+
event.preventDefault();
|
|
292
|
+
this.moveActive(1);
|
|
293
|
+
break;
|
|
294
|
+
case "ArrowUp":
|
|
295
|
+
event.preventDefault();
|
|
296
|
+
this.moveActive(-1);
|
|
297
|
+
break;
|
|
298
|
+
case "Home":
|
|
299
|
+
event.preventDefault();
|
|
300
|
+
this.activeIndex.set(0);
|
|
301
|
+
this.scrollActiveIntoView();
|
|
302
|
+
break;
|
|
303
|
+
case "End":
|
|
304
|
+
event.preventDefault();
|
|
305
|
+
this.activeIndex.set(this.motions().length - 1);
|
|
306
|
+
this.scrollActiveIntoView();
|
|
307
|
+
break;
|
|
308
|
+
case "Enter":
|
|
309
|
+
case " ":
|
|
310
|
+
event.preventDefault();
|
|
311
|
+
if (this.activeIndex() >= 0)
|
|
312
|
+
this.choose(this.activeIndex());
|
|
313
|
+
break;
|
|
314
|
+
case "Escape":
|
|
315
|
+
event.preventDefault();
|
|
316
|
+
this.closeList();
|
|
317
|
+
break;
|
|
318
|
+
case "PageUp":
|
|
319
|
+
event.preventDefault();
|
|
320
|
+
this.moveActive(-10);
|
|
321
|
+
break;
|
|
322
|
+
case "PageDown":
|
|
323
|
+
// ±10, clamped: an APG-optional key for long motion lists.
|
|
324
|
+
event.preventDefault();
|
|
325
|
+
this.moveActive(10);
|
|
326
|
+
break;
|
|
327
|
+
case "Tab":
|
|
328
|
+
// Tab moves on — but focus goes to the button FIRST, without
|
|
329
|
+
// cancelling the key. Hiding the focused list drops focus to
|
|
330
|
+
// <body>, and the browser then computes the default Tab move
|
|
331
|
+
// from the top of the document, so tabbing out of an open
|
|
332
|
+
// picker teleported the user to the page's first tab stop.
|
|
333
|
+
// From the button, the default Tab lands exactly where leaving
|
|
334
|
+
// the picker should. The button always exists, so no
|
|
335
|
+
// detectChanges is needed before the focus move; guard the
|
|
336
|
+
// METHOD because jsdom-shaped hosts may not implement it.
|
|
337
|
+
this.buttonRef().nativeElement.focus?.({ preventScroll: true });
|
|
338
|
+
this.closeList(false);
|
|
339
|
+
break;
|
|
340
|
+
default:
|
|
341
|
+
if (event.key.length === 1 &&
|
|
342
|
+
!event.ctrlKey &&
|
|
343
|
+
!event.metaKey &&
|
|
344
|
+
!event.altKey) {
|
|
345
|
+
this.runTypeahead(event.key);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
onRootFocusOut(event) {
|
|
350
|
+
const next = event.relatedTarget;
|
|
351
|
+
if (next && this.rootRef().nativeElement.contains(next))
|
|
352
|
+
return;
|
|
353
|
+
this.closeList(false);
|
|
354
|
+
}
|
|
355
|
+
onDocumentClick(event) {
|
|
356
|
+
if (!this.open())
|
|
357
|
+
return;
|
|
358
|
+
const t = event.target;
|
|
359
|
+
if (t && !this.rootRef().nativeElement.contains(t))
|
|
360
|
+
this.closeList(false);
|
|
361
|
+
}
|
|
362
|
+
// ---------------------------------------------------------------
|
|
363
|
+
// Apply
|
|
364
|
+
// ---------------------------------------------------------------
|
|
365
|
+
applyMotion(slug) {
|
|
366
|
+
if (typeof document === "undefined" || !slug)
|
|
367
|
+
return;
|
|
368
|
+
(this.target() ?? document.documentElement).setAttribute("data-motion", slug);
|
|
369
|
+
const sk = this.storageKey();
|
|
370
|
+
if (sk) {
|
|
371
|
+
try {
|
|
372
|
+
localStorage.setItem(sk, slug);
|
|
373
|
+
}
|
|
374
|
+
catch {
|
|
375
|
+
// ignore quota / privacy errors
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
this.motionChange.emit(slug);
|
|
379
|
+
}
|
|
380
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: MotionPicker, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
381
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.4", type: MotionPicker, isStandalone: true, selector: "lily-motion-picker", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, motions: { classPropertyName: "motions", publicName: "motions", 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 }, motionLabels: { classPropertyName: "motionLabels", publicName: "motionLabels", isSignal: true, isRequired: false, transformFunction: null }, className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", motionChange: "motionChange" }, 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: `
|
|
382
|
+
<div
|
|
383
|
+
#rootEl
|
|
384
|
+
class="motion-picker {{ className() }}"
|
|
385
|
+
(focusout)="onRootFocusOut($event)"
|
|
386
|
+
>
|
|
387
|
+
<input type="hidden" [name]="name()" [value]="value()" />
|
|
388
|
+
|
|
389
|
+
<button
|
|
390
|
+
#buttonEl
|
|
391
|
+
type="button"
|
|
392
|
+
class="motion-picker-button"
|
|
393
|
+
[attr.aria-label]="label() || null"
|
|
394
|
+
aria-haspopup="listbox"
|
|
395
|
+
[attr.aria-expanded]="open()"
|
|
396
|
+
[attr.aria-controls]="listId"
|
|
397
|
+
(click)="toggle()"
|
|
398
|
+
(keydown)="onButtonKeydown($event)"
|
|
399
|
+
>
|
|
400
|
+
@if (iconTemplate(); as tpl) {
|
|
401
|
+
<ng-container
|
|
402
|
+
[ngTemplateOutlet]="tpl"
|
|
403
|
+
[ngTemplateOutletContext]="childContext()"
|
|
404
|
+
/>
|
|
405
|
+
} @else {
|
|
406
|
+
<svg
|
|
407
|
+
class="motion-picker-icon"
|
|
408
|
+
viewBox="0 0 16 16"
|
|
409
|
+
width="1.05rem"
|
|
410
|
+
height="1.05rem"
|
|
411
|
+
aria-hidden="true"
|
|
412
|
+
fill="none"
|
|
413
|
+
stroke="currentColor"
|
|
414
|
+
stroke-width="1.6"
|
|
415
|
+
stroke-linecap="round"
|
|
416
|
+
stroke-linejoin="round"
|
|
417
|
+
>
|
|
418
|
+
<path d="M5 3v10M11 3v10" />
|
|
419
|
+
</svg>
|
|
420
|
+
}
|
|
421
|
+
</button>
|
|
422
|
+
|
|
423
|
+
<ul
|
|
424
|
+
#listEl
|
|
425
|
+
class="motion-picker-list"
|
|
426
|
+
[id]="listId"
|
|
427
|
+
role="listbox"
|
|
428
|
+
[attr.aria-label]="label() || null"
|
|
429
|
+
[attr.aria-activedescendant]="activeDescendant()"
|
|
430
|
+
tabindex="-1"
|
|
431
|
+
[attr.hidden]="open() ? null : ''"
|
|
432
|
+
(keydown)="onListKeydown($event)"
|
|
433
|
+
>
|
|
434
|
+
@for (motion of motions(); track motion; let i = $index) {
|
|
435
|
+
<li
|
|
436
|
+
class="motion-picker-option"
|
|
437
|
+
[id]="optionId(i)"
|
|
438
|
+
role="option"
|
|
439
|
+
[attr.aria-selected]="motion === value()"
|
|
440
|
+
[attr.data-active]="i === activeIndex() ? '' : null"
|
|
441
|
+
(click)="choose(i)"
|
|
442
|
+
>
|
|
443
|
+
{{ labelFor(motion) }}
|
|
444
|
+
</li>
|
|
445
|
+
}
|
|
446
|
+
</ul>
|
|
447
|
+
</div>
|
|
448
|
+
`, isInline: true, dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
449
|
+
}
|
|
450
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: MotionPicker, decorators: [{
|
|
451
|
+
type: Component,
|
|
452
|
+
args: [{
|
|
453
|
+
selector: "lily-motion-picker",
|
|
454
|
+
standalone: true,
|
|
455
|
+
imports: [NgTemplateOutlet],
|
|
456
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
457
|
+
host: {
|
|
458
|
+
"(document:click)": "onDocumentClick($event)",
|
|
459
|
+
},
|
|
460
|
+
template: `
|
|
461
|
+
<div
|
|
462
|
+
#rootEl
|
|
463
|
+
class="motion-picker {{ className() }}"
|
|
464
|
+
(focusout)="onRootFocusOut($event)"
|
|
465
|
+
>
|
|
466
|
+
<input type="hidden" [name]="name()" [value]="value()" />
|
|
467
|
+
|
|
468
|
+
<button
|
|
469
|
+
#buttonEl
|
|
470
|
+
type="button"
|
|
471
|
+
class="motion-picker-button"
|
|
472
|
+
[attr.aria-label]="label() || null"
|
|
473
|
+
aria-haspopup="listbox"
|
|
474
|
+
[attr.aria-expanded]="open()"
|
|
475
|
+
[attr.aria-controls]="listId"
|
|
476
|
+
(click)="toggle()"
|
|
477
|
+
(keydown)="onButtonKeydown($event)"
|
|
478
|
+
>
|
|
479
|
+
@if (iconTemplate(); as tpl) {
|
|
480
|
+
<ng-container
|
|
481
|
+
[ngTemplateOutlet]="tpl"
|
|
482
|
+
[ngTemplateOutletContext]="childContext()"
|
|
483
|
+
/>
|
|
484
|
+
} @else {
|
|
485
|
+
<svg
|
|
486
|
+
class="motion-picker-icon"
|
|
487
|
+
viewBox="0 0 16 16"
|
|
488
|
+
width="1.05rem"
|
|
489
|
+
height="1.05rem"
|
|
490
|
+
aria-hidden="true"
|
|
491
|
+
fill="none"
|
|
492
|
+
stroke="currentColor"
|
|
493
|
+
stroke-width="1.6"
|
|
494
|
+
stroke-linecap="round"
|
|
495
|
+
stroke-linejoin="round"
|
|
496
|
+
>
|
|
497
|
+
<path d="M5 3v10M11 3v10" />
|
|
498
|
+
</svg>
|
|
499
|
+
}
|
|
500
|
+
</button>
|
|
501
|
+
|
|
502
|
+
<ul
|
|
503
|
+
#listEl
|
|
504
|
+
class="motion-picker-list"
|
|
505
|
+
[id]="listId"
|
|
506
|
+
role="listbox"
|
|
507
|
+
[attr.aria-label]="label() || null"
|
|
508
|
+
[attr.aria-activedescendant]="activeDescendant()"
|
|
509
|
+
tabindex="-1"
|
|
510
|
+
[attr.hidden]="open() ? null : ''"
|
|
511
|
+
(keydown)="onListKeydown($event)"
|
|
512
|
+
>
|
|
513
|
+
@for (motion of motions(); track motion; let i = $index) {
|
|
514
|
+
<li
|
|
515
|
+
class="motion-picker-option"
|
|
516
|
+
[id]="optionId(i)"
|
|
517
|
+
role="option"
|
|
518
|
+
[attr.aria-selected]="motion === value()"
|
|
519
|
+
[attr.data-active]="i === activeIndex() ? '' : null"
|
|
520
|
+
(click)="choose(i)"
|
|
521
|
+
>
|
|
522
|
+
{{ labelFor(motion) }}
|
|
523
|
+
</li>
|
|
524
|
+
}
|
|
525
|
+
</ul>
|
|
526
|
+
</div>
|
|
527
|
+
`,
|
|
528
|
+
}]
|
|
529
|
+
}], ctorParameters: () => [], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], motions: [{ type: i0.Input, args: [{ isSignal: true, alias: "motions", 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 }] }], motionLabels: [{ type: i0.Input, args: [{ isSignal: true, alias: "motionLabels", required: false }] }], className: [{ type: i0.Input, args: [{ isSignal: true, alias: "className", required: false }] }], motionChange: [{ type: i0.Output, args: ["motionChange"] }], 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 }] }] } });
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Generated bundle index. Do not edit.
|
|
533
|
+
*/
|
|
534
|
+
|
|
535
|
+
export { MotionPicker, MotionPickerIcon, motionName, nextMotionPickerId, prefersReducedMotion };
|
|
536
|
+
//# sourceMappingURL=lilydesignsystem-angular-motion-picker.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lilydesignsystem-angular-motion-picker.mjs","sources":["../../motion-picker.component.ts","../../lilydesignsystem-angular-motion-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 (two pause bars), not a Unicode\n * character. Reversed 2026-09-16 from the font-dependent-glyph\n * convention (was U+23F8 PAUSE SIGN + U+FE0E, exported as `PAUSE_SIGN`\n * — removed, not renamed). \"Stop the moving parts\" still reads\n * directly from two bars; a bundled outline SVG matches the other four\n * picker icons as one consistent visual family regardless of the\n * consumer's fonts.\n */\n\n/** Context passed to a custom icon `<ng-template>` (the button icon). */\nexport type ChildArgs = {\n /** Currently selected motion slug. */\n value: string;\n /** Is the listbox open? */\n open: boolean;\n /** Resolve a slug to its display label. */\n labelFor: (motion: string) => string;\n};\n\n/**\n * Resolve a motion slug to its display label: each hyphen-separated\n * word title-cased, so a slug like \"no-preference\" renders as\n * \"No Preference\". Mirrors `sizeName` in text-size-picker and\n * `themeName` in theme-picker.\n */\nexport function motionName(motion: string): string {\n return motion\n .split(\"-\")\n .map((word) => word.charAt(0).toUpperCase() + word.slice(1))\n .join(\" \");\n}\n\n/**\n * True when the platform reports a preference for reduced motion.\n * SSR-safe: `window`/`matchMedia` are absent on the server, so this\n * resolves to `false` there and the client re-derives it on mount.\n */\nexport function prefersReducedMotion(): boolean {\n if (typeof window === \"undefined\" || typeof window.matchMedia !== \"function\") {\n return false;\n }\n try {\n return window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n } catch {\n return false;\n }\n}\n\nlet uid = 0;\n/** Stable per-instance id prefix; SSR-safe (no Math.random / Date.now). */\nexport function nextMotionPickerId(): string {\n uid += 1;\n return `motion-picker-${uid}`;\n}\n\n/**\n * Optional marker for the projected icon template. Gives consumers typed\n * `let-` variables:\n *\n * ```html\n * <lily-motion-picker ...>\n * <ng-template lilyMotionPickerIcon let-args>{{ args.labelFor(args.value) }}</ng-template>\n * </lily-motion-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[lilyMotionPickerIcon]\",\n standalone: true,\n})\nexport class MotionPickerIcon {\n static ngTemplateContextGuard(\n _dir: MotionPickerIcon,\n _ctx: unknown,\n ): _ctx is ChildArgs & { $implicit: ChildArgs } {\n return true;\n }\n}\n\n/**\n * MotionPicker — `data-motion` reduced-motion picker.\n *\n * Renders an icon button that opens a WAI-ARIA APG listbox of motion\n * slugs. On every change the component sets `data-motion=\"{slug}\"` on\n * the document root (or on a consumer-supplied target), with optional\n * `localStorage` persistence. Its initial value defers to the\n * platform's `(prefers-reduced-motion: reduce)` media query before\n * falling back to a fixed default — the one behaviour difference from\n * its `theme-picker`/`text-size-picker` siblings. Ships no CSS; the\n * consumer decides what `[data-motion=\"reduce\"]` actually suppresses.\n * See `spec/index.md` for the full contract.\n */\n@Component({\n selector: \"lily-motion-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=\"motion-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=\"motion-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=\"motion-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=\"M5 3v10M11 3v10\" />\n </svg>\n }\n </button>\n\n <ul\n #listEl\n class=\"motion-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 (motion of motions(); track motion; let i = $index) {\n <li\n class=\"motion-picker-option\"\n [id]=\"optionId(i)\"\n role=\"option\"\n [attr.aria-selected]=\"motion === value()\"\n [attr.data-active]=\"i === activeIndex() ? '' : null\"\n (click)=\"choose(i)\"\n >\n {{ labelFor(motion) }}\n </li>\n }\n </ul>\n </div>\n `,\n})\nexport class MotionPicker {\n readonly label = input.required<string>();\n readonly motions = input.required<string[]>();\n readonly value = model<string>(\"\");\n readonly defaultValue = input<string>(\"\");\n readonly storageKey = input<string>(\"\");\n readonly name = input<string>(\"motion\");\n readonly target = input<HTMLElement | null>(null);\n readonly motionLabels = input<Record<string, string>>({});\n readonly className = input<string>(\"\");\n readonly motionChange = 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 = nextMotionPickerId();\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: (motion: string) => this.labelFor(motion),\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 // Unlike text-size-picker's \"medium\" default, motion has a\n // real external signal to defer to: the platform's own\n // (prefers-reduced-motion: reduce) media query.\n const motions = this.motions();\n const dv = this.defaultValue();\n const osPreferred = prefersReducedMotion() ? \"reduce\" : \"no-preference\";\n initial =\n dv ||\n (motions.includes(osPreferred) ? osPreferred : undefined) ||\n motions[0] ||\n \"\";\n }\n\n if (initial && initial !== current) {\n this.value.set(initial);\n return;\n }\n }\n\n if (current) this.applyMotion(current);\n });\n }\n\n protected optionId(index: number): string {\n return `${this.baseId}-option-${index}`;\n }\n\n labelFor(motion: string): string {\n const labels = this.motionLabels();\n if (motion in labels) return labels[motion];\n return motionName(motion);\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.motions().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.motions().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.motions()[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.motions().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 motions = this.motions();\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 < motions.length; n++) {\n const i = (start + n) % motions.length;\n if (this.labelFor(motions[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.motions().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.motions().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 motion 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 applyMotion(slug: string): void {\n if (typeof document === \"undefined\" || !slug) return;\n (this.target() ?? document.documentElement).setAttribute(\n \"data-motion\",\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.motionChange.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,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lilydesignsystem/angular-motion-picker",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"engines": {
|
|
5
|
+
"node": "=26"
|
|
6
|
+
},
|
|
7
|
+
"description": "Lily Design System Angular 20 motion (reduced-motion) picker: an icon button opening an APG listbox that sets data-motion on the document, defaulting to the OS's own prefers-reduced-motion signal. 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
|
+
"motion",
|
|
19
|
+
"reduced-motion",
|
|
20
|
+
"picker"
|
|
21
|
+
],
|
|
22
|
+
"author": "Joel Parker Henderson <joel@joelparkerhenderson.com>",
|
|
23
|
+
"license": "MIT OR Apache-2.0 OR GPL-2.0-only OR GPL-3.0-only OR BSD-3-Clause",
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/LilyDesignSystem/lily-design-system-angular-helpers.git",
|
|
27
|
+
"directory": "lily-design-system-angular-motion-picker"
|
|
28
|
+
},
|
|
29
|
+
"homepage": "https://lilydesignsystem.com/",
|
|
30
|
+
"bugs": {
|
|
31
|
+
"url": "https://github.com/LilyDesignSystem/lily-design-system/issues"
|
|
32
|
+
},
|
|
33
|
+
"module": "fesm2022/lilydesignsystem-angular-motion-picker.mjs",
|
|
34
|
+
"typings": "types/lilydesignsystem-angular-motion-picker.d.ts",
|
|
35
|
+
"exports": {
|
|
36
|
+
"./package.json": {
|
|
37
|
+
"default": "./package.json"
|
|
38
|
+
},
|
|
39
|
+
".": {
|
|
40
|
+
"types": "./types/lilydesignsystem-angular-motion-picker.d.ts",
|
|
41
|
+
"default": "./fesm2022/lilydesignsystem-angular-motion-picker.mjs"
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"type": "module",
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"tslib": "^2.3.0"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import * as _angular_core from '@angular/core';
|
|
2
|
+
import { TemplateRef } from '@angular/core';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Default button icon: a bundled SVG (two pause bars), not a Unicode
|
|
6
|
+
* character. Reversed 2026-09-16 from the font-dependent-glyph
|
|
7
|
+
* convention (was U+23F8 PAUSE SIGN + U+FE0E, exported as `PAUSE_SIGN`
|
|
8
|
+
* — removed, not renamed). "Stop the moving parts" still reads
|
|
9
|
+
* directly from two bars; a bundled outline SVG matches the other four
|
|
10
|
+
* picker icons as one consistent visual family regardless of the
|
|
11
|
+
* consumer's fonts.
|
|
12
|
+
*/
|
|
13
|
+
/** Context passed to a custom icon `<ng-template>` (the button icon). */
|
|
14
|
+
type ChildArgs = {
|
|
15
|
+
/** Currently selected motion slug. */
|
|
16
|
+
value: string;
|
|
17
|
+
/** Is the listbox open? */
|
|
18
|
+
open: boolean;
|
|
19
|
+
/** Resolve a slug to its display label. */
|
|
20
|
+
labelFor: (motion: string) => string;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Resolve a motion slug to its display label: each hyphen-separated
|
|
24
|
+
* word title-cased, so a slug like "no-preference" renders as
|
|
25
|
+
* "No Preference". Mirrors `sizeName` in text-size-picker and
|
|
26
|
+
* `themeName` in theme-picker.
|
|
27
|
+
*/
|
|
28
|
+
declare function motionName(motion: string): string;
|
|
29
|
+
/**
|
|
30
|
+
* True when the platform reports a preference for reduced motion.
|
|
31
|
+
* SSR-safe: `window`/`matchMedia` are absent on the server, so this
|
|
32
|
+
* resolves to `false` there and the client re-derives it on mount.
|
|
33
|
+
*/
|
|
34
|
+
declare function prefersReducedMotion(): boolean;
|
|
35
|
+
/** Stable per-instance id prefix; SSR-safe (no Math.random / Date.now). */
|
|
36
|
+
declare function nextMotionPickerId(): string;
|
|
37
|
+
/**
|
|
38
|
+
* Optional marker for the projected icon template. Gives consumers typed
|
|
39
|
+
* `let-` variables:
|
|
40
|
+
*
|
|
41
|
+
* ```html
|
|
42
|
+
* <lily-motion-picker ...>
|
|
43
|
+
* <ng-template lilyMotionPickerIcon let-args>{{ args.labelFor(args.value) }}</ng-template>
|
|
44
|
+
* </lily-motion-picker>
|
|
45
|
+
* ```
|
|
46
|
+
*
|
|
47
|
+
* The component queries any projected `<ng-template>`, so the marker is
|
|
48
|
+
* for type-checking and readability, not for matching.
|
|
49
|
+
*/
|
|
50
|
+
declare class MotionPickerIcon {
|
|
51
|
+
static ngTemplateContextGuard(_dir: MotionPickerIcon, _ctx: unknown): _ctx is ChildArgs & {
|
|
52
|
+
$implicit: ChildArgs;
|
|
53
|
+
};
|
|
54
|
+
static ɵfac: _angular_core.ɵɵFactoryDeclaration<MotionPickerIcon, never>;
|
|
55
|
+
static ɵdir: _angular_core.ɵɵDirectiveDeclaration<MotionPickerIcon, "ng-template[lilyMotionPickerIcon]", never, {}, {}, never, never, true, never>;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* MotionPicker — `data-motion` reduced-motion picker.
|
|
59
|
+
*
|
|
60
|
+
* Renders an icon button that opens a WAI-ARIA APG listbox of motion
|
|
61
|
+
* slugs. On every change the component sets `data-motion="{slug}"` on
|
|
62
|
+
* the document root (or on a consumer-supplied target), with optional
|
|
63
|
+
* `localStorage` persistence. Its initial value defers to the
|
|
64
|
+
* platform's `(prefers-reduced-motion: reduce)` media query before
|
|
65
|
+
* falling back to a fixed default — the one behaviour difference from
|
|
66
|
+
* its `theme-picker`/`text-size-picker` siblings. Ships no CSS; the
|
|
67
|
+
* consumer decides what `[data-motion="reduce"]` actually suppresses.
|
|
68
|
+
* See `spec/index.md` for the full contract.
|
|
69
|
+
*/
|
|
70
|
+
declare class MotionPicker {
|
|
71
|
+
readonly label: _angular_core.InputSignal<string>;
|
|
72
|
+
readonly motions: _angular_core.InputSignal<string[]>;
|
|
73
|
+
readonly value: _angular_core.ModelSignal<string>;
|
|
74
|
+
readonly defaultValue: _angular_core.InputSignal<string>;
|
|
75
|
+
readonly storageKey: _angular_core.InputSignal<string>;
|
|
76
|
+
readonly name: _angular_core.InputSignal<string>;
|
|
77
|
+
readonly target: _angular_core.InputSignal<HTMLElement | null>;
|
|
78
|
+
readonly motionLabels: _angular_core.InputSignal<Record<string, string>>;
|
|
79
|
+
readonly className: _angular_core.InputSignal<string>;
|
|
80
|
+
readonly motionChange: _angular_core.OutputEmitterRef<string>;
|
|
81
|
+
/** Projected icon template; replaces the default icon when supplied. */
|
|
82
|
+
protected readonly iconTemplate: _angular_core.Signal<TemplateRef<any> | undefined>;
|
|
83
|
+
private readonly rootRef;
|
|
84
|
+
private readonly buttonRef;
|
|
85
|
+
private readonly listRef;
|
|
86
|
+
private readonly baseId;
|
|
87
|
+
protected readonly listId: string;
|
|
88
|
+
protected readonly open: _angular_core.WritableSignal<boolean>;
|
|
89
|
+
protected readonly activeIndex: _angular_core.WritableSignal<number>;
|
|
90
|
+
/** `aria-activedescendant` is only meaningful while the listbox is open. */
|
|
91
|
+
protected readonly activeDescendant: _angular_core.Signal<string | null>;
|
|
92
|
+
protected readonly childContext: _angular_core.Signal<{
|
|
93
|
+
/** Currently selected motion slug. */
|
|
94
|
+
value: string;
|
|
95
|
+
/** Is the listbox open? */
|
|
96
|
+
open: boolean;
|
|
97
|
+
/** Resolve a slug to its display label. */
|
|
98
|
+
labelFor: (motion: string) => string;
|
|
99
|
+
$implicit: ChildArgs;
|
|
100
|
+
}>;
|
|
101
|
+
private typeahead;
|
|
102
|
+
private typeaheadTimer;
|
|
103
|
+
private initialised;
|
|
104
|
+
constructor();
|
|
105
|
+
protected optionId(index: number): string;
|
|
106
|
+
labelFor(motion: string): string;
|
|
107
|
+
/** Under zoneless change detection the signal write that un-hides the
|
|
108
|
+
* list has not reached the DOM when a microtask runs, so focusing the
|
|
109
|
+
* still-hidden list fails silently and Escape lands on the button.
|
|
110
|
+
* Flushing detection first is the Angular equivalent of Svelte's
|
|
111
|
+
* synchronous update (same fix the date-time-picker port recorded). */
|
|
112
|
+
private readonly cdr;
|
|
113
|
+
protected toggle(): void;
|
|
114
|
+
/** Open the listbox, activating `startIndex` (default: the selection). */
|
|
115
|
+
openList(startIndex?: number): void;
|
|
116
|
+
/** Close the listbox; `refocus` returns focus to the button. */
|
|
117
|
+
closeList(refocus?: boolean): void;
|
|
118
|
+
protected choose(index: number): void;
|
|
119
|
+
private scrollActiveIntoView;
|
|
120
|
+
private moveActive;
|
|
121
|
+
private runTypeahead;
|
|
122
|
+
protected onButtonKeydown(event: KeyboardEvent): void;
|
|
123
|
+
protected onListKeydown(event: KeyboardEvent): void;
|
|
124
|
+
protected onRootFocusOut(event: FocusEvent): void;
|
|
125
|
+
protected onDocumentClick(event: Event): void;
|
|
126
|
+
private applyMotion;
|
|
127
|
+
static ɵfac: _angular_core.ɵɵFactoryDeclaration<MotionPicker, never>;
|
|
128
|
+
static ɵcmp: _angular_core.ɵɵComponentDeclaration<MotionPicker, "lily-motion-picker", never, { "label": { "alias": "label"; "required": true; "isSignal": true; }; "motions": { "alias": "motions"; "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; }; "motionLabels": { "alias": "motionLabels"; "required": false; "isSignal": true; }; "className": { "alias": "className"; "required": false; "isSignal": true; }; }, { "value": "valueChange"; "motionChange": "motionChange"; }, ["iconTemplate"], never, true, never>;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export { MotionPicker, MotionPickerIcon, motionName, nextMotionPickerId, prefersReducedMotion };
|
|
132
|
+
export type { ChildArgs };
|
|
133
|
+
//# sourceMappingURL=lilydesignsystem-angular-motion-picker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lilydesignsystem-angular-motion-picker.d.ts","sources":["../../motion-picker.component.ts"],"mappings":";;;AAoBA;;;;;;;;AAQG;AAEH;AACM,KAAM,SAAS;;;;;;AAMnB;;AAGF;;;;;AAKG;AACH,iBAAgB,UAAU;AAO1B;;;;AAIG;AACH,iBAAgB,oBAAoB;AAYpC;AACA,iBAAgB,kBAAkB;AAKlC;;;;;;;;;;;;AAYG;AACH,cAIa,gBAAgB;AAC3B,wCACQ,gBAAgB,0BAEb,SAAS;mBAAgB,SAAS;AAAE;oDAJpC,gBAAgB;sDAAhB,gBAAgB;AAO5B;AAED;;;;;;;;;;;;AAYG;AACH,cA6Ea,YAAY;oBACT,aAAA,CAAA,WAAA;sBACE,aAAA,CAAA,WAAA;oBACF,aAAA,CAAA,WAAA;2BACO,aAAA,CAAA,WAAA;yBACF,aAAA,CAAA,WAAA;mBACN,aAAA,CAAA,WAAA;qBACE,aAAA,CAAA,WAAA,CAAA,WAAA;2BACM,aAAA,CAAA,WAAA,CAAA,MAAA;wBACH,aAAA,CAAA,WAAA;2BACG,aAAA,CAAA,gBAAA;;AAGrB,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;;;;;;AA7LrB;;AAoMP;;;;;AAsDH;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;oDA5SW,YAAY;sDAAZ,YAAY;AA6TxB;;","names":[]}
|