semmet-angular 0.41.0 → 0.43.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 (202) hide show
  1. package/README.en.md +2 -315
  2. package/README.md +169 -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 +4 -33
  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 +15 -0
  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/form/files/__name@dasherize__/__name@dasherize__.css.template +1 -77
  69. package/src/form/files/__name@dasherize__/__name@dasherize__.html.template +7 -7
  70. package/src/form/files/__name@dasherize__/__name@dasherize__.ts.template +20 -7
  71. package/src/form/schema.json +19 -4
  72. package/src/input/files/__name@dasherize__/__name@dasherize__.css.template +2 -58
  73. package/src/input/schema.json +19 -4
  74. package/src/input-group/files/__name@dasherize__/__name@dasherize__.css.template +2 -68
  75. package/src/input-group/files/__name@dasherize__/__name@dasherize__.html.template +13 -5
  76. package/src/input-group/files/__name@dasherize__/__name@dasherize__.ts.template +21 -6
  77. package/src/input-group/schema.json +19 -4
  78. package/src/landmarks/files/__name@dasherize__/__name@dasherize__.css.template +0 -24
  79. package/src/landmarks/files/__name@dasherize__/__name@dasherize__.html.template +2 -2
  80. package/src/landmarks/files/__name@dasherize__/__name@dasherize__.ts.template +5 -2
  81. package/src/landmarks/schema.json +19 -4
  82. package/src/list-group/files/__name@dasherize__/__name@dasherize__.css.template +0 -123
  83. package/src/list-group/files/__name@dasherize__/__name@dasherize__.e2e-spec.ts.template +8 -9
  84. package/src/list-group/files/__name@dasherize__/__name@dasherize__.html.template +13 -47
  85. package/src/list-group/files/__name@dasherize__/__name@dasherize__.spec.ts.template +46 -24
  86. package/src/list-group/files/__name@dasherize__/__name@dasherize__.ts.template +97 -39
  87. package/src/list-group/schema.json +19 -4
  88. package/src/listbox/files/__name@dasherize__/__name@dasherize__.css.template +3 -37
  89. package/src/listbox/files/__name@dasherize__/__name@dasherize__.html.template +2 -2
  90. package/src/listbox/files/__name@dasherize__/__name@dasherize__.ts.template +3 -1
  91. package/src/listbox/schema.json +19 -4
  92. package/src/menu-button/files/__name@dasherize__/__name@dasherize__.css.template +7 -47
  93. package/src/menu-button/files/__name@dasherize__/__name@dasherize__.html.template +3 -3
  94. package/src/menu-button/files/__name@dasherize__/__name@dasherize__.ts.template +10 -7
  95. package/src/menu-button/schema.json +19 -4
  96. package/src/meter/files/__name@dasherize__/__name@dasherize__.css.template +0 -12
  97. package/src/meter/files/__name@dasherize__/__name@dasherize__.html.template +2 -2
  98. package/src/meter/files/__name@dasherize__/__name@dasherize__.ts.template +3 -1
  99. package/src/meter/schema.json +19 -4
  100. package/src/navbar/files/__name@dasherize__/__name@dasherize__.css.template +6 -62
  101. package/src/navbar/schema.json +19 -4
  102. package/src/navigation-menu/files/__name@dasherize__/__name@dasherize__.css.template +6 -37
  103. package/src/navigation-menu/files/__name@dasherize__/__name@dasherize__.html.template +7 -6
  104. package/src/navigation-menu/files/__name@dasherize__/__name@dasherize__.ts.template +15 -1
  105. package/src/navigation-menu/schema.json +19 -4
  106. package/src/offcanvas/files/__name@dasherize__/__name@dasherize__.css.template +3 -70
  107. package/src/offcanvas/schema.json +19 -4
  108. package/src/pagination/files/__name@dasherize__/__name@dasherize__.css.template +2 -33
  109. package/src/pagination/files/__name@dasherize__/__name@dasherize__.html.template +6 -6
  110. package/src/pagination/files/__name@dasherize__/__name@dasherize__.ts.template +7 -1
  111. package/src/pagination/schema.json +19 -4
  112. package/src/popover/files/__name@dasherize__/__name@dasherize__.css.template +8 -69
  113. package/src/popover/schema.json +19 -4
  114. package/src/progress-bar/files/__name@dasherize__/__name@dasherize__.css.template +1 -31
  115. package/src/progress-bar/files/__name@dasherize__/__name@dasherize__.html.template +3 -3
  116. package/src/progress-bar/files/__name@dasherize__/__name@dasherize__.ts.template +5 -1
  117. package/src/progress-bar/schema.json +19 -4
  118. package/src/radio-group/files/__name@dasherize__/__name@dasherize__.css.template +4 -31
  119. package/src/radio-group/files/__name@dasherize__/__name@dasherize__.html.template +2 -2
  120. package/src/radio-group/files/__name@dasherize__/__name@dasherize__.ts.template +2 -1
  121. package/src/radio-group/schema.json +19 -4
  122. package/src/rating/files/__name@dasherize__/__name@dasherize__.css.template +17 -0
  123. package/src/rating/files/__name@dasherize__/__name@dasherize__.html.template +34 -0
  124. package/src/rating/files/__name@dasherize__/__name@dasherize__.spec.ts.template +38 -0
  125. package/src/rating/files/__name@dasherize__/__name@dasherize__.ts.template +75 -0
  126. package/src/rating/index.d.ts +1 -0
  127. package/src/rating/index.js +6 -0
  128. package/src/rating/index.js.map +1 -0
  129. package/src/rating/index.ts +3 -0
  130. package/src/rating/schema.d.ts +1 -0
  131. package/src/rating/schema.js +3 -0
  132. package/src/rating/schema.js.map +1 -0
  133. package/src/rating/schema.json +44 -0
  134. package/src/rating/schema.ts +1 -0
  135. package/src/select/files/__name@dasherize__/__name@dasherize__.css.template +4 -55
  136. package/src/select/schema.json +19 -4
  137. package/src/skeleton/files/__name@dasherize__/__name@dasherize__.css.template +2 -29
  138. package/src/skeleton/files/__name@dasherize__/__name@dasherize__.ts.template +1 -3
  139. package/src/skeleton/schema.json +19 -4
  140. package/src/skip-link/files/__name@dasherize__/__name@dasherize__.css.template +2 -14
  141. package/src/skip-link/files/__name@dasherize__/__name@dasherize__.html.template +2 -2
  142. package/src/skip-link/files/__name@dasherize__/__name@dasherize__.ts.template +5 -2
  143. package/src/skip-link/schema.json +19 -4
  144. package/src/slider/files/__name@dasherize__/__name@dasherize__.css.template +6 -20
  145. package/src/slider/files/__name@dasherize__/__name@dasherize__.html.template +2 -2
  146. package/src/slider/files/__name@dasherize__/__name@dasherize__.ts.template +3 -1
  147. package/src/slider/schema.json +19 -4
  148. package/src/spinbutton/files/__name@dasherize__/__name@dasherize__.css.template +6 -35
  149. package/src/spinbutton/files/__name@dasherize__/__name@dasherize__.html.template +4 -4
  150. package/src/spinbutton/files/__name@dasherize__/__name@dasherize__.ts.template +5 -1
  151. package/src/spinbutton/schema.json +19 -4
  152. package/src/spinner/files/__name@dasherize__/__name@dasherize__.css.template +0 -43
  153. package/src/spinner/files/__name@dasherize__/__name@dasherize__.ts.template +2 -5
  154. package/src/spinner/schema.json +19 -4
  155. package/src/steps/files/__name@dasherize__/__name@dasherize__.css.template +36 -0
  156. package/src/steps/files/__name@dasherize__/__name@dasherize__.html.template +44 -0
  157. package/src/steps/files/__name@dasherize__/__name@dasherize__.spec.ts.template +35 -0
  158. package/src/steps/files/__name@dasherize__/__name@dasherize__.ts.template +107 -0
  159. package/src/steps/index.d.ts +1 -0
  160. package/src/steps/index.js +6 -0
  161. package/src/steps/index.js.map +1 -0
  162. package/src/steps/index.ts +3 -0
  163. package/src/steps/schema.d.ts +1 -0
  164. package/src/steps/schema.js +3 -0
  165. package/src/steps/schema.js.map +1 -0
  166. package/src/steps/schema.json +44 -0
  167. package/src/steps/schema.ts +1 -0
  168. package/src/switch/files/__name@dasherize__/__name@dasherize__.css.template +7 -22
  169. package/src/switch/schema.json +19 -4
  170. package/src/table/files/__name@dasherize__/__name@dasherize__.css.template +0 -25
  171. package/src/table/files/__name@dasherize__/__name@dasherize__.html.template +3 -3
  172. package/src/table/files/__name@dasherize__/__name@dasherize__.ts.template +3 -0
  173. package/src/table/schema.json +19 -4
  174. package/src/tabs/files/__name@dasherize__/__name@dasherize__.css.template +5 -44
  175. package/src/tabs/files/__name@dasherize__/__name@dasherize__.html.template +2 -2
  176. package/src/tabs/files/__name@dasherize__/__name@dasherize__.ts.template +2 -0
  177. package/src/tabs/schema.json +19 -4
  178. package/src/textarea/files/__name@dasherize__/__name@dasherize__.css.template +2 -59
  179. package/src/textarea/schema.json +19 -4
  180. package/src/toast/files/__name@dasherize__/__name@dasherize__.css.template +1 -35
  181. package/src/toast/files/__name@dasherize__/__name@dasherize__.html.template +4 -4
  182. package/src/toast/files/__name@dasherize__/__name@dasherize__.ts.template +5 -1
  183. package/src/toast/schema.json +19 -4
  184. package/src/toolbar/files/__name@dasherize__/__name@dasherize__.css.template +2 -36
  185. package/src/toolbar/files/__name@dasherize__/__name@dasherize__.html.template +3 -3
  186. package/src/toolbar/files/__name@dasherize__/__name@dasherize__.ts.template +11 -8
  187. package/src/toolbar/schema.json +19 -4
  188. package/src/tooltip/files/__name@dasherize__/__name@dasherize__.css.template +1 -32
  189. package/src/tooltip/schema.json +19 -4
  190. package/src/tree-view/files/__name@dasherize__/__name@dasherize__.css.template +5 -27
  191. package/src/tree-view/files/__name@dasherize__/__name@dasherize__.html.template +3 -3
  192. package/src/tree-view/files/__name@dasherize__/__name@dasherize__.ts.template +16 -13
  193. package/src/tree-view/schema.json +19 -4
  194. package/src/utils/generate-component.js +3 -1
  195. package/src/utils/generate-component.js.map +1 -1
  196. package/src/utils/generate-component.ts +3 -1
  197. package/src/utils/generate-composition.js +7 -2
  198. package/src/utils/generate-composition.js.map +1 -1
  199. package/src/utils/generate-composition.ts +9 -2
  200. package/src/utils/schema-types.d.ts +1 -0
  201. package/src/utils/schema-types.ts +1 -0
  202. package/README.pt-BR.md +0 -330
