semmet-angular 0.45.0 → 0.46.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/package.json +1 -1
  2. package/src/accordion/files/__name@dasherize__/__name@dasherize__.css.template +0 -39
  3. package/src/accordion/files/__name@dasherize__/__name@dasherize__.e2e-spec.ts.template +16 -17
  4. package/src/accordion/files/__name@dasherize__/__name@dasherize__.html.template +14 -34
  5. package/src/accordion/files/__name@dasherize__/__name@dasherize__.spec.ts.template +52 -62
  6. package/src/accordion/files/__name@dasherize__/__name@dasherize__.ts.template +88 -82
  7. package/src/accordion/schema.json +1 -1
  8. package/src/block/files/__name@dasherize__/__name@dasherize__.html.template +51 -23
  9. package/src/block/files/__name@dasherize__/__name@dasherize__.ts.template +16 -10
  10. package/src/breadcrumb/files/__name@dasherize__/__name@dasherize__.css.template +0 -32
  11. package/src/breadcrumb/files/__name@dasherize__/__name@dasherize__.e2e-spec.ts.template +5 -2
  12. package/src/breadcrumb/files/__name@dasherize__/__name@dasherize__.html.template +8 -7
  13. package/src/breadcrumb/files/__name@dasherize__/__name@dasherize__.spec.ts.template +51 -0
  14. package/src/breadcrumb/files/__name@dasherize__/__name@dasherize__.ts.template +18 -19
  15. package/src/breadcrumb/index.js +1 -1
  16. package/src/breadcrumb/index.js.map +1 -1
  17. package/src/breadcrumb/index.ts +1 -1
  18. package/src/breadcrumb/schema.json +1 -1
  19. package/src/collection.json +9 -9
  20. package/src/menu-button/files/__name@dasherize__/__name@dasherize__.css.template +0 -48
  21. package/src/menu-button/files/__name@dasherize__/__name@dasherize__.e2e-spec.ts.template +2 -2
  22. package/src/menu-button/files/__name@dasherize__/__name@dasherize__.html.template +14 -35
  23. package/src/menu-button/files/__name@dasherize__/__name@dasherize__.spec.ts.template +53 -11
  24. package/src/menu-button/files/__name@dasherize__/__name@dasherize__.ts.template +128 -54
  25. package/src/menu-button/schema.json +1 -1
  26. package/src/navbar/files/__name@dasherize__/__name@dasherize__.css.template +0 -119
  27. package/src/navbar/files/__name@dasherize__/__name@dasherize__.e2e-spec.ts.template +10 -7
  28. package/src/navbar/files/__name@dasherize__/__name@dasherize__.html.template +18 -42
  29. package/src/navbar/files/__name@dasherize__/__name@dasherize__.spec.ts.template +46 -23
  30. package/src/navbar/files/__name@dasherize__/__name@dasherize__.ts.template +54 -50
  31. package/src/navbar/schema.json +1 -1
  32. package/src/pagination/files/__name@dasherize__/__name@dasherize__.css.template +0 -35
  33. package/src/pagination/files/__name@dasherize__/__name@dasherize__.e2e-spec.ts.template +2 -2
  34. package/src/pagination/files/__name@dasherize__/__name@dasherize__.html.template +11 -13
  35. package/src/pagination/files/__name@dasherize__/__name@dasherize__.spec.ts.template +58 -8
  36. package/src/pagination/files/__name@dasherize__/__name@dasherize__.ts.template +17 -31
  37. package/src/pagination/schema.json +1 -1
  38. package/src/table/files/__name@dasherize__/__name@dasherize__.css.template +0 -17
  39. package/src/table/files/__name@dasherize__/__name@dasherize__.e2e-spec.ts.template +1 -1
  40. package/src/table/files/__name@dasherize__/__name@dasherize__.html.template +13 -25
  41. package/src/table/files/__name@dasherize__/__name@dasherize__.ts.template +3 -35
  42. package/src/table/schema.json +1 -1
  43. package/src/tabs/files/__name@dasherize__/__name@dasherize__.css.template +0 -40
  44. package/src/tabs/files/__name@dasherize__/__name@dasherize__.e2e-spec.ts.template +2 -2
  45. package/src/tabs/files/__name@dasherize__/__name@dasherize__.html.template +18 -41
  46. package/src/tabs/files/__name@dasherize__/__name@dasherize__.spec.ts.template +39 -31
  47. package/src/tabs/files/__name@dasherize__/__name@dasherize__.ts.template +111 -113
  48. package/src/tabs/schema.json +1 -1
  49. package/src/toolbar/files/__name@dasherize__/__name@dasherize__.css.template +0 -19
  50. package/src/toolbar/files/__name@dasherize__/__name@dasherize__.e2e-spec.ts.template +3 -4
  51. package/src/toolbar/files/__name@dasherize__/__name@dasherize__.html.template +7 -16
  52. package/src/toolbar/files/__name@dasherize__/__name@dasherize__.spec.ts.template +26 -11
  53. package/src/toolbar/files/__name@dasherize__/__name@dasherize__.ts.template +59 -37
  54. package/src/toolbar/schema.json +1 -1
  55. package/src/tree-view/files/__name@dasherize__/__name@dasherize__.css.template +0 -39
  56. package/src/tree-view/files/__name@dasherize__/__name@dasherize__.e2e-spec.ts.template +4 -5
  57. package/src/tree-view/files/__name@dasherize__/__name@dasherize__.html.template +12 -31
  58. package/src/tree-view/files/__name@dasherize__/__name@dasherize__.spec.ts.template +80 -43
  59. package/src/tree-view/files/__name@dasherize__/__name@dasherize__.ts.template +105 -88
  60. package/src/tree-view/schema.json +1 -1
