@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.
@@ -1,276 +0,0 @@
1
- import { Component, viewChild } from "@angular/core";
2
- import { ComponentFixture, TestBed } from "@angular/core/testing";
3
- import { afterEach, describe, expect, test, vi } from "vitest";
4
- import { readFileSync } from "node:fs";
5
- import { resolve } from "node:path";
6
-
7
- import {
8
- MenuPicker,
9
- MenuPickerIcon,
10
- nextMenuPickerId,
11
- } from "./menu-picker.component";
12
-
13
- const flush = () => new Promise<void>((r) => setTimeout(r, 0));
14
-
15
- @Component({
16
- standalone: true,
17
- imports: [MenuPicker, MenuPickerIcon],
18
- template: `
19
- <lily-menu-picker #menu label="Menu" [closeOnSelect]="closeOnSelect" [(open)]="isOpen" className="{{ extra }}" [attr.data-x]="'1'">
20
- <a href="/a/">Alpha</a>
21
- <button type="button" class="b">Beta</button>
22
- <div data-menu-picker-keep-open><button type="button" class="keep">Keep</button></div>
23
- <button type="button" class="closer" (click)="$event.stopPropagation(); menu.close()">Closer</button>
24
- </lily-menu-picker>
25
- `,
26
- })
27
- class Host {
28
- closeOnSelect = true;
29
- isOpen = false;
30
- extra = "";
31
- picker = viewChild.required(MenuPicker);
32
- }
33
-
34
- @Component({
35
- standalone: true,
36
- imports: [MenuPicker, MenuPickerIcon],
37
- template: `<lily-menu-picker label="Menu"><ng-template lilyMenuPickerIcon let-args><span class="mine">{{ args.open ? "-" : "+" }}</span></ng-template></lily-menu-picker>`,
38
- })
39
- class IconHost {}
40
-
41
- @Component({
42
- standalone: true,
43
- imports: [MenuPicker],
44
- template: `<lily-menu-picker label="Menu" />`,
45
- })
46
- class EmptyHost {}
47
-
48
- let fixtures: ComponentFixture<unknown>[] = [];
49
-
50
- function mount(patch: Partial<Host> = {}): ComponentFixture<Host> {
51
- const fixture = TestBed.createComponent(Host);
52
- Object.assign(fixture.componentInstance, patch);
53
- fixture.detectChanges();
54
- fixtures.push(fixture);
55
- return fixture;
56
- }
57
-
58
- const q = <T extends Element>(f: ComponentFixture<unknown>, sel: string) =>
59
- f.nativeElement.querySelector(sel) as T;
60
- const trigger = (f: ComponentFixture<unknown>) => q<HTMLButtonElement>(f, ".menu-picker-button");
61
- const panel = (f: ComponentFixture<unknown>) => q<HTMLDivElement>(f, ".menu-picker-panel");
62
-
63
- async function press(f: ComponentFixture<unknown>, target: HTMLElement, key: string): Promise<void> {
64
- target.dispatchEvent(new KeyboardEvent("keydown", { key, bubbles: true, cancelable: true }));
65
- f.detectChanges();
66
- await flush();
67
- f.detectChanges();
68
- }
69
-
70
- async function click(f: ComponentFixture<unknown>, target: HTMLElement): Promise<void> {
71
- target.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true, button: 0 }));
72
- f.detectChanges();
73
- await flush();
74
- f.detectChanges();
75
- }
76
-
77
- afterEach(() => {
78
- for (const f of fixtures) f.destroy();
79
- fixtures = [];
80
- });
81
-
82
- describe("MenuPicker", () => {
83
- test("7.1 renders the root, button, tooltip and a hidden panel", () => {
84
- const f = mount();
85
- expect(q(f, "div.menu-picker")).toBeTruthy();
86
- expect(trigger(f)).toBeTruthy();
87
- expect(q(f, ".menu-picker-tooltip")).toBeTruthy();
88
- expect(panel(f).hasAttribute("hidden")).toBe(true);
89
- });
90
-
91
- test("7.2 the button is named by label with aria-expanded and aria-controls", () => {
92
- const f = mount();
93
- expect(trigger(f).getAttribute("aria-label")).toBe("Menu");
94
- expect(trigger(f).getAttribute("aria-expanded")).toBe("false");
95
- expect(trigger(f).getAttribute("aria-controls")).toBe(panel(f).id);
96
- });
97
-
98
- test("7.3 the default icon is an aria-hidden svg; a projected template replaces it", () => {
99
- const f = mount();
100
- const svg = q<SVGElement>(f, "svg.menu-picker-icon");
101
- expect(svg.getAttribute("aria-hidden")).toBe("true");
102
- expect(svg.getAttribute("viewBox")).toBe("0 0 16 16");
103
- const hf = TestBed.createComponent(IconHost);
104
- hf.detectChanges();
105
- fixtures.push(hf);
106
- expect(hf.nativeElement.querySelector(".mine")).toBeTruthy();
107
- expect(hf.nativeElement.querySelector("svg.menu-picker-icon")).toBeNull();
108
- });
109
-
110
- test("7.4 the panel is a role=group named with label", () => {
111
- const f = mount();
112
- expect(panel(f).getAttribute("role")).toBe("group");
113
- expect(panel(f).getAttribute("aria-label")).toBe("Menu");
114
- });
115
-
116
- test("7.5 the app's content renders inside; with none the panel is empty", () => {
117
- const f = mount();
118
- expect(panel(f).querySelector("a")?.textContent).toBe("Alpha");
119
- const ef = TestBed.createComponent(EmptyHost);
120
- ef.detectChanges();
121
- fixtures.push(ef);
122
- expect(q<HTMLElement>(ef, ".menu-picker-panel").textContent?.trim()).toBe("");
123
- });
124
-
125
- test("7.6 click opens and leaves focus on the button", async () => {
126
- const f = mount();
127
- trigger(f).focus();
128
- await click(f, trigger(f));
129
- expect(panel(f).hasAttribute("hidden")).toBe(false);
130
- expect(trigger(f).getAttribute("aria-expanded")).toBe("true");
131
- expect(document.activeElement).toBe(trigger(f));
132
- });
133
-
134
- test("7.7 a second click closes", async () => {
135
- const f = mount();
136
- await click(f, trigger(f));
137
- await click(f, trigger(f));
138
- expect(panel(f).hasAttribute("hidden")).toBe(true);
139
- });
140
-
141
- test("7.8 ArrowDown opens at the first focusable, ArrowUp at the last", async () => {
142
- const f = mount();
143
- await press(f, trigger(f), "ArrowDown");
144
- expect(panel(f).hasAttribute("hidden")).toBe(false);
145
- expect(document.activeElement).toBe(panel(f).querySelector("a"));
146
- const g = mount();
147
- await press(g, trigger(g), "ArrowUp");
148
- expect(document.activeElement).toBe(panel(g).querySelector(".closer"));
149
- });
150
-
151
- test("7.9 Escape closes and returns focus to the button", async () => {
152
- const f = mount();
153
- await press(f, trigger(f), "ArrowDown");
154
- await press(f, panel(f), "Escape");
155
- expect(panel(f).hasAttribute("hidden")).toBe(true);
156
- expect(document.activeElement).toBe(trigger(f));
157
- });
158
-
159
- test("7.10 Tab closes without sending focus to the document top", async () => {
160
- const f = mount();
161
- await press(f, trigger(f), "ArrowDown");
162
- await press(f, panel(f), "Tab");
163
- expect(panel(f).hasAttribute("hidden")).toBe(true);
164
- expect(document.activeElement).toBe(trigger(f));
165
- });
166
-
167
- test("7.11 clicking outside closes", async () => {
168
- const f = mount();
169
- await click(f, trigger(f));
170
- document.body.dispatchEvent(new MouseEvent("click", { bubbles: true }));
171
- f.detectChanges();
172
- await flush();
173
- f.detectChanges();
174
- expect(panel(f).hasAttribute("hidden")).toBe(true);
175
- });
176
-
177
- test("7.12 focus leaving the picker closes", async () => {
178
- const f = mount();
179
- await click(f, trigger(f));
180
- const outside = document.createElement("button");
181
- document.body.appendChild(outside);
182
- q(f, "div.menu-picker").dispatchEvent(new FocusEvent("focusout", { bubbles: true, relatedTarget: outside }));
183
- f.detectChanges();
184
- expect(panel(f).hasAttribute("hidden")).toBe(true);
185
- outside.remove();
186
- });
187
-
188
- test("7.13 activating a link or button closes; keep-open and closeOnSelect=false do not", async () => {
189
- const f = mount();
190
- await click(f, trigger(f));
191
- panel(f).querySelector("a")!.addEventListener("click", (e) => e.preventDefault());
192
- await click(f, panel(f).querySelector("a")!);
193
- expect(panel(f).hasAttribute("hidden")).toBe(true);
194
- expect(document.activeElement).toBe(trigger(f));
195
- await click(f, trigger(f));
196
- await click(f, panel(f).querySelector(".b")!);
197
- expect(panel(f).hasAttribute("hidden")).toBe(true);
198
- await click(f, trigger(f));
199
- await click(f, panel(f).querySelector(".keep")!);
200
- expect(panel(f).hasAttribute("hidden")).toBe(false);
201
- const g = mount({ closeOnSelect: false });
202
- await click(g, trigger(g));
203
- await click(g, panel(g).querySelector(".b")!);
204
- expect(panel(g).hasAttribute("hidden")).toBe(false);
205
- });
206
-
207
- test("7.14 the content can call close()", async () => {
208
- const f = mount({ closeOnSelect: false });
209
- await click(f, trigger(f));
210
- await click(f, panel(f).querySelector(".closer")!);
211
- expect(panel(f).hasAttribute("hidden")).toBe(true);
212
- });
213
-
214
- test("7.15 [(open)] updates once per actual change", async () => {
215
- const f = mount();
216
- await click(f, trigger(f));
217
- expect(f.componentInstance.isOpen).toBe(true);
218
- await click(f, trigger(f));
219
- expect(f.componentInstance.isOpen).toBe(false);
220
- const g = mount({ isOpen: true });
221
- expect(panel(g).hasAttribute("hidden")).toBe(false);
222
- });
223
-
224
- test("7.16 the tooltip is a hidden role=tooltip holding the label", () => {
225
- const f = mount();
226
- const tip = q<HTMLElement>(f, ".menu-picker-tooltip");
227
- expect(tip.getAttribute("role")).toBe("tooltip");
228
- expect(tip.textContent).toBe("Menu");
229
- expect(tip.hasAttribute("hidden")).toBe(true);
230
- });
231
-
232
- test("7.17 the tooltip shows on hover, hides on Escape, and never shows while open", async () => {
233
- const f = mount();
234
- const tip = q<HTMLElement>(f, ".menu-picker-tooltip");
235
- const host = q<HTMLElement>(f, "lily-icon-button");
236
- host.dispatchEvent(new MouseEvent("mouseenter"));
237
- f.detectChanges();
238
- expect(tip.hasAttribute("hidden")).toBe(false);
239
- await flush();
240
- document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape" }));
241
- f.detectChanges();
242
- expect(tip.hasAttribute("hidden")).toBe(true);
243
- host.dispatchEvent(new MouseEvent("mouseleave"));
244
- host.dispatchEvent(new MouseEvent("mouseenter"));
245
- f.detectChanges();
246
- expect(tip.hasAttribute("hidden")).toBe(false);
247
- await click(f, trigger(f));
248
- expect(tip.hasAttribute("hidden")).toBe(true);
249
- });
250
-
251
- test("7.18 the tooltip is not wired with aria-describedby", () => {
252
- const f = mount();
253
- expect(trigger(f).hasAttribute("aria-describedby")).toBe(false);
254
- });
255
-
256
- test("7.19 className is appended to the root", () => {
257
- const f = mount({ extra: "extra" });
258
- expect(q(f, "div.menu-picker").classList.contains("extra")).toBe(true);
259
- });
260
-
261
- test("7.20 two instances get distinct ids", () => {
262
- expect(nextMenuPickerId()).not.toBe(nextMenuPickerId());
263
- const a = mount();
264
- const b = mount();
265
- expect(panel(a).id).not.toBe(panel(b).id);
266
- });
267
-
268
- test("7.21 the source ships no stylesheet, inline style or English default", () => {
269
- const src = readFileSync(resolve(__dirname, "menu-picker.component.ts"), "utf8")
270
- .replace(/<!--[\s\S]*?-->/g, "")
271
- .replace(/\/\*[\s\S]*?\*\//g, "")
272
- .replace(/^\s*\/\/.*$/gm, "");
273
- expect(src).not.toMatch(/styles:|\sstyle=/);
274
- expect(src).not.toMatch(/(label|href):\s*["'`]/);
275
- });
276
- });
@@ -1,325 +0,0 @@
1
- import { NgTemplateOutlet } from "@angular/common";
2
- import {
3
- ChangeDetectionStrategy,
4
- Component,
5
- Directive,
6
- ElementRef,
7
- TemplateRef,
8
- computed,
9
- contentChild,
10
- effect,
11
- input,
12
- model,
13
- signal,
14
- viewChild,
15
- } from "@angular/core";
16
- // Only the trigger button composes a headless primitive. The panel is a disclosure of whatever the
17
- // app projects into it — links, buttons, forms — so it carries no ARIA menu roles (role="menu" would
18
- // promise a roving-focus menuitem widget that arbitrary content is not). See spec/index.md §3.
19
- import { IconButton } from "@lilydesignsystem/angular-headless";
20
-
21
- /** Context passed to a custom icon `<ng-template>` (the button icon). */
22
- export type ChildArgs = {
23
- /** Is the panel open? */
24
- open: boolean;
25
- };
26
-
27
- let uid = 0;
28
- /** Stable per-instance id prefix; SSR-safe (no Math.random / Date.now). */
29
- export function nextMenuPickerId(): string {
30
- uid += 1;
31
- return `menu-picker-${uid}`;
32
- }
33
-
34
- /** The selector for focusable things inside the panel. */
35
- export const FOCUSABLE =
36
- 'a[href], button:not([disabled]), input:not([disabled]):not([type="hidden"]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
37
-
38
- /**
39
- * Marker for the projected icon template. Gives consumers typed `let-` variables:
40
- *
41
- * ```html
42
- * <lily-menu-picker label="Menu">
43
- * <ng-template lilyMenuPickerIcon let-args>{{ args.open ? "×" : "☰" }}</ng-template>
44
- * …the panel's content…
45
- * </lily-menu-picker>
46
- * ```
47
- */
48
- @Directive({
49
- selector: "ng-template[lilyMenuPickerIcon]",
50
- standalone: true,
51
- })
52
- export class MenuPickerIcon {
53
- static ngTemplateContextGuard(
54
- _dir: MenuPickerIcon,
55
- _ctx: unknown,
56
- ): _ctx is ChildArgs & { $implicit: ChildArgs } {
57
- return true;
58
- }
59
- }
60
-
61
- /**
62
- * MenuPicker — a headless dropdown control.
63
- *
64
- * A single-icon button (a bundled hamburger SVG) that opens a disclosure panel holding whatever
65
- * the app projects into it. It applies nothing to the document and persists nothing. Give the
66
- * element a template reference (`#menu`) to call `menu.close()` from the projected content.
67
- * See `spec/index.md`.
68
- */
69
- @Component({
70
- selector: "lily-menu-picker",
71
- standalone: true,
72
- exportAs: "lilyMenuPicker",
73
- imports: [NgTemplateOutlet, IconButton],
74
- changeDetection: ChangeDetectionStrategy.OnPush,
75
- host: {
76
- "(document:click)": "onDocumentClick($event)",
77
- },
78
- template: `
79
- <div
80
- #rootEl
81
- class="menu-picker {{ className() }}"
82
- (focusout)="onRootFocusOut($event)"
83
- >
84
- <lily-icon-button
85
- #buttonEl
86
- [label]="label()"
87
- baseClass="menu-picker-button"
88
- [ariaExpanded]="open()"
89
- [ariaControls]="panelId"
90
- (click)="onButtonClick(); hoverButton.set(false)"
91
- (mouseenter)="onTooltipButtonEnter()"
92
- (mouseleave)="hoverButton.set(false)"
93
- (focusin)="onTooltipButtonFocusIn()"
94
- (focusout)="onTooltipButtonFocusOut()"
95
- (keydown)="onButtonKeydown($event); onTooltipKeydown($event)"
96
- >
97
- @if (iconTemplate(); as tpl) {
98
- <ng-container
99
- [ngTemplateOutlet]="tpl"
100
- [ngTemplateOutletContext]="childContext()"
101
- />
102
- } @else {
103
- <svg
104
- class="menu-picker-icon"
105
- viewBox="0 0 16 16"
106
- width="1.05rem"
107
- height="1.05rem"
108
- aria-hidden="true"
109
- fill="none"
110
- stroke="currentColor"
111
- stroke-width="1.6"
112
- stroke-linecap="round"
113
- stroke-linejoin="round"
114
- >
115
- <path d="M1.5 3.5h13M1.5 8h13M1.5 12.5h13" />
116
- </svg>
117
- }
118
- </lily-icon-button>
119
-
120
- <!-- Purely visual: the same text is already the button's aria-label, so it is not wired
121
- with aria-describedby (that would announce the name twice). -->
122
- <div
123
- class="menu-picker-tooltip"
124
- role="tooltip"
125
- [id]="tooltipId"
126
- [attr.hidden]="tooltipVisible() ? null : ''"
127
- (mouseenter)="hoverTooltip.set(true)"
128
- (mouseleave)="hoverTooltip.set(false)"
129
- >{{ label() }}</div>
130
-
131
- <!-- A named group, not a menu: the content is the app's. The handlers are pure delegation
132
- for whatever focusable content the app projects. -->
133
- <div
134
- #panelEl
135
- class="menu-picker-panel"
136
- [id]="panelId"
137
- role="group"
138
- [attr.aria-label]="label() || null"
139
- [attr.hidden]="open() ? null : ''"
140
- (keydown)="onPanelKeydown($event)"
141
- (click)="onPanelClick($event)"
142
- >
143
- <ng-content />
144
- </div>
145
- </div>
146
- `,
147
- })
148
- export class MenuPicker {
149
- /** Accessible name for the button, the panel and the tooltip. */
150
- readonly label = input.required<string>();
151
- /** Is the panel open? Two-way bindable: `[(open)]`. */
152
- readonly open = model<boolean>(false);
153
- /**
154
- * Close the panel when a link, button or `[role="menuitem"]` inside it is activated. Add
155
- * `data-menu-picker-keep-open` to an element (or an ancestor) to opt it out. Default `true`.
156
- */
157
- readonly closeOnSelect = input<boolean>(true);
158
- /** Extra CSS class on the root div. */
159
- readonly className = input<string>("");
160
-
161
- /** Projected icon template; replaces the default icon when supplied. */
162
- protected readonly iconTemplate = contentChild(MenuPickerIcon, { read: TemplateRef });
163
-
164
- private readonly rootRef =
165
- viewChild.required<ElementRef<HTMLDivElement>>("rootEl");
166
- // A template-ref-variable on a component tag resolves to the component INSTANCE, which is what
167
- // is needed to call the headless component's own public `focus()` method.
168
- private readonly buttonRef = viewChild.required<IconButton>("buttonEl");
169
- private readonly panelRef =
170
- viewChild.required<ElementRef<HTMLDivElement>>("panelEl");
171
-
172
- private readonly baseId = nextMenuPickerId();
173
- protected readonly panelId = `${this.baseId}-panel`;
174
-
175
- // Tooltip: shown while the pointer is over the button or the tooltip itself (hoverable, WCAG
176
- // 1.4.13) or while the button has keyboard focus; Escape dismisses it without moving focus;
177
- // never shown while the popup is open, since the popup then explains the control.
178
- protected readonly tooltipId = `${this.baseId}-tooltip`;
179
- protected readonly hoverButton = signal(false);
180
- protected readonly hoverTooltip = signal(false);
181
- protected readonly focusButton = signal(false);
182
- protected readonly dismissed = signal(false);
183
- protected readonly tooltipVisible = computed(
184
- () =>
185
- !this.open() &&
186
- !this.dismissed() &&
187
- (this.hoverButton() || this.hoverTooltip() || this.focusButton()),
188
- );
189
-
190
- protected onTooltipButtonEnter(): void {
191
- this.hoverButton.set(true);
192
- this.dismissed.set(false);
193
- }
194
-
195
- protected onTooltipButtonFocusIn(): void {
196
- // Keyboard focus only: a mouse click also focuses the button in Chromium, and the tooltip
197
- // should not stick after a click.
198
- try {
199
- this.focusButton.set(
200
- this.buttonRef().element?.matches(":focus-visible") ?? false,
201
- );
202
- } catch {
203
- this.focusButton.set(true); // engine without :focus-visible — err towards showing
204
- }
205
- }
206
-
207
- protected onTooltipButtonFocusOut(): void {
208
- this.focusButton.set(false);
209
- this.dismissed.set(false);
210
- }
211
-
212
- protected onTooltipKeydown(event: KeyboardEvent): void {
213
- if (event.key === "Escape" && this.tooltipVisible()) this.dismissed.set(true);
214
- }
215
-
216
- // WCAG 1.4.13 "dismissable": while the tooltip is visible, Escape works wherever focus is. The
217
- // listener exists only while visible; the effect cleanup removes it on hide and on destroy.
218
- private readonly tooltipEscapeListener = effect((onCleanup) => {
219
- if (!this.tooltipVisible() || typeof document === "undefined") return;
220
- const onKey = (event: KeyboardEvent): void => {
221
- if (event.key === "Escape") this.dismissed.set(true);
222
- };
223
- document.addEventListener("keydown", onKey);
224
- onCleanup(() => document.removeEventListener("keydown", onKey));
225
- });
226
-
227
- protected readonly childContext = computed(() => {
228
- const args: ChildArgs = { open: this.open() };
229
- return { $implicit: args, ...args };
230
- });
231
-
232
- private focusables(): HTMLElement[] {
233
- return Array.from(
234
- this.panelRef().nativeElement.querySelectorAll<HTMLElement>(FOCUSABLE),
235
- );
236
- }
237
-
238
- // ---------------------------------------------------------------
239
- // Open / close
240
- // ---------------------------------------------------------------
241
-
242
- /** Open the panel, optionally focusing its first or last focusable element. */
243
- openPanel(focus: "none" | "first" | "last" = "none"): void {
244
- this.open.set(true);
245
- if (focus === "none") return;
246
- // Deferred so the `hidden` attribute is gone before focus is moved.
247
- queueMicrotask(() => {
248
- const all = this.focusables();
249
- (focus === "last" ? all[all.length - 1] : all[0])?.focus({ preventScroll: true });
250
- });
251
- }
252
-
253
- /** Close the panel; `refocus` returns focus to the trigger. */
254
- closePanel(refocus = true): void {
255
- if (!this.open()) return;
256
- this.open.set(false);
257
- if (refocus) queueMicrotask(() => this.buttonRef().focus({ preventScroll: true }));
258
- }
259
-
260
- /** Close the panel and return focus to the button. For the projected content to call. */
261
- close(): void {
262
- this.closePanel();
263
- }
264
-
265
- protected onButtonClick(): void {
266
- if (this.open()) this.closePanel();
267
- else this.openPanel();
268
- }
269
-
270
- protected onPanelClick(event: MouseEvent): void {
271
- if (!this.closeOnSelect()) return;
272
- const target = event.target as Element | null;
273
- const hit = target?.closest?.('a[href], button, [role="menuitem"]');
274
- if (!hit || !this.panelRef().nativeElement.contains(hit)) return;
275
- if (hit.closest("[data-menu-picker-keep-open]")) return;
276
- this.closePanel();
277
- }
278
-
279
- // ---------------------------------------------------------------
280
- // Keyboard
281
- // ---------------------------------------------------------------
282
-
283
- protected onButtonKeydown(event: KeyboardEvent): void {
284
- if (event.key === "Escape" && this.open()) {
285
- event.preventDefault();
286
- this.closePanel();
287
- } else if (event.key === "ArrowDown") {
288
- // Enter and Space already produce a click; the arrows open and move into the panel.
289
- event.preventDefault();
290
- if (!this.open()) this.openPanel("first");
291
- else this.focusables()[0]?.focus({ preventScroll: true });
292
- } else if (event.key === "ArrowUp") {
293
- event.preventDefault();
294
- if (!this.open()) this.openPanel("last");
295
- else {
296
- const all = this.focusables();
297
- all[all.length - 1]?.focus({ preventScroll: true });
298
- }
299
- }
300
- }
301
-
302
- protected onPanelKeydown(event: KeyboardEvent): void {
303
- if (event.key === "Escape") {
304
- event.preventDefault();
305
- this.closePanel();
306
- } else if (event.key === "Tab") {
307
- // Focus goes to the button FIRST, without cancelling the key, so the browser continues
308
- // from the picker's position rather than from <body>.
309
- this.buttonRef().focus?.({ preventScroll: true });
310
- this.closePanel(false);
311
- }
312
- }
313
-
314
- protected onRootFocusOut(event: FocusEvent): void {
315
- const next = event.relatedTarget as Node | null;
316
- if (next && this.rootRef().nativeElement.contains(next)) return;
317
- this.closePanel(false);
318
- }
319
-
320
- protected onDocumentClick(event: Event): void {
321
- if (!this.open()) return;
322
- const t = event.target as Node | null;
323
- if (t && !this.rootRef().nativeElement.contains(t)) this.closePanel(false);
324
- }
325
- }
package/ng-package.json DELETED
@@ -1,10 +0,0 @@
1
- {
2
- "$schema": "../node_modules/ng-packagr/ng-package.schema.json",
3
- "dest": "./dist",
4
- "lib": {
5
- "entryFile": "index.ts"
6
- },
7
- "allowedNonPeerDependencies": [
8
- "@lilydesignsystem/angular-headless"
9
- ]
10
- }