semmet-angular 0.42.0 → 0.44.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 (218) hide show
  1. package/README.en.md +2 -315
  2. package/README.md +172 -264
  3. package/package.json +2 -2
  4. package/src/accordion/files/__name@dasherize__/__name@dasherize__.css.template +4 -41
  5. package/src/accordion/schema.json +19 -4
  6. package/src/alert/files/__name@dasherize__/__name@dasherize__.css.template +1 -25
  7. package/src/alert/files/__name@dasherize__/__name@dasherize__.html.template +3 -3
  8. package/src/alert/files/__name@dasherize__/__name@dasherize__.ts.template +4 -1
  9. package/src/alert/schema.json +19 -4
  10. package/src/alert-dialog/files/__name@dasherize__/__name@dasherize__.css.template +3 -56
  11. package/src/alert-dialog/files/__name@dasherize__/__name@dasherize__.html.template +6 -6
  12. package/src/alert-dialog/files/__name@dasherize__/__name@dasherize__.ts.template +7 -1
  13. package/src/alert-dialog/schema.json +19 -4
  14. package/src/avatar/files/__name@dasherize__/__name@dasherize__.css.template +17 -0
  15. package/src/avatar/files/__name@dasherize__/__name@dasherize__.html.template +28 -0
  16. package/src/avatar/files/__name@dasherize__/__name@dasherize__.spec.ts.template +27 -0
  17. package/src/avatar/files/__name@dasherize__/__name@dasherize__.ts.template +35 -0
  18. package/src/avatar/index.d.ts +1 -0
  19. package/src/avatar/index.js +6 -0
  20. package/src/avatar/index.js.map +1 -0
  21. package/src/avatar/index.ts +3 -0
  22. package/src/avatar/schema.d.ts +1 -0
  23. package/src/avatar/schema.js +3 -0
  24. package/src/avatar/schema.js.map +1 -0
  25. package/src/avatar/schema.json +44 -0
  26. package/src/avatar/schema.ts +1 -0
  27. package/src/badge/files/__name@dasherize__/__name@dasherize__.css.template +0 -77
  28. package/src/badge/files/__name@dasherize__/__name@dasherize__.ts.template +2 -5
  29. package/src/badge/schema.json +19 -4
  30. package/src/block/files/__name@dasherize__/__name@dasherize__.css.template +10 -81
  31. package/src/block/files/__name@dasherize__/__name@dasherize__.html.template +33 -25
  32. package/src/block/files/__name@dasherize__/__name@dasherize__.ts.template +41 -10
  33. package/src/block/schema.json +19 -4
  34. package/src/breadcrumb/files/__name@dasherize__/__name@dasherize__.css.template +2 -23
  35. package/src/breadcrumb/files/__name@dasherize__/__name@dasherize__.html.template +4 -4
  36. package/src/breadcrumb/files/__name@dasherize__/__name@dasherize__.ts.template +9 -5
  37. package/src/breadcrumb/schema.json +19 -4
  38. package/src/button/files/__name@dasherize__/__name@dasherize__.css.template +2 -79
  39. package/src/button/files/__name@dasherize__/__name@dasherize__.ts.template +2 -5
  40. package/src/button/schema.json +19 -4
  41. package/src/button-group/files/__name@dasherize__/__name@dasherize__.css.template +2 -70
  42. package/src/button-group/files/__name@dasherize__/__name@dasherize__.ts.template +1 -2
  43. package/src/button-group/schema.json +19 -4
  44. package/src/card/files/__name@dasherize__/__name@dasherize__.css.template +0 -67
  45. package/src/card/files/__name@dasherize__/__name@dasherize__.html.template +10 -55
  46. package/src/card/files/__name@dasherize__/__name@dasherize__.ts.template +2 -45
  47. package/src/card/schema.json +19 -4
  48. package/src/carousel/files/__name@dasherize__/__name@dasherize__.css.template +2 -32
  49. package/src/carousel/files/__name@dasherize__/__name@dasherize__.html.template +4 -4
  50. package/src/carousel/files/__name@dasherize__/__name@dasherize__.ts.template +4 -0
  51. package/src/carousel/schema.json +19 -4
  52. package/src/checkbox/files/__name@dasherize__/__name@dasherize__.css.template +5 -21
  53. package/src/checkbox/schema.json +19 -4
  54. package/src/close-button/files/__name@dasherize__/__name@dasherize__.css.template +9 -47
  55. package/src/close-button/files/__name@dasherize__/__name@dasherize__.ts.template +2 -5
  56. package/src/close-button/schema.json +19 -4
  57. package/src/collection.json +21 -1
  58. package/src/combobox/files/__name@dasherize__/__name@dasherize__.css.template +4 -49
  59. package/src/combobox/files/__name@dasherize__/__name@dasherize__.html.template +2 -2
  60. package/src/combobox/files/__name@dasherize__/__name@dasherize__.ts.template +11 -8
  61. package/src/combobox/schema.json +19 -4
  62. package/src/dialog/files/__name@dasherize__/__name@dasherize__.css.template +3 -45
  63. package/src/dialog/files/__name@dasherize__/__name@dasherize__.html.template +5 -5
  64. package/src/dialog/files/__name@dasherize__/__name@dasherize__.ts.template +6 -1
  65. package/src/dialog/schema.json +19 -4
  66. package/src/disclosure/files/__name@dasherize__/__name@dasherize__.css.template +4 -36
  67. package/src/disclosure/schema.json +19 -4
  68. package/src/e2e/index.d.ts +2 -0
  69. package/src/e2e/index.js +90 -0
  70. package/src/e2e/index.js.map +1 -0
  71. package/src/e2e/index.ts +106 -0
  72. package/src/e2e/schema.d.ts +2 -0
  73. package/src/e2e/schema.js +3 -0
  74. package/src/e2e/schema.js.map +1 -0
  75. package/src/e2e/schema.json +9 -0
  76. package/src/e2e/schema.ts +1 -0
  77. package/src/form/files/__name@dasherize__/__name@dasherize__.css.template +1 -77
  78. package/src/form/files/__name@dasherize__/__name@dasherize__.html.template +7 -7
  79. package/src/form/files/__name@dasherize__/__name@dasherize__.ts.template +20 -7
  80. package/src/form/schema.json +19 -4
  81. package/src/input/files/__name@dasherize__/__name@dasherize__.css.template +2 -58
  82. package/src/input/schema.json +19 -4
  83. package/src/input-group/files/__name@dasherize__/__name@dasherize__.css.template +2 -68
  84. package/src/input-group/files/__name@dasherize__/__name@dasherize__.html.template +13 -5
  85. package/src/input-group/files/__name@dasherize__/__name@dasherize__.ts.template +21 -6
  86. package/src/input-group/schema.json +19 -4
  87. package/src/landmarks/files/__name@dasherize__/__name@dasherize__.css.template +0 -24
  88. package/src/landmarks/files/__name@dasherize__/__name@dasherize__.html.template +2 -2
  89. package/src/landmarks/files/__name@dasherize__/__name@dasherize__.ts.template +5 -2
  90. package/src/landmarks/schema.json +19 -4
  91. package/src/list-group/files/__name@dasherize__/__name@dasherize__.css.template +0 -123
  92. package/src/list-group/files/__name@dasherize__/__name@dasherize__.e2e-spec.ts.template +8 -9
  93. package/src/list-group/files/__name@dasherize__/__name@dasherize__.html.template +13 -47
  94. package/src/list-group/files/__name@dasherize__/__name@dasherize__.spec.ts.template +46 -24
  95. package/src/list-group/files/__name@dasherize__/__name@dasherize__.ts.template +97 -39
  96. package/src/list-group/schema.json +19 -4
  97. package/src/listbox/files/__name@dasherize__/__name@dasherize__.css.template +3 -37
  98. package/src/listbox/files/__name@dasherize__/__name@dasherize__.html.template +2 -2
  99. package/src/listbox/files/__name@dasherize__/__name@dasherize__.ts.template +3 -1
  100. package/src/listbox/schema.json +19 -4
  101. package/src/menu-button/files/__name@dasherize__/__name@dasherize__.css.template +7 -47
  102. package/src/menu-button/files/__name@dasherize__/__name@dasherize__.html.template +3 -3
  103. package/src/menu-button/files/__name@dasherize__/__name@dasherize__.ts.template +10 -7
  104. package/src/menu-button/schema.json +19 -4
  105. package/src/meter/files/__name@dasherize__/__name@dasherize__.css.template +0 -12
  106. package/src/meter/files/__name@dasherize__/__name@dasherize__.html.template +2 -2
  107. package/src/meter/files/__name@dasherize__/__name@dasherize__.ts.template +3 -1
  108. package/src/meter/schema.json +19 -4
  109. package/src/navbar/files/__name@dasherize__/__name@dasherize__.css.template +6 -62
  110. package/src/navbar/schema.json +19 -4
  111. package/src/navigation-menu/files/__name@dasherize__/__name@dasherize__.css.template +6 -37
  112. package/src/navigation-menu/files/__name@dasherize__/__name@dasherize__.html.template +7 -6
  113. package/src/navigation-menu/files/__name@dasherize__/__name@dasherize__.ts.template +15 -1
  114. package/src/navigation-menu/schema.json +19 -4
  115. package/src/ng-add/index.d.ts +1 -1
  116. package/src/ng-add/index.js +2 -72
  117. package/src/ng-add/index.js.map +1 -1
  118. package/src/ng-add/index.ts +3 -86
  119. package/src/ng-add/schema.d.ts +0 -1
  120. package/src/ng-add/schema.json +0 -5
  121. package/src/ng-add/schema.ts +0 -1
  122. package/src/offcanvas/files/__name@dasherize__/__name@dasherize__.css.template +3 -70
  123. package/src/offcanvas/schema.json +19 -4
  124. package/src/pagination/files/__name@dasherize__/__name@dasherize__.css.template +2 -33
  125. package/src/pagination/files/__name@dasherize__/__name@dasherize__.html.template +6 -6
  126. package/src/pagination/files/__name@dasherize__/__name@dasherize__.ts.template +7 -1
  127. package/src/pagination/schema.json +19 -4
  128. package/src/popover/files/__name@dasherize__/__name@dasherize__.css.template +8 -69
  129. package/src/popover/schema.json +19 -4
  130. package/src/progress-bar/files/__name@dasherize__/__name@dasherize__.css.template +1 -31
  131. package/src/progress-bar/files/__name@dasherize__/__name@dasherize__.html.template +3 -3
  132. package/src/progress-bar/files/__name@dasherize__/__name@dasherize__.ts.template +5 -1
  133. package/src/progress-bar/schema.json +19 -4
  134. package/src/radio-group/files/__name@dasherize__/__name@dasherize__.css.template +4 -31
  135. package/src/radio-group/files/__name@dasherize__/__name@dasherize__.html.template +2 -2
  136. package/src/radio-group/files/__name@dasherize__/__name@dasherize__.ts.template +2 -1
  137. package/src/radio-group/schema.json +19 -4
  138. package/src/rating/files/__name@dasherize__/__name@dasherize__.css.template +17 -0
  139. package/src/rating/files/__name@dasherize__/__name@dasherize__.html.template +34 -0
  140. package/src/rating/files/__name@dasherize__/__name@dasherize__.spec.ts.template +38 -0
  141. package/src/rating/files/__name@dasherize__/__name@dasherize__.ts.template +75 -0
  142. package/src/rating/index.d.ts +1 -0
  143. package/src/rating/index.js +6 -0
  144. package/src/rating/index.js.map +1 -0
  145. package/src/rating/index.ts +3 -0
  146. package/src/rating/schema.d.ts +1 -0
  147. package/src/rating/schema.js +3 -0
  148. package/src/rating/schema.js.map +1 -0
  149. package/src/rating/schema.json +44 -0
  150. package/src/rating/schema.ts +1 -0
  151. package/src/select/files/__name@dasherize__/__name@dasherize__.css.template +4 -55
  152. package/src/select/schema.json +19 -4
  153. package/src/skeleton/files/__name@dasherize__/__name@dasherize__.css.template +2 -29
  154. package/src/skeleton/files/__name@dasherize__/__name@dasherize__.ts.template +1 -3
  155. package/src/skeleton/schema.json +19 -4
  156. package/src/skip-link/files/__name@dasherize__/__name@dasherize__.css.template +2 -14
  157. package/src/skip-link/files/__name@dasherize__/__name@dasherize__.html.template +2 -2
  158. package/src/skip-link/files/__name@dasherize__/__name@dasherize__.ts.template +5 -2
  159. package/src/skip-link/schema.json +19 -4
  160. package/src/slider/files/__name@dasherize__/__name@dasherize__.css.template +6 -20
  161. package/src/slider/files/__name@dasherize__/__name@dasherize__.html.template +2 -2
  162. package/src/slider/files/__name@dasherize__/__name@dasherize__.ts.template +3 -1
  163. package/src/slider/schema.json +19 -4
  164. package/src/spinbutton/files/__name@dasherize__/__name@dasherize__.css.template +6 -35
  165. package/src/spinbutton/files/__name@dasherize__/__name@dasherize__.html.template +4 -4
  166. package/src/spinbutton/files/__name@dasherize__/__name@dasherize__.ts.template +5 -1
  167. package/src/spinbutton/schema.json +19 -4
  168. package/src/spinner/files/__name@dasherize__/__name@dasherize__.css.template +0 -43
  169. package/src/spinner/files/__name@dasherize__/__name@dasherize__.ts.template +2 -5
  170. package/src/spinner/schema.json +19 -4
  171. package/src/steps/files/__name@dasherize__/__name@dasherize__.css.template +36 -0
  172. package/src/steps/files/__name@dasherize__/__name@dasherize__.html.template +44 -0
  173. package/src/steps/files/__name@dasherize__/__name@dasherize__.spec.ts.template +35 -0
  174. package/src/steps/files/__name@dasherize__/__name@dasherize__.ts.template +107 -0
  175. package/src/steps/index.d.ts +1 -0
  176. package/src/steps/index.js +6 -0
  177. package/src/steps/index.js.map +1 -0
  178. package/src/steps/index.ts +3 -0
  179. package/src/steps/schema.d.ts +1 -0
  180. package/src/steps/schema.js +3 -0
  181. package/src/steps/schema.js.map +1 -0
  182. package/src/steps/schema.json +44 -0
  183. package/src/steps/schema.ts +1 -0
  184. package/src/switch/files/__name@dasherize__/__name@dasherize__.css.template +7 -22
  185. package/src/switch/schema.json +19 -4
  186. package/src/table/files/__name@dasherize__/__name@dasherize__.css.template +0 -25
  187. package/src/table/files/__name@dasherize__/__name@dasherize__.html.template +3 -3
  188. package/src/table/files/__name@dasherize__/__name@dasherize__.ts.template +3 -0
  189. package/src/table/schema.json +19 -4
  190. package/src/tabs/files/__name@dasherize__/__name@dasherize__.css.template +5 -44
  191. package/src/tabs/files/__name@dasherize__/__name@dasherize__.html.template +2 -2
  192. package/src/tabs/files/__name@dasherize__/__name@dasherize__.ts.template +2 -0
  193. package/src/tabs/schema.json +19 -4
  194. package/src/textarea/files/__name@dasherize__/__name@dasherize__.css.template +2 -59
  195. package/src/textarea/schema.json +19 -4
  196. package/src/toast/files/__name@dasherize__/__name@dasherize__.css.template +1 -35
  197. package/src/toast/files/__name@dasherize__/__name@dasherize__.html.template +4 -4
  198. package/src/toast/files/__name@dasherize__/__name@dasherize__.ts.template +5 -1
  199. package/src/toast/schema.json +19 -4
  200. package/src/toolbar/files/__name@dasherize__/__name@dasherize__.css.template +2 -36
  201. package/src/toolbar/files/__name@dasherize__/__name@dasherize__.html.template +3 -3
  202. package/src/toolbar/files/__name@dasherize__/__name@dasherize__.ts.template +11 -8
  203. package/src/toolbar/schema.json +19 -4
  204. package/src/tooltip/files/__name@dasherize__/__name@dasherize__.css.template +1 -32
  205. package/src/tooltip/schema.json +19 -4
  206. package/src/tree-view/files/__name@dasherize__/__name@dasherize__.css.template +5 -27
  207. package/src/tree-view/files/__name@dasherize__/__name@dasherize__.html.template +3 -3
  208. package/src/tree-view/files/__name@dasherize__/__name@dasherize__.ts.template +16 -13
  209. package/src/tree-view/schema.json +19 -4
  210. package/src/utils/generate-component.js +3 -1
  211. package/src/utils/generate-component.js.map +1 -1
  212. package/src/utils/generate-component.ts +3 -1
  213. package/src/utils/generate-composition.js +0 -1
  214. package/src/utils/generate-composition.js.map +1 -1
  215. package/src/utils/generate-composition.ts +0 -1
  216. package/src/utils/schema-types.d.ts +1 -0
  217. package/src/utils/schema-types.ts +1 -0
  218. package/README.pt-BR.md +0 -330
