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,31 +1,41 @@
|
|
|
1
1
|
import { Component } from '@angular/core';
|
|
2
2
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
3
|
-
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
<%= classify(name) %>,
|
|
6
|
+
<%= classify(name) %>List,
|
|
7
|
+
<%= classify(name) %>Panel,
|
|
8
|
+
<%= classify(name) %>Tab,
|
|
9
|
+
} from './<%= dasherize(name) %>';
|
|
4
10
|
|
|
5
11
|
@Component({
|
|
6
|
-
imports: [<%= classify(name) %>, <%= classify(name) %>
|
|
12
|
+
imports: [<%= classify(name) %>, <%= classify(name) %>List, <%= classify(name) %>Panel, <%= classify(name) %>Tab],
|
|
7
13
|
template: `
|
|
8
|
-
|
|
9
|
-
<
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
</
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
</<%= selector %>>
|
|
14
|
+
<div <%= camelize(name) %>>
|
|
15
|
+
<div <%= camelize(name) %>List label="Account">
|
|
16
|
+
@for (item of items; track item.id) {
|
|
17
|
+
<button type="button" <%= camelize(name) %>Tab [value]="item.id" [disabled]="item.disabled">
|
|
18
|
+
{{ item.title }}
|
|
19
|
+
</button>
|
|
20
|
+
}
|
|
21
|
+
</div>
|
|
22
|
+
@for (item of items; track item.id) {
|
|
23
|
+
<div <%= camelize(name) %>Panel [value]="item.id">{{ item.title }} content</div>
|
|
24
|
+
}
|
|
25
|
+
</div>
|
|
21
26
|
`,
|
|
22
27
|
})
|
|
23
28
|
class HostComponent {
|
|
24
|
-
|
|
29
|
+
readonly items = [
|
|
30
|
+
{ id: 'profile', title: 'Profile', disabled: false },
|
|
31
|
+
{ id: 'security', title: 'Security', disabled: false },
|
|
32
|
+
{ id: 'billing', title: 'Billing', disabled: true },
|
|
33
|
+
];
|
|
25
34
|
}
|
|
26
35
|
|
|
27
36
|
describe('<%= classify(name) %>', () => {
|
|
28
37
|
let fixture: ComponentFixture<HostComponent>;
|
|
38
|
+
let host: HTMLElement;
|
|
29
39
|
|
|
30
40
|
beforeEach(async () => {
|
|
31
41
|
await TestBed.configureTestingModule({
|
|
@@ -34,23 +44,23 @@ describe('<%= classify(name) %>', () => {
|
|
|
34
44
|
|
|
35
45
|
fixture = TestBed.createComponent(HostComponent);
|
|
36
46
|
fixture.detectChanges();
|
|
47
|
+
host = fixture.nativeElement as HTMLElement;
|
|
37
48
|
});
|
|
38
49
|
|
|
39
50
|
function tabs(): HTMLButtonElement[] {
|
|
40
|
-
return Array.from(
|
|
51
|
+
return Array.from(host.querySelectorAll('[role="tab"]'));
|
|
41
52
|
}
|
|
42
53
|
|
|
43
54
|
function panels(): HTMLElement[] {
|
|
44
|
-
return Array.from(
|
|
55
|
+
return Array.from(host.querySelectorAll('[role="tabpanel"]'));
|
|
45
56
|
}
|
|
46
57
|
|
|
47
|
-
it('
|
|
48
|
-
const tablist =
|
|
58
|
+
it('decorates consumer-owned markup with ARIA tab/tabpanel relationships', () => {
|
|
59
|
+
const tablist = host.querySelector('[role="tablist"]');
|
|
49
60
|
const [profileTab, settingsTab] = tabs();
|
|
50
61
|
const [profilePanel, settingsPanel] = panels();
|
|
51
62
|
|
|
52
|
-
expect(tablist).
|
|
53
|
-
expect(profileTab.textContent?.trim()).toBe('Profile');
|
|
63
|
+
expect(tablist?.getAttribute('aria-label')).toBe('Account');
|
|
54
64
|
expect(profileTab.getAttribute('aria-selected')).toBe('true');
|
|
55
65
|
expect(profileTab.getAttribute('aria-controls')).toBe(profilePanel.id);
|
|
56
66
|
expect(profileTab.tabIndex).toBe(0);
|
|
@@ -58,7 +68,6 @@ describe('<%= classify(name) %>', () => {
|
|
|
58
68
|
expect(profilePanel.hidden).toBe(false);
|
|
59
69
|
|
|
60
70
|
expect(settingsTab.getAttribute('aria-selected')).toBe('false');
|
|
61
|
-
expect(settingsTab.getAttribute('aria-controls')).toBe(settingsPanel.id);
|
|
62
71
|
expect(settingsTab.tabIndex).toBe(-1);
|
|
63
72
|
expect(settingsPanel.hidden).toBe(true);
|
|
64
73
|
});
|
|
@@ -73,7 +82,7 @@ describe('<%= classify(name) %>', () => {
|
|
|
73
82
|
expect(settingsTab.getAttribute('aria-selected')).toBe('true');
|
|
74
83
|
expect(settingsTab.tabIndex).toBe(0);
|
|
75
84
|
expect(settingsPanel.hidden).toBe(false);
|
|
76
|
-
expect(settingsPanel.textContent).toContain('
|
|
85
|
+
expect(settingsPanel.textContent).toContain('Security content');
|
|
77
86
|
expect(profilePanel.hidden).toBe(true);
|
|
78
87
|
});
|
|
79
88
|
|
|
@@ -81,33 +90,32 @@ describe('<%= classify(name) %>', () => {
|
|
|
81
90
|
const disabledTab = tabs()[2];
|
|
82
91
|
const disabledPanel = panels()[2];
|
|
83
92
|
|
|
84
|
-
expect(disabledTab.disabled).toBe(true);
|
|
85
|
-
|
|
86
93
|
disabledTab.click();
|
|
87
94
|
fixture.detectChanges();
|
|
88
95
|
|
|
96
|
+
expect(disabledTab.getAttribute('aria-disabled')).toBe('true');
|
|
89
97
|
expect(disabledTab.getAttribute('aria-selected')).toBe('false');
|
|
90
98
|
expect(disabledPanel.hidden).toBe(true);
|
|
91
99
|
});
|
|
92
100
|
|
|
93
101
|
it('moves focus and selection with arrow, Home, and End keys while skipping disabled tabs', () => {
|
|
102
|
+
const tablist = host.querySelector<HTMLElement>('[role="tablist"]');
|
|
94
103
|
const [profileTab, settingsTab, billingTab] = tabs();
|
|
95
104
|
|
|
96
|
-
|
|
97
|
-
profileTab.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true }));
|
|
105
|
+
tablist?.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true }));
|
|
98
106
|
fixture.detectChanges();
|
|
99
107
|
expect(document.activeElement).toBe(settingsTab);
|
|
100
108
|
expect(settingsTab.getAttribute('aria-selected')).toBe('true');
|
|
101
109
|
|
|
102
|
-
|
|
110
|
+
tablist?.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true }));
|
|
103
111
|
fixture.detectChanges();
|
|
104
112
|
expect(document.activeElement).toBe(profileTab);
|
|
105
113
|
expect(profileTab.getAttribute('aria-selected')).toBe('true');
|
|
106
114
|
|
|
107
|
-
|
|
115
|
+
tablist?.dispatchEvent(new KeyboardEvent('keydown', { key: 'End', bubbles: true }));
|
|
108
116
|
fixture.detectChanges();
|
|
109
117
|
expect(document.activeElement).toBe(settingsTab);
|
|
110
118
|
|
|
111
|
-
expect(billingTab.disabled).toBe(true);
|
|
119
|
+
expect(billingTab.getAttribute('aria-disabled')).toBe('true');
|
|
112
120
|
});
|
|
113
121
|
});
|
|
@@ -1,157 +1,155 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
Directive,
|
|
5
|
-
ElementRef,
|
|
6
|
-
TemplateRef,
|
|
7
|
-
booleanAttribute,
|
|
8
|
-
computed,
|
|
9
|
-
contentChildren,
|
|
10
|
-
inject,
|
|
11
|
-
input,
|
|
12
|
-
signal,
|
|
13
|
-
viewChildren,
|
|
14
|
-
} from '@angular/core';
|
|
15
|
-
|
|
16
|
-
type <%= classify(name) %>ItemId = string | number;
|
|
1
|
+
import { Directive, ElementRef, booleanAttribute, computed, contentChildren, inject, input, model } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
type <%= classify(name) %>Id = string;
|
|
17
4
|
|
|
18
5
|
let nextId = 0;
|
|
19
|
-
let nextItemId = 0;
|
|
20
6
|
|
|
21
7
|
@Directive({
|
|
22
|
-
selector: '
|
|
23
|
-
|
|
24
|
-
export class <%= classify(name) %>Item {
|
|
25
|
-
readonly templateRef = inject<TemplateRef<unknown>>(TemplateRef);
|
|
26
|
-
|
|
27
|
-
readonly id = input<<%= classify(name) %>ItemId>(`<%= dasherize(name) %>-item-${nextItemId++}`);
|
|
28
|
-
readonly label = input.required<string>();
|
|
29
|
-
readonly selected = input(false, { transform: booleanAttribute });
|
|
30
|
-
readonly disabled = input(false, { transform: booleanAttribute });
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
@Component({
|
|
34
|
-
selector: '<%= selector %>',
|
|
35
|
-
imports: [NgTemplateOutlet],
|
|
36
|
-
templateUrl: './<%= dasherize(name) %>.html',
|
|
37
|
-
styleUrl: './<%= dasherize(name) %>.css',
|
|
8
|
+
selector: '[<%= camelize(name) %>]',
|
|
9
|
+
exportAs: '<%= camelize(name) %>',
|
|
38
10
|
})
|
|
39
11
|
export class <%= classify(name) %> {
|
|
40
|
-
private readonly
|
|
41
|
-
|
|
42
|
-
protected readonly items = contentChildren(<%= classify(name) %>Item);
|
|
43
|
-
protected readonly instanceId = `<%= dasherize(name) %>-${nextId++}`;
|
|
12
|
+
private readonly instanceId = `<%= dasherize(name) %>-${nextId++}`;
|
|
13
|
+
private readonly tabs = contentChildren(<%= classify(name) %>Tab, { descendants: true });
|
|
44
14
|
|
|
45
|
-
readonly
|
|
46
|
-
readonly preserveContent = input(true, { transform: booleanAttribute });
|
|
15
|
+
readonly activeId = model<<%= classify(name) %>Id | null>(null);
|
|
47
16
|
|
|
48
|
-
private readonly
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const items = this.items();
|
|
54
|
-
if (selectedId !== null && items.some((item) => item.id() === selectedId && !item.disabled())) {
|
|
55
|
-
return selectedId;
|
|
17
|
+
private readonly resolvedActiveId = computed<<%= classify(name) %>Id | null>(() => {
|
|
18
|
+
const activeId = this.activeId();
|
|
19
|
+
const tabs = this.tabs();
|
|
20
|
+
if (activeId !== null && tabs.some((tab) => tab.value() === activeId && !tab.disabled())) {
|
|
21
|
+
return activeId;
|
|
56
22
|
}
|
|
57
23
|
|
|
58
24
|
return (
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
)?.
|
|
25
|
+
tabs.find((tab) => tab.selected() && !tab.disabled()) ??
|
|
26
|
+
tabs.find((tab) => !tab.disabled()) ??
|
|
27
|
+
tabs[0]
|
|
28
|
+
)?.value() ?? null;
|
|
63
29
|
});
|
|
64
30
|
|
|
65
|
-
|
|
66
|
-
return this.
|
|
31
|
+
isActive(value: <%= classify(name) %>Id): boolean {
|
|
32
|
+
return this.resolvedActiveId() === value;
|
|
67
33
|
}
|
|
68
34
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
protected select(item: <%= classify(name) %>Item): void {
|
|
74
|
-
if (item.disabled()) {
|
|
35
|
+
select(tab: <%= classify(name) %>Tab): void {
|
|
36
|
+
if (tab.disabled()) {
|
|
75
37
|
return;
|
|
76
38
|
}
|
|
77
39
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
this.markRendered(activeId);
|
|
81
|
-
}
|
|
40
|
+
this.activeId.set(tab.value());
|
|
41
|
+
}
|
|
82
42
|
|
|
83
|
-
|
|
84
|
-
this.
|
|
85
|
-
this.selectedId.set(id);
|
|
43
|
+
tabDomId(value: <%= classify(name) %>Id): string {
|
|
44
|
+
return `${this.instanceId}-tab-${value}`;
|
|
86
45
|
}
|
|
87
46
|
|
|
47
|
+
panelDomId(value: <%= classify(name) %>Id): string {
|
|
48
|
+
return `${this.instanceId}-panel-${value}`;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@Directive({
|
|
53
|
+
selector: '[<%= camelize(name) %>List]',
|
|
54
|
+
exportAs: '<%= camelize(name) %>List',
|
|
55
|
+
host: {
|
|
56
|
+
role: 'tablist',
|
|
57
|
+
'[attr.aria-label]': 'label() || null',
|
|
58
|
+
'(keydown)': 'handleKeydown($event)',
|
|
59
|
+
},
|
|
60
|
+
})
|
|
61
|
+
export class <%= classify(name) %>List {
|
|
62
|
+
private readonly root = inject(<%= classify(name) %>);
|
|
63
|
+
private readonly tabs = contentChildren(<%= classify(name) %>Tab, { descendants: true });
|
|
64
|
+
|
|
65
|
+
readonly label = input('');
|
|
66
|
+
|
|
88
67
|
// Left/Right/Home/End move focus AND selection (automatic activation model, per APG tabs pattern).
|
|
89
|
-
|
|
90
|
-
const
|
|
91
|
-
const
|
|
92
|
-
|
|
68
|
+
handleKeydown(event: KeyboardEvent): void {
|
|
69
|
+
const enabledTabs = this.tabs().filter((tab) => !tab.disabled());
|
|
70
|
+
const currentIndex = enabledTabs.findIndex((tab) => tab.active());
|
|
71
|
+
|
|
72
|
+
if (event.key === 'ArrowRight') {
|
|
73
|
+
event.preventDefault();
|
|
74
|
+
this.focusAt(enabledTabs, currentIndex + 1);
|
|
93
75
|
return;
|
|
94
76
|
}
|
|
95
77
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
break;
|
|
101
|
-
case 'ArrowLeft':
|
|
102
|
-
nextIndex = this.nextEnabledIndex(items, index, -1);
|
|
103
|
-
break;
|
|
104
|
-
case 'Home':
|
|
105
|
-
nextIndex = this.firstEnabledIndex(items);
|
|
106
|
-
break;
|
|
107
|
-
case 'End':
|
|
108
|
-
nextIndex = this.lastEnabledIndex(items);
|
|
109
|
-
break;
|
|
110
|
-
default:
|
|
111
|
-
return;
|
|
78
|
+
if (event.key === 'ArrowLeft') {
|
|
79
|
+
event.preventDefault();
|
|
80
|
+
this.focusAt(enabledTabs, currentIndex - 1);
|
|
81
|
+
return;
|
|
112
82
|
}
|
|
113
83
|
|
|
114
|
-
if (
|
|
84
|
+
if (event.key === 'Home') {
|
|
85
|
+
event.preventDefault();
|
|
86
|
+
this.focusAt(enabledTabs, 0);
|
|
115
87
|
return;
|
|
116
88
|
}
|
|
117
89
|
|
|
118
|
-
event.
|
|
119
|
-
|
|
120
|
-
|
|
90
|
+
if (event.key === 'End') {
|
|
91
|
+
event.preventDefault();
|
|
92
|
+
this.focusAt(enabledTabs, enabledTabs.length - 1);
|
|
93
|
+
}
|
|
121
94
|
}
|
|
122
95
|
|
|
123
|
-
private
|
|
124
|
-
|
|
125
|
-
if (renderedIds.has(id)) {
|
|
96
|
+
private focusAt(tabs: readonly <%= classify(name) %>Tab[], index: number): void {
|
|
97
|
+
if (!tabs.length) {
|
|
126
98
|
return;
|
|
127
99
|
}
|
|
128
100
|
|
|
129
|
-
|
|
101
|
+
const nextIndex = (index + tabs.length) % tabs.length;
|
|
102
|
+
const tab = tabs[nextIndex];
|
|
103
|
+
this.root.select(tab);
|
|
104
|
+
tab.focus();
|
|
130
105
|
}
|
|
106
|
+
}
|
|
131
107
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
108
|
+
@Directive({
|
|
109
|
+
selector: '[<%= camelize(name) %>Tab]',
|
|
110
|
+
exportAs: '<%= camelize(name) %>Tab',
|
|
111
|
+
host: {
|
|
112
|
+
role: 'tab',
|
|
113
|
+
'[id]': 'root.tabDomId(value())',
|
|
114
|
+
'[attr.aria-selected]': 'active()',
|
|
115
|
+
'[attr.aria-controls]': 'root.panelDomId(value())',
|
|
116
|
+
'[attr.aria-disabled]': 'disabled() ? "true" : null',
|
|
117
|
+
'[attr.tabindex]': 'active() ? 0 : -1',
|
|
118
|
+
'(click)': 'select()',
|
|
119
|
+
},
|
|
120
|
+
})
|
|
121
|
+
export class <%= classify(name) %>Tab {
|
|
122
|
+
protected readonly root = inject(<%= classify(name) %>);
|
|
123
|
+
private readonly elementRef = inject<ElementRef<HTMLElement>>(ElementRef);
|
|
139
124
|
|
|
140
|
-
|
|
125
|
+
readonly value = input.required<<%= classify(name) %>Id>();
|
|
126
|
+
readonly selected = input(false, { transform: booleanAttribute });
|
|
127
|
+
readonly disabled = input(false, { transform: booleanAttribute });
|
|
128
|
+
readonly active = computed(() => this.root.isActive(this.value()));
|
|
129
|
+
|
|
130
|
+
select(): void {
|
|
131
|
+
this.root.select(this);
|
|
141
132
|
}
|
|
142
133
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
return index === -1 ? null : index;
|
|
134
|
+
focus(): void {
|
|
135
|
+
this.elementRef.nativeElement.focus();
|
|
146
136
|
}
|
|
137
|
+
}
|
|
147
138
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
139
|
+
@Directive({
|
|
140
|
+
selector: '[<%= camelize(name) %>Panel]',
|
|
141
|
+
exportAs: '<%= camelize(name) %>Panel',
|
|
142
|
+
host: {
|
|
143
|
+
role: 'tabpanel',
|
|
144
|
+
'[id]': 'root.panelDomId(value())',
|
|
145
|
+
'[attr.aria-labelledby]': 'root.tabDomId(value())',
|
|
146
|
+
'[attr.tabindex]': '0',
|
|
147
|
+
'[hidden]': '!active()',
|
|
148
|
+
},
|
|
149
|
+
})
|
|
150
|
+
export class <%= classify(name) %>Panel {
|
|
151
|
+
protected readonly root = inject(<%= classify(name) %>);
|
|
154
152
|
|
|
155
|
-
|
|
156
|
-
|
|
153
|
+
readonly value = input.required<<%= classify(name) %>Id>();
|
|
154
|
+
readonly active = computed(() => this.root.isActive(this.value()));
|
|
157
155
|
}
|
package/src/tabs/schema.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"$id": "SemmetAngularTabs",
|
|
4
4
|
"title": "Semmet Angular Tabs Options Schema",
|
|
5
5
|
"type": "object",
|
|
6
|
-
"description": "Generates
|
|
6
|
+
"description": "Generates headless Angular Tabs directives (root, list, tab, panel) with signals-based state and roving tabindex keyboard support, coordinated over consumer-owned markup, following the W3C ARIA Authoring Practices Guide.",
|
|
7
7
|
"additionalProperties": false,
|
|
8
8
|
"properties": {
|
|
9
9
|
"name": {
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
:host {
|
|
2
|
-
display: inline-block;
|
|
3
|
-
}
|
|
4
|
-
|
|
5
|
-
[role='toolbar'] {
|
|
6
|
-
display: inline-flex;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
button {
|
|
10
|
-
font: inherit;
|
|
11
|
-
background: none;
|
|
12
|
-
border: none;
|
|
13
|
-
cursor: pointer;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
button:focus-visible {
|
|
17
|
-
outline: 2px solid currentColor;
|
|
18
|
-
outline-offset: 2px;
|
|
19
|
-
}
|
|
@@ -4,13 +4,12 @@ test.describe('<%= classify(name) %>', () => {
|
|
|
4
4
|
test('toggles mounted toolbar buttons and moves focus with arrows', async ({ page }) => {
|
|
5
5
|
await page.goto('/');
|
|
6
6
|
|
|
7
|
-
const
|
|
7
|
+
const toolbar = page.locator('[<%= camelize(name) %>]').first();
|
|
8
8
|
test.skip(
|
|
9
|
-
(await
|
|
10
|
-
'Mount
|
|
9
|
+
(await toolbar.count()) === 0,
|
|
10
|
+
'Mount an element with [<%= camelize(name) %>] in a route or host component to enable this generated e2e test.'
|
|
11
11
|
);
|
|
12
12
|
|
|
13
|
-
const toolbar = component.getByRole('toolbar').first();
|
|
14
13
|
const buttons = toolbar.getByRole('button');
|
|
15
14
|
test.skip(
|
|
16
15
|
(await buttons.count()) < 2,
|
|
@@ -1,21 +1,12 @@
|
|
|
1
1
|
<!-- Semmet Angular: Toolbar — https://www.w3.org/WAI/ARIA/apg/patterns/toolbar/ -->
|
|
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 container and buttons.
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
<
|
|
9
|
-
|
|
10
|
-
<button
|
|
11
|
-
#toolbarButton
|
|
12
|
-
type="button"
|
|
13
|
-
[attr.aria-pressed]="isPressed(action.id)"
|
|
14
|
-
[tabIndex]="activeId() === action.id ? 0 : -1"
|
|
15
|
-
(click)="toggle(action.id)"
|
|
16
|
-
(keydown)="navigateToolbar($event, i)"
|
|
17
|
-
>
|
|
18
|
-
{{ action.label }}
|
|
19
|
-
</button>
|
|
20
|
-
}
|
|
7
|
+
<div <%= camelize(name) %> label="Text formatting">
|
|
8
|
+
<button type="button" <%= camelize(name) %>Item value="bold" [(pressed)]="isBold">Bold</button>
|
|
9
|
+
<button type="button" <%= camelize(name) %>Item value="italic" [(pressed)]="isItalic">Italic</button>
|
|
10
|
+
<button type="button" <%= camelize(name) %>Item value="underline" [(pressed)]="isUnderline">Underline</button>
|
|
21
11
|
</div>
|
|
12
|
+
-->
|
|
@@ -1,25 +1,40 @@
|
|
|
1
|
+
import { Component } from '@angular/core';
|
|
1
2
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
2
|
-
|
|
3
|
+
|
|
4
|
+
import { <%= classify(name) %>, <%= classify(name) %>Item } from './<%= dasherize(name) %>';
|
|
5
|
+
|
|
6
|
+
@Component({
|
|
7
|
+
imports: [<%= classify(name) %>, <%= classify(name) %>Item],
|
|
8
|
+
template: `
|
|
9
|
+
<div <%= camelize(name) %> label="Text formatting">
|
|
10
|
+
<button type="button" <%= camelize(name) %>Item value="bold">Bold</button>
|
|
11
|
+
<button type="button" <%= camelize(name) %>Item value="italic">Italic</button>
|
|
12
|
+
<button type="button" <%= camelize(name) %>Item value="underline">Underline</button>
|
|
13
|
+
</div>
|
|
14
|
+
`,
|
|
15
|
+
})
|
|
16
|
+
class HostComponent {}
|
|
3
17
|
|
|
4
18
|
describe('<%= classify(name) %>', () => {
|
|
5
|
-
let fixture: ComponentFixture
|
|
19
|
+
let fixture: ComponentFixture<HostComponent>;
|
|
20
|
+
let host: HTMLElement;
|
|
6
21
|
|
|
7
22
|
beforeEach(async () => {
|
|
8
|
-
await TestBed.configureTestingModule({ imports: [
|
|
9
|
-
fixture = TestBed.createComponent(
|
|
23
|
+
await TestBed.configureTestingModule({ imports: [HostComponent] }).compileComponents();
|
|
24
|
+
fixture = TestBed.createComponent(HostComponent);
|
|
10
25
|
fixture.detectChanges();
|
|
26
|
+
host = fixture.nativeElement as HTMLElement;
|
|
11
27
|
});
|
|
12
28
|
|
|
13
29
|
function buttons(): HTMLButtonElement[] {
|
|
14
|
-
return Array.from(
|
|
30
|
+
return Array.from(host.querySelectorAll('button'));
|
|
15
31
|
}
|
|
16
32
|
|
|
17
|
-
it('
|
|
18
|
-
const toolbar =
|
|
33
|
+
it('decorates consumer-owned markup with a labelled toolbar and roving tabindex', () => {
|
|
34
|
+
const toolbar = host.querySelector('[role="toolbar"]');
|
|
19
35
|
const [bold, italic] = buttons();
|
|
20
36
|
|
|
21
|
-
expect(toolbar
|
|
22
|
-
expect(bold.textContent?.trim()).toBe('Bold');
|
|
37
|
+
expect(toolbar?.getAttribute('aria-label')).toBe('Text formatting');
|
|
23
38
|
expect(bold.tabIndex).toBe(0);
|
|
24
39
|
expect(italic.tabIndex).toBe(-1);
|
|
25
40
|
expect(bold.getAttribute('aria-pressed')).toBe('false');
|
|
@@ -33,13 +48,13 @@ describe('<%= classify(name) %>', () => {
|
|
|
33
48
|
expect(bold.getAttribute('aria-pressed')).toBe('true');
|
|
34
49
|
});
|
|
35
50
|
|
|
36
|
-
it('moves focus with
|
|
51
|
+
it('moves focus with ArrowRight and updates roving tabindex', () => {
|
|
37
52
|
const [bold, italic] = buttons();
|
|
38
|
-
bold.focus();
|
|
39
53
|
bold.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true }));
|
|
40
54
|
fixture.detectChanges();
|
|
41
55
|
|
|
42
56
|
expect(document.activeElement).toBe(italic);
|
|
43
57
|
expect(italic.tabIndex).toBe(0);
|
|
58
|
+
expect(bold.tabIndex).toBe(-1);
|
|
44
59
|
});
|
|
45
60
|
});
|