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.
Files changed (61) hide show
  1. package/README.md +2 -0
  2. package/package.json +1 -1
  3. package/src/accordion/files/__name@dasherize__/__name@dasherize__.css.template +0 -39
  4. package/src/accordion/files/__name@dasherize__/__name@dasherize__.e2e-spec.ts.template +16 -17
  5. package/src/accordion/files/__name@dasherize__/__name@dasherize__.html.template +14 -34
  6. package/src/accordion/files/__name@dasherize__/__name@dasherize__.spec.ts.template +52 -62
  7. package/src/accordion/files/__name@dasherize__/__name@dasherize__.ts.template +88 -82
  8. package/src/accordion/schema.json +1 -1
  9. package/src/block/files/__name@dasherize__/__name@dasherize__.html.template +51 -23
  10. package/src/block/files/__name@dasherize__/__name@dasherize__.ts.template +16 -10
  11. package/src/breadcrumb/files/__name@dasherize__/__name@dasherize__.css.template +0 -32
  12. package/src/breadcrumb/files/__name@dasherize__/__name@dasherize__.e2e-spec.ts.template +5 -2
  13. package/src/breadcrumb/files/__name@dasherize__/__name@dasherize__.html.template +8 -7
  14. package/src/breadcrumb/files/__name@dasherize__/__name@dasherize__.spec.ts.template +51 -0
  15. package/src/breadcrumb/files/__name@dasherize__/__name@dasherize__.ts.template +18 -19
  16. package/src/breadcrumb/index.js +1 -1
  17. package/src/breadcrumb/index.js.map +1 -1
  18. package/src/breadcrumb/index.ts +1 -1
  19. package/src/breadcrumb/schema.json +1 -1
  20. package/src/collection.json +9 -9
  21. package/src/menu-button/files/__name@dasherize__/__name@dasherize__.css.template +0 -48
  22. package/src/menu-button/files/__name@dasherize__/__name@dasherize__.e2e-spec.ts.template +2 -2
  23. package/src/menu-button/files/__name@dasherize__/__name@dasherize__.html.template +14 -35
  24. package/src/menu-button/files/__name@dasherize__/__name@dasherize__.spec.ts.template +53 -11
  25. package/src/menu-button/files/__name@dasherize__/__name@dasherize__.ts.template +128 -54
  26. package/src/menu-button/schema.json +1 -1
  27. package/src/navbar/files/__name@dasherize__/__name@dasherize__.css.template +0 -119
  28. package/src/navbar/files/__name@dasherize__/__name@dasherize__.e2e-spec.ts.template +10 -7
  29. package/src/navbar/files/__name@dasherize__/__name@dasherize__.html.template +18 -42
  30. package/src/navbar/files/__name@dasherize__/__name@dasherize__.spec.ts.template +46 -23
  31. package/src/navbar/files/__name@dasherize__/__name@dasherize__.ts.template +54 -50
  32. package/src/navbar/schema.json +1 -1
  33. package/src/pagination/files/__name@dasherize__/__name@dasherize__.css.template +0 -35
  34. package/src/pagination/files/__name@dasherize__/__name@dasherize__.e2e-spec.ts.template +2 -2
  35. package/src/pagination/files/__name@dasherize__/__name@dasherize__.html.template +11 -13
  36. package/src/pagination/files/__name@dasherize__/__name@dasherize__.spec.ts.template +58 -8
  37. package/src/pagination/files/__name@dasherize__/__name@dasherize__.ts.template +17 -31
  38. package/src/pagination/schema.json +1 -1
  39. package/src/table/files/__name@dasherize__/__name@dasherize__.css.template +0 -17
  40. package/src/table/files/__name@dasherize__/__name@dasherize__.e2e-spec.ts.template +1 -1
  41. package/src/table/files/__name@dasherize__/__name@dasherize__.html.template +13 -25
  42. package/src/table/files/__name@dasherize__/__name@dasherize__.ts.template +3 -35
  43. package/src/table/schema.json +1 -1
  44. package/src/tabs/files/__name@dasherize__/__name@dasherize__.css.template +0 -40
  45. package/src/tabs/files/__name@dasherize__/__name@dasherize__.e2e-spec.ts.template +2 -2
  46. package/src/tabs/files/__name@dasherize__/__name@dasherize__.html.template +18 -41
  47. package/src/tabs/files/__name@dasherize__/__name@dasherize__.spec.ts.template +39 -31
  48. package/src/tabs/files/__name@dasherize__/__name@dasherize__.ts.template +111 -113
  49. package/src/tabs/schema.json +1 -1
  50. package/src/toolbar/files/__name@dasherize__/__name@dasherize__.css.template +0 -19
  51. package/src/toolbar/files/__name@dasherize__/__name@dasherize__.e2e-spec.ts.template +3 -4
  52. package/src/toolbar/files/__name@dasherize__/__name@dasherize__.html.template +7 -16
  53. package/src/toolbar/files/__name@dasherize__/__name@dasherize__.spec.ts.template +26 -11
  54. package/src/toolbar/files/__name@dasherize__/__name@dasherize__.ts.template +59 -37
  55. package/src/toolbar/schema.json +1 -1
  56. package/src/tree-view/files/__name@dasherize__/__name@dasherize__.css.template +0 -39
  57. package/src/tree-view/files/__name@dasherize__/__name@dasherize__.e2e-spec.ts.template +4 -5
  58. package/src/tree-view/files/__name@dasherize__/__name@dasherize__.html.template +12 -31
  59. package/src/tree-view/files/__name@dasherize__/__name@dasherize__.spec.ts.template +80 -43
  60. package/src/tree-view/files/__name@dasherize__/__name@dasherize__.ts.template +105 -88
  61. 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