@@ -1,22 +1,36 @@
1
1
  import { Component } from '@angular/core';
2
2
  import { ComponentFixture, TestBed } from '@angular/core/testing';
3
3
 
4
- import { <%= classify(name) %> } from './<%= dasherize(name) %>';
4
+ import { <%= classify(name) %>, <%= classify(name) %>Item } from './<%= dasherize(name) %>';
5
5
 
6
6
  @Component({
7
- imports: [<%= classify(name) %>],
7
+ imports: [<%= classify(name) %>, <%= classify(name) %>Item],
8
8
  template: `
9
- <<%= selector %>
9
+ <ul
10
+ <%= camelize(name) %>
10
11
  label="Account sections"
11
- [items]="items"
12
- />
12
+ [(activeId)]="activeId"
13
+ >
14
+ @for (item of items; track item.id) {
15
+ <li
16
+ <%= camelize(name) %>Item
17
+ [value]="item.id"
18
+ [disabled]="item.disabled"
19
+ #section="<%= camelize(name) %>Item"
20
+ [class.active]="section.active()"
21
+ >
22
+ <a [href]="item.href">{{ item.title }}</a>
23
+ </li>
24
+ }
25
+ </ul>
13
26
  `,
14
27
  })
15
28
  class HostComponent {
29
+ activeId = 'profile';
16
30
  readonly items = [
17
- { id: 'profile', title: 'Profile', description: 'Public identity', active: true },
18
- { id: 'security', title: 'Security', description: 'Password settings', badge: '2' },
19
- { id: 'billing', title: 'Billing', description: 'Invoices', disabled: true },
31
+ { id: 'profile', title: 'Profile', href: '/profile', disabled: false },
32
+ { id: 'security', title: 'Security', href: '/security', disabled: false },
33
+ { id: 'billing', title: 'Billing', href: '/billing', disabled: true },
20
34
  ];
21
35
  }