@@ -1,30 +1,6 @@
1
1
  :host {
2
- --semmet-color-outline: #d4d4d8;
3
- --semmet-color-on-surface: #18181b;
4
- --semmet-color-on-surface-variant: #52525b;
5
- --semmet-space-xs: 16px;
6
- --semmet-space-sm: 24px;
7
-
8
2
  display: grid;
9
3
  grid-template-columns: 1fr;
10
- gap: var(--semmet-space-sm);
11
- font-family: Roboto, system-ui, -apple-system, 'Segoe UI', sans-serif;
12
- color: var(--semmet-color-on-surface);
13
- }
14
-
15
- header,
16
- nav,
17
- footer {
18
- padding: var(--semmet-space-xs) 0;
19
- }
20
-
21
- header {
22
- border-bottom: 1px solid var(--semmet-color-outline);
23
- }
24
-
25
- footer {
26
- border-top: 1px solid var(--semmet-color-outline);
27
- color: var(--semmet-color-on-surface-variant);
28
4
  }
29
5
 
30
6
  main {
@@ -15,13 +15,13 @@ Import <%= classify(name) %> in that parent component.
15
15
  <header>
16
16
  <ng-content select="[header]"></ng-content>
17
17
  </header>
18
- <nav aria-label="Main">
18
+ <nav [attr.aria-label]="navigationLabel()">
19
19
  <ng-content select="[nav]"></ng-content>
20
20
  </nav>
21
21
  <main>
22
22
  <ng-content></ng-content>
23
23
  </main>
24
- <aside aria-label="Related links">
24
+ <aside [attr.aria-label]="asideLabel()">
25
25
  <ng-content select="[aside]"></ng-content>
26
26
  </aside>
27
27
  <footer>
@@ -1,4 +1,4 @@
1
- import { Component } from '@angular/core';
1
+ import { Component, input } from '@angular/core';
2
2
 
3
3
  @Component({
4
4
  selector: '<%= selector %>',
@@ -6,4 +6,7 @@ import { Component } from '@angular/core';
6
6
  templateUrl: './<%= dasherize(name) %>.html',
7
7
  styleUrl: './<%= dasherize(name) %>.css',
8
8
  })
9
- export class <%= classify(name) %> {}
9
+ export class <%= classify(name) %> {
10
+ readonly navigationLabel = input('Main');
11
+ readonly asideLabel = input('Related links');
12
+ }
@@ -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 layout 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,123 +0,0 @@
1
- :host {
2
- --semmet-color-primary: #18181b;
3
- --semmet-color-on-primary: #ffffff;
4
- --semmet-color-surface: #ffffff;
5
- --semmet-color-surface-muted: #f4f4f5;
6
- --semmet-color-outline: #d4d4d8;
7
- --semmet-color-on-surface: #27272a;
8
- --semmet-color-on-surface-variant: #71717a;
9
- --semmet-radius-md: 10px;
10
- --semmet-space-3xs: 4px;
11
- --semmet-space-2xs: 8px;
12
- --semmet-space-xs: 16px;
13
- --semmet-space-xl: 64px;
14
- --semmet-target-sm: 32px;
15
- --semmet-motion: 160ms cubic-bezier(0.2, 0, 0, 1);
16
-
17
- display: block;
18
- font-family: Roboto, system-ui, -apple-system, 'Segoe UI', sans-serif;
19
- font-size: 0.9375rem;
20
- }
21
-
22
- .semmet-list-group {
23
- display: grid;
24
- margin: 0;
25
- padding: 0;
26
- overflow: hidden;
27
- border: 1px solid var(--semmet-color-outline);
28
- border-radius: var(--semmet-radius-md);
29
- background: var(--semmet-color-surface);
30
- list-style: none;
31
- }
32
-
33
- .semmet-list-group--flush {
34
- border-radius: 0;
35
- }
36
-
37
- .semmet-list-group__item + .semmet-list-group__item {
38
- border-block-start: 1px solid var(--semmet-color-outline);
39
- }
40
-
41
- .semmet-list-group__action {
42
- display: flex;
43
- align-items: center;
44
- justify-content: space-between;
45
- gap: var(--semmet-space-xs);
46
- width: 100%;
47
- min-height: var(--semmet-space-xl);
48
- padding: var(--semmet-space-xs);
49
- border: 0;
50
- color: var(--semmet-color-on-surface);
51
- background: var(--semmet-color-surface);
52
- font: inherit;
53
- text-align: start;
54
- text-decoration: none;
55
- cursor: pointer;
56
- transition:
57
- background-color var(--semmet-motion),
58
- color var(--semmet-motion),
59
- opacity var(--semmet-motion);
60
- }
61
-
62
- .semmet-list-group__action:hover:not(:disabled):not(.semmet-list-group__action--disabled) {
63
- background: var(--semmet-color-surface-muted);
64
- }
65
-
66
- .semmet-list-group__action--active {
67
- color: var(--semmet-color-on-primary);
68
- background: var(--semmet-color-primary);
69
- }
70
-
71
- .semmet-list-group__action:disabled,
72
- .semmet-list-group__action--disabled {
73
- cursor: not-allowed;
74
- opacity: 0.54;
75
- }
76
-
77
- .semmet-list-group__action:focus-visible {
78
- position: relative;
79
- z-index: 1;
80
- outline: 2px solid var(--semmet-color-primary);
81
- outline-offset: -2px;
82
- }
83
-
84
- .semmet-list-group__content {
85
- display: grid;
86
- min-width: 0;
87
- gap: var(--semmet-space-3xs);
88
- }
89
-
90
- .semmet-list-group__title {
91
- font-weight: 700;
92
- line-height: 1.25;
93
- }
94
-
95
- .semmet-list-group__description {
96
- color: var(--semmet-color-on-surface-variant);
97
- font-size: 0.875rem;
98
- line-height: 1.4;
99
- }
100
-
101
- .semmet-list-group__action--active .semmet-list-group__description {
102
- color: rgba(255, 255, 255, 0.76);
103
- }
104
-
105
- .semmet-list-group__badge {
106
- display: inline-flex;
107
- align-items: center;
108
- justify-content: center;
109
- min-width: var(--semmet-target-sm);
110
- min-height: var(--semmet-target-sm);
111
- padding: var(--semmet-space-3xs) var(--semmet-space-2xs);
112
- border-radius: 999px;
113
- color: var(--semmet-color-on-primary);
114
- background: var(--semmet-color-primary);
115
- font-size: 0.75rem;
116
- font-weight: 700;
117
- line-height: 1;
118
- }
119
-
120
- .semmet-list-group__action--active .semmet-list-group__badge {
121
- color: var(--semmet-color-primary);
122
- background: var(--semmet-color-on-primary);
123
- }
@@ -4,23 +4,22 @@ test.describe('<%= classify(name) %>', () => {
4
4
  test('updates mounted list group active action', async ({ page }) => {
5
5
  await page.goto('/');
6
6
 
7
- const component = page.locator('<%= selector %>').first();
7
+ const component = page.locator('[<%= camelize(name) %>]').first();
8
8
  test.skip(
9
9
  (await component.count()) === 0,
10
- 'Mount <<%= selector %> /> in a route or host component to enable this generated e2e test.'
10
+ 'Mount a <%= camelize(name) %> list in a route or host component to enable this generated e2e test.'
11
11
  );
12
12
 
13
- const list = component.locator('ul').first();
14
- const actions = component.locator('.semmet-list-group__action');
13
+ const items = component.locator('[<%= camelize(name) %>Item]');
15
14
  test.skip(
16
- (await actions.count()) < 2,
15
+ (await items.count()) < 2,
17
16
  'Mount at least two list group actions to enable this generated list-group e2e test.'
18
17
  );
19
18
 
20
- await expect(list).toHaveAttribute('aria-label', /.+/);
21
- await expect(actions.first()).toHaveAttribute('aria-current', /page|true/);
19
+ await expect(component).toHaveAttribute('aria-label', /.+/);
20
+ await expect(items.first()).toHaveAttribute('aria-current', /page|true/);
22
21
 
23
- await actions.nth(1).click();
24
- await expect(actions.nth(1)).toHaveAttribute('aria-current', /page|true/);
22
+ await items.nth(1).click();
23
+ await expect(items.nth(1)).toHaveAttribute('aria-current', /page|true/);
25
24
  });
26
25
  });
@@ -1,53 +1,19 @@
1
- <!-- Semmet Angular: List Group - semantic list with optional action and active states. -->
1
+ <!-- Semmet Angular: List Group - headless list relationship directives. -->
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
5
 
6
- <<%= selector %> label="Settings sections" />
7
- -->
8
- <ul [id]="instanceId" [class]="listClasses()" [attr.aria-label]="label()">
9
- @for (item of items(); track item.id) {
10
- <li class="semmet-list-group__item">
11
- @if (item.href) {
12
- <a
13
- class="semmet-list-group__action"
14
- [class.semmet-list-group__action--active]="isActive(item)"
15
- [class.semmet-list-group__action--disabled]="item.disabled"
16
- [href]="item.disabled ? null : item.href"
17
- [attr.aria-current]="isActive(item) ? 'page' : null"
18
- [attr.aria-disabled]="item.disabled ? 'true' : null"
19
- (click)="select(item)"
20
- >
21
- <span class="semmet-list-group__content">
22
- <span class="semmet-list-group__title">{{ item.title }}</span>
23
- @if (item.description) {
24
- <span class="semmet-list-group__description">{{ item.description }}</span>
25
- }
26
- </span>
27
- @if (item.badge) {
28
- <span class="semmet-list-group__badge">{{ item.badge }}</span>
29
- }
30
- </a>
31
- } @else {
32
- <button
33
- type="button"
34
- class="semmet-list-group__action"
35
- [class.semmet-list-group__action--active]="isActive(item)"
36
- [disabled]="item.disabled"
37
- [attr.aria-current]="isActive(item) ? 'true' : null"
38
- (click)="select(item)"
39
- >
40
- <span class="semmet-list-group__content">
41
- <span class="semmet-list-group__title">{{ item.title }}</span>
42
- @if (item.description) {
43
- <span class="semmet-list-group__description">{{ item.description }}</span>
44
- }
45
- </span>
46
- @if (item.badge) {
47
- <span class="semmet-list-group__badge">{{ item.badge }}</span>
48
- }
49
- </button>
50
- }
6
+ <ul <%= camelize(name) %> label="Settings sections" [(activeId)]="activeSection">
7
+ @for (item of sections; track item.id) {
8
+ <li
9
+ <%= camelize(name) %>Item
10
+ [value]="item.id"
11
+ [disabled]="item.disabled"
12
+ #sectionItem="<%= camelize(name) %>Item"
13
+ [class.active]="sectionItem.active()"
14
+ >
15
+ <a [href]="item.href">{{ item.label }}</a>
51
16
  </li>
52
17
  }
53
18
  </ul>
19
+ -->
@@ -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
  }