@lilydesignsystem/angular-menu-picker 0.1.0 → 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/package.json CHANGED
@@ -1,22 +1,19 @@
1
1
  {
2
2
  "name": "@lilydesignsystem/angular-menu-picker",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "engines": {
5
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.2.0"
9
+ "@lilydesignsystem/angular-headless": "^0.2.0",
10
+ "tslib": "^2.3.0"
10
11
  },
11
12
  "peerDependencies": {
12
13
  "@angular/core": ">=20.0.0 <23.0.0",
13
14
  "@angular/common": ">=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",
@@ -37,5 +34,17 @@
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
+ }
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.
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,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`).
@@ -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.
package/index.ts DELETED
@@ -1,7 +0,0 @@
1
- export {
2
- MenuPicker,
3
- MenuPickerIcon,
4
- nextMenuPickerId,
5
- FOCUSABLE,
6
- } from "./menu-picker.component";
7
- export type { ChildArgs } from "./menu-picker.component";
@@ -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
- }
package/spec/index.md DELETED
@@ -1,124 +0,0 @@
1
- # MenuPicker (Angular) — spec
2
-
3
- **Package:** `@lilydesignsystem/angular-menu-picker` · **Canonical contract:** this file; the other seven catalogs port it.
4
- Topic summary: [spec/menu-picker](../../../spec/menu-picker/index.md).
5
-
6
- ## 1. Goal
7
-
8
- An icon button — a typical **hamburger** (three horizontal lines) — that opens a dropdown panel holding **whatever the app
9
- provides**: links, buttons, a search form, a small list of settings. It owns the whole interaction: open/close, focus,
10
- keyboard, tooltip. It owns no content and no words: the panel's content and every accessible name come from the consumer.
11
-
12
- ## 2. Non-goals
13
-
14
- - No default content, no English text, no routing, no data.
15
- - Not an ARIA menu: the panel is a named group of arbitrary content, so it has no `role="menu"` and no roving focus.
16
- - Not a site navigation bar (use `BreadcrumbNav`, `SectionNav`) and not a modal dialog (use `Dialog`).
17
- - No persistence.
18
-
19
- ## 3. Architectural decisions
20
-
21
- - **A disclosure of arbitrary content, not a menu.** `role="menu"` promises a roving-focus `menuitem` widget; arbitrary app
22
- content (links, inputs, headings) is not that, and the WAI-ARIA APG's disclosure pattern fits it exactly. The trigger carries
23
- `aria-expanded` and `aria-controls`; the panel is a `role="group"` named by `label`.
24
- - **Only the trigger composes headless** (`IconButton`); the panel is self-built, as in `link-picker` and `share-picker`.
25
- - **The app provides the panel's content** through `children` (a template, slot, `ChildContent`, children …). It receives
26
- `{ open, close }` so its own controls can close the panel.
27
- - **Closes on select by default.** Activating a link, a button or a `[role="menuitem"]` inside the panel closes it and returns
28
- focus to the button. `closeOnSelect={false}` turns that off for the whole panel; an ancestor with
29
- `data-menu-picker-keep-open` opts one element (or a region, e.g. a form) out.
30
- - **Focus is not moved on a click.** A disclosure must not steal focus on open; `ArrowDown`/`ArrowUp` on the button move into
31
- the panel's first/last focusable element for keyboard users.
32
-
33
- ## 4. Public API
34
-
35
- ### 4.1 Props
36
-
37
- | Prop | Type | Required | Notes |
38
- | --- | --- | --- | --- |
39
- | `label` | `string` | yes | Accessible name of the button **and** the panel; also the tooltip text. |
40
- | `[(open)]` | `boolean` (model) | no | Is the panel open? Default `false`. |
41
- | `closeOnSelect` | `boolean` | no | Close when a link/button/menuitem inside is activated. Default `true`. |
42
- | `(openChange)` | output `boolean` | no | Emitted whenever the open state changes (the `[(open)]` update). |
43
- | projected content | `<ng-content>` | no | The panel's content. Call `menu.close()` on a `#menu="lilyMenuPicker"` reference to close it. |
44
- | `<ng-template lilyMenuPickerIcon>` | context `{ open }` | no | Replaces the hamburger **icon** inside the button. |
45
- | `className` | `string` | no | Appended to the root class. |
46
- | `...rest` | | | Spread on the root. |
47
-
48
- ### 4.2 DOM contract
49
-
50
- ```html
51
- <div class="menu-picker {class}">
52
- <button class="menu-picker-button" type="button" aria-label="{label}" aria-expanded="false" aria-controls="{id}-panel">
53
- <svg class="menu-picker-icon" aria-hidden="true" viewBox="0 0 16 16" …three lines…/>
54
- </button>
55
- <div class="menu-picker-tooltip" role="tooltip" id="{id}-tooltip" hidden>{label}</div>
56
- <div class="menu-picker-panel" id="{id}-panel" role="group" aria-label="{label}" hidden>
57
- …whatever the app provides…
58
- </div>
59
- </div>
60
- ```
61
-
62
- ### 4.3 Re-exports
63
-
64
- `MenuPicker`, `MenuPickerIcon`, `nextMenuPickerId`, `FOCUSABLE`, type `ChildArgs`.
65
-
66
- **Differences from the Svelte contract (idiom, not behaviour).** Angular renders the same DOM in a `<lily-menu-picker>` element; the bindable `open` is a `model()` (`[(open)]`), the content is projected, the icon override is a projected `<ng-template lilyMenuPickerIcon>`, `class` is the `className` input, `close()` is a public method reachable through `exportAs: "lilyMenuPicker"`, and ids come from a module counter.
67
-
68
- ## 5. Behaviour
69
-
70
- ### 5.1 Activation
71
-
72
- Click toggles the panel; focus stays on the button. Activating a link, button or menuitem inside the panel closes it (see
73
- §3) and returns focus to the button.
74
-
75
- ### 5.2 Keyboard
76
-
77
- - Button: `Enter`/`Space` toggle; `ArrowDown` opens and focuses the panel's first focusable element (or moves into an open
78
- panel); `ArrowUp` focuses the last; `Escape` closes an open panel.
79
- - Panel: `Escape` closes and returns focus to the button; `Tab` closes and moves on (focus goes to the button first so the
80
- browser continues from the picker's position). Everything else is the app's content's own behaviour.
81
- - Clicking outside, or focus leaving the picker, closes the panel.
82
-
83
- ### 5.3 Tooltip
84
-
85
- `<div class="menu-picker-tooltip" role="tooltip" hidden>` holds the button's `label`. Visible while the pointer is over the
86
- button or the tooltip (hoverable) or while the button has keyboard focus (`:focus-visible`); `Escape` dismisses it wherever
87
- focus is; never shown while the panel is open. Not linked with `aria-describedby` (it duplicates the name).
88
-
89
- ## 6. Accessibility
90
-
91
- WCAG 2.2 AAA target. The button has an accessible name and `aria-expanded`/`aria-controls`; the icon is `aria-hidden`; the
92
- panel is a named group; focus stays visible; no motion. The content's own accessibility is the app's.
93
-
94
- ## 7. Testing acceptance criteria
95
-
96
- 1. Renders `div.menu-picker` containing `button.menu-picker-button`, a tooltip and a hidden `div.menu-picker-panel`.
97
- 2. The button's accessible name is `label`; it has `aria-expanded="false"` and `aria-controls` pointing at the panel.
98
- 3. The default icon is an `aria-hidden` SVG with class `menu-picker-icon`; `icon` replaces it.
99
- 4. The panel is a `role="group"` named with `label`.
100
- 5. The app's content renders inside the panel; with no content the panel is empty (nothing is invented).
101
- 6. Click opens: panel visible, `aria-expanded="true"`, focus stays on the button.
102
- 7. Click again closes.
103
- 8. `ArrowDown` on the button opens and focuses the first focusable element in the panel; `ArrowUp` focuses the last.
104
- 9. `Escape` closes and returns focus to the button.
105
- 10. `Tab` closes without teleporting focus.
106
- 11. Clicking outside closes.
107
- 12. Focus moving out of the picker closes.
108
- 13. Activating a link or button inside closes the panel and focuses the button; an element inside
109
- `data-menu-picker-keep-open` does not close it; `closeOnSelect=false` never closes on select.
110
- 14. The content receives `close()`, which closes the panel.
111
- 15. `open` is bindable and `onOpenChange` fires once per actual change.
112
- 16. The tooltip exists with `role="tooltip"`, holds `label`, and is hidden at rest.
113
- 17. The tooltip shows on hover, hides on `Escape`, and never shows while the panel is open.
114
- 18. The tooltip is not referenced by `aria-describedby`.
115
- 19. `class` is appended to the root; rest props are spread on the root.
116
- 20. Two instances get distinct ids.
117
- 21. The package source contains no stylesheet, no inline `style` and no English default text.
118
-
119
- ## 8. Tracking
120
-
121
- - Package: `@lilydesignsystem/angular-menu-picker`
122
- - Version: 0.1.0 (new, 2026-10-08)
123
- - Framework: Angular `>=20 <23` (signals, standalone, OnPush)
124
- - Test runner: vitest + TestBed
package/tsconfig.lib.json DELETED
@@ -1,13 +0,0 @@
1
- {
2
- "extends": "../tsconfig.json",
3
- "compilerOptions": {
4
- "outDir": "./out-tsc/lib",
5
- "declaration": true,
6
- "declarationMap": true,
7
- "types": []
8
- },
9
- "angularCompilerOptions": {
10
- "compilationMode": "partial"
11
- },
12
- "exclude": ["**/*.spec.ts", "node_modules", "dist"]
13
- }
File without changes