22
36
 
@@ -34,33 +48,41 @@ describe('<%= classify(name) %>', () => {
34
48
  host = fixture.nativeElement as HTMLElement;
35
49
  });
36
50
 
37
- it('renders a labeled semantic list with active and disabled states', () => {
51
+ it('decorates consumer-owned markup with list semantics and state', () => {
38
52
  const list = host.querySelector<HTMLUListElement>('ul');
39
- const buttons = Array.from(host.querySelectorAll<HTMLButtonElement>('button'));
53
+ const items = Array.from(host.querySelectorAll<HTMLLIElement>('li'));
40
54
 
41
55
  expect(list?.getAttribute('aria-label')).toBe('Account sections');
42
- expect(buttons.map((button) => button.textContent?.replace(/\s+/g, ' ').trim())).toEqual([
43
- 'Profile Public identity',
44
- 'Security Password settings 2',
45
- 'Billing Invoices',
46
- ]);
47
- expect(buttons[0].getAttribute('aria-current')).toBe('true');
48
- expect(buttons[2].disabled).toBe(true);
56
+ expect(list?.getAttribute('role')).toBe('list');
57
+ expect(items.map((item) => item.textContent?.trim())).toEqual(['Profile', 'Security', 'Billing']);
58
+ expect(items[0].getAttribute('aria-current')).toBe('true');
59
+ expect(items[0].getAttribute('tabindex')).toBe('0');
60
+ expect(items[2].getAttribute('aria-disabled')).toBe('true');
49
61
  });
50
62
 
51
63
  it('updates selected item when an enabled item is activated', () => {
52
- const buttons = Array.from(host.querySelectorAll<HTMLButtonElement>('button'));
64
+ const items = Array.from(host.querySelectorAll<HTMLLIElement>('li'));
53
65
 
54
- buttons[1].click();
66
+ items[1].click();
55
67
  fixture.detectChanges();
56
68
 
57
- expect(buttons[0].getAttribute('aria-current')).toBeNull();
58
- expect(buttons[1].getAttribute('aria-current')).toBe('true');
69
+ expect(items[0].getAttribute('aria-current')).toBeNull();
70
+ expect(items[1].getAttribute('aria-current')).toBe('true');
59
71
 
60
- buttons[2].click();
72
+ items[2].click();
61
73
  fixture.detectChanges();
62
74
 
63
- expect(buttons[1].getAttribute('aria-current')).toBe('true');
64
- expect(buttons[2].getAttribute('aria-current')).toBeNull();
75
+ expect(items[1].getAttribute('aria-current')).toBe('true');
76
+ expect(items[2].getAttribute('aria-current')).toBeNull();
77
+ });
78
+
79
+ it('moves active state with keyboard without owning item content', () => {
80
+ const list = host.querySelector<HTMLUListElement>('ul');
81
+ const items = Array.from(host.querySelectorAll<HTMLLIElement>('li'));
82
+
83
+ list?.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }));
84
+ fixture.detectChanges();
85
+
86
+ expect(items[1].getAttribute('aria-current')).toBe('true');
65
87
  });