- import { <%= classify(name) %>, <%= classify(name) %>Item } from './<%= dasherize(name) %>';
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) %>Item],
12
+ imports: [<%= classify(name) %>, <%= classify(name) %>List, <%= classify(name) %>Panel, <%= classify(name) %>Tab],
7
13
  template: `
8
- <<%= selector %> [preserveContent]="preserveContent">
9
- <ng-template <%= camelize(name) %>Item id="profile" label="Profile" [selected]="true">
10
- <p>Profile content</p>
11
- </ng-template>
12
-
13
- <ng-template <%= camelize(name) %>Item id="settings" label="Settings">
14
- <p>Settings content</p>
15
- </ng-template>
16
-
17
- <ng-template <%= camelize(name) %>Item id="billing" label="Billing" disabled>
18
- <p>Billing content</p>
19
- </ng-template>
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
- preserveContent = true;
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(fixture.nativeElement.querySelectorAll('[role="tab"]'));
51
+ return Array.from(host.querySelectorAll('[role="tab"]'));
41
52
  }
42
53
 
43
54
  function panels(): HTMLElement[] {
44
- return Array.from(fixture.nativeElement.querySelectorAll('[role="tabpanel"]'));
55
+ return Array.from(host.querySelectorAll('[role="tabpanel"]'));
45
56
  }
46
57
 
47
- it('renders a tablist with ARIA relationships', () => {
48
- const tablist = fixture.nativeElement.querySelector('[role="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).toBeTruthy();
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('Settings content');
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
- profileTab.focus();
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
- settingsTab.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true }));
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
- profileTab.dispatchEvent(new KeyboardEvent('keydown', { key: 'End', bubbles: true }));
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 { NgTemplateOutlet } from '@angular/common';
2
- import {
3
- Component,
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: 'ng-template[<%= camelize(name) %>Item]',
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 tabButtons = viewChildren<ElementRef<HTMLButtonElement>>('tabButton');
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 label = input('Tabs');
46
- readonly preserveContent = input(true, { transform: booleanAttribute });
15
+ readonly activeId = model<<%= classify(name) %>Id | null>(null);
47
16
 
48
- private readonly renderedIds = signal<ReadonlySet<<%= classify(name) %>ItemId>>(new Set());
49
- private readonly selectedId = signal<<%= classify(name) %>ItemId | null>(null);
50
-
51
- private readonly activeId = computed<<%= classify(name) %>ItemId | null>(() => {
52
- const selectedId = this.selectedId();
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
- items.find((item) => item.selected() && !item.disabled()) ??
60
- items.find((item) => !item.disabled()) ??
61
- items[0]
62
- )?.id() ?? null;
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
- protected isActive(id: <%= classify(name) %>ItemId): boolean {
66
- return this.activeId() === id;
31
+ isActive(value: <%= classify(name) %>Id): boolean {
32
+ return this.resolvedActiveId() === value;
67
33
  }
68
34
 
69
- protected shouldRender(id: <%= classify(name) %>ItemId): boolean {
70
- return this.isActive(id) || (this.preserveContent() && this.renderedIds().has(id));
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
- const activeId = this.activeId();
79
- if (activeId !== null) {
80
- this.markRendered(activeId);
81
- }
40
+ this.activeId.set(tab.value());
41
+ }
82
42
 
83
- const id = item.id();
84
- this.markRendered(id);
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
- protected navigateTabs(event: KeyboardEvent, index: number): void {
90
- const items = this.items();
91
- const buttons = this.tabButtons();
92
- if (buttons.length === 0 || items.length === 0) {
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
- let nextIndex: number | null;
97
- switch (event.key) {
98
- case 'ArrowRight':
99
- nextIndex = this.nextEnabledIndex(items, index, 1);
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 (nextIndex === null) {
84
+ if (event.key === 'Home') {
85
+ event.preventDefault();
86
+ this.focusAt(enabledTabs, 0);
115
87
  return;
116
88
  }
117
89
 
118
- event.preventDefault();
119
- this.select(items[nextIndex]);
120
- buttons[nextIndex]?.nativeElement.focus();
90
+ if (event.key === 'End') {
91
+ event.preventDefault();
92
+ this.focusAt(enabledTabs, enabledTabs.length - 1);
93
+ }
121
94
  }
122
95
 
123
- private markRendered(id: <%= classify(name) %>ItemId): void {
124
- const renderedIds = this.renderedIds();
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
- this.renderedIds.set(new Set(renderedIds).add(id));
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
- private nextEnabledIndex(items: readonly <%= classify(name) %>Item[], index: number, direction: 1 | -1): number | null {
133
- for (let step = 1; step <= items.length; step++) {
134
- const nextIndex = (index + step * direction + items.length) % items.length;
135
- if (!items[nextIndex].disabled()) {
136
- return nextIndex;
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
- return null;
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
- private firstEnabledIndex(items: readonly <%= classify(name) %>Item[]): number | null {
144
- const index = items.findIndex((item) => !item.disabled());
145
- return index === -1 ? null : index;
134
+ focus(): void {
135
+ this.elementRef.nativeElement.focus();
146
136
  }
137
+ }
147
138
 
148
- private lastEnabledIndex(items: readonly <%= classify(name) %>Item[]): number | null {
149
- for (let index = items.length - 1; index >= 0; index--) {
150
- if (!items[index].disabled()) {
151
- return index;
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
- return null;
156
- }
153
+ readonly value = input.required<<%= classify(name) %>Id>();
154
+ readonly active = computed(() => this.root.isActive(this.value()));
157
155
  }
@@ -3,7 +3,7 @@
3
3
  "$id": "SemmetAngularTabs",
4
4
  "title": "Semmet Angular Tabs Options Schema",
5
5
  "type": "object",
6
- "description": "Generates a standalone Angular Tabs component (signals-based state, roving tabindex keyboard support) following the W3C ARIA Authoring Practices Guide.",
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 component = page.locator('<%= selector %>').first();
7
+ const toolbar = page.locator('[<%= camelize(name) %>]').first();
8
8
  test.skip(
9
- (await component.count()) === 0,
10
- 'Mount <<%= selector %> /> in a route or host component to enable this generated e2e test.'
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
- <<%= selector %> label="Text formatting" [actions]="formattingActions" />
7
- -->
8
- <div role="toolbar" [attr.aria-label]="label()">
9
- @for (action of actions(); track action.id; let i = $index) {
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
- import { <%= classify(name) %> } from './<%= dasherize(name) %>';
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<<%= classify(name) %>>;
19
+ let fixture: ComponentFixture<HostComponent>;
20
+ let host: HTMLElement;
6
21
 
7
22
  beforeEach(async () => {
8
- await TestBed.configureTestingModule({ imports: [<%= classify(name) %>] }).compileComponents();
9
- fixture = TestBed.createComponent(<%= classify(name) %>);
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(fixture.nativeElement.querySelectorAll('button'));
30
+ return Array.from(host.querySelectorAll('button'));
15
31
  }
16
32
 
17
- it('renders a labelled toolbar with roving tabindex', () => {
18
- const toolbar = fixture.nativeElement.querySelector('[role="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.getAttribute('aria-label')).toBe('Text formatting');
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 keyboard', () => {
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
  });