@lilydesignsystem/angular-menu-picker 0.0.0-stage → 0.1.1

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 CHANGED
@@ -1,3 +1,63 @@
1
- # Temporary Holding Version
1
+ # MenuPicker (Angular helper)
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A headless Angular control for **a dropdown of whatever your app provides**: a single-icon button (a bundled
4
+ three-line hamburger SVG) that opens a panel you fill with links, buttons, a search form, a few settings — anything.
5
+
6
+ The single source of truth is [spec/index.md](./spec/index.md). This file is the human-readable guide.
7
+
8
+ ## Install
9
+
10
+ ```ts
11
+ import { MenuPicker, MenuPickerIcon } from "@lilydesignsystem/angular-menu-picker";
12
+ ```
13
+
14
+ ## Quick start
15
+
16
+ ```ts
17
+ @Component({
18
+ standalone: true,
19
+ imports: [MenuPicker],
20
+ template: `
21
+ <lily-menu-picker label="Menu">
22
+ <ul>
23
+ <li><a href="/">Home</a></li>
24
+ <li><a href="/about/">About Us</a></li>
25
+ <li><button type="button" (click)="signOut()">Sign out</button></li>
26
+ </ul>
27
+ </lily-menu-picker>
28
+ `,
29
+ })
30
+ export class Header {}
31
+ ```
32
+
33
+ `label` is the button's accessible name, the panel's name and the tooltip text. There is **no default content**: the package
34
+ ships no links and no English, so the panel's content, and every word on it, is yours.
35
+
36
+ ## Closing from your content
37
+
38
+ Give the element a template reference to its `exportAs`:
39
+
40
+ ```html
41
+ <lily-menu-picker #menu="lilyMenuPicker" label="Menu">
42
+ <form (submit)="$event.preventDefault(); search(); menu.close()">
43
+ <input type="search" aria-label="Search" />
44
+ </form>
45
+ </lily-menu-picker>
46
+ ```
47
+
48
+ By default, activating a link, a button or a `[role="menuitem"]` inside the panel closes it. Turn that off with
49
+ `[closeOnSelect]="false"`, or opt one element or region out with a `data-menu-picker-keep-open` ancestor.
50
+
51
+ ## Inputs and outputs
52
+
53
+ `label` (required), `[(open)]`, `closeOnSelect` (default `true`), `className`; projected content is the panel, and a projected
54
+ `<ng-template lilyMenuPickerIcon let-args>` replaces the **icon**, not the content.
55
+
56
+ ## Styling
57
+
58
+ Headless: no CSS ships. Target `.menu-picker`, `.menu-picker-button`, `.menu-picker-icon`, `.menu-picker-tooltip`,
59
+ `.menu-picker-panel`. The 45 reference themes in the monorepo's `themes/` style all of them, including the popup placement.
60
+
61
+ ## Examples
62
+
63
+ [examples/](./examples/README.md): a links panel, a panel with a form and `close`, and a custom icon.
@@ -0,0 +1,494 @@
1
+ import { NgTemplateOutlet } from "@angular/common";
2
+ import * as i0 from "@angular/core";
3
+ import { ChangeDetectionStrategy, Component, Directive, TemplateRef, computed, contentChild, effect, input, model, signal, viewChild } from "@angular/core";
4
+ import { IconButton } from "@lilydesignsystem/angular-headless";
5
+ let uid = 0;
6
+ function nextMenuPickerId() {
7
+ uid += 1;
8
+ return `menu-picker-${uid}`;
9
+ }
10
+ const FOCUSABLE = "a[href], button:not([disabled]), input:not([disabled]):not([type=\"hidden\"]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"])";
11
+ var MenuPickerIcon = class MenuPickerIcon {
12
+ static ngTemplateContextGuard(_dir, _ctx) {
13
+ return true;
14
+ }
15
+ static {
16
+ this.ɵfac = i0.ɵɵngDeclareFactory({
17
+ minVersion: "12.0.0",
18
+ version: "22.2.1",
19
+ ngImport: i0,
20
+ type: MenuPickerIcon,
21
+ deps: [],
22
+ target: i0.ɵɵFactoryTarget.Directive
23
+ });
24
+ }
25
+ static {
26
+ this.ɵdir = i0.ɵɵngDeclareDirective({
27
+ minVersion: "14.0.0",
28
+ version: "22.2.1",
29
+ type: MenuPickerIcon,
30
+ isStandalone: true,
31
+ selector: "ng-template[lilyMenuPickerIcon]",
32
+ ngImport: i0
33
+ });
34
+ }
35
+ };
36
+ i0.ɵɵngDeclareClassMetadata({
37
+ minVersion: "12.0.0",
38
+ version: "22.2.1",
39
+ ngImport: i0,
40
+ type: MenuPickerIcon,
41
+ decorators: [{
42
+ type: Directive,
43
+ args: [{
44
+ selector: "ng-template[lilyMenuPickerIcon]",
45
+ standalone: true
46
+ }]
47
+ }]
48
+ });
49
+ var MenuPicker = class MenuPicker {
50
+ constructor() {
51
+ this.label = input.required(...ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []);
52
+ this.open = model(false, ...ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []);
53
+ this.closeOnSelect = input(true, ...ngDevMode ? [{ debugName: "closeOnSelect" }] : /* istanbul ignore next */ []);
54
+ this.className = input("", ...ngDevMode ? [{ debugName: "className" }] : /* istanbul ignore next */ []);
55
+ this.iconTemplate = contentChild(MenuPickerIcon, {
56
+ ...ngDevMode ? { debugName: "iconTemplate" } : /* istanbul ignore next */ {},
57
+ read: TemplateRef
58
+ });
59
+ this.rootRef = viewChild.required("rootEl", ...ngDevMode ? [{ debugName: "rootRef" }] : /* istanbul ignore next */ []);
60
+ this.buttonRef = viewChild.required("buttonEl", ...ngDevMode ? [{ debugName: "buttonRef" }] : /* istanbul ignore next */ []);
61
+ this.panelRef = viewChild.required("panelEl", ...ngDevMode ? [{ debugName: "panelRef" }] : /* istanbul ignore next */ []);
62
+ this.baseId = nextMenuPickerId();
63
+ this.panelId = `${this.baseId}-panel`;
64
+ this.tooltipId = `${this.baseId}-tooltip`;
65
+ this.hoverButton = signal(false, ...ngDevMode ? [{ debugName: "hoverButton" }] : /* istanbul ignore next */ []);
66
+ this.hoverTooltip = signal(false, ...ngDevMode ? [{ debugName: "hoverTooltip" }] : /* istanbul ignore next */ []);
67
+ this.focusButton = signal(false, ...ngDevMode ? [{ debugName: "focusButton" }] : /* istanbul ignore next */ []);
68
+ this.dismissed = signal(false, ...ngDevMode ? [{ debugName: "dismissed" }] : /* istanbul ignore next */ []);
69
+ this.tooltipVisible = computed(() => !this.open() && !this.dismissed() && (this.hoverButton() || this.hoverTooltip() || this.focusButton()), ...ngDevMode ? [{ debugName: "tooltipVisible" }] : /* istanbul ignore next */ []);
70
+ this.tooltipEscapeListener = effect((onCleanup) => {
71
+ if (!this.tooltipVisible() || typeof document === "undefined") return;
72
+ const onKey = (event) => {
73
+ if (event.key === "Escape") this.dismissed.set(true);
74
+ };
75
+ document.addEventListener("keydown", onKey);
76
+ onCleanup(() => document.removeEventListener("keydown", onKey));
77
+ }, ...ngDevMode ? [{ debugName: "tooltipEscapeListener" }] : /* istanbul ignore next */ []);
78
+ this.childContext = computed(() => {
79
+ const args = { open: this.open() };
80
+ return {
81
+ $implicit: args,
82
+ ...args
83
+ };
84
+ }, ...ngDevMode ? [{ debugName: "childContext" }] : /* istanbul ignore next */ []);
85
+ }
86
+ onTooltipButtonEnter() {
87
+ this.hoverButton.set(true);
88
+ this.dismissed.set(false);
89
+ }
90
+ onTooltipButtonFocusIn() {
91
+ try {
92
+ this.focusButton.set(this.buttonRef().element?.matches(":focus-visible") ?? false);
93
+ } catch {
94
+ this.focusButton.set(true);
95
+ }
96
+ }
97
+ onTooltipButtonFocusOut() {
98
+ this.focusButton.set(false);
99
+ this.dismissed.set(false);
100
+ }
101
+ onTooltipKeydown(event) {
102
+ if (event.key === "Escape" && this.tooltipVisible()) this.dismissed.set(true);
103
+ }
104
+ focusables() {
105
+ return Array.from(this.panelRef().nativeElement.querySelectorAll(FOCUSABLE));
106
+ }
107
+ openPanel(focus = "none") {
108
+ this.open.set(true);
109
+ if (focus === "none") return;
110
+ queueMicrotask(() => {
111
+ const all = this.focusables();
112
+ (focus === "last" ? all[all.length - 1] : all[0])?.focus({ preventScroll: true });
113
+ });
114
+ }
115
+ closePanel(refocus = true) {
116
+ if (!this.open()) return;
117
+ this.open.set(false);
118
+ if (refocus) queueMicrotask(() => this.buttonRef().focus({ preventScroll: true }));
119
+ }
120
+ close() {
121
+ this.closePanel();
122
+ }
123
+ onButtonClick() {
124
+ if (this.open()) this.closePanel();
125
+ else this.openPanel();
126
+ }
127
+ onPanelClick(event) {
128
+ if (!this.closeOnSelect()) return;
129
+ const hit = event.target?.closest?.("a[href], button, [role=\"menuitem\"]");
130
+ if (!hit || !this.panelRef().nativeElement.contains(hit)) return;
131
+ if (hit.closest("[data-menu-picker-keep-open]")) return;
132
+ this.closePanel();
133
+ }
134
+ onButtonKeydown(event) {
135
+ if (event.key === "Escape" && this.open()) {
136
+ event.preventDefault();
137
+ this.closePanel();
138
+ } else if (event.key === "ArrowDown") {
139
+ event.preventDefault();
140
+ if (!this.open()) this.openPanel("first");
141
+ else this.focusables()[0]?.focus({ preventScroll: true });
142
+ } else if (event.key === "ArrowUp") {
143
+ event.preventDefault();
144
+ if (!this.open()) this.openPanel("last");
145
+ else {
146
+ const all = this.focusables();
147
+ all[all.length - 1]?.focus({ preventScroll: true });
148
+ }
149
+ }
150
+ }
151
+ onPanelKeydown(event) {
152
+ if (event.key === "Escape") {
153
+ event.preventDefault();
154
+ this.closePanel();
155
+ } else if (event.key === "Tab") {
156
+ this.buttonRef().focus?.({ preventScroll: true });
157
+ this.closePanel(false);
158
+ }
159
+ }
160
+ onRootFocusOut(event) {
161
+ const next = event.relatedTarget;
162
+ if (next && this.rootRef().nativeElement.contains(next)) return;
163
+ this.closePanel(false);
164
+ }
165
+ onDocumentClick(event) {
166
+ if (!this.open()) return;
167
+ const t = event.target;
168
+ if (t && !this.rootRef().nativeElement.contains(t)) this.closePanel(false);
169
+ }
170
+ static {
171
+ this.ɵfac = i0.ɵɵngDeclareFactory({
172
+ minVersion: "12.0.0",
173
+ version: "22.2.1",
174
+ ngImport: i0,
175
+ type: MenuPicker,
176
+ deps: [],
177
+ target: i0.ɵɵFactoryTarget.Component
178
+ });
179
+ }
180
+ static {
181
+ this.ɵcmp = i0.ɵɵngDeclareComponent({
182
+ minVersion: "17.0.0",
183
+ version: "22.2.1",
184
+ type: MenuPicker,
185
+ isStandalone: true,
186
+ selector: "lily-menu-picker",
187
+ inputs: {
188
+ label: {
189
+ classPropertyName: "label",
190
+ publicName: "label",
191
+ isSignal: true,
192
+ isRequired: true,
193
+ transformFunction: null
194
+ },
195
+ open: {
196
+ classPropertyName: "open",
197
+ publicName: "open",
198
+ isSignal: true,
199
+ isRequired: false,
200
+ transformFunction: null
201
+ },
202
+ closeOnSelect: {
203
+ classPropertyName: "closeOnSelect",
204
+ publicName: "closeOnSelect",
205
+ isSignal: true,
206
+ isRequired: false,
207
+ transformFunction: null
208
+ },
209
+ className: {
210
+ classPropertyName: "className",
211
+ publicName: "className",
212
+ isSignal: true,
213
+ isRequired: false,
214
+ transformFunction: null
215
+ }
216
+ },
217
+ outputs: { open: "openChange" },
218
+ host: { listeners: { "document:click": "onDocumentClick($event)" } },
219
+ queries: [{
220
+ propertyName: "iconTemplate",
221
+ first: true,
222
+ predicate: MenuPickerIcon,
223
+ descendants: true,
224
+ read: TemplateRef,
225
+ isSignal: true
226
+ }],
227
+ viewQueries: [
228
+ {
229
+ propertyName: "rootRef",
230
+ first: true,
231
+ predicate: ["rootEl"],
232
+ descendants: true,
233
+ isSignal: true
234
+ },
235
+ {
236
+ propertyName: "buttonRef",
237
+ first: true,
238
+ predicate: ["buttonEl"],
239
+ descendants: true,
240
+ isSignal: true
241
+ },
242
+ {
243
+ propertyName: "panelRef",
244
+ first: true,
245
+ predicate: ["panelEl"],
246
+ descendants: true,
247
+ isSignal: true
248
+ }
249
+ ],
250
+ exportAs: ["lilyMenuPicker"],
251
+ ngImport: i0,
252
+ template: `
253
+ <div
254
+ #rootEl
255
+ class="menu-picker {{ className() }}"
256
+ (focusout)="onRootFocusOut($event)"
257
+ >
258
+ <lily-icon-button
259
+ #buttonEl
260
+ [label]="label()"
261
+ baseClass="menu-picker-button"
262
+ [ariaExpanded]="open()"
263
+ [ariaControls]="panelId"
264
+ (click)="onButtonClick(); hoverButton.set(false)"
265
+ (mouseenter)="onTooltipButtonEnter()"
266
+ (mouseleave)="hoverButton.set(false)"
267
+ (focusin)="onTooltipButtonFocusIn()"
268
+ (focusout)="onTooltipButtonFocusOut()"
269
+ (keydown)="onButtonKeydown($event); onTooltipKeydown($event)"
270
+ >
271
+ @if (iconTemplate(); as tpl) {
272
+ <ng-container
273
+ [ngTemplateOutlet]="tpl"
274
+ [ngTemplateOutletContext]="childContext()"
275
+ />
276
+ } @else {
277
+ <svg
278
+ class="menu-picker-icon"
279
+ viewBox="0 0 16 16"
280
+ width="1.05rem"
281
+ height="1.05rem"
282
+ aria-hidden="true"
283
+ fill="none"
284
+ stroke="currentColor"
285
+ stroke-width="1.6"
286
+ stroke-linecap="round"
287
+ stroke-linejoin="round"
288
+ >
289
+ <path d="M1.5 3.5h13M1.5 8h13M1.5 12.5h13" />
290
+ </svg>
291
+ }
292
+ </lily-icon-button>
293
+
294
+ <!-- Purely visual: the same text is already the button's aria-label, so it is not wired
295
+ with aria-describedby (that would announce the name twice). -->
296
+ <div
297
+ class="menu-picker-tooltip"
298
+ role="tooltip"
299
+ [id]="tooltipId"
300
+ [attr.hidden]="tooltipVisible() ? null : ''"
301
+ (mouseenter)="hoverTooltip.set(true)"
302
+ (mouseleave)="hoverTooltip.set(false)"
303
+ >{{ label() }}</div>
304
+
305
+ <!-- A named group, not a menu: the content is the app's. The handlers are pure delegation
306
+ for whatever focusable content the app projects. -->
307
+ <div
308
+ #panelEl
309
+ class="menu-picker-panel"
310
+ [id]="panelId"
311
+ role="group"
312
+ [attr.aria-label]="label() || null"
313
+ [attr.hidden]="open() ? null : ''"
314
+ (keydown)="onPanelKeydown($event)"
315
+ (click)="onPanelClick($event)"
316
+ >
317
+ <ng-content />
318
+ </div>
319
+ </div>
320
+ `,
321
+ isInline: true,
322
+ dependencies: [{
323
+ kind: "directive",
324
+ type: NgTemplateOutlet,
325
+ selector: "[ngTemplateOutlet]",
326
+ inputs: [
327
+ "ngTemplateOutletContext",
328
+ "ngTemplateOutlet",
329
+ "ngTemplateOutletInjector"
330
+ ]
331
+ }, {
332
+ kind: "component",
333
+ type: IconButton,
334
+ selector: "lily-icon-button",
335
+ inputs: [
336
+ "label",
337
+ "className",
338
+ "baseClass",
339
+ "ariaHaspopup",
340
+ "ariaExpanded",
341
+ "ariaControls",
342
+ "tabIndex",
343
+ "disabled"
344
+ ]
345
+ }],
346
+ changeDetection: i0.ChangeDetectionStrategy.OnPush
347
+ });
348
+ }
349
+ };
350
+ i0.ɵɵngDeclareClassMetadata({
351
+ minVersion: "12.0.0",
352
+ version: "22.2.1",
353
+ ngImport: i0,
354
+ type: MenuPicker,
355
+ decorators: [{
356
+ type: Component,
357
+ args: [{
358
+ selector: "lily-menu-picker",
359
+ standalone: true,
360
+ exportAs: "lilyMenuPicker",
361
+ imports: [NgTemplateOutlet, IconButton],
362
+ changeDetection: ChangeDetectionStrategy.OnPush,
363
+ host: { "(document:click)": "onDocumentClick($event)" },
364
+ template: `
365
+ <div
366
+ #rootEl
367
+ class="menu-picker {{ className() }}"
368
+ (focusout)="onRootFocusOut($event)"
369
+ >
370
+ <lily-icon-button
371
+ #buttonEl
372
+ [label]="label()"
373
+ baseClass="menu-picker-button"
374
+ [ariaExpanded]="open()"
375
+ [ariaControls]="panelId"
376
+ (click)="onButtonClick(); hoverButton.set(false)"
377
+ (mouseenter)="onTooltipButtonEnter()"
378
+ (mouseleave)="hoverButton.set(false)"
379
+ (focusin)="onTooltipButtonFocusIn()"
380
+ (focusout)="onTooltipButtonFocusOut()"
381
+ (keydown)="onButtonKeydown($event); onTooltipKeydown($event)"
382
+ >
383
+ @if (iconTemplate(); as tpl) {
384
+ <ng-container
385
+ [ngTemplateOutlet]="tpl"
386
+ [ngTemplateOutletContext]="childContext()"
387
+ />
388
+ } @else {
389
+ <svg
390
+ class="menu-picker-icon"
391
+ viewBox="0 0 16 16"
392
+ width="1.05rem"
393
+ height="1.05rem"
394
+ aria-hidden="true"
395
+ fill="none"
396
+ stroke="currentColor"
397
+ stroke-width="1.6"
398
+ stroke-linecap="round"
399
+ stroke-linejoin="round"
400
+ >
401
+ <path d="M1.5 3.5h13M1.5 8h13M1.5 12.5h13" />
402
+ </svg>
403
+ }
404
+ </lily-icon-button>
405
+
406
+ <!-- Purely visual: the same text is already the button's aria-label, so it is not wired
407
+ with aria-describedby (that would announce the name twice). -->
408
+ <div
409
+ class="menu-picker-tooltip"
410
+ role="tooltip"
411
+ [id]="tooltipId"
412
+ [attr.hidden]="tooltipVisible() ? null : ''"
413
+ (mouseenter)="hoverTooltip.set(true)"
414
+ (mouseleave)="hoverTooltip.set(false)"
415
+ >{{ label() }}</div>
416
+
417
+ <!-- A named group, not a menu: the content is the app's. The handlers are pure delegation
418
+ for whatever focusable content the app projects. -->
419
+ <div
420
+ #panelEl
421
+ class="menu-picker-panel"
422
+ [id]="panelId"
423
+ role="group"
424
+ [attr.aria-label]="label() || null"
425
+ [attr.hidden]="open() ? null : ''"
426
+ (keydown)="onPanelKeydown($event)"
427
+ (click)="onPanelClick($event)"
428
+ >
429
+ <ng-content />
430
+ </div>
431
+ </div>
432
+ `
433
+ }]
434
+ }],
435
+ propDecorators: {
436
+ label: [{
437
+ type: i0.Input,
438
+ args: [{
439
+ isSignal: true,
440
+ alias: "label",
441
+ required: true
442
+ }]
443
+ }],
444
+ open: [{
445
+ type: i0.Input,
446
+ args: [{
447
+ isSignal: true,
448
+ alias: "open",
449
+ required: false
450
+ }]
451
+ }, {
452
+ type: i0.Output,
453
+ args: ["openChange"]
454
+ }],
455
+ closeOnSelect: [{
456
+ type: i0.Input,
457
+ args: [{
458
+ isSignal: true,
459
+ alias: "closeOnSelect",
460
+ required: false
461
+ }]
462
+ }],
463
+ className: [{
464
+ type: i0.Input,
465
+ args: [{
466
+ isSignal: true,
467
+ alias: "className",
468
+ required: false
469
+ }]
470
+ }],
471
+ iconTemplate: [{
472
+ type: i0.ContentChild,
473
+ args: [i0.forwardRef(() => MenuPickerIcon), {
474
+ read: TemplateRef,
475
+ isSignal: true
476
+ }]
477
+ }],
478
+ rootRef: [{
479
+ type: i0.ViewChild,
480
+ args: ["rootEl", { isSignal: true }]
481
+ }],
482
+ buttonRef: [{
483
+ type: i0.ViewChild,
484
+ args: ["buttonEl", { isSignal: true }]
485
+ }],
486
+ panelRef: [{
487
+ type: i0.ViewChild,
488
+ args: ["panelEl", { isSignal: true }]
489
+ }]
490
+ }
491
+ });
492
+ export { FOCUSABLE, MenuPicker, MenuPickerIcon, nextMenuPickerId };
493
+
494
+ //# sourceMappingURL=lilydesignsystem-angular-menu-picker.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lilydesignsystem-angular-menu-picker.mjs","names":[],"sources":["../../menu-picker.component.ts"],"sourcesContent":["import { NgTemplateOutlet } from \"@angular/common\";\nimport {\n ChangeDetectionStrategy,\n Component,\n Directive,\n ElementRef,\n TemplateRef,\n computed,\n contentChild,\n effect,\n input,\n model,\n signal,\n viewChild,\n} from \"@angular/core\";\n// Only the trigger button composes a headless primitive. The panel is a disclosure of whatever the\n// app projects into it — links, buttons, forms — so it carries no ARIA menu roles (role=\"menu\" would\n// promise a roving-focus menuitem widget that arbitrary content is not). See spec/index.md §3.\nimport { IconButton } from \"@lilydesignsystem/angular-headless\";\n\n/** Context passed to a custom icon `<ng-template>` (the button icon). */\nexport type ChildArgs = {\n /** Is the panel open? */\n open: boolean;\n};\n\nlet uid = 0;\n/** Stable per-instance id prefix; SSR-safe (no Math.random / Date.now). */\nexport function nextMenuPickerId(): string {\n uid += 1;\n return `menu-picker-${uid}`;\n}\n\n/** The selector for focusable things inside the panel. */\nexport const FOCUSABLE =\n 'a[href], button:not([disabled]), input:not([disabled]):not([type=\"hidden\"]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"])';\n\n/**\n * Marker for the projected icon template. Gives consumers typed `let-` variables:\n *\n * ```html\n * <lily-menu-picker label=\"Menu\">\n * <ng-template lilyMenuPickerIcon let-args>{{ args.open ? \"×\" : \"☰\" }}</ng-template>\n * …the panel's content…\n * </lily-menu-picker>\n * ```\n */\n@Directive({\n selector: \"ng-template[lilyMenuPickerIcon]\",\n standalone: true,\n})\nexport class MenuPickerIcon {\n static ngTemplateContextGuard(\n _dir: MenuPickerIcon,\n _ctx: unknown,\n ): _ctx is ChildArgs & { $implicit: ChildArgs } {\n return true;\n }\n}\n\n/**\n * MenuPicker — a headless dropdown control.\n *\n * A single-icon button (a bundled hamburger SVG) that opens a disclosure panel holding whatever\n * the app projects into it. It applies nothing to the document and persists nothing. Give the\n * element a template reference (`#menu`) to call `menu.close()` from the projected content.\n * See `spec/index.md`.\n */\n@Component({\n selector: \"lily-menu-picker\",\n standalone: true,\n exportAs: \"lilyMenuPicker\",\n imports: [NgTemplateOutlet, IconButton],\n changeDetection: ChangeDetectionStrategy.OnPush,\n host: {\n \"(document:click)\": \"onDocumentClick($event)\",\n },\n template: `\n <div\n #rootEl\n class=\"menu-picker {{ className() }}\"\n (focusout)=\"onRootFocusOut($event)\"\n >\n <lily-icon-button\n #buttonEl\n [label]=\"label()\"\n baseClass=\"menu-picker-button\"\n [ariaExpanded]=\"open()\"\n [ariaControls]=\"panelId\"\n (click)=\"onButtonClick(); hoverButton.set(false)\"\n (mouseenter)=\"onTooltipButtonEnter()\"\n (mouseleave)=\"hoverButton.set(false)\"\n (focusin)=\"onTooltipButtonFocusIn()\"\n (focusout)=\"onTooltipButtonFocusOut()\"\n (keydown)=\"onButtonKeydown($event); onTooltipKeydown($event)\"\n >\n @if (iconTemplate(); as tpl) {\n <ng-container\n [ngTemplateOutlet]=\"tpl\"\n [ngTemplateOutletContext]=\"childContext()\"\n />\n } @else {\n <svg\n class=\"menu-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=\"M1.5 3.5h13M1.5 8h13M1.5 12.5h13\" />\n </svg>\n }\n </lily-icon-button>\n\n <!-- Purely visual: the same text is already the button's aria-label, so it is not wired\n with aria-describedby (that would announce the name twice). -->\n <div\n class=\"menu-picker-tooltip\"\n role=\"tooltip\"\n [id]=\"tooltipId\"\n [attr.hidden]=\"tooltipVisible() ? null : ''\"\n (mouseenter)=\"hoverTooltip.set(true)\"\n (mouseleave)=\"hoverTooltip.set(false)\"\n >{{ label() }}</div>\n\n <!-- A named group, not a menu: the content is the app's. The handlers are pure delegation\n for whatever focusable content the app projects. -->\n <div\n #panelEl\n class=\"menu-picker-panel\"\n [id]=\"panelId\"\n role=\"group\"\n [attr.aria-label]=\"label() || null\"\n [attr.hidden]=\"open() ? null : ''\"\n (keydown)=\"onPanelKeydown($event)\"\n (click)=\"onPanelClick($event)\"\n >\n <ng-content />\n </div>\n </div>\n `,\n})\nexport class MenuPicker {\n /** Accessible name for the button, the panel and the tooltip. */\n readonly label = input.required<string>();\n /** Is the panel open? Two-way bindable: `[(open)]`. */\n readonly open = model<boolean>(false);\n /**\n * Close the panel when a link, button or `[role=\"menuitem\"]` inside it is activated. Add\n * `data-menu-picker-keep-open` to an element (or an ancestor) to opt it out. Default `true`.\n */\n readonly closeOnSelect = input<boolean>(true);\n /** Extra CSS class on the root div. */\n readonly className = input<string>(\"\");\n\n /** Projected icon template; replaces the default icon when supplied. */\n protected readonly iconTemplate = contentChild(MenuPickerIcon, { read: TemplateRef });\n\n private readonly rootRef =\n viewChild.required<ElementRef<HTMLDivElement>>(\"rootEl\");\n // A template-ref-variable on a component tag resolves to the component INSTANCE, which is what\n // is needed to call the headless component's own public `focus()` method.\n private readonly buttonRef = viewChild.required<IconButton>(\"buttonEl\");\n private readonly panelRef =\n viewChild.required<ElementRef<HTMLDivElement>>(\"panelEl\");\n\n private readonly baseId = nextMenuPickerId();\n protected readonly panelId = `${this.baseId}-panel`;\n\n // Tooltip: shown while the pointer is over the button or the tooltip itself (hoverable, WCAG\n // 1.4.13) or while the button has keyboard focus; Escape dismisses it without moving focus;\n // never shown while the popup is open, since the popup then explains the control.\n protected readonly tooltipId = `${this.baseId}-tooltip`;\n protected readonly hoverButton = signal(false);\n protected readonly hoverTooltip = signal(false);\n protected readonly focusButton = signal(false);\n protected readonly dismissed = signal(false);\n protected readonly tooltipVisible = computed(\n () =>\n !this.open() &&\n !this.dismissed() &&\n (this.hoverButton() || this.hoverTooltip() || this.focusButton()),\n );\n\n protected onTooltipButtonEnter(): void {\n this.hoverButton.set(true);\n this.dismissed.set(false);\n }\n\n protected onTooltipButtonFocusIn(): void {\n // Keyboard focus only: a mouse click also focuses the button in Chromium, and the tooltip\n // should not stick after a click.\n try {\n this.focusButton.set(\n this.buttonRef().element?.matches(\":focus-visible\") ?? false,\n );\n } catch {\n this.focusButton.set(true); // engine without :focus-visible — err towards showing\n }\n }\n\n protected onTooltipButtonFocusOut(): void {\n this.focusButton.set(false);\n this.dismissed.set(false);\n }\n\n protected onTooltipKeydown(event: KeyboardEvent): void {\n if (event.key === \"Escape\" && this.tooltipVisible()) this.dismissed.set(true);\n }\n\n // WCAG 1.4.13 \"dismissable\": while the tooltip is visible, Escape works wherever focus is. The\n // listener exists only while visible; the effect cleanup removes it on hide and on destroy.\n private readonly tooltipEscapeListener = effect((onCleanup) => {\n if (!this.tooltipVisible() || typeof document === \"undefined\") return;\n const onKey = (event: KeyboardEvent): void => {\n if (event.key === \"Escape\") this.dismissed.set(true);\n };\n document.addEventListener(\"keydown\", onKey);\n onCleanup(() => document.removeEventListener(\"keydown\", onKey));\n });\n\n protected readonly childContext = computed(() => {\n const args: ChildArgs = { open: this.open() };\n return { $implicit: args, ...args };\n });\n\n private focusables(): HTMLElement[] {\n return Array.from(\n this.panelRef().nativeElement.querySelectorAll<HTMLElement>(FOCUSABLE),\n );\n }\n\n // ---------------------------------------------------------------\n // Open / close\n // ---------------------------------------------------------------\n\n /** Open the panel, optionally focusing its first or last focusable element. */\n openPanel(focus: \"none\" | \"first\" | \"last\" = \"none\"): void {\n this.open.set(true);\n if (focus === \"none\") return;\n // Deferred so the `hidden` attribute is gone before focus is moved.\n queueMicrotask(() => {\n const all = this.focusables();\n (focus === \"last\" ? all[all.length - 1] : all[0])?.focus({ preventScroll: true });\n });\n }\n\n /** Close the panel; `refocus` returns focus to the trigger. */\n closePanel(refocus = true): void {\n if (!this.open()) return;\n this.open.set(false);\n if (refocus) queueMicrotask(() => this.buttonRef().focus({ preventScroll: true }));\n }\n\n /** Close the panel and return focus to the button. For the projected content to call. */\n close(): void {\n this.closePanel();\n }\n\n protected onButtonClick(): void {\n if (this.open()) this.closePanel();\n else this.openPanel();\n }\n\n protected onPanelClick(event: MouseEvent): void {\n if (!this.closeOnSelect()) return;\n const target = event.target as Element | null;\n const hit = target?.closest?.('a[href], button, [role=\"menuitem\"]');\n if (!hit || !this.panelRef().nativeElement.contains(hit)) return;\n if (hit.closest(\"[data-menu-picker-keep-open]\")) return;\n this.closePanel();\n }\n\n // ---------------------------------------------------------------\n // Keyboard\n // ---------------------------------------------------------------\n\n protected onButtonKeydown(event: KeyboardEvent): void {\n if (event.key === \"Escape\" && this.open()) {\n event.preventDefault();\n this.closePanel();\n } else if (event.key === \"ArrowDown\") {\n // Enter and Space already produce a click; the arrows open and move into the panel.\n event.preventDefault();\n if (!this.open()) this.openPanel(\"first\");\n else this.focusables()[0]?.focus({ preventScroll: true });\n } else if (event.key === \"ArrowUp\") {\n event.preventDefault();\n if (!this.open()) this.openPanel(\"last\");\n else {\n const all = this.focusables();\n all[all.length - 1]?.focus({ preventScroll: true });\n }\n }\n }\n\n protected onPanelKeydown(event: KeyboardEvent): void {\n if (event.key === \"Escape\") {\n event.preventDefault();\n this.closePanel();\n } else if (event.key === \"Tab\") {\n // Focus goes to the button FIRST, without cancelling the key, so the browser continues\n // from the picker's position rather than from <body>.\n this.buttonRef().focus?.({ preventScroll: true });\n this.closePanel(false);\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.closePanel(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.closePanel(false);\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,50 @@
1
1
  {
2
2
  "name": "@lilydesignsystem/angular-menu-picker",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.1",
4
+ "engines": {
5
+ "node": "=26"
6
+ },
7
+ "description": "Lily Design System Angular menu picker: an icon button (a hamburger icon) opening a dropdown of whatever the app provides. Headless, SSR-safe, no CSS.",
8
+ "dependencies": {
9
+ "@lilydesignsystem/angular-headless": "^0.2.0",
10
+ "tslib": "^2.3.0"
11
+ },
12
+ "peerDependencies": {
13
+ "@angular/core": ">=20.0.0 <23.0.0",
14
+ "@angular/common": ">=20.0.0 <23.0.0"
15
+ },
16
+ "sideEffects": false,
17
+ "keywords": [
18
+ "lily",
19
+ "design",
20
+ "system",
21
+ "angular",
22
+ "menu",
23
+ "hamburger",
24
+ "dropdown",
25
+ "picker"
26
+ ],
27
+ "author": "Joel Parker Henderson <joel@joelparkerhenderson.com>",
28
+ "license": "MIT OR Apache-2.0 OR GPL-2.0-only OR GPL-3.0-only OR BSD-3-Clause",
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/LilyDesignSystem/lily-design-system-angular-helpers.git",
32
+ "directory": "lily-design-system-angular-menu-picker"
33
+ },
34
+ "homepage": "https://lilydesignsystem.com/",
35
+ "bugs": {
36
+ "url": "https://github.com/LilyDesignSystem/lily-design-system/issues"
37
+ },
38
+ "module": "fesm2022/lilydesignsystem-angular-menu-picker.mjs",
39
+ "typings": "types/lilydesignsystem-angular-menu-picker.d.ts",
40
+ "exports": {
41
+ "./package.json": {
42
+ "default": "./package.json"
43
+ },
44
+ ".": {
45
+ "types": "./types/lilydesignsystem-angular-menu-picker.d.ts",
46
+ "default": "./fesm2022/lilydesignsystem-angular-menu-picker.mjs"
47
+ }
48
+ },
49
+ "type": "module"
6
50
  }