@@ -38,14 +38,15 @@ Import <%= classify(name) %> in that parent component.
38
38
  <h2 id="<%= dasherize(name) %>-title">{{ title }}</h2>
39
39
  <p class="semmet-block__summary">{{ summary }}</p>
40
40
  </header>
41
- <div class="semmet-block__stack">
42
- <<%= supportSelector %>>
43
- @for (item of items; track item.title; let first = $first) {
44
- <ng-template <%= camelize(supportName) %>Item [id]="item.title" [title]="item.title" [expanded]="first">
45
- <p>{{ item.text }}</p>
46
- </ng-template>
47
- }
48
- </<%= supportSelector %>>
41
+ <div class="semmet-block__stack" <%= camelize(supportName) %>>
42
+ @for (item of items; track item.title; let first = $first) {
43
+ <h3>
44
+ <button type="button" <%= camelize(supportName) %>Header [value]="item.title" [expanded]="first">{{ item.title }}</button>
45
+ </h3>
46
+ <div <%= camelize(supportName) %>Panel [value]="item.title">
47
+ <p>{{ item.text }}</p>
48
+ </div>
49
+ }
49
50
  </div>
50
51
  </section>
51
52
  <% } else if (kind === 'section-table') { %>
@@ -56,9 +57,21 @@ Import <%= classify(name) %> in that parent component.
56
57
  <p class="semmet-block__summary">{{ summary }}</p>
57
58
  </header>
58
59
  <<%= supportSelector %>>
59
- <ng-template <%= camelize(supportName) %>Caption>
60
- {{ title }}
61
- </ng-template>
60
+ <caption>{{ title }}</caption>
61
+ <thead>
62
+ <tr>
63
+ <th scope="col">Item</th>
64
+ <th scope="col">Details</th>
65
+ </tr>
66
+ </thead>
67
+ <tbody>
68
+ @for (item of items; track item.title) {
69
+ <tr>
70
+ <th scope="row">{{ item.title }}</th>
71
+ <td>{{ item.text }}</td>
72
+ </tr>
73
+ }
74
+ </tbody>
62
75
  </<%= supportSelector %>>
63
76
  </section>
64
77
  <% } else if (kind === 'section-form') { %>
@@ -77,13 +90,18 @@ Import <%= classify(name) %> in that parent component.
77
90
  <h2 id="<%= dasherize(name) %>-title">{{ title }}</h2>
78
91
  <p class="semmet-block__summary">{{ summary }}</p>
79
92
  </header>
80
- <<%= supportSelector %>>
81
- @for (item of items; track item.title; let first = $first) {
82
- <ng-template <%= camelize(supportName) %>Item [id]="item.title" [label]="item.title" [selected]="first">
93
+ <div <%= camelize(supportName) %>>
94
+ <div <%= camelize(supportName) %>List [label]="title">
95
+ @for (item of items; track item.title; let first = $first) {
96
+ <button type="button" <%= camelize(supportName) %>Tab [value]="item.title" [selected]="first">{{ item.title }}</button>
97
+ }
98
+ </div>
99
+ @for (item of items; track item.title) {
100
+ <div <%= camelize(supportName) %>Panel [value]="item.title">
83
101
  <p>{{ item.text }}</p>
84
- </ng-template>
102
+ </div>
85
103
  }
86
- </<%= supportSelector %>>
104
+ </div>
87
105
  </section>
88
106
  <% } else if (kind === 'section-list-group') { %>
89
107
  <section class="semmet-block" aria-labelledby="<%= dasherize(name) %>-title">
@@ -137,14 +155,24 @@ Import <%= classify(name) %> in that parent component.
137
155
  </section>
138
156
  <% } else if (kind === 'header-navbar') { %>
139
157
  <header class="semmet-block semmet-block--header">
140
- <<%= supportSelector %>>
141
- <ng-template <%= camelize(supportName) %>Brand>
142
- {{ title }}
143
- </ng-template>
144
- <ng-template <%= camelize(supportName) %>Actions>
158
+ <nav <%= camelize(supportName) %> label="Main navigation" #<%= camelize(supportName) %>Ref="<%= camelize(supportName) %>">
159
+ <a class="semmet-block__brand" href="/">{{ title }}</a>
160
+
161
+ <button type="button" <%= camelize(supportName) %>Toggle aria-label="Toggle navigation">
162
+ <span aria-hidden="true"></span>
163
+ </button>
164
+
165
+ <div <%= camelize(supportName) %>Menu>
166
+ <ul>
167
+ @for (item of items; track item.title) {
168
+ <li>
169
+ <a <%= camelize(supportName) %>Item href="#<%= dasherize(name) %>-{{ item.title }}">{{ item.title }}</a>
170
+ </li>
171
+ }
172
+ </ul>
145
173
  <a href="#main-content">Skip to content</a>
146
- </ng-template>
147
- </<%= supportSelector %>>
174
+ </div>
175
+ </nav>
148
176
  </header>
149
177
  <% } else if (kind === 'footer-navigation') { %>
150
178
  <footer class="semmet-block semmet-block--footer">
@@ -4,20 +4,23 @@ import { Component } from '@angular/core';
4
4
  } from './<%= dasherize(supportName) %>/<%= dasherize(supportName) %>';
5
5
  <% } else if (supportVariable === 'accordion') { %>import {
6
6
  <%= classify(supportName) %>,
7
- <%= classify(supportName) %>Item,
7
+ <%= classify(supportName) %>Header,
8
+ <%= classify(supportName) %>Panel,
8
9
  } from './<%= dasherize(supportName) %>/<%= dasherize(supportName) %>';
9
10
  <% } else if (supportVariable === 'table') { %>import {
10
11
  <%= classify(supportName) %>,
11
- <%= classify(supportName) %>Caption,
12
12
  } from './<%= dasherize(supportName) %>/<%= dasherize(supportName) %>';
13
13
  <% } else if (supportVariable === 'navbar') { %>import {
14
14
  <%= classify(supportName) %>,
15
- <%= classify(supportName) %>Actions,
16
- <%= classify(supportName) %>Brand,
15
+ <%= classify(supportName) %>Item,
16
+ <%= classify(supportName) %>Menu,
17
+ <%= classify(supportName) %>Toggle,
17
18
  } from './<%= dasherize(supportName) %>/<%= dasherize(supportName) %>';
18
19
  <% } else if (supportVariable === 'tabs') { %>import {
19
20
  <%= classify(supportName) %>,
20
- <%= classify(supportName) %>Item,
21
+ <%= classify(supportName) %>List,
22
+ <%= classify(supportName) %>Panel,
23
+ <%= classify(supportName) %>Tab,
21
24
  } from './<%= dasherize(supportName) %>/<%= dasherize(supportName) %>';
22
25
  <% } else if (supportVariable === 'carousel') { %>import {
23
26
  <%= classify(supportName) %>,
@@ -49,14 +52,17 @@ interface <%= classify(name) %>FooterGroup {
49
52
  <%= classify(supportName) %>,
50
53
  <% if (supportVariable === 'card') { %>
51
54
  <% } else if (supportVariable === 'accordion') { %>
52
- <%= classify(supportName) %>Item,
55
+ <%= classify(supportName) %>Header,
56
+ <%= classify(supportName) %>Panel,
53
57
  <% } else if (supportVariable === 'table') { %>
54
- <%= classify(supportName) %>Caption,
55
58
  <% } else if (supportVariable === 'navbar') { %>
56
- <%= classify(supportName) %>Actions,
57
- <%= classify(supportName) %>Brand,
58
- <% } else if (supportVariable === 'tabs') { %>
59
59
  <%= classify(supportName) %>Item,
60
+ <%= classify(supportName) %>Menu,
61
+ <%= classify(supportName) %>Toggle,
62
+ <% } else if (supportVariable === 'tabs') { %>
63
+ <%= classify(supportName) %>List,
64
+ <%= classify(supportName) %>Panel,
65
+ <%= classify(supportName) %>Tab,
60
66
  <% } else if (supportVariable === 'carousel') { %>
61
67
  <%= classify(supportName) %>Slide,
62
68
  <% } else if (supportVariable === 'listGroup') { %>
@@ -1,32 +0,0 @@
1
- :host {
2
- display: block;
3
- }
4
-
5
- ol {
6
- display: flex;
7
- flex-wrap: wrap;
8
- align-items: center;
9
- margin: 0;
10
- padding: 0;
11
- list-style: none;
12
- }
13
-
14
- li {
15
- display: flex;
16
- align-items: center;
17
- }
18
-
19
- li:not(:last-child)::after {
20
- content: '/';
21
- opacity: 0.6;
22
- }
23
-
24
- a {
25
- color: inherit;
26
- text-decoration: none;
27
- }
28
-
29
- a:focus-visible {
30
- outline: 2px solid currentColor;
31
- outline-offset: 2px;
32
- }
@@ -3,8 +3,11 @@ import { expect, test } from '@playwright/test';
3
3
  test.describe('<%= classify(name) %>', () => {
4
4
  test('renders mounted breadcrumb navigation with current page', async ({ page }) => {
5
5
  await page.goto('/');
6
- const component = page.locator('<%= selector %>').first();
7
- test.skip((await component.count()) === 0, 'Mount <<%= selector %> /> in a route or host component to enable this generated e2e test.');
6
+ const component = page.locator('[<%= camelize(name) %>]').first();
7
+ test.skip(
8
+ (await component.count()) === 0,
9
+ 'Mount an element with [<%= camelize(name) %>] in a route or host component to enable this generated e2e test.'
10
+ );
8
11
 
9
12
  await expect(component.getByRole('navigation', { name: /breadcrumb/i })).toBeVisible();
10
13
  await expect(component.locator('[aria-current="page"]')).toBeVisible();
@@ -1,15 +1,16 @@
1
1
  <!-- Semmet Angular: Breadcrumb — https://www.w3.org/WAI/ARIA/apg/patterns/breadcrumb/ -->
2
2
  <!--
3
3
  Usage from a parent component template:
4
- Import <%= classify(name) %> in that parent component.
4
+ Import <%= classify(name) %> and <%= classify(name) %>Item in that parent component.
5
+ This directive set never owns markup: bring your own <nav>/<ol>/<li> structure.
5
6
 
6
- <<%= selector %> label="Breadcrumb" currentPageLabel="Current page" [items]="breadcrumbItems" />
7
- -->
8
- <nav [attr.aria-label]="label()">
7
+ <nav <%= camelize(name) %>>
9
8
  <ol>
10
- @for (item of items(); track item.id) {
11
- <li><a [href]="item.href">{{ item.label }}</a></li>
9
+ @for (item of trail; track item.href) {
10
+ <li>
11
+ <a <%= camelize(name) %>Item [href]="item.href" [current]="item.current">{{ item.label }}</a>
12
+ </li>
12
13
  }
13
- <li aria-current="page">{{ currentPageLabel() }}</li>
14
14
  </ol>
15
15
  </nav>
16
+ -->
@@ -0,0 +1,51 @@
1
+ import { Component } from '@angular/core';
2
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
3
+
4
+ import { <%= classify(name) %>, <%= classify(name) %>Item } from './<%= dasherize(name) %>';
5
+
6
+ @Component({
7
+ imports: [<%= classify(name) %>, <%= classify(name) %>Item],
8
+ template: `
9
+ <nav <%= camelize(name) %>>
10
+ <ol>
11
+ @for (item of trail; track item.href) {
12
+ <li>
13
+ <a <%= camelize(name) %>Item [href]="item.href" [current]="item.current">{{ item.label }}</a>
14
+ </li>
15
+ }
16
+ </ol>
17
+ </nav>
18
+ `,
19
+ })
20
+ class HostComponent {
21
+ readonly trail = [
22
+ { label: 'Home', href: '/', current: false },
23
+ { label: 'Category', href: '/category', current: false },
24
+ { label: 'Current item', href: '/category/current-item', current: true },
25
+ ];
26
+ }
27
+
28
+ describe('<%= classify(name) %>', () => {
29
+ let fixture: ComponentFixture<HostComponent>;
30
+ let host: HTMLElement;
31
+
32
+ beforeEach(async () => {
33
+ await TestBed.configureTestingModule({
34
+ imports: [HostComponent],
35
+ }).compileComponents();
36
+
37
+ fixture = TestBed.createComponent(HostComponent);
38
+ fixture.detectChanges();
39
+ host = fixture.nativeElement as HTMLElement;
40
+ });
41
+
42
+ it('decorates consumer-owned markup with breadcrumb navigation semantics', () => {
43
+ const nav = host.querySelector<HTMLElement>('nav');
44
+ const links = Array.from(host.querySelectorAll<HTMLAnchorElement>('a'));
45
+
46
+ expect(nav?.getAttribute('aria-label')).toBe('Breadcrumb');
47
+ expect(links.length).toBe(3);
48
+ expect(links[0].getAttribute('aria-current')).toBeNull();
49
+ expect(links[2].getAttribute('aria-current')).toBe('page');
50
+ });
51
+ });
@@ -1,24 +1,23 @@
1
- import { Component, input } from '@angular/core';
1
+ import { Directive, booleanAttribute, input } from '@angular/core';
2
2
 
3
- interface <%= classify(name) %>Item {
4
- id: number;
5
- label: string;
6
- href: string;
7
- }
8
-
9
- const DEFAULT_ITEMS: readonly <%= classify(name) %>Item[] = [
10
- { id: 1, label: 'Home', href: '/' },
11
- { id: 2, label: 'Category', href: '/category' },
12
- ];
13
-
14
- @Component({
15
- selector: '<%= selector %>',
16
- imports: [],
17
- templateUrl: './<%= dasherize(name) %>.html',
18
- styleUrl: './<%= dasherize(name) %>.css',
3
+ @Directive({
4
+ selector: '[<%= camelize(name) %>]',
5
+ exportAs: '<%= camelize(name) %>',
6
+ host: {
7
+ '[attr.aria-label]': 'label() || null',
8
+ },
19
9
  })
20
10
  export class <%= classify(name) %> {
21
11
  readonly label = input('Breadcrumb');
22
- readonly currentPageLabel = input('Current page');
23
- readonly items = input<readonly <%= classify(name) %>Item[]>(DEFAULT_ITEMS);
12
+ }
13
+
14
+ @Directive({
15
+ selector: '[<%= camelize(name) %>Item]',
16
+ exportAs: '<%= camelize(name) %>Item',
17
+ host: {
18
+ '[attr.aria-current]': 'current() ? "page" : null',
19
+ },
20
+ })
21
+ export class <%= classify(name) %>Item {
22
+ readonly current = input(false, { transform: booleanAttribute });
24
23
  }
@@ -2,5 +2,5 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.breadcrumb = void 0;
4
4
  const generate_component_1 = require("../utils/generate-component");
5
- exports.breadcrumb = (0, generate_component_1.createAriaComponentSchematic)({ includeSharedE2eTest: false });
5
+ exports.breadcrumb = (0, generate_component_1.createAriaComponentSchematic)({ includeSharedUnitTest: false, includeSharedE2eTest: false });
6
6
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":";;;AAAA,oEAA2E;AAE9D,QAAA,UAAU,GAAG,IAAA,iDAA4B,EAAC,EAAE,oBAAoB,EAAE,KAAK,EAAE,CAAC,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":";;;AAAA,oEAA2E;AAE9D,QAAA,UAAU,GAAG,IAAA,iDAA4B,EAAC,EAAE,qBAAqB,EAAE,KAAK,EAAE,oBAAoB,EAAE,KAAK,EAAE,CAAC,CAAC"}
@@ -1,3 +1,3 @@
1
1
  import { createAriaComponentSchematic } from '../utils/generate-component';
2
2
 
3
- export const breadcrumb = createAriaComponentSchematic({ includeSharedE2eTest: false });
3
+ export const breadcrumb = createAriaComponentSchematic({ includeSharedUnitTest: false, includeSharedE2eTest: false });
@@ -3,7 +3,7 @@
3
3
  "$id": "SemmetAngularBreadcrumb",
4
4
  "title": "Semmet Angular Breadcrumb Options Schema",
5
5
  "type": "object",
6
- "description": "Generates a standalone Angular Breadcrumb component following the W3C ARIA Authoring Practices Guide.",
6
+ "description": "Generates headless Angular Breadcrumb directives (root, item) coordinated over consumer-owned nav/ol/li markup, following the W3C ARIA Authoring Practices Guide.",
7
7
  "additionalProperties": false,
8
8
  "properties": {
9
9
  "name": {
@@ -32,12 +32,12 @@
32
32
  "schema": "./block/schema.json"
33
33
  },
34
34
  "accordion": {
35
- "description": "Generates a standalone Angular Accordion component following the W3C ARIA Authoring Practices Guide.",
35
+ "description": "Generates headless Angular Accordion directives (root, header, panel) coordinated over consumer-owned markup, following the W3C ARIA Authoring Practices Guide.",
36
36
  "factory": "./accordion/index#accordion",
37
37
  "schema": "./accordion/schema.json"
38
38
  },
39
39
  "tabs": {
40
- "description": "Generates a standalone Angular Tabs component following the W3C ARIA Authoring Practices Guide.",
40
+ "description": "Generates headless Angular Tabs directives (root, list, tab, panel) coordinated over consumer-owned markup, following the W3C ARIA Authoring Practices Guide.",
41
41
  "factory": "./tabs/index#tabs",
42
42
  "schema": "./tabs/schema.json"
43
43
  },
@@ -62,7 +62,7 @@
62
62
  "schema": "./tooltip/schema.json"
63
63
  },
64
64
  "menu-button": {
65
- "description": "Generates a standalone Angular Menu Button (dropdown) component following the W3C ARIA Authoring Practices Guide.",
65
+ "description": "Generates headless Angular Menu Button directives (root, trigger, menu, item) coordinated over consumer-owned markup, following the W3C ARIA Authoring Practices Guide.",
66
66
  "factory": "./menu-button/index#menuButton",
67
67
  "schema": "./menu-button/schema.json"
68
68
  },
@@ -117,7 +117,7 @@
117
117
  "schema": "./combobox/schema.json"
118
118
  },
119
119
  "breadcrumb": {
120
- "description": "Generates a standalone Angular Breadcrumb component following the W3C ARIA Authoring Practices Guide.",
120
+ "description": "Generates headless Angular Breadcrumb directives (root, item) coordinated over consumer-owned markup, following the W3C ARIA Authoring Practices Guide.",
121
121
  "factory": "./breadcrumb/index#breadcrumb",
122
122
  "schema": "./breadcrumb/schema.json"
123
123
  },
@@ -127,12 +127,12 @@
127
127
  "schema": "./navigation-menu/schema.json"
128
128
  },
129
129
  "navbar": {
130
- "description": "Generates a standalone Angular Navbar component with semantic navigation, responsive disclosure toggle, and current-page state.",
130
+ "description": "Generates headless Angular Navbar directives (root, item, toggle, menu) for semantic navigation, responsive disclosure toggle, and current-page state, coordinated over consumer-owned markup.",
131
131
  "factory": "./navbar/index#navbar",
132
132
  "schema": "./navbar/schema.json"
133
133
  },
134
134
  "pagination": {
135
- "description": "Generates a standalone Angular Pagination component with working page state.",
135
+ "description": "Generates headless Angular Pagination directives (root, item) coordinated over consumer-owned markup.",
136
136
  "factory": "./pagination/index#pagination",
137
137
  "schema": "./pagination/schema.json"
138
138
  },
@@ -147,12 +147,12 @@
147
147
  "schema": "./landmarks/schema.json"
148
148
  },
149
149
  "tree-view": {
150
- "description": "Generates a standalone Angular Tree View component following the W3C ARIA Authoring Practices Guide.",
150
+ "description": "Generates headless Angular Tree View directives (root, item, group) coordinated over consumer-owned, arbitrarily nested markup, following the W3C ARIA Authoring Practices Guide.",
151
151
  "factory": "./tree-view/index#treeView",
152
152
  "schema": "./tree-view/schema.json"
153
153
  },
154
154
  "toolbar": {
155
- "description": "Generates a standalone Angular Toolbar component following the W3C ARIA Authoring Practices Guide.",
155
+ "description": "Generates headless Angular Toolbar directives (root, item) coordinated over consumer-owned markup, following the W3C ARIA Authoring Practices Guide.",
156
156
  "factory": "./toolbar/index#toolbar",
157
157
  "schema": "./toolbar/schema.json"
158
158
  },
@@ -162,7 +162,7 @@
162
162
  "schema": "./card/schema.json"
163
163
  },
164
164
  "table": {
165
- "description": "Generates a standalone Angular accessible data table component following the W3C ARIA Authoring Practices Guide.",
165
+ "description": "Generates a minimal standalone Angular table wrapper with content projection — no fixed column count or row shape — following the W3C ARIA Authoring Practices Guide.",
166
166
  "factory": "./table/index#table",
167
167
  "schema": "./table/schema.json"
168
168
  },
@@ -1,48 +0,0 @@
1
- :host {
2
- position: relative;
3
- display: inline-block;
4
- }
5
-
6
- button {
7
- display: flex;
8
- align-items: center;
9
- font: inherit;
10
- cursor: pointer;
11
- }
12
-
13
- button:focus-visible {
14
- outline: 2px solid currentColor;
15
- outline-offset: 2px;
16
- }
17
-
18
- button::after {
19
- content: '';
20
- width: 0.4rem;
21
- height: 0.4rem;
22
- border-right: 2px solid currentColor;
23
- border-bottom: 2px solid currentColor;
24
- transform: rotate(45deg);
25
- }
26
-
27
- button[aria-expanded='true']::after {
28
- transform: rotate(-135deg);
29
- }
30
-
31
- ul {
32
- position: absolute;
33
- z-index: 1;
34
- top: 100%;
35
- left: 0;
36
- margin: 0;
37
- padding: 0;
38
- list-style: none;
39
- }
40
-
41
- [role='menuitem'] {
42
- cursor: pointer;
43
- }
44
-
45
- [role='menuitem']:focus-visible {
46
- outline: 2px solid currentColor;
47
- outline-offset: 2px;
48
- }
@@ -4,10 +4,10 @@ test.describe('<%= classify(name) %>', () => {
4
4
  test('opens mounted menu button and closes it with Escape', async ({ page }) => {
5
5
  await page.goto('/');
6
6
 
7
- const component = page.locator('<%= selector %>').first();
7
+ const component = page.locator('[<%= camelize(name) %>]').first();
8
8
  test.skip(
9
9
  (await component.count()) === 0,
10
- 'Mount <<%= selector %> /> in a route or host component to enable this generated e2e test.'
10
+ 'Mount an element with [<%= camelize(name) %>] in a route or host component to enable this generated e2e test.'
11
11
  );
12
12
 
13
13
  const button = component.getByRole('button').first();
@@ -1,40 +1,19 @@
1
1
  <!-- Semmet Angular: Menu Button — https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/ -->
2
2
  <!--
3
3
  Usage from a parent component template:
4
- Import <%= classify(name) %> in that parent component.
4
+ Import <%= classify(name) %>, <%= classify(name) %>Trigger, <%= classify(name) %>Menu and <%= classify(name) %>Item
5
+ in that parent component. This directive set never owns markup: bring your own wrapper, trigger
6
+ button and popup list, in whatever structure the layout needs.
5
7
 
6
- <<%= selector %> label="Options" [items]="menuItems" />
8
+ <div <%= camelize(name) %> style="position: relative; display: inline-block;">
9
+ <button type="button" <%= camelize(name) %>Trigger>Options</button>
10
+
11
+ <ul <%= camelize(name) %>Menu style="position: absolute;">
12
+ @for (item of items; track item.id) {
13
+ <li role="none">
14
+ <div <%= camelize(name) %>Item>{{ item.label }}</div>
15
+ </li>
16
+ }
17
+ </ul>
18
+ </div>
7
19
  -->
8
- <button
9
- #menuButton
10
- type="button"
11
- [id]="instanceId + '-button'"
12
- aria-haspopup="true"
13
- [attr.aria-expanded]="isOpen()"
14
- [attr.aria-controls]="instanceId + '-menu'"
15
- (click)="toggle()"
16
- (keydown)="handleButtonKeydown($event)"
17
- >
18
- {{ label() }}
19
- </button>
20
- <ul
21
- #menu
22
- role="menu"
23
- [id]="instanceId + '-menu'"
24
- [attr.aria-labelledby]="instanceId + '-button'"
25
- [hidden]="!isOpen()"
26
- >
27
- @for (item of items(); track item.id; let i = $index) {
28
- <li role="none">
29
- <div
30
- #menuItem
31
- role="menuitem"
32
- tabindex="-1"
33
- (click)="select(item)"
34
- (keydown)="handleMenuKeydown($event, item, i)"
35
- >
36
- {{ item.label }}
37
- </div>
38
- </li>
39
- }
40
- </ul>
@@ -1,28 +1,60 @@
1
+ import { Component } from '@angular/core';
1
2
  import { ComponentFixture, TestBed } from '@angular/core/testing';
2
- import { <%= classify(name) %> } from './<%= dasherize(name) %>';
3
+
4
+ import {
5
+ <%= classify(name) %>,
6
+ <%= classify(name) %>Item,
7
+ <%= classify(name) %>Menu,
8
+ <%= classify(name) %>Trigger,
9
+ } from './<%= dasherize(name) %>';
10
+
11
+ @Component({
12
+ imports: [<%= classify(name) %>, <%= classify(name) %>Item, <%= classify(name) %>Menu, <%= classify(name) %>Trigger],
13
+ template: `
14
+ <div <%= camelize(name) %>>
15
+ <button type="button" <%= camelize(name) %>Trigger>Options</button>
16
+ <ul <%= camelize(name) %>Menu>
17
+ @for (item of items; track item.id) {
18
+ <li role="none">
19
+ <div <%= camelize(name) %>Item>{{ item.label }}</div>
20
+ </li>
21
+ }
22
+ </ul>
23
+ </div>
24
+ `,
25
+ })
26
+ class HostComponent {
27
+ readonly items = [
28
+ { id: 1, label: 'First option' },
29
+ { id: 2, label: 'Second option' },
30
+ { id: 3, label: 'Third option' },
31
+ ];
32
+ }
3
33
 
4
34
  describe('<%= classify(name) %>', () => {
5
- let fixture: ComponentFixture<<%= classify(name) %>>;
35
+ let fixture: ComponentFixture<HostComponent>;
36
+ let host: HTMLElement;
6
37
 
7
38
  beforeEach(async () => {
8
- await TestBed.configureTestingModule({ imports: [<%= classify(name) %>] }).compileComponents();
9
- fixture = TestBed.createComponent(<%= classify(name) %>);
39
+ await TestBed.configureTestingModule({ imports: [HostComponent] }).compileComponents();
40
+ fixture = TestBed.createComponent(HostComponent);
10
41
  fixture.detectChanges();
42
+ host = fixture.nativeElement as HTMLElement;
11
43
  });
12
44
 
13
45
  function trigger(): HTMLButtonElement {
14
- return fixture.nativeElement.querySelector('button');
46
+ return host.querySelector('button')!;
15
47
  }
16
48
 
17
49
  function menu(): HTMLElement {
18
- return fixture.nativeElement.querySelector('[role="menu"]');
50
+ return host.querySelector('[role="menu"]')!;
19
51
  }
20
52
 
21
53
  function menuItems(): HTMLElement[] {
22
- return Array.from(fixture.nativeElement.querySelectorAll('[role="menuitem"]'));
54
+ return Array.from(host.querySelectorAll('[role="menuitem"]'));
23
55
  }
24
56
 
25
- it('starts closed with menu button semantics', () => {
57
+ it('starts closed with consumer-owned menu button semantics', () => {
26
58
  expect(trigger().getAttribute('aria-haspopup')).toBe('true');
27
59
  expect(trigger().getAttribute('aria-expanded')).toBe('false');
28
60
  expect(trigger().getAttribute('aria-controls')).toBe(menu().id);
@@ -39,7 +71,7 @@ describe('<%= classify(name) %>', () => {
39
71
  expect(menuItems()[0].textContent?.trim()).toBe('First option');
40
72
  });
41
73
 
42
- it('opens with keyboard and closes with Escape', () => {
74
+ it('opens with keyboard and closes with Escape, returning focus to the trigger', () => {
43
75
  trigger().dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }));
44
76
  fixture.detectChanges();
45
77
  expect(menu().hidden).toBe(false);
@@ -51,14 +83,24 @@ describe('<%= classify(name) %>', () => {
51
83
  expect(document.activeElement).toBe(trigger());
52
84
  });
53
85
 
54
- it('moves focus between menu items', () => {
86
+ it('moves focus between menu items with ArrowDown/ArrowUp', () => {
55
87
  trigger().click();
56
88
  fixture.detectChanges();
57
89
 
58
- menuItems()[0].focus();
59
90
  menuItems()[0].dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }));
60
91
  fixture.detectChanges();
61
92
 
62
93
  expect(document.activeElement).toBe(menuItems()[1]);
63
94
  });
95
+
96
+ it('closes on an outside click without selecting an item', () => {
97
+ trigger().click();
98
+ fixture.detectChanges();
99
+
100
+ document.body.dispatchEvent(new MouseEvent('click', { bubbles: true }));
101
+ fixture.detectChanges();
102
+
103
+ expect(trigger().getAttribute('aria-expanded')).toBe('false');
104
+ expect(menu().hidden).toBe(true);
105
+ });
64
106
  });