@lilydesignsystem/angular-menu-picker 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/{dist/README.md → README.md} +17 -4
- package/{dist/fesm2022 → fesm2022}/lilydesignsystem-angular-menu-picker.mjs +10 -9
- package/fesm2022/lilydesignsystem-angular-menu-picker.mjs.map +1 -0
- package/package.json +21 -12
- package/{dist/types → types}/lilydesignsystem-angular-menu-picker.d.ts +12 -11
- package/types/lilydesignsystem-angular-menu-picker.d.ts.map +1 -0
- package/AGENTS.md +0 -49
- package/CHANGELOG.md +0 -12
- package/dist/fesm2022/lilydesignsystem-angular-menu-picker.mjs.map +0 -1
- package/dist/package.json +0 -50
- package/dist/types/lilydesignsystem-angular-menu-picker.d.ts.map +0 -1
- package/docs/accessibility.md +0 -31
- package/examples/README.md +0 -9
- package/examples/basic.component.ts +0 -19
- package/examples/custom-icon.component.ts +0 -17
- package/examples/form.component.ts +0 -19
- package/index.md +0 -63
- package/index.ts +0 -7
- package/menu-picker.component.spec.ts +0 -276
- package/menu-picker.component.ts +0 -325
- package/ng-package.json +0 -10
- package/spec/index.md +0 -124
- package/tsconfig.lib.json +0 -13
|
@@ -22,7 +22,14 @@ import { MenuPicker, MenuPickerIcon } from "@lilydesignsystem/angular-menu-picke
|
|
|
22
22
|
<ul>
|
|
23
23
|
<li><a href="/">Home</a></li>
|
|
24
24
|
<li><a href="/about/">About Us</a></li>
|
|
25
|
-
<li
|
|
25
|
+
<li>
|
|
26
|
+
<button
|
|
27
|
+
type="button"
|
|
28
|
+
(click)="signOut()"
|
|
29
|
+
>
|
|
30
|
+
Sign out
|
|
31
|
+
</button>
|
|
32
|
+
</li>
|
|
26
33
|
</ul>
|
|
27
34
|
</lily-menu-picker>
|
|
28
35
|
`,
|
|
@@ -38,9 +45,15 @@ ships no links and no English, so the panel's content, and every word on it, is
|
|
|
38
45
|
Give the element a template reference to its `exportAs`:
|
|
39
46
|
|
|
40
47
|
```html
|
|
41
|
-
<lily-menu-picker
|
|
48
|
+
<lily-menu-picker
|
|
49
|
+
#menu="lilyMenuPicker"
|
|
50
|
+
label="Menu"
|
|
51
|
+
>
|
|
42
52
|
<form (submit)="$event.preventDefault(); search(); menu.close()">
|
|
43
|
-
<input
|
|
53
|
+
<input
|
|
54
|
+
type="search"
|
|
55
|
+
aria-label="Search"
|
|
56
|
+
/>
|
|
44
57
|
</form>
|
|
45
58
|
</lily-menu-picker>
|
|
46
59
|
```
|
|
@@ -56,7 +69,7 @@ By default, activating a link, a button or a `[role="menuitem"]` inside the pane
|
|
|
56
69
|
## Styling
|
|
57
70
|
|
|
58
71
|
Headless: no CSS ships. Target `.menu-picker`, `.menu-picker-button`, `.menu-picker-icon`, `.menu-picker-tooltip`,
|
|
59
|
-
`.menu-picker-panel`. The
|
|
72
|
+
`.menu-picker-panel`. The 63 reference themes in the monorepo's `themes/` style all of them, including the popup placement.
|
|
60
73
|
|
|
61
74
|
## Examples
|
|
62
75
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { NgTemplateOutlet } from "@angular/common";
|
|
2
2
|
import * as i0 from "@angular/core";
|
|
3
|
-
import { ChangeDetectionStrategy, Component, Directive, TemplateRef, computed, contentChild, effect, input, model, signal, viewChild } from "@angular/core";
|
|
3
|
+
import { ChangeDetectionStrategy, Component, Directive, Injector, TemplateRef, afterNextRender, computed, contentChild, effect, inject, input, model, signal, viewChild } from "@angular/core";
|
|
4
4
|
import { IconButton } from "@lilydesignsystem/angular-headless";
|
|
5
5
|
let uid = 0;
|
|
6
6
|
function nextMenuPickerId() {
|
|
@@ -15,7 +15,7 @@ var MenuPickerIcon = class MenuPickerIcon {
|
|
|
15
15
|
static {
|
|
16
16
|
this.ɵfac = i0.ɵɵngDeclareFactory({
|
|
17
17
|
minVersion: "12.0.0",
|
|
18
|
-
version: "22.2.
|
|
18
|
+
version: "22.2.4",
|
|
19
19
|
ngImport: i0,
|
|
20
20
|
type: MenuPickerIcon,
|
|
21
21
|
deps: [],
|
|
@@ -25,7 +25,7 @@ var MenuPickerIcon = class MenuPickerIcon {
|
|
|
25
25
|
static {
|
|
26
26
|
this.ɵdir = i0.ɵɵngDeclareDirective({
|
|
27
27
|
minVersion: "14.0.0",
|
|
28
|
-
version: "22.2.
|
|
28
|
+
version: "22.2.4",
|
|
29
29
|
type: MenuPickerIcon,
|
|
30
30
|
isStandalone: true,
|
|
31
31
|
selector: "ng-template[lilyMenuPickerIcon]",
|
|
@@ -35,7 +35,7 @@ var MenuPickerIcon = class MenuPickerIcon {
|
|
|
35
35
|
};
|
|
36
36
|
i0.ɵɵngDeclareClassMetadata({
|
|
37
37
|
minVersion: "12.0.0",
|
|
38
|
-
version: "22.2.
|
|
38
|
+
version: "22.2.4",
|
|
39
39
|
ngImport: i0,
|
|
40
40
|
type: MenuPickerIcon,
|
|
41
41
|
decorators: [{
|
|
@@ -62,6 +62,7 @@ var MenuPicker = class MenuPicker {
|
|
|
62
62
|
this.baseId = nextMenuPickerId();
|
|
63
63
|
this.panelId = `${this.baseId}-panel`;
|
|
64
64
|
this.tooltipId = `${this.baseId}-tooltip`;
|
|
65
|
+
this.injector = inject(Injector);
|
|
65
66
|
this.hoverButton = signal(false, ...ngDevMode ? [{ debugName: "hoverButton" }] : /* istanbul ignore next */ []);
|
|
66
67
|
this.hoverTooltip = signal(false, ...ngDevMode ? [{ debugName: "hoverTooltip" }] : /* istanbul ignore next */ []);
|
|
67
68
|
this.focusButton = signal(false, ...ngDevMode ? [{ debugName: "focusButton" }] : /* istanbul ignore next */ []);
|
|
@@ -107,10 +108,10 @@ var MenuPicker = class MenuPicker {
|
|
|
107
108
|
openPanel(focus = "none") {
|
|
108
109
|
this.open.set(true);
|
|
109
110
|
if (focus === "none") return;
|
|
110
|
-
|
|
111
|
+
afterNextRender(() => {
|
|
111
112
|
const all = this.focusables();
|
|
112
113
|
(focus === "last" ? all[all.length - 1] : all[0])?.focus({ preventScroll: true });
|
|
113
|
-
});
|
|
114
|
+
}, { injector: this.injector });
|
|
114
115
|
}
|
|
115
116
|
closePanel(refocus = true) {
|
|
116
117
|
if (!this.open()) return;
|
|
@@ -170,7 +171,7 @@ var MenuPicker = class MenuPicker {
|
|
|
170
171
|
static {
|
|
171
172
|
this.ɵfac = i0.ɵɵngDeclareFactory({
|
|
172
173
|
minVersion: "12.0.0",
|
|
173
|
-
version: "22.2.
|
|
174
|
+
version: "22.2.4",
|
|
174
175
|
ngImport: i0,
|
|
175
176
|
type: MenuPicker,
|
|
176
177
|
deps: [],
|
|
@@ -180,7 +181,7 @@ var MenuPicker = class MenuPicker {
|
|
|
180
181
|
static {
|
|
181
182
|
this.ɵcmp = i0.ɵɵngDeclareComponent({
|
|
182
183
|
minVersion: "17.0.0",
|
|
183
|
-
version: "22.2.
|
|
184
|
+
version: "22.2.4",
|
|
184
185
|
type: MenuPicker,
|
|
185
186
|
isStandalone: true,
|
|
186
187
|
selector: "lily-menu-picker",
|
|
@@ -349,7 +350,7 @@ var MenuPicker = class MenuPicker {
|
|
|
349
350
|
};
|
|
350
351
|
i0.ɵɵngDeclareClassMetadata({
|
|
351
352
|
minVersion: "12.0.0",
|
|
352
|
-
version: "22.2.
|
|
353
|
+
version: "22.2.4",
|
|
353
354
|
ngImport: i0,
|
|
354
355
|
type: MenuPicker,
|
|
355
356
|
decorators: [{
|
|
@@ -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 Injector,\n TemplateRef,\n afterNextRender,\n computed,\n contentChild,\n effect,\n inject,\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 = 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 = 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 private readonly injector = inject(Injector);\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(this.buttonRef().element?.matches(\":focus-visible\") ?? false);\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(this.panelRef().nativeElement.querySelectorAll<HTMLElement>(FOCUSABLE));\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 // After the next render, so the `hidden` attribute is gone: a hidden element cannot take\n // focus (a microtask ran before Angular rendered, and the focus silently went nowhere).\n afterNextRender(\n () => {\n const all = this.focusables();\n (focus === \"last\" ? all[all.length - 1] : all[0])?.focus({ preventScroll: true });\n },\n { injector: this.injector },\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,22 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lilydesignsystem/angular-menu-picker",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"engines": {
|
|
5
|
-
"node": "
|
|
5
|
+
"node": ">=26"
|
|
6
6
|
},
|
|
7
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
8
|
"dependencies": {
|
|
9
|
-
"@lilydesignsystem/angular-headless": "^0.
|
|
9
|
+
"@lilydesignsystem/angular-headless": "^0.8.0",
|
|
10
|
+
"tslib": "^2.3.0"
|
|
10
11
|
},
|
|
11
12
|
"peerDependencies": {
|
|
12
|
-
"@angular/
|
|
13
|
-
"@angular/
|
|
13
|
+
"@angular/common": ">=20.0.0 <23.0.0",
|
|
14
|
+
"@angular/core": ">=20.0.0 <23.0.0"
|
|
14
15
|
},
|
|
15
16
|
"sideEffects": false,
|
|
16
|
-
"scripts": {
|
|
17
|
-
"build": "cd .. && pnpm build",
|
|
18
|
-
"prepublishOnly": "cd .. && pnpm build"
|
|
19
|
-
},
|
|
20
17
|
"keywords": [
|
|
21
18
|
"lily",
|
|
22
19
|
"design",
|
|
@@ -31,11 +28,23 @@
|
|
|
31
28
|
"license": "MIT OR Apache-2.0 OR GPL-2.0-only OR GPL-3.0-only OR BSD-3-Clause",
|
|
32
29
|
"repository": {
|
|
33
30
|
"type": "git",
|
|
34
|
-
"url": "git+https://github.com/LilyDesignSystem/lily-design-system
|
|
31
|
+
"url": "git+https://github.com/LilyDesignSystem/lily-design-system.git",
|
|
35
32
|
"directory": "lily-design-system-angular-menu-picker"
|
|
36
33
|
},
|
|
37
34
|
"homepage": "https://lilydesignsystem.com/",
|
|
38
35
|
"bugs": {
|
|
39
36
|
"url": "https://github.com/LilyDesignSystem/lily-design-system/issues"
|
|
40
|
-
}
|
|
41
|
-
|
|
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"
|
|
50
|
+
}
|
|
@@ -36,35 +36,36 @@ export declare class MenuPickerIcon {
|
|
|
36
36
|
*/
|
|
37
37
|
export declare class MenuPicker {
|
|
38
38
|
/** Accessible name for the button, the panel and the tooltip. */
|
|
39
|
-
readonly label: import("@angular/core").InputSignal<string>;
|
|
39
|
+
readonly label: import("@angular/core/types/core").InputSignal<string>;
|
|
40
40
|
/** Is the panel open? Two-way bindable: `[(open)]`. */
|
|
41
|
-
readonly open: import("@angular/core").ModelSignal<boolean>;
|
|
41
|
+
readonly open: import("@angular/core/types/core").ModelSignal<boolean>;
|
|
42
42
|
/**
|
|
43
43
|
* Close the panel when a link, button or `[role="menuitem"]` inside it is activated. Add
|
|
44
44
|
* `data-menu-picker-keep-open` to an element (or an ancestor) to opt it out. Default `true`.
|
|
45
45
|
*/
|
|
46
|
-
readonly closeOnSelect: import("@angular/core").InputSignal<boolean>;
|
|
46
|
+
readonly closeOnSelect: import("@angular/core/types/core").InputSignal<boolean>;
|
|
47
47
|
/** Extra CSS class on the root div. */
|
|
48
|
-
readonly className: import("@angular/core").InputSignal<string>;
|
|
48
|
+
readonly className: import("@angular/core/types/core").InputSignal<string>;
|
|
49
49
|
/** Projected icon template; replaces the default icon when supplied. */
|
|
50
|
-
protected readonly iconTemplate: import("@angular/core").Signal<TemplateRef<any> | undefined>;
|
|
50
|
+
protected readonly iconTemplate: import("@angular/core/types/_chrome_dev_tools_performance-chunk").Signal<TemplateRef<any> | undefined>;
|
|
51
51
|
private readonly rootRef;
|
|
52
52
|
private readonly buttonRef;
|
|
53
53
|
private readonly panelRef;
|
|
54
54
|
private readonly baseId;
|
|
55
55
|
protected readonly panelId: string;
|
|
56
56
|
protected readonly tooltipId: string;
|
|
57
|
-
|
|
58
|
-
protected readonly
|
|
59
|
-
protected readonly
|
|
60
|
-
protected readonly
|
|
61
|
-
protected readonly
|
|
57
|
+
private readonly injector;
|
|
58
|
+
protected readonly hoverButton: import("@angular/core/types/_chrome_dev_tools_performance-chunk").WritableSignal<boolean>;
|
|
59
|
+
protected readonly hoverTooltip: import("@angular/core/types/_chrome_dev_tools_performance-chunk").WritableSignal<boolean>;
|
|
60
|
+
protected readonly focusButton: import("@angular/core/types/_chrome_dev_tools_performance-chunk").WritableSignal<boolean>;
|
|
61
|
+
protected readonly dismissed: import("@angular/core/types/_chrome_dev_tools_performance-chunk").WritableSignal<boolean>;
|
|
62
|
+
protected readonly tooltipVisible: import("@angular/core/types/_chrome_dev_tools_performance-chunk").Signal<boolean>;
|
|
62
63
|
protected onTooltipButtonEnter(): void;
|
|
63
64
|
protected onTooltipButtonFocusIn(): void;
|
|
64
65
|
protected onTooltipButtonFocusOut(): void;
|
|
65
66
|
protected onTooltipKeydown(event: KeyboardEvent): void;
|
|
66
67
|
private readonly tooltipEscapeListener;
|
|
67
|
-
protected readonly childContext: import("@angular/core").Signal<{
|
|
68
|
+
protected readonly childContext: import("@angular/core/types/_chrome_dev_tools_performance-chunk").Signal<{
|
|
68
69
|
/** Is the panel open? */
|
|
69
70
|
open: boolean;
|
|
70
71
|
$implicit: ChildArgs;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lilydesignsystem-angular-menu-picker.d.ts","names":[],"sources":["../../menu-picker.component.ts"],"mappings":";;;KAwBY;;EAER;;;wBAKY;;qBAMH;;;;;;;;;;;qBAiBA;SACF,uBACH,MAAM,gBACN,gBACD,QAAQ;IAAc,WAAW;;SAJ3B,MAAA,GAAA,qBAAA;SAAA,MAAA,GAAA,uBAAA;;;;;;;;;;qBAgGA;;WAEA,0CAAK;;WAEL,yCAAI;;;;;WAKJ,kDAAa;;WAEb,8CAAS;;qBAGC,gFAAY,OAAA;mBAEd;mBAGA;mBACA;mBAEA;qBACE;qBAKA;mBACF;qBACE,+EAAW;qBACX,gFAAY;qBACZ,+EAAW;qBACX,6EAAS;qBACT,kFAAc;YAOvB;YAKA;YAUA;YAKA,iBAAiB,OAAO;mBAMjB;qBASE,gFAAY;;IAxMzB;;;UA6ME;;EASR,UAAU;;EAeV,WAAW;;EAOX;YAIU;YAKA,aAAa,OAAO;YAapB,gBAAgB,OAAO;YAmBvB,eAAe,OAAO;YAYtB,eAAe,OAAO;YAMtB,gBAAgB,OAAO;SA3KxB,MAAA,GAAA,qBAAA;SAAA,MAAA,GAAA,uBAAA;IAAU;MAAA;MAAA;MAAA;;IAAA;MAAA;MAAA;MAAA;;IAAA;MAAA;MAAA;MAAA;;IAAA;MAAA;MAAA;MAAA;;;IAAA"}
|
package/AGENTS.md
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
# AGENTS — MenuPicker (Angular helper)
|
|
2
|
-
|
|
3
|
-
Single source of truth: [spec/index.md](./spec/index.md). Read it first; everything below is a fast index.
|
|
4
|
-
|
|
5
|
-
## What this package is
|
|
6
|
-
|
|
7
|
-
A Angular headless dropdown control. A single-icon button (a bundled **hamburger** SVG) that opens a disclosure panel holding
|
|
8
|
-
**whatever the app provides** — links, buttons, forms. Ships no CSS, no content and no English; the one bundled asset is the
|
|
9
|
-
default button icon.
|
|
10
|
-
|
|
11
|
-
## Files
|
|
12
|
-
|
|
13
|
-
| File | Purpose |
|
|
14
|
-
| ---- | ------- |
|
|
15
|
-
| `spec/index.md` | Specification-driven contract (canonical). |
|
|
16
|
-
| `menu-picker.component.ts` | Implementation. Angular signals + TypeScript. |
|
|
17
|
-
| `menu-picker.component.spec.ts` | Vitest spec, mapped to the §7 clauses. |
|
|
18
|
-
| `index.ts` | Barrel re-export. |
|
|
19
|
-
| `index.md` | User guide. |
|
|
20
|
-
| `docs/accessibility.md` | Tradeoffs, stated plainly. |
|
|
21
|
-
| `examples/` | A links panel, a form panel using `close`, and a custom icon. |
|
|
22
|
-
|
|
23
|
-
## Public surface
|
|
24
|
-
|
|
25
|
-
Selector `lily-menu-picker` (`exportAs: "lilyMenuPicker"`); exports `MenuPicker`, `MenuPickerIcon`, `nextMenuPickerId`, `FOCUSABLE`; type `ChildArgs`.
|
|
26
|
-
Required input: `label`.
|
|
27
|
-
|
|
28
|
-
## Behaviour contract (one paragraph)
|
|
29
|
-
|
|
30
|
-
Activating the button toggles a panel of projected content (`<ng-content>`). Focus stays on the button on click;
|
|
31
|
-
`ArrowDown`/`ArrowUp` on the button move into the panel's first/last focusable element. `Escape` closes and refocuses the
|
|
32
|
-
button; `Tab`, an outside click or focus leaving closes. Activating a link, button or `[role="menuitem"]` inside closes the
|
|
33
|
-
panel unless `closeOnSelect={false}` or a `data-menu-picker-keep-open` ancestor says otherwise. Nothing is applied to the
|
|
34
|
-
document and nothing is persisted.
|
|
35
|
-
|
|
36
|
-
## HTML
|
|
37
|
-
|
|
38
|
-
`<div class="menu-picker">` → `<button class="menu-picker-button">` with an `aria-hidden` SVG (`menu-picker-icon`) →
|
|
39
|
-
`<div class="menu-picker-tooltip" role="tooltip">` → `<div class="menu-picker-panel" role="group" hidden>`.
|
|
40
|
-
|
|
41
|
-
**Not an ARIA menu.** The panel's content is arbitrary, so there is no `role="menu"` and no roving focus; it is a disclosure.
|
|
42
|
-
|
|
43
|
-
## Conventions this package follows
|
|
44
|
-
|
|
45
|
-
- Angular signals; strict TypeScript on the public surface.
|
|
46
|
-
- Runtime dependency: `@lilydesignsystem/angular-headless` (the trigger's `IconButton`) beside `@angular/core`.
|
|
47
|
-
- No bundled CSS, fonts, images, links or English. The one bundled asset is the default hamburger icon, always overridable
|
|
48
|
-
through `icon`.
|
|
49
|
-
- Every user-facing string comes from props or from the app's own content.
|
package/CHANGELOG.md
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
# Changelog — MenuPicker (Angular)
|
|
2
|
-
|
|
3
|
-
All notable changes to this helper are documented in this file. The format is loosely based on
|
|
4
|
-
[Keep a Changelog](https://keepachangelog.com/) and the project follows [Semantic Versioning](https://semver.org/).
|
|
5
|
-
|
|
6
|
-
## 0.1.0 — 2026-10-08
|
|
7
|
-
|
|
8
|
-
**First release.** An icon button (a bundled three-line hamburger SVG) opening a disclosure panel that holds whatever the
|
|
9
|
-
app provides. Props: `label`, `open` (bindable), `closeOnSelect`, `onOpenChange`, `children({ open, close })`, `icon`.
|
|
10
|
-
A named `role="group"` panel (not an ARIA menu), closes on select / Escape / Tab / outside click / focus leaving,
|
|
11
|
-
hover/keyboard-focus tooltip. No default content and no English. Spec §7 has 21 acceptance clauses; the suite has one test
|
|
12
|
-
each.
|
|
@@ -1 +0,0 @@
|
|
|
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/dist/package.json
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@lilydesignsystem/angular-menu-picker",
|
|
3
|
-
"version": "0.1.0",
|
|
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"
|
|
50
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
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"}
|
package/docs/accessibility.md
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
# Accessibility
|
|
2
|
-
|
|
3
|
-
WCAG 2.2 AAA is the target. This document states what the control does well and what it costs.
|
|
4
|
-
|
|
5
|
-
## What it does
|
|
6
|
-
|
|
7
|
-
- The icon is `aria-hidden="true"`; the accessible name is the consumer-supplied `label`, so it localises with your copy.
|
|
8
|
-
- `aria-expanded` on the trigger reflects the panel state, and `aria-controls` points at it. The panel is a `role="group"`
|
|
9
|
-
named with the same `label`.
|
|
10
|
-
- **It is a disclosure, not an ARIA menu.** Arbitrary content (links, buttons, inputs, headings) inside `role="menu"` would
|
|
11
|
-
promise arrow-key `menuitem` behaviour it does not have, and would strip the native semantics of links. Native semantics
|
|
12
|
-
are kept; the APG's disclosure pattern is the model.
|
|
13
|
-
- Keyboard: `Enter`/`Space` toggle; `ArrowDown`/`ArrowUp` on the button open and move to the panel's first/last focusable
|
|
14
|
-
element; `Escape` closes and returns focus to the button; `Tab` closes — focus lands on the button first so the default Tab
|
|
15
|
-
proceeds from the picker's position instead of restarting from `<body>`.
|
|
16
|
-
- A click does not move focus into the panel (a disclosure must not steal focus); keyboard users reach it with the arrows or
|
|
17
|
-
Tab.
|
|
18
|
-
- Clicking outside, or focus leaving the picker, closes the panel.
|
|
19
|
-
- The tooltip is hoverable and dismissable (WCAG 1.4.13): visible on pointer hover or keyboard focus, `Escape` hides it
|
|
20
|
-
wherever focus is, and it is never shown while the panel is open.
|
|
21
|
-
|
|
22
|
-
## What it costs
|
|
23
|
-
|
|
24
|
-
**The name rests entirely on `label`.** An icon-only control has no visible text fallback. The hamburger is widely understood,
|
|
25
|
-
but if `label` is wrong, missing or untranslated there is nothing else to go on. Choose a label that says what the panel is
|
|
26
|
-
("Menu", "More"), not what the icon is.
|
|
27
|
-
|
|
28
|
-
**The content's accessibility is yours.** The panel contains whatever you put in it; give links real text, inputs real labels
|
|
29
|
-
and headings a sensible order.
|
|
30
|
-
|
|
31
|
-
**It hides things behind a click.** For primary site navigation, use a visible `nav` (see `BreadcrumbNav`, `SectionNav`).
|
package/examples/README.md
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
# Examples — MenuPicker (Angular)
|
|
2
|
-
|
|
3
|
-
| File | Shows |
|
|
4
|
-
| ---- | ----- |
|
|
5
|
-
| [basic.component.ts](./basic.component.ts) | A panel of links and a button: Home, About Us, Sign out |
|
|
6
|
-
| [form.component.ts](./form.component.ts) | A search form in the panel, closing it through `exportAs` |
|
|
7
|
-
| [custom-icon.component.ts](./custom-icon.component.ts) | Replacing the hamburger through `lilyMenuPickerIcon` |
|
|
8
|
-
|
|
9
|
-
Every user-facing string is yours — the package ships no default content.
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { Component } from "@angular/core";
|
|
2
|
-
import { MenuPicker } from "../menu-picker.component";
|
|
3
|
-
|
|
4
|
-
// The app provides the panel's content: the package ships no links and no English.
|
|
5
|
-
@Component({
|
|
6
|
-
selector: "example-basic",
|
|
7
|
-
standalone: true,
|
|
8
|
-
imports: [MenuPicker],
|
|
9
|
-
template: `
|
|
10
|
-
<lily-menu-picker label="Menu">
|
|
11
|
-
<ul>
|
|
12
|
-
<li><a href="/">Home</a></li>
|
|
13
|
-
<li><a href="/about/">About Us</a></li>
|
|
14
|
-
<li><button type="button">Sign out</button></li>
|
|
15
|
-
</ul>
|
|
16
|
-
</lily-menu-picker>
|
|
17
|
-
`,
|
|
18
|
-
})
|
|
19
|
-
export class Basic {}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { Component } from "@angular/core";
|
|
2
|
-
import { MenuPicker, MenuPickerIcon } from "../menu-picker.component";
|
|
3
|
-
|
|
4
|
-
// A projected <ng-template lilyMenuPickerIcon> replaces the hamburger inside the button, never the
|
|
5
|
-
// panel's content.
|
|
6
|
-
@Component({
|
|
7
|
-
selector: "example-custom-icon",
|
|
8
|
-
standalone: true,
|
|
9
|
-
imports: [MenuPicker, MenuPickerIcon],
|
|
10
|
-
template: `
|
|
11
|
-
<lily-menu-picker label="More">
|
|
12
|
-
<ng-template lilyMenuPickerIcon let-args><span aria-hidden="true">{{ args.open ? "×" : "⋯" }}</span></ng-template>
|
|
13
|
-
<a href="/help/">Help</a>
|
|
14
|
-
</lily-menu-picker>
|
|
15
|
-
`,
|
|
16
|
-
})
|
|
17
|
-
export class CustomIcon {}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { Component } from "@angular/core";
|
|
2
|
-
import { MenuPicker } from "../menu-picker.component";
|
|
3
|
-
|
|
4
|
-
// A form in the panel. `#menu="lilyMenuPicker"` gives the content a way to close the panel;
|
|
5
|
-
// typing in the input does not close it (it is not a link or button).
|
|
6
|
-
@Component({
|
|
7
|
-
selector: "example-form",
|
|
8
|
-
standalone: true,
|
|
9
|
-
imports: [MenuPicker],
|
|
10
|
-
template: `
|
|
11
|
-
<lily-menu-picker #menu="lilyMenuPicker" label="Menu">
|
|
12
|
-
<form (submit)="$event.preventDefault(); menu.close()">
|
|
13
|
-
<input type="search" aria-label="Search" />
|
|
14
|
-
<button type="submit" data-menu-picker-keep-open>Go</button>
|
|
15
|
-
</form>
|
|
16
|
-
</lily-menu-picker>
|
|
17
|
-
`,
|
|
18
|
-
})
|
|
19
|
-
export class Form {}
|
package/index.md
DELETED
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
# MenuPicker (Angular helper)
|
|
2
|
-
|
|
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.
|