@@ -0,0 +1,112 @@
1
+ import * as i0 from "@angular/core";
2
+ import { TemplateRef } from "@angular/core";
3
+ /** Context passed to a custom icon `<ng-template>` (the button icon). */
4
+ type ChildArgs = {
5
+ /** Is the panel open? */
6
+ open: boolean;
7
+ };
8
+ /** Stable per-instance id prefix; SSR-safe (no Math.random / Date.now). */
9
+ export declare function nextMenuPickerId(): string;
10
+ /** The selector for focusable things inside the panel. */
11
+ export declare const FOCUSABLE = "a[href], button:not([disabled]), input:not([disabled]):not([type=\"hidden\"]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"])";
12
+ /**
13
+ * Marker for the projected icon template. Gives consumers typed `let-` variables:
14
+ *
15
+ * ```html
16
+ * <lily-menu-picker label="Menu">
17
+ * <ng-template lilyMenuPickerIcon let-args>{{ args.open ? "×" : "☰" }}</ng-template>
18
+ * …the panel's content…
19
+ * </lily-menu-picker>
20
+ * ```
21
+ */
22
+ export declare class MenuPickerIcon {
23
+ static ngTemplateContextGuard(_dir: MenuPickerIcon, _ctx: unknown): _ctx is ChildArgs & {
24
+ $implicit: ChildArgs;
25
+ };
26
+ static ɵfac: i0.ɵɵFactoryDeclaration<MenuPickerIcon, never>;
27
+ static ɵdir: i0.ɵɵDirectiveDeclaration<MenuPickerIcon, "ng-template[lilyMenuPickerIcon]", never, {}, {}, never, never, true, never>;
28
+ }
29
+ /**
30
+ * MenuPicker — a headless dropdown control.
31
+ *
32
+ * A single-icon button (a bundled hamburger SVG) that opens a disclosure panel holding whatever
33
+ * the app projects into it. It applies nothing to the document and persists nothing. Give the
34
+ * element a template reference (`#menu`) to call `menu.close()` from the projected content.
35
+ * See `spec/index.md`.
36
+ */
37
+ export declare class MenuPicker {
38
+ /** Accessible name for the button, the panel and the tooltip. */
39
+ readonly label: import("@angular/core").InputSignal<string>;
40
+ /** Is the panel open? Two-way bindable: `[(open)]`. */
41
+ readonly open: import("@angular/core").ModelSignal<boolean>;
42
+ /**
43
+ * Close the panel when a link, button or `[role="menuitem"]` inside it is activated. Add
44
+ * `data-menu-picker-keep-open` to an element (or an ancestor) to opt it out. Default `true`.
45
+ */
46
+ readonly closeOnSelect: import("@angular/core").InputSignal<boolean>;
47
+ /** Extra CSS class on the root div. */
48
+ readonly className: import("@angular/core").InputSignal<string>;
49
+ /** Projected icon template; replaces the default icon when supplied. */
50
+ protected readonly iconTemplate: import("@angular/core").Signal<TemplateRef<any> | undefined>;
51
+ private readonly rootRef;
52
+ private readonly buttonRef;
53
+ private readonly panelRef;
54
+ private readonly baseId;
55
+ protected readonly panelId: string;
56
+ protected readonly tooltipId: string;
57
+ protected readonly hoverButton: import("@angular/core").WritableSignal<boolean>;
58
+ protected readonly hoverTooltip: import("@angular/core").WritableSignal<boolean>;
59
+ protected readonly focusButton: import("@angular/core").WritableSignal<boolean>;
60
+ protected readonly dismissed: import("@angular/core").WritableSignal<boolean>;
61
+ protected readonly tooltipVisible: import("@angular/core").Signal<boolean>;
62
+ protected onTooltipButtonEnter(): void;
63
+ protected onTooltipButtonFocusIn(): void;
64
+ protected onTooltipButtonFocusOut(): void;
65
+ protected onTooltipKeydown(event: KeyboardEvent): void;
66
+ private readonly tooltipEscapeListener;
67
+ protected readonly childContext: import("@angular/core").Signal<{
68
+ /** Is the panel open? */
69
+ open: boolean;
70
+ $implicit: ChildArgs;
71
+ }>;
72
+ private focusables;
73
+ /** Open the panel, optionally focusing its first or last focusable element. */
74
+ openPanel(focus?: "none" | "first" | "last"): void;
75
+ /** Close the panel; `refocus` returns focus to the trigger. */
76
+ closePanel(refocus?: boolean): void;
77
+ /** Close the panel and return focus to the button. For the projected content to call. */
78
+ close(): void;
79
+ protected onButtonClick(): void;
80
+ protected onPanelClick(event: MouseEvent): void;
81
+ protected onButtonKeydown(event: KeyboardEvent): void;
82
+ protected onPanelKeydown(event: KeyboardEvent): void;
83
+ protected onRootFocusOut(event: FocusEvent): void;
84
+ protected onDocumentClick(event: Event): void;
85
+ static ɵfac: i0.ɵɵFactoryDeclaration<MenuPicker, never>;
86
+ static ɵcmp: i0.ɵɵComponentDeclaration<MenuPicker, "lily-menu-picker", ["lilyMenuPicker"], {
87
+ "label": {
88
+ "alias": "label";
89
+ "required": true;
90
+ "isSignal": true;
91
+ };
92
+ "open": {
93
+ "alias": "open";
94
+ "required": false;
95
+ "isSignal": true;
96
+ };
97
+ "closeOnSelect": {
98
+ "alias": "closeOnSelect";
99
+ "required": false;
100
+ "isSignal": true;
101
+ };
102
+ "className": {
103
+ "alias": "className";
104
+ "required": false;
105
+ "isSignal": true;
106
+ };
107
+ }, {
108
+ "open": "openChange";
109
+ }, ["iconTemplate"], ["*"], true, never>;
110
+ }
111
+ export type { ChildArgs };
112
+ //# sourceMappingURL=lilydesignsystem-angular-menu-picker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lilydesignsystem-angular-menu-picker.d.ts","names":[],"sources":["../../menu-picker.component.ts"],"mappings":";;;KAqBY;;EAEV;;;wBAKc;;qBAMH;;;;;;;;;;;qBAiBA;SACJ,uBACL,MAAM,gBACN,gBACC,QAAQ;IAAc,WAAW;;SAJzB,MAAA,GAAA,qBAAA;SAAA,MAAA,GAAA,uBAAA;;;;;;;;;;qBAgGA;;WAEF,+BAAK;;WAEL,8BAAI;;;;;WAKJ,uCAAa;;WAEb,mCAAS;;qBAGC,sCAAY,OAAA;mBAEd;mBAIA;mBACA;mBAGA;qBACE;qBAKA;qBACA,qCAAW;qBACX,sCAAY;qBACZ,qCAAW;qBACX,mCAAS;qBACT,wCAAc;YAOvB;YAKA;YAYA;YAKA,iBAAiB,OAAO;mBAMjB;qBASE,sCAAY;;IA3MzB;;;UAgNE;;EAWR,UAAU;;EAWV,WAAW;;EAOX;YAIU;YAKA,aAAa,OAAO;YAapB,gBAAgB,OAAO;YAmBvB,eAAe,OAAO;YAYtB,eAAe,OAAO;YAMtB,gBAAgB,OAAO;SA5KtB,MAAA,GAAA,qBAAA;SAAA,MAAA,GAAA,uBAAA;IAAU;MAAA;MAAA;MAAA;;IAAA;MAAA;MAAA;MAAA;;IAAA;MAAA;MAAA;MAAA;;IAAA;MAAA;MAAA;MAAA;;;IAAA"}