66
88
  });
@@ -1,55 +1,113 @@
1
- import { Component, booleanAttribute, computed, input, signal } from '@angular/core';
2
-
3
- interface <%= classify(name) %>Item {
4
- id: string;
5
- title: string;
6
- description?: string;
7
- badge?: string;
8
- href?: string;
9
- disabled?: boolean;
10
- active?: boolean;
11
- }
12
-
13
- const DEFAULT_ITEMS: readonly <%= classify(name) %>Item[] = [
14
- { id: 'profile', title: 'Profile', description: 'Personal details and public identity', active: true },
15
- { id: 'security', title: 'Security', description: 'Password, sessions, and two-factor settings', badge: '2' },
16
- { id: 'billing', title: 'Billing', description: 'Invoices and payment methods' },
17
- ];
1
+ import { Directive, ElementRef, booleanAttribute, computed, contentChildren, inject, input, model } from '@angular/core';
18
2
 
19
- let nextId = 0;
3
+ type <%= classify(name) %>Orientation = 'vertical' | 'horizontal';
20
4
 
21
- @Component({
22
- selector: '<%= selector %>',
23
- imports: [],
24
- templateUrl: './<%= dasherize(name) %>.html',
25
- styleUrl: './<%= dasherize(name) %>.css',
5
+ @Directive({
6
+ selector: '[<%= camelize(name) %>]',
7
+ exportAs: '<%= camelize(name) %>',
8
+ host: {
9
+ role: 'list',
10
+ '[attr.aria-label]': 'label() || null',
11
+ '[attr.aria-orientation]': 'orientation()',
12
+ '(keydown)': 'handleKeydown($event)',
13
+ },
26
14
  })
27
15
  export class <%= classify(name) %> {
28
- protected readonly instanceId = `<%= dasherize(name) %>-${nextId++}`;
16
+ private readonly items = contentChildren(<%= classify(name) %>Item, { descendants: true });
29
17
 
30
- readonly label = input('Settings sections');
31
- readonly flush = input(false, { transform: booleanAttribute });
18
+ readonly label = input('');
19
+ readonly orientation = input<<%= classify(name) %>Orientation>('vertical');
32
20
  readonly selectable = input(true, { transform: booleanAttribute });
33
- readonly items = input<readonly <%= classify(name) %>Item[]>(DEFAULT_ITEMS);
21
+ readonly activeId = model<string | null>(null);
34
22
 
35
- protected readonly selectedId = signal<string | null>(
36
- DEFAULT_ITEMS.find((item) => item.active)?.id ?? DEFAULT_ITEMS[0]?.id ?? null,
37
- );
23
+ isActive(item: <%= classify(name) %>Item): boolean {
24
+ return this.activeId() === item.value();
25
+ }
38
26
 
39
- protected readonly listClasses = computed(() => [
40
- 'semmet-list-group',
41
- this.flush() ? 'semmet-list-group--flush' : '',
42
- ].filter(Boolean).join(' '));
27
+ select(item: <%= classify(name) %>Item): void {
28
+ if (!this.selectable() || item.disabled()) {
29
+ return;
30
+ }
43
31
 
44
- protected isActive(item: <%= classify(name) %>Item): boolean {
45
- return this.selectable() ? this.selectedId() === item.id : Boolean(item.active);
32
+ this.activeId.set(item.value());
46
33
  }
47
34
 
48
- protected select(item: <%= classify(name) %>Item): void {
49
- if (!this.selectable() || item.disabled) {
35
+ handleKeydown(event: KeyboardEvent): void {
36
+ const enabledItems = this.items().filter((item) => !item.disabled());
37
+ const currentIndex = enabledItems.findIndex((item) => this.isActive(item));
38
+ const nextKey = this.orientation() === 'horizontal' ? 'ArrowRight' : 'ArrowDown';
39
+ const previousKey = this.orientation() === 'horizontal' ? 'ArrowLeft' : 'ArrowUp';
40
+
41
+ if (event.key === nextKey) {
42
+ event.preventDefault();
43
+ this.focusAt(enabledItems, currentIndex + 1);
44
+ return;
45
+ }
46
+
47
+ if (event.key === previousKey) {
48
+ event.preventDefault();
49
+ this.focusAt(enabledItems, currentIndex - 1);
50
+ return;
51
+ }
52
+
53
+ if (event.key === 'Home') {
54
+ event.preventDefault();
55
+ this.focusAt(enabledItems, 0);
56
+ return;
57
+ }
58
+
59
+ if (event.key === 'End') {
60
+ event.preventDefault();
61
+ this.focusAt(enabledItems, enabledItems.length - 1);
50
62
  return;
51
63
  }
52
64
 
53
- this.selectedId.set(item.id);
65
+ if (event.key === ' ' || event.key === 'Enter') {
66
+ const activeItem = enabledItems[currentIndex];
67
+ if (activeItem) {
68
+ event.preventDefault();
69
+ this.select(activeItem);
70
+ }
71
+ }
72
+ }
73
+
74
+ private focusAt(items: readonly <%= classify(name) %>Item[], index: number): void {
75
+ if (!items.length) {
76
+ return;
77
+ }
78
+
79
+ const nextIndex = (index + items.length) % items.length;
80
+ const item = items[nextIndex];
81
+ this.activeId.set(item.value());
82
+ item.focus();
83
+ }
84
+ }
85
+
86
+ @Directive({
87
+ selector: '[<%= camelize(name) %>Item]',
88
+ exportAs: '<%= camelize(name) %>Item',
89
+ host: {
90
+ role: 'listitem',
91
+ '[attr.aria-current]': 'active() ? ariaCurrent() : null',
92
+ '[attr.aria-disabled]': 'disabled() ? "true" : null',
93
+ '[attr.tabindex]': 'disabled() ? -1 : active() ? 0 : -1',
94
+ '(click)': 'select()',
95
+ },
96
+ })
97
+ export class <%= classify(name) %>Item {
98
+ private readonly listGroup = inject(<%= classify(name) %>);
99
+ private readonly elementRef = inject<ElementRef<HTMLElement>>(ElementRef);
100
+
101
+ readonly value = input.required<string>();
102
+ readonly disabled = input(false, { transform: booleanAttribute });
103
+ readonly ariaCurrent = input('true');
104
+ readonly active = computed(() => this.listGroup.isActive(this));
105
+
106
+ select(): void {
107
+ this.listGroup.select(this);
108
+ }
109
+
110
+ focus(): void {
111
+ this.elementRef.nativeElement.focus();
54
112
  }
55
113
  }
@@ -9,21 +9,36 @@
9
9
  "name": {
10
10
  "type": "string",
11
11
  "description": "The name for the new component. Used to derive the class name, selector, and file names.",
12
- "$default": { "$source": "argv", "index": 0 },
12
+ "$default": {
13
+ "$source": "argv",
14
+ "index": 0
15
+ },
13
16
  "x-prompt": "What name would you like to use for the list group component?"
14
17
  },
18
+ "e2e": {
19
+ "type": "boolean",
20
+ "default": false,
21
+ "description": "Generate a Playwright e2e spec for this component. Requires Playwright to be installed/configured in the consuming app."
22
+ },
15
23
  "path": {
16
24
  "type": "string",
17
25
  "format": "path",
18
- "$default": { "$source": "workingDirectory" },
26
+ "$default": {
27
+ "$source": "workingDirectory"
28
+ },
19
29
  "description": "The path where the component files should be created, relative to the workspace root. Defaults to the project's source root.",
20
30
  "visible": false
21
31
  },
22
32
  "project": {
23
33
  "type": "string",
24
34
  "description": "The name of the project to add the component to.",
25
- "$default": { "$source": "projectName" }
35
+ "$default": {
36
+ "$source": "projectName"
37
+ }
26
38
  }
27
39
  },
28
- "required": ["name", "project"]
40
+ "required": [
41
+ "name",
42
+ "project"
43
+ ]
29
44
  }
@@ -1,56 +1,22 @@
1
1
  :host {
2
- --semmet-color-primary: #18181b;
3
- --semmet-color-surface: #ffffff;
4
- --semmet-color-outline: #d4d4d8;
5
- --semmet-color-on-surface: #18181b;
6
- --semmet-radius-sm: 8px;
7
- --semmet-space-2xs: 8px;
8
- --semmet-space-xs: 16px;
9
- --semmet-target-md: 40px;
10
- --semmet-motion: 160ms cubic-bezier(0.2, 0, 0, 1);
11
-
12
2
  display: block;
13
- font-family: Roboto, system-ui, -apple-system, 'Segoe UI', sans-serif;
14
- font-size: 0.9375rem;
15
3
  }
16
4
 
17
5
  span {
18
6
  display: block;
19
- margin-bottom: var(--semmet-space-2xs);
20
- font-weight: 500;
21
- color: var(--semmet-color-on-surface);
22
7
  }
23
8
 
24
9
  ul {
25
10
  margin: 0;
26
- padding: var(--semmet-space-2xs);
11
+ padding: 0;
27
12
  list-style: none;
28
- border: 1px solid var(--semmet-color-outline);
29
- border-radius: var(--semmet-radius-sm);
30
- background: var(--semmet-color-surface);
31
- max-width: 16rem;
32
13
  }
33
14
 
34
15
  ul:focus-visible {
35
- outline: 2px solid var(--semmet-color-primary);
36
- outline-offset: 1px;
16
+ outline: 2px solid currentColor;
17
+ outline-offset: 2px;
37
18
  }
38
19
 
39
20
  [role='option'] {
40
- min-height: var(--semmet-target-md);
41
- padding: var(--semmet-space-2xs) var(--semmet-space-xs);
42
- border-radius: 6px;
43
- color: var(--semmet-color-on-surface);
44
21
  cursor: pointer;
45
- transition: background-color var(--semmet-motion);
46
- }
47
-
48
- [role='option'][aria-selected='true'] {
49
- background: color-mix(in srgb, var(--semmet-color-primary) 12%, transparent);
50
- color: var(--semmet-color-primary);
51
- font-weight: 500;
52
- }
53
-
54
- [role='option']:hover {
55
- background: color-mix(in srgb, var(--semmet-color-on-surface) 6%, transparent);
56
22
  }
@@ -3,9 +3,9 @@
3
3
  Usage from a parent component template:
4
4
  Import <%= classify(name) %> in that parent component.
5
5
 
6
- <<%= selector %> />
6
+ <<%= selector %> label="Choose an option" />
7
7
  -->
8
- <span [id]="instanceId + '-label'">Choose an option</span>
8
+ <span [id]="instanceId + '-label'">{{ label() }}</span>
9
9
  <ul
10
10
  role="listbox"
11
11
  [attr.aria-labelledby]="instanceId + '-label'"
@@ -1,4 +1,4 @@
1
- import { Component, computed, signal } from '@angular/core';
1
+ import { Component, computed, input, signal } from '@angular/core';
2
2
 
3
3
  interface <%= classify(name) %>Option {
4
4
  id: number;
@@ -22,6 +22,8 @@ export class <%= classify(name) %> {
22
22
 
23
23
  protected readonly activeIndex = signal(0);
24
24
 
25
+ readonly label = input('Choose an option');
26
+
25
27
  protected readonly activeDescendantId = computed(
26
28
  () => `${this.instanceId}-option-${this.options[this.activeIndex()].id}`
27
29
  );
@@ -9,21 +9,36 @@
9
9
  "name": {
10
10
  "type": "string",
11
11
  "description": "The name for the new component. Used to derive the class name, selector, and file names.",
12
- "$default": { "$source": "argv", "index": 0 },
12
+ "$default": {
13
+ "$source": "argv",
14
+ "index": 0
15
+ },
13
16
  "x-prompt": "What name would you like to use for the listbox component?"
14
17
  },
18
+ "e2e": {
19
+ "type": "boolean",
20
+ "default": false,
21
+ "description": "Generate a Playwright e2e spec for this component. Requires Playwright to be installed/configured in the consuming app."
22
+ },
15
23
  "path": {
16
24
  "type": "string",
17
25
  "format": "path",
18
- "$default": { "$source": "workingDirectory" },
26
+ "$default": {
27
+ "$source": "workingDirectory"
28
+ },
19
29
  "description": "The path where the component files should be created, relative to the workspace root. Defaults to the project's source root.",
20
30
  "visible": false
21
31
  },
22
32
  "project": {
23
33
  "type": "string",
24
34
  "description": "The name of the project to add the component to.",
25
- "$default": { "$source": "projectName" }
35
+ "$default": {
36
+ "$source": "projectName"
37
+ }
26
38
  }
27
39
  },
28
- "required": ["name", "project"]
40
+ "required": [
41
+ "name",
42
+ "project"
43
+ ]
29
44
  }
@@ -1,46 +1,17 @@
1
1
  :host {
2
- --semmet-color-primary: #18181b;
3
- --semmet-color-surface: #ffffff;
4
- --semmet-color-outline: #d4d4d8;
5
- --semmet-color-on-surface: #18181b;
6
- --semmet-color-on-surface-variant: #52525b;
7
- --semmet-radius-sm: 8px;
8
- --semmet-radius-md: 12px;
9
- --semmet-space-3xs: 4px;
10
- --semmet-space-2xs: 8px;
11
- --semmet-space-xs: 16px;
12
- --semmet-target-md: 40px;
13
- --semmet-elevation-3: 0 8px 24px rgba(20, 24, 23, 0.18), 0 2px 8px rgba(20, 24, 23, 0.1);
14
- --semmet-motion: 160ms cubic-bezier(0.2, 0, 0, 1);
15
-
16
2
  position: relative;
17
3
  display: inline-block;
18
- font-family: Roboto, system-ui, -apple-system, 'Segoe UI', sans-serif;
19
- font-size: 0.9375rem;
20
4
  }
21
5
 
22
6
  button {
23
7
  display: flex;
24
8
  align-items: center;
25
- gap: var(--semmet-space-2xs);
26
- min-height: var(--semmet-target-md);
27
9
  font: inherit;
28
- font-weight: 500;
29
- color: var(--semmet-color-on-surface);
30
- background: var(--semmet-color-surface);
31
- border: 1px solid var(--semmet-color-outline);
32
- border-radius: var(--semmet-radius-sm);
33
- padding: var(--semmet-space-2xs) var(--semmet-space-xs);
34
10
  cursor: pointer;
35
- transition: background-color var(--semmet-motion);
36
- }
37
-
38
- button:hover {
39
- background: color-mix(in srgb, var(--semmet-color-on-surface) 6%, transparent);
40
11
  }
41
12
 
42
13
  button:focus-visible {
43
- outline: 2px solid var(--semmet-color-primary);
14
+ outline: 2px solid currentColor;
44
15
  outline-offset: 2px;
45
16
  }
46
17
 
@@ -48,10 +19,9 @@ button::after {
48
19
  content: '';
49
20
  width: 0.4rem;
50
21
  height: 0.4rem;
51
- border-right: 2px solid var(--semmet-color-on-surface-variant);
52
- border-bottom: 2px solid var(--semmet-color-on-surface-variant);
22
+ border-right: 2px solid currentColor;
23
+ border-bottom: 2px solid currentColor;
53
24
  transform: rotate(45deg);
54
- transition: transform var(--semmet-motion);
55
25
  }
56
26
 
57
27
  button[aria-expanded='true']::after {
@@ -61,28 +31,18 @@ button[aria-expanded='true']::after {
61
31
  ul {
62
32
  position: absolute;
63
33
  z-index: 1;
64
- top: calc(100% + var(--semmet-space-3xs));
34
+ top: 100%;
65
35
  left: 0;
66
- min-width: 10rem;
67
36
  margin: 0;
68
- padding: var(--semmet-space-2xs);
37
+ padding: 0;
69
38
  list-style: none;
70
- background: var(--semmet-color-surface);
71
- border-radius: var(--semmet-radius-md);
72
- box-shadow: var(--semmet-elevation-3);
73
39
  }
74
40
 
75
41
  [role='menuitem'] {
76
- min-height: var(--semmet-target-md);
77
- padding: var(--semmet-space-2xs) var(--semmet-space-xs);
78
- border-radius: 6px;
79
- color: var(--semmet-color-on-surface);
80
42
  cursor: pointer;
81
- transition: background-color var(--semmet-motion);
82
43
  }
83
44
 
84
- [role='menuitem']:hover,
85
45
  [role='menuitem']:focus-visible {
86
- background: color-mix(in srgb, var(--semmet-color-on-surface) 6%, transparent);
87
- outline: none;
46
+ outline: 2px solid currentColor;
47
+ outline-offset: 2px;
88
48
  }
@@ -3,7 +3,7 @@
3
3
  Usage from a parent component template:
4
4
  Import <%= classify(name) %> in that parent component.
5
5
 
6
- <<%= selector %> />
6
+ <<%= selector %> label="Options" [items]="menuItems" />
7
7
  -->
8
8
  <button
9
9
  #menuButton
@@ -15,7 +15,7 @@ Import <%= classify(name) %> in that parent component.
15
15
  (click)="toggle()"
16
16
  (keydown)="handleButtonKeydown($event)"
17
17
  >
18
- Options
18
+ {{ label() }}
19
19
  </button>
20
20
  <ul
21
21
  #menu
@@ -24,7 +24,7 @@ Import <%= classify(name) %> in that parent component.
24
24
  [attr.aria-labelledby]="instanceId + '-button'"
25
25
  [hidden]="!isOpen()"
26
26
  >
27
- @for (item of items; track item.id; let i = $index) {
27
+ @for (item of items(); track item.id; let i = $index) {
28
28
  <li role="none">
29
29
  <div
30
30
  #menuItem
@@ -1,10 +1,16 @@
1
- import { Component, ElementRef, Injector, afterNextRender, inject, signal, viewChild, viewChildren } from '@angular/core';
1
+ import { Component, ElementRef, Injector, afterNextRender, inject, input, signal, viewChild, viewChildren } from '@angular/core';
2
2
 
3
3
  interface <%= classify(name) %>Item {
4
4
  id: number;
5
5
  label: string;
6
6
  }
7
7
 
8
+ const DEFAULT_ITEMS: readonly <%= classify(name) %>Item[] = [
9
+ { id: 1, label: 'First option' },
10
+ { id: 2, label: 'Second option' },
11
+ { id: 3, label: 'Third option' },
12
+ ];
13
+
8
14
  let nextId = 0;
9
15
 
10
16
  @Component({
@@ -23,14 +29,11 @@ export class <%= classify(name) %> {
23
29
  private readonly menuItems = viewChildren<ElementRef<HTMLElement>>('menuItem');
24
30
 
25
31
  protected readonly instanceId = `<%= dasherize(name) %>-${nextId++}`;
26
- protected readonly items: <%= classify(name) %>Item[] = [
27
- { id: 1, label: 'First option' },
28
- { id: 2, label: 'Second option' },
29
- { id: 3, label: 'Third option' },
30
- ];
31
-
32
32
  protected readonly isOpen = signal(false);
33
33
 
34
+ readonly label = input('Options');
35
+ readonly items = input<readonly <%= classify(name) %>Item[]>(DEFAULT_ITEMS);
36
+
34
37
  protected toggle(): void {
35
38
  this.isOpen.set(true);
36
39
  this.focusFirstItem();
@@ -9,21 +9,36 @@
9
9
  "name": {
10
10
  "type": "string",
11
11
  "description": "The name for the new component. Used to derive the class name, selector, and file names.",
12
- "$default": { "$source": "argv", "index": 0 },
12
+ "$default": {
13
+ "$source": "argv",
14
+ "index": 0
15
+ },
13
16
  "x-prompt": "What name would you like to use for the menu button component?"
14
17
  },
18
+ "e2e": {
19
+ "type": "boolean",
20
+ "default": false,
21
+ "description": "Generate a Playwright e2e spec for this component. Requires Playwright to be installed/configured in the consuming app."
22
+ },
15
23
  "path": {
16
24
  "type": "string",
17
25
  "format": "path",
18
- "$default": { "$source": "workingDirectory" },
26
+ "$default": {
27
+ "$source": "workingDirectory"
28
+ },
19
29
  "description": "The path where the component files should be created, relative to the workspace root. Defaults to the project's source root.",
20
30
  "visible": false
21
31
  },
22
32
  "project": {
23
33
  "type": "string",
24
34
  "description": "The name of the project to add the component to.",
25
- "$default": { "$source": "projectName" }
35
+ "$default": {
36
+ "$source": "projectName"
37
+ }
26
38
  }
27
39
  },
28
- "required": ["name", "project"]
40
+ "required": [
41
+ "name",
42
+ "project"
43
+ ]
29
44
  }
@@ -1,23 +1,11 @@
1
1
  :host {
2
- --semmet-color-primary: #18181b;
3
- --semmet-color-on-surface: #18181b;
4
- --semmet-space-2xs: 8px;
5
-
6
2
  display: block;
7
- font-family: Roboto, system-ui, -apple-system, 'Segoe UI', sans-serif;
8
- font-size: 0.9375rem;
9
3
  }
10
4
 
11
5
  label {
12
6
  display: block;
13
- margin-bottom: var(--semmet-space-2xs);
14
- font-weight: 500;
15
- color: var(--semmet-color-on-surface);
16
7
  }
17
8
 
18
9
  meter {
19
10
  width: 100%;
20
- max-width: 20rem;
21
- height: 0.75rem;
22
- accent-color: var(--semmet-color-primary);
23
11
  }
@@ -3,9 +3,9 @@
3
3
  Usage from a parent component template:
4
4
  Import <%= classify(name) %> in that parent component.
5
5
 
6
- <<%= selector %> />
6
+ <<%= selector %> label="Disk usage" />
7
7
  -->
8
- <label [for]="instanceId">Disk usage</label>
8
+ <label [for]="instanceId">{{ label() }}</label>
9
9
  <meter [id]="instanceId" min="0" max="100" [value]="value()" low="50" high="90" optimum="0">
10
10
  {{ value() }}%
11
11
  </meter>