semmet-angular 0.44.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.
- package/README.md +2 -0
- package/package.json +1 -1
- package/src/accordion/files/__name@dasherize__/__name@dasherize__.css.template +0 -39
- package/src/accordion/files/__name@dasherize__/__name@dasherize__.e2e-spec.ts.template +16 -17
- package/src/accordion/files/__name@dasherize__/__name@dasherize__.html.template +14 -34
- package/src/accordion/files/__name@dasherize__/__name@dasherize__.spec.ts.template +52 -62
- package/src/accordion/files/__name@dasherize__/__name@dasherize__.ts.template +88 -82
- package/src/accordion/schema.json +1 -1
- package/src/block/files/__name@dasherize__/__name@dasherize__.html.template +51 -23
- package/src/block/files/__name@dasherize__/__name@dasherize__.ts.template +16 -10
- package/src/breadcrumb/files/__name@dasherize__/__name@dasherize__.css.template +0 -32
- package/src/breadcrumb/files/__name@dasherize__/__name@dasherize__.e2e-spec.ts.template +5 -2
- package/src/breadcrumb/files/__name@dasherize__/__name@dasherize__.html.template +8 -7
- package/src/breadcrumb/files/__name@dasherize__/__name@dasherize__.spec.ts.template +51 -0
- package/src/breadcrumb/files/__name@dasherize__/__name@dasherize__.ts.template +18 -19
- package/src/breadcrumb/index.js +1 -1
- package/src/breadcrumb/index.js.map +1 -1
- package/src/breadcrumb/index.ts +1 -1
- package/src/breadcrumb/schema.json +1 -1
- package/src/collection.json +9 -9
- package/src/menu-button/files/__name@dasherize__/__name@dasherize__.css.template +0 -48
- package/src/menu-button/files/__name@dasherize__/__name@dasherize__.e2e-spec.ts.template +2 -2
- package/src/menu-button/files/__name@dasherize__/__name@dasherize__.html.template +14 -35
- package/src/menu-button/files/__name@dasherize__/__name@dasherize__.spec.ts.template +53 -11
- package/src/menu-button/files/__name@dasherize__/__name@dasherize__.ts.template +128 -54
- package/src/menu-button/schema.json +1 -1
- package/src/navbar/files/__name@dasherize__/__name@dasherize__.css.template +0 -119
- package/src/navbar/files/__name@dasherize__/__name@dasherize__.e2e-spec.ts.template +10 -7
- package/src/navbar/files/__name@dasherize__/__name@dasherize__.html.template +18 -42
- package/src/navbar/files/__name@dasherize__/__name@dasherize__.spec.ts.template +46 -23
- package/src/navbar/files/__name@dasherize__/__name@dasherize__.ts.template +54 -50
- package/src/navbar/schema.json +1 -1
- package/src/pagination/files/__name@dasherize__/__name@dasherize__.css.template +0 -35
- package/src/pagination/files/__name@dasherize__/__name@dasherize__.e2e-spec.ts.template +2 -2
- package/src/pagination/files/__name@dasherize__/__name@dasherize__.html.template +11 -13
- package/src/pagination/files/__name@dasherize__/__name@dasherize__.spec.ts.template +58 -8
- package/src/pagination/files/__name@dasherize__/__name@dasherize__.ts.template +17 -31
- package/src/pagination/schema.json +1 -1
- package/src/table/files/__name@dasherize__/__name@dasherize__.css.template +0 -17
- package/src/table/files/__name@dasherize__/__name@dasherize__.e2e-spec.ts.template +1 -1
- package/src/table/files/__name@dasherize__/__name@dasherize__.html.template +13 -25
- package/src/table/files/__name@dasherize__/__name@dasherize__.ts.template +3 -35
- package/src/table/schema.json +1 -1
- package/src/tabs/files/__name@dasherize__/__name@dasherize__.css.template +0 -40
- package/src/tabs/files/__name@dasherize__/__name@dasherize__.e2e-spec.ts.template +2 -2
- package/src/tabs/files/__name@dasherize__/__name@dasherize__.html.template +18 -41
- package/src/tabs/files/__name@dasherize__/__name@dasherize__.spec.ts.template +39 -31
- package/src/tabs/files/__name@dasherize__/__name@dasherize__.ts.template +111 -113
- package/src/tabs/schema.json +1 -1
- package/src/toolbar/files/__name@dasherize__/__name@dasherize__.css.template +0 -19
- package/src/toolbar/files/__name@dasherize__/__name@dasherize__.e2e-spec.ts.template +3 -4
- package/src/toolbar/files/__name@dasherize__/__name@dasherize__.html.template +7 -16
- package/src/toolbar/files/__name@dasherize__/__name@dasherize__.spec.ts.template +26 -11
- package/src/toolbar/files/__name@dasherize__/__name@dasherize__.ts.template +59 -37
- package/src/toolbar/schema.json +1 -1
- package/src/tree-view/files/__name@dasherize__/__name@dasherize__.css.template +0 -39
- package/src/tree-view/files/__name@dasherize__/__name@dasherize__.e2e-spec.ts.template +4 -5
- package/src/tree-view/files/__name@dasherize__/__name@dasherize__.html.template +12 -31
- package/src/tree-view/files/__name@dasherize__/__name@dasherize__.spec.ts.template +80 -43
- package/src/tree-view/files/__name@dasherize__/__name@dasherize__.ts.template +105 -88
- package/src/tree-view/schema.json +1 -1
|
@@ -1,64 +1,63 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
{ id: 3, label: 'Third option' },
|
|
12
|
-
];
|
|
1
|
+
import {
|
|
2
|
+
Directive,
|
|
3
|
+
ElementRef,
|
|
4
|
+
Injector,
|
|
5
|
+
afterNextRender,
|
|
6
|
+
contentChild,
|
|
7
|
+
contentChildren,
|
|
8
|
+
inject,
|
|
9
|
+
signal,
|
|
10
|
+
} from '@angular/core';
|
|
13
11
|
|
|
14
12
|
let nextId = 0;
|
|
15
13
|
|
|
16
|
-
@
|
|
17
|
-
selector: '<%=
|
|
18
|
-
|
|
19
|
-
templateUrl: './<%= dasherize(name) %>.html',
|
|
20
|
-
styleUrl: './<%= dasherize(name) %>.css',
|
|
21
|
-
host: {
|
|
22
|
-
'(document:click)': 'closeOnOutsideClick($event)',
|
|
23
|
-
},
|
|
14
|
+
@Directive({
|
|
15
|
+
selector: '[<%= camelize(name) %>]',
|
|
16
|
+
exportAs: '<%= camelize(name) %>',
|
|
24
17
|
})
|
|
25
18
|
export class <%= classify(name) %> {
|
|
26
19
|
private readonly injector = inject(Injector);
|
|
27
|
-
private readonly
|
|
28
|
-
private readonly
|
|
29
|
-
private readonly
|
|
20
|
+
private readonly instanceId = `<%= dasherize(name) %>-${nextId++}`;
|
|
21
|
+
private readonly trigger = contentChild(<%= classify(name) %>Trigger);
|
|
22
|
+
private readonly items = contentChildren(<%= classify(name) %>Item, { descendants: true });
|
|
30
23
|
|
|
31
|
-
|
|
32
|
-
protected readonly isOpen = signal(false);
|
|
24
|
+
readonly isOpen = signal(false);
|
|
33
25
|
|
|
34
|
-
|
|
35
|
-
|
|
26
|
+
buttonDomId(): string {
|
|
27
|
+
return `${this.instanceId}-button`;
|
|
28
|
+
}
|
|
36
29
|
|
|
37
|
-
|
|
30
|
+
menuDomId(): string {
|
|
31
|
+
return `${this.instanceId}-menu`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
isInsideTrigger(node: Node): boolean {
|
|
35
|
+
return this.trigger()?.contains(node) ?? false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
openAndFocusFirst(): void {
|
|
38
39
|
this.isOpen.set(true);
|
|
39
|
-
|
|
40
|
+
// Focus moves into the menu only after Angular flushes the DOM update that un-hides it.
|
|
41
|
+
afterNextRender(() => this.items()[0]?.focus(), { injector: this.injector });
|
|
40
42
|
}
|
|
41
43
|
|
|
42
|
-
|
|
44
|
+
close(): void {
|
|
43
45
|
this.isOpen.set(false);
|
|
44
|
-
this.
|
|
46
|
+
this.trigger()?.focus();
|
|
45
47
|
}
|
|
46
48
|
|
|
47
|
-
|
|
49
|
+
select(item: <%= classify(name) %>Item): void {
|
|
48
50
|
// Wire up the selected item's action here.
|
|
49
51
|
this.close();
|
|
50
52
|
}
|
|
51
53
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
navigate(current: <%= classify(name) %>Item, event: KeyboardEvent): void {
|
|
55
|
+
const items = this.items();
|
|
56
|
+
const index = items.indexOf(current);
|
|
57
|
+
if (index === -1) {
|
|
58
|
+
return;
|
|
57
59
|
}
|
|
58
|
-
}
|
|
59
60
|
|
|
60
|
-
protected handleMenuKeydown(event: KeyboardEvent, item: <%= classify(name) %>Item, index: number): void {
|
|
61
|
-
const items = this.menuItems();
|
|
62
61
|
switch (event.key) {
|
|
63
62
|
case 'Escape':
|
|
64
63
|
event.preventDefault();
|
|
@@ -66,42 +65,117 @@ export class <%= classify(name) %> {
|
|
|
66
65
|
break;
|
|
67
66
|
case 'ArrowDown':
|
|
68
67
|
event.preventDefault();
|
|
69
|
-
items[(index + 1) % items.length]?.
|
|
68
|
+
items[(index + 1) % items.length]?.focus();
|
|
70
69
|
break;
|
|
71
70
|
case 'ArrowUp':
|
|
72
71
|
event.preventDefault();
|
|
73
|
-
items[(index - 1 + items.length) % items.length]?.
|
|
72
|
+
items[(index - 1 + items.length) % items.length]?.focus();
|
|
74
73
|
break;
|
|
75
74
|
case 'Home':
|
|
76
75
|
event.preventDefault();
|
|
77
|
-
items[0]?.
|
|
76
|
+
items[0]?.focus();
|
|
78
77
|
break;
|
|
79
78
|
case 'End':
|
|
80
79
|
event.preventDefault();
|
|
81
|
-
items[items.length - 1]?.
|
|
80
|
+
items[items.length - 1]?.focus();
|
|
82
81
|
break;
|
|
83
82
|
case 'Enter':
|
|
84
83
|
case ' ':
|
|
85
84
|
event.preventDefault();
|
|
86
|
-
this.select(
|
|
85
|
+
this.select(current);
|
|
87
86
|
break;
|
|
88
87
|
}
|
|
89
88
|
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@Directive({
|
|
92
|
+
selector: '[<%= camelize(name) %>Trigger]',
|
|
93
|
+
exportAs: '<%= camelize(name) %>Trigger',
|
|
94
|
+
host: {
|
|
95
|
+
'aria-haspopup': 'true',
|
|
96
|
+
'[id]': 'root.buttonDomId()',
|
|
97
|
+
'[attr.aria-expanded]': 'root.isOpen()',
|
|
98
|
+
'[attr.aria-controls]': 'root.menuDomId()',
|
|
99
|
+
'(click)': 'toggle()',
|
|
100
|
+
'(keydown)': 'handleKeydown($event)',
|
|
101
|
+
},
|
|
102
|
+
})
|
|
103
|
+
export class <%= classify(name) %>Trigger {
|
|
104
|
+
protected readonly root = inject(<%= classify(name) %>);
|
|
105
|
+
private readonly elementRef = inject<ElementRef<HTMLElement>>(ElementRef);
|
|
106
|
+
|
|
107
|
+
toggle(): void {
|
|
108
|
+
if (this.root.isOpen()) {
|
|
109
|
+
this.root.close();
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
this.root.openAndFocusFirst();
|
|
114
|
+
}
|
|
90
115
|
|
|
91
|
-
|
|
92
|
-
if (
|
|
116
|
+
handleKeydown(event: KeyboardEvent): void {
|
|
117
|
+
if (event.key === 'ArrowDown' || event.key === 'Enter' || event.key === ' ') {
|
|
118
|
+
event.preventDefault();
|
|
119
|
+
this.root.openAndFocusFirst();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
contains(node: Node): boolean {
|
|
124
|
+
return this.elementRef.nativeElement.contains(node);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
focus(): void {
|
|
128
|
+
this.elementRef.nativeElement.focus();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
@Directive({
|
|
133
|
+
selector: '[<%= camelize(name) %>Menu]',
|
|
134
|
+
exportAs: '<%= camelize(name) %>Menu',
|
|
135
|
+
host: {
|
|
136
|
+
role: 'menu',
|
|
137
|
+
'[id]': 'root.menuDomId()',
|
|
138
|
+
'[attr.aria-labelledby]': 'root.buttonDomId()',
|
|
139
|
+
'[hidden]': '!root.isOpen()',
|
|
140
|
+
'(document:click)': 'closeOnOutsideClick($event)',
|
|
141
|
+
},
|
|
142
|
+
})
|
|
143
|
+
export class <%= classify(name) %>Menu {
|
|
144
|
+
protected readonly root = inject(<%= classify(name) %>);
|
|
145
|
+
private readonly elementRef = inject<ElementRef<HTMLElement>>(ElementRef);
|
|
146
|
+
|
|
147
|
+
closeOnOutsideClick(event: MouseEvent): void {
|
|
148
|
+
if (!this.root.isOpen()) {
|
|
93
149
|
return;
|
|
94
150
|
}
|
|
151
|
+
|
|
95
152
|
const target = event.target as Node;
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
this.isOpen.set(false);
|
|
153
|
+
const insideMenu = this.elementRef.nativeElement.contains(target);
|
|
154
|
+
if (!insideMenu && !this.root.isInsideTrigger(target)) {
|
|
155
|
+
this.root.close();
|
|
100
156
|
}
|
|
101
157
|
}
|
|
158
|
+
}
|
|
102
159
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
160
|
+
@Directive({
|
|
161
|
+
selector: '[<%= camelize(name) %>Item]',
|
|
162
|
+
exportAs: '<%= camelize(name) %>Item',
|
|
163
|
+
host: {
|
|
164
|
+
role: 'menuitem',
|
|
165
|
+
tabindex: '-1',
|
|
166
|
+
'(click)': 'select()',
|
|
167
|
+
'(keydown)': 'root.navigate(this, $event)',
|
|
168
|
+
},
|
|
169
|
+
})
|
|
170
|
+
export class <%= classify(name) %>Item {
|
|
171
|
+
protected readonly root = inject(<%= classify(name) %>);
|
|
172
|
+
private readonly elementRef = inject<ElementRef<HTMLElement>>(ElementRef);
|
|
173
|
+
|
|
174
|
+
select(): void {
|
|
175
|
+
this.root.select(this);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
focus(): void {
|
|
179
|
+
this.elementRef.nativeElement.focus();
|
|
106
180
|
}
|
|
107
181
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"$id": "SemmetAngularMenuButton",
|
|
4
4
|
"title": "Semmet Angular Menu Button Options Schema",
|
|
5
5
|
"type": "object",
|
|
6
|
-
"description": "Generates
|
|
6
|
+
"description": "Generates headless Angular Menu Button directives (root, trigger, menu, item) with full keyboard support and outside-click dismissal, coordinated over consumer-owned markup, following the W3C ARIA Authoring Practices Guide.",
|
|
7
7
|
"additionalProperties": false,
|
|
8
8
|
"properties": {
|
|
9
9
|
"name": {
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
:host {
|
|
2
|
-
display: block;
|
|
3
|
-
}
|
|
4
|
-
|
|
5
|
-
.semmet-navbar {
|
|
6
|
-
display: grid;
|
|
7
|
-
grid-template-columns: 1fr auto;
|
|
8
|
-
align-items: center;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
.semmet-navbar__brand {
|
|
12
|
-
display: inline-flex;
|
|
13
|
-
align-items: center;
|
|
14
|
-
color: inherit;
|
|
15
|
-
text-decoration: none;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
.semmet-navbar__toggle {
|
|
19
|
-
position: relative;
|
|
20
|
-
display: inline-flex;
|
|
21
|
-
align-items: center;
|
|
22
|
-
justify-content: center;
|
|
23
|
-
color: inherit;
|
|
24
|
-
background: none;
|
|
25
|
-
border: 0;
|
|
26
|
-
cursor: pointer;
|
|
27
|
-
font: inherit;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
.semmet-navbar__toggle span,
|
|
31
|
-
.semmet-navbar__toggle span::before,
|
|
32
|
-
.semmet-navbar__toggle span::after {
|
|
33
|
-
display: block;
|
|
34
|
-
width: 1.125rem;
|
|
35
|
-
height: 0.125rem;
|
|
36
|
-
background: currentColor;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
.semmet-navbar__toggle span {
|
|
40
|
-
position: relative;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
.semmet-navbar__toggle span::before,
|
|
44
|
-
.semmet-navbar__toggle span::after {
|
|
45
|
-
position: absolute;
|
|
46
|
-
left: 0;
|
|
47
|
-
content: '';
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
.semmet-navbar__toggle span::before {
|
|
51
|
-
top: -0.375rem;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
.semmet-navbar__toggle span::after {
|
|
55
|
-
top: 0.375rem;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
.semmet-navbar__menu {
|
|
59
|
-
display: none;
|
|
60
|
-
grid-column: 1 / -1;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
.semmet-navbar__menu--open {
|
|
64
|
-
display: block;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
.semmet-navbar__list {
|
|
68
|
-
display: grid;
|
|
69
|
-
margin: 0;
|
|
70
|
-
padding: 0;
|
|
71
|
-
list-style: none;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
.semmet-navbar__actions {
|
|
75
|
-
display: flex;
|
|
76
|
-
flex-wrap: wrap;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
.semmet-navbar__link {
|
|
80
|
-
display: flex;
|
|
81
|
-
align-items: center;
|
|
82
|
-
color: inherit;
|
|
83
|
-
text-decoration: none;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
.semmet-navbar__link--disabled {
|
|
87
|
-
cursor: not-allowed;
|
|
88
|
-
opacity: 0.52;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
.semmet-navbar__brand:focus-visible,
|
|
92
|
-
.semmet-navbar__toggle:focus-visible,
|
|
93
|
-
.semmet-navbar__link:focus-visible,
|
|
94
|
-
.semmet-navbar__actions :is(a, button):focus-visible {
|
|
95
|
-
outline: 2px solid currentColor;
|
|
96
|
-
outline-offset: 2px;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
@media (min-width: 48rem) {
|
|
100
|
-
.semmet-navbar {
|
|
101
|
-
grid-template-columns: auto 1fr;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
.semmet-navbar__toggle {
|
|
105
|
-
display: none;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
.semmet-navbar__menu {
|
|
109
|
-
display: flex;
|
|
110
|
-
align-items: center;
|
|
111
|
-
grid-column: auto;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
.semmet-navbar__list {
|
|
115
|
-
display: flex;
|
|
116
|
-
align-items: center;
|
|
117
|
-
padding: 0;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
@@ -4,22 +4,25 @@ test.describe('<%= classify(name) %>', () => {
|
|
|
4
4
|
test('toggles mounted responsive navigation menu', async ({ page }) => {
|
|
5
5
|
await page.goto('/');
|
|
6
6
|
|
|
7
|
-
const
|
|
7
|
+
const nav = page.locator('[<%= camelize(name) %>]').first();
|
|
8
8
|
test.skip(
|
|
9
|
-
(await
|
|
10
|
-
'Mount
|
|
9
|
+
(await nav.count()) === 0,
|
|
10
|
+
'Mount a [<%= camelize(name) %>] navigation in a route or host component to enable this generated e2e test.'
|
|
11
11
|
);
|
|
12
12
|
|
|
13
|
-
const
|
|
14
|
-
|
|
13
|
+
const toggle = nav.locator('[<%= camelize(name) %>Toggle]').first();
|
|
14
|
+
test.skip(
|
|
15
|
+
(await toggle.count()) === 0,
|
|
16
|
+
'Add a [<%= camelize(name) %>Toggle] button to enable this generated e2e test.'
|
|
17
|
+
);
|
|
15
18
|
|
|
16
|
-
await expect(nav).
|
|
19
|
+
await expect(nav).toHaveAttribute('aria-label', /.+/);
|
|
17
20
|
await expect(toggle).toHaveAttribute('aria-expanded', 'false');
|
|
18
21
|
|
|
19
22
|
await toggle.click();
|
|
20
23
|
await expect(toggle).toHaveAttribute('aria-expanded', 'true');
|
|
21
24
|
|
|
22
|
-
const current =
|
|
25
|
+
const current = nav.locator('[aria-current="page"]').first();
|
|
23
26
|
if ((await current.count()) > 0) {
|
|
24
27
|
await expect(current).toBeVisible();
|
|
25
28
|
}
|
|
@@ -1,58 +1,34 @@
|
|
|
1
|
-
<!-- Semmet Angular: Navbar -
|
|
1
|
+
<!-- Semmet Angular: Navbar - headless navigation coordination directives. -->
|
|
2
2
|
<!--
|
|
3
3
|
Usage from a parent component template:
|
|
4
|
-
Import <%= classify(name) %>
|
|
4
|
+
Import <%= classify(name) %>, <%= classify(name) %>Item, <%= classify(name) %>Toggle and <%= classify(name) %>Menu
|
|
5
|
+
in that parent component. This directive set never owns markup: bring your own <nav>, brand, toggle
|
|
6
|
+
button and links, in whatever structure the layout needs.
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
<ng-template <%= camelize(name) %>Actions>
|
|
9
|
-
<a href="/account">Account</a>
|
|
10
|
-
</ng-template>
|
|
11
|
-
</<%= selector %>>
|
|
12
|
-
-->
|
|
13
|
-
<!-- Deliberately avoids role="menu"; regular site navigation should stay as nav/list/link semantics. -->
|
|
14
|
-
<nav class="semmet-navbar" [attr.aria-label]="label()">
|
|
15
|
-
<a class="semmet-navbar__brand" [href]="brandHref()">
|
|
16
|
-
@if (brandTemplate(); as brandSlot) {
|
|
17
|
-
<ng-container [ngTemplateOutlet]="brandSlot.templateRef" />
|
|
18
|
-
} @else {
|
|
19
|
-
{{ brand() }}
|
|
20
|
-
}
|
|
21
|
-
</a>
|
|
8
|
+
<nav <%= camelize(name) %> label="Main navigation" #nav="<%= camelize(name) %>">
|
|
9
|
+
<a class="brand" href="/">Acme</a>
|
|
22
10
|
|
|
23
|
-
<button
|
|
24
|
-
type="button"
|
|
25
|
-
class="semmet-navbar__toggle"
|
|
26
|
-
[attr.aria-label]="toggleLabel()"
|
|
27
|
-
[attr.aria-controls]="instanceId + '-menu'"
|
|
28
|
-
[attr.aria-expanded]="expanded()"
|
|
29
|
-
(click)="toggle()"
|
|
30
|
-
>
|
|
11
|
+
<button type="button" <%= camelize(name) %>Toggle aria-label="Toggle navigation">
|
|
31
12
|
<span aria-hidden="true"></span>
|
|
32
13
|
</button>
|
|
33
14
|
|
|
34
|
-
<div
|
|
35
|
-
<ul
|
|
36
|
-
@for (item of items
|
|
37
|
-
<li
|
|
15
|
+
<div <%= camelize(name) %>Menu>
|
|
16
|
+
<ul>
|
|
17
|
+
@for (item of items; track item.href) {
|
|
18
|
+
<li>
|
|
38
19
|
<a
|
|
39
|
-
|
|
40
|
-
[
|
|
41
|
-
[
|
|
42
|
-
[
|
|
43
|
-
|
|
44
|
-
[
|
|
45
|
-
(click)="close()"
|
|
20
|
+
<%= camelize(name) %>Item
|
|
21
|
+
[href]="item.href"
|
|
22
|
+
[current]="item.current"
|
|
23
|
+
[disabled]="item.disabled"
|
|
24
|
+
#navLink="<%= camelize(name) %>Item"
|
|
25
|
+
[class.active]="item.current"
|
|
46
26
|
>
|
|
47
27
|
{{ item.label }}
|
|
48
28
|
</a>
|
|
49
29
|
</li>
|
|
50
30
|
}
|
|
51
31
|
</ul>
|
|
52
|
-
@if (actions(); as actionsSlot) {
|
|
53
|
-
<div class="semmet-navbar__actions">
|
|
54
|
-
<ng-container [ngTemplateOutlet]="actionsSlot.templateRef" />
|
|
55
|
-
</div>
|
|
56
|
-
}
|
|
57
32
|
</div>
|
|
58
33
|
</nav>
|
|
34
|
+
-->
|
|
@@ -1,20 +1,40 @@
|
|
|
1
1
|
import { Component } from '@angular/core';
|
|
2
2
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
<%= classify(name) %>,
|
|
6
|
+
<%= classify(name) %>Item,
|
|
7
|
+
<%= classify(name) %>Menu,
|
|
8
|
+
<%= classify(name) %>Toggle,
|
|
9
|
+
} from './<%= dasherize(name) %>';
|
|
5
10
|
|
|
6
11
|
@Component({
|
|
7
|
-
imports: [<%= classify(name) %>, <%= classify(name) %>
|
|
12
|
+
imports: [<%= classify(name) %>, <%= classify(name) %>Item, <%= classify(name) %>Menu, <%= classify(name) %>Toggle],
|
|
8
13
|
template: `
|
|
9
|
-
|
|
10
|
-
brand="Acme
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
<nav <%= camelize(name) %> label="Main navigation" #nav="<%= camelize(name) %>">
|
|
15
|
+
<a class="brand" href="/">Acme</a>
|
|
16
|
+
|
|
17
|
+
<button type="button" <%= camelize(name) %>Toggle aria-label="Toggle navigation">
|
|
18
|
+
<span aria-hidden="true"></span>
|
|
19
|
+
</button>
|
|
20
|
+
|
|
21
|
+
<div <%= camelize(name) %>Menu>
|
|
22
|
+
<ul>
|
|
23
|
+
@for (item of items; track item.href) {
|
|
24
|
+
<li>
|
|
25
|
+
<a
|
|
26
|
+
<%= camelize(name) %>Item
|
|
27
|
+
[href]="item.href"
|
|
28
|
+
[current]="item.current"
|
|
29
|
+
[disabled]="item.disabled"
|
|
30
|
+
>
|
|
31
|
+
{{ item.label }}
|
|
32
|
+
</a>
|
|
33
|
+
</li>
|
|
34
|
+
}
|
|
35
|
+
</ul>
|
|
36
|
+
</div>
|
|
37
|
+
</nav>
|
|
18
38
|
`,
|
|
19
39
|
})
|
|
20
40
|
class HostComponent {
|
|
@@ -39,36 +59,39 @@ describe('<%= classify(name) %>', () => {
|
|
|
39
59
|
host = fixture.nativeElement as HTMLElement;
|
|
40
60
|
});
|
|
41
61
|
|
|
42
|
-
it('
|
|
62
|
+
it('decorates consumer-owned markup with navigation semantics', () => {
|
|
43
63
|
const nav = host.querySelector<HTMLElement>('nav');
|
|
44
|
-
const links = Array.from(host.querySelectorAll<HTMLAnchorElement>('.semmet-navbar__link'));
|
|
45
64
|
|
|
46
65
|
expect(nav?.getAttribute('aria-label')).toBe('Main navigation');
|
|
47
|
-
expect(host.
|
|
66
|
+
expect(host.textContent).toContain('Acme');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('reflects aria-current and aria-disabled on items without owning their markup', () => {
|
|
70
|
+
const links = Array.from(host.querySelectorAll<HTMLAnchorElement>('li a'));
|
|
71
|
+
|
|
48
72
|
expect(links[0].getAttribute('aria-current')).toBe('page');
|
|
49
73
|
expect(links[2].getAttribute('aria-disabled')).toBe('true');
|
|
50
|
-
expect(links[2].getAttribute('href')).toBeNull();
|
|
51
|
-
expect(host.textContent).toContain('Account');
|
|
52
74
|
});
|
|
53
75
|
|
|
54
|
-
it('toggles
|
|
55
|
-
const toggle = host.querySelector<HTMLButtonElement>('
|
|
56
|
-
const menu = host.querySelector<HTMLElement>('
|
|
57
|
-
const projectsLink = Array.from(host.querySelectorAll<HTMLAnchorElement>('
|
|
76
|
+
it('toggles the collapsible menu and closes it when a link is activated', () => {
|
|
77
|
+
const toggle = host.querySelector<HTMLButtonElement>('button');
|
|
78
|
+
const menu = host.querySelector<HTMLElement>('div');
|
|
79
|
+
const projectsLink = Array.from(host.querySelectorAll<HTMLAnchorElement>('li a'))[1];
|
|
58
80
|
|
|
59
81
|
expect(toggle?.getAttribute('aria-expanded')).toBe('false');
|
|
60
|
-
expect(menu?.
|
|
82
|
+
expect(menu?.hidden).toBe(true);
|
|
83
|
+
expect(toggle?.getAttribute('aria-controls')).toBe(menu?.id);
|
|
61
84
|
|
|
62
85
|
toggle?.click();
|
|
63
86
|
fixture.detectChanges();
|
|
64
87
|
|
|
65
88
|
expect(toggle?.getAttribute('aria-expanded')).toBe('true');
|
|
66
|
-
expect(menu?.
|
|
89
|
+
expect(menu?.hidden).toBe(false);
|
|
67
90
|
|
|
68
91
|
projectsLink.click();
|
|
69
92
|
fixture.detectChanges();
|
|
70
93
|
|
|
71
94
|
expect(toggle?.getAttribute('aria-expanded')).toBe('false');
|
|
72
|
-
expect(menu?.
|
|
95
|
+
expect(menu?.hidden).toBe(true);
|
|
73
96
|
});
|
|
74
97
|
});
|