@statistikzh/leu 0.5.1 → 0.7.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 (236) hide show
  1. package/.husky/commit-msg +0 -3
  2. package/.husky/pre-commit +0 -3
  3. package/CHANGELOG.md +54 -0
  4. package/dist/Accordion.d.ts +10 -9
  5. package/dist/Accordion.d.ts.map +1 -1
  6. package/dist/Accordion.js +12 -11
  7. package/dist/Breadcrumb.d.ts +4 -4
  8. package/dist/Breadcrumb.d.ts.map +1 -1
  9. package/dist/Breadcrumb.js +28 -24
  10. package/dist/{Button-5326c982.d.ts → Button-7370f901.d.ts} +10 -11
  11. package/dist/Button-7370f901.d.ts.map +1 -0
  12. package/dist/{Button-5326c982.js → Button-7370f901.js} +57 -67
  13. package/dist/Button.d.ts +1 -1
  14. package/dist/Button.js +3 -3
  15. package/dist/ButtonGroup.d.ts +2 -2
  16. package/dist/ButtonGroup.d.ts.map +1 -1
  17. package/dist/ButtonGroup.js +3 -3
  18. package/dist/Checkbox.d.ts +4 -3
  19. package/dist/Checkbox.d.ts.map +1 -1
  20. package/dist/Checkbox.js +14 -17
  21. package/dist/CheckboxGroup.d.ts +2 -2
  22. package/dist/CheckboxGroup.d.ts.map +1 -1
  23. package/dist/CheckboxGroup.js +4 -4
  24. package/dist/Chip.d.ts +2 -2
  25. package/dist/Chip.d.ts.map +1 -1
  26. package/dist/Chip.js +23 -28
  27. package/dist/ChipGroup.d.ts +16 -8
  28. package/dist/ChipGroup.d.ts.map +1 -1
  29. package/dist/ChipGroup.js +39 -9
  30. package/dist/ChipLink.d.ts +2 -1
  31. package/dist/ChipLink.d.ts.map +1 -1
  32. package/dist/ChipLink.js +4 -7
  33. package/dist/ChipRemovable.d.ts +0 -2
  34. package/dist/ChipRemovable.d.ts.map +1 -1
  35. package/dist/ChipRemovable.js +8 -11
  36. package/dist/ChipSelectable.d.ts +12 -2
  37. package/dist/ChipSelectable.d.ts.map +1 -1
  38. package/dist/ChipSelectable.js +24 -26
  39. package/dist/Dropdown.d.ts +9 -5
  40. package/dist/Dropdown.d.ts.map +1 -1
  41. package/dist/Dropdown.js +68 -32
  42. package/dist/Icon.d.ts +116 -0
  43. package/dist/Icon.d.ts.map +1 -0
  44. package/dist/{icon-03e86700.js → Icon.js} +61 -32
  45. package/dist/Input.d.ts +13 -17
  46. package/dist/Input.d.ts.map +1 -1
  47. package/dist/Input.js +33 -24
  48. package/dist/LeuElement-ba5ea33d.d.ts +7 -0
  49. package/dist/LeuElement-ba5ea33d.d.ts.map +1 -0
  50. package/dist/{_rollupPluginBabelHelpers-20f659f4.js → LeuElement-ba5ea33d.js} +20 -1
  51. package/dist/Menu.d.ts +24 -2
  52. package/dist/Menu.d.ts.map +1 -1
  53. package/dist/Menu.js +120 -3
  54. package/dist/MenuItem.d.ts +28 -11
  55. package/dist/MenuItem.d.ts.map +1 -1
  56. package/dist/MenuItem.js +110 -63
  57. package/dist/Pagination.d.ts +10 -3
  58. package/dist/Pagination.d.ts.map +1 -1
  59. package/dist/Pagination.js +24 -21
  60. package/dist/Popup.d.ts +21 -3
  61. package/dist/Popup.d.ts.map +1 -1
  62. package/dist/Popup.js +44 -17
  63. package/dist/Radio.d.ts +4 -2
  64. package/dist/Radio.d.ts.map +1 -1
  65. package/dist/Radio.js +9 -14
  66. package/dist/RadioGroup.d.ts +2 -2
  67. package/dist/RadioGroup.d.ts.map +1 -1
  68. package/dist/RadioGroup.js +20 -11
  69. package/dist/ScrollTop.d.ts +2 -2
  70. package/dist/ScrollTop.d.ts.map +1 -1
  71. package/dist/ScrollTop.js +10 -8
  72. package/dist/Select.d.ts +75 -37
  73. package/dist/Select.d.ts.map +1 -1
  74. package/dist/Select.js +279 -181
  75. package/dist/Table.d.ts +2 -6
  76. package/dist/Table.d.ts.map +1 -1
  77. package/dist/Table.js +16 -16
  78. package/dist/VisuallyHidden.d.ts +2 -2
  79. package/dist/VisuallyHidden.d.ts.map +1 -1
  80. package/dist/VisuallyHidden.js +3 -3
  81. package/dist/index.d.ts +2 -2
  82. package/dist/index.js +5 -14
  83. package/dist/leu-accordion.d.ts.map +1 -1
  84. package/dist/leu-accordion.js +2 -3
  85. package/dist/leu-breadcrumb.d.ts.map +1 -1
  86. package/dist/leu-breadcrumb.js +4 -10
  87. package/dist/leu-button-group.d.ts.map +1 -1
  88. package/dist/leu-button-group.js +2 -3
  89. package/dist/leu-button.d.ts +1 -1
  90. package/dist/leu-button.d.ts.map +1 -1
  91. package/dist/leu-button.js +4 -5
  92. package/dist/leu-checkbox-group.d.ts.map +1 -1
  93. package/dist/leu-checkbox-group.js +2 -3
  94. package/dist/leu-checkbox.d.ts.map +1 -1
  95. package/dist/leu-checkbox.js +3 -4
  96. package/dist/leu-chip-group.d.ts.map +1 -1
  97. package/dist/leu-chip-group.js +2 -3
  98. package/dist/leu-chip-link.d.ts.map +1 -1
  99. package/dist/leu-chip-link.js +2 -3
  100. package/dist/leu-chip-removable.d.ts.map +1 -1
  101. package/dist/leu-chip-removable.js +3 -4
  102. package/dist/leu-chip-selectable.d.ts.map +1 -1
  103. package/dist/leu-chip-selectable.js +2 -3
  104. package/dist/leu-dropdown.d.ts.map +1 -1
  105. package/dist/leu-dropdown.js +5 -10
  106. package/dist/leu-icon.d.ts +3 -0
  107. package/dist/leu-icon.d.ts.map +1 -0
  108. package/dist/leu-icon.js +7 -0
  109. package/dist/leu-input.d.ts.map +1 -1
  110. package/dist/leu-input.js +3 -4
  111. package/dist/leu-menu-item.d.ts.map +1 -1
  112. package/dist/leu-menu-item.js +3 -5
  113. package/dist/leu-menu.d.ts.map +1 -1
  114. package/dist/leu-menu.js +5 -3
  115. package/dist/leu-pagination.d.ts.map +1 -1
  116. package/dist/leu-pagination.js +4 -7
  117. package/dist/leu-popup.d.ts.map +1 -1
  118. package/dist/leu-popup.js +2 -3
  119. package/dist/leu-radio-group.d.ts.map +1 -1
  120. package/dist/leu-radio-group.js +2 -3
  121. package/dist/leu-radio.d.ts.map +1 -1
  122. package/dist/leu-radio.js +2 -3
  123. package/dist/leu-scroll-top.d.ts.map +1 -1
  124. package/dist/leu-scroll-top.js +4 -6
  125. package/dist/leu-select.d.ts.map +1 -1
  126. package/dist/leu-select.js +5 -13
  127. package/dist/leu-table.d.ts.map +1 -1
  128. package/dist/leu-table.js +4 -8
  129. package/dist/leu-visually-hidden.d.ts.map +1 -1
  130. package/dist/leu-visually-hidden.js +2 -3
  131. package/dist/theme.css +2 -0
  132. package/dist/vscode.html-custom-data.json +124 -74
  133. package/dist/vue/index.d.ts +83 -67
  134. package/dist/web-types.json +256 -142
  135. package/package.json +9 -12
  136. package/scripts/generate-component/templates/[Name].js +6 -3
  137. package/scripts/generate-component/templates/test/[name].test.js +1 -1
  138. package/src/components/accordion/Accordion.js +13 -10
  139. package/src/components/accordion/leu-accordion.js +1 -2
  140. package/src/components/breadcrumb/Breadcrumb.js +31 -18
  141. package/src/components/breadcrumb/leu-breadcrumb.js +1 -2
  142. package/src/components/button/Button.js +45 -71
  143. package/src/components/button/button.css +11 -9
  144. package/src/components/button/leu-button.js +1 -2
  145. package/src/components/button/stories/button.stories.js +60 -19
  146. package/src/components/button/test/button.test.js +26 -63
  147. package/src/components/button-group/ButtonGroup.js +4 -2
  148. package/src/components/button-group/leu-button-group.js +1 -2
  149. package/src/components/checkbox/Checkbox.js +17 -11
  150. package/src/components/checkbox/CheckboxGroup.js +6 -3
  151. package/src/components/checkbox/leu-checkbox-group.js +1 -2
  152. package/src/components/checkbox/leu-checkbox.js +1 -2
  153. package/src/components/checkbox/stories/checkbox-group.stories.js +10 -26
  154. package/src/components/checkbox/stories/checkbox.stories.js +2 -7
  155. package/src/components/checkbox/test/checkbox-group.test.js +6 -21
  156. package/src/components/checkbox/test/checkbox.test.js +1 -12
  157. package/src/components/chip/Chip.js +5 -4
  158. package/src/components/chip/ChipGroup.js +38 -8
  159. package/src/components/chip/ChipLink.js +3 -7
  160. package/src/components/chip/ChipRemovable.js +8 -11
  161. package/src/components/chip/ChipSelectable.js +23 -27
  162. package/src/components/chip/chip.css +19 -20
  163. package/src/components/chip/leu-chip-group.js +1 -2
  164. package/src/components/chip/leu-chip-link.js +1 -2
  165. package/src/components/chip/leu-chip-removable.js +1 -2
  166. package/src/components/chip/leu-chip-selectable.js +1 -2
  167. package/src/components/chip/stories/chip-group.stories.js +6 -9
  168. package/src/components/chip/stories/chip-link.stories.js +3 -5
  169. package/src/components/chip/stories/chip-removable.stories.js +3 -4
  170. package/src/components/chip/stories/chip-selectable.stories.js +3 -3
  171. package/src/components/chip/test/chip-group.test.js +82 -30
  172. package/src/components/chip/test/chip-link.test.js +2 -6
  173. package/src/components/chip/test/chip-removable.test.js +4 -10
  174. package/src/components/chip/test/chip-selectable.test.js +10 -12
  175. package/src/components/dropdown/Dropdown.js +79 -26
  176. package/src/components/dropdown/leu-dropdown.js +1 -2
  177. package/src/components/dropdown/stories/dropdown.stories.js +30 -7
  178. package/src/components/dropdown/test/dropdown.test.js +5 -5
  179. package/src/components/icon/Icon.js +55 -0
  180. package/src/components/icon/icon.css +6 -0
  181. package/src/components/icon/leu-icon.js +5 -0
  182. package/src/components/icon/{icon.js → paths.js} +4 -37
  183. package/src/components/icon/stories/icon.stories.js +47 -0
  184. package/src/components/icon/test/icon.test.js +23 -40
  185. package/src/components/input/Input.js +31 -20
  186. package/src/components/input/input.css +4 -2
  187. package/src/components/input/leu-input.js +1 -2
  188. package/src/components/input/stories/input.stories.js +5 -5
  189. package/src/components/input/test/input.test.js +22 -0
  190. package/src/components/menu/Menu.js +143 -2
  191. package/src/components/menu/MenuItem.js +104 -52
  192. package/src/components/menu/leu-menu-item.js +1 -2
  193. package/src/components/menu/leu-menu.js +1 -2
  194. package/src/components/menu/menu-item.css +11 -4
  195. package/src/components/menu/stories/menu-item.stories.js +15 -4
  196. package/src/components/menu/stories/menu.stories.js +34 -7
  197. package/src/components/menu/test/menu-item.test.js +88 -82
  198. package/src/components/menu/test/menu.test.js +101 -8
  199. package/src/components/pagination/Pagination.js +27 -18
  200. package/src/components/pagination/leu-pagination.js +1 -2
  201. package/src/components/popup/Popup.js +39 -16
  202. package/src/components/popup/leu-popup.js +1 -2
  203. package/src/components/popup/popup.css +1 -0
  204. package/src/components/radio/Radio.js +12 -7
  205. package/src/components/radio/RadioGroup.js +18 -12
  206. package/src/components/radio/leu-radio-group.js +1 -2
  207. package/src/components/radio/leu-radio.js +1 -2
  208. package/src/components/radio/stories/radio-group.stories.js +5 -19
  209. package/src/components/radio/stories/radio.stories.js +2 -7
  210. package/src/components/radio/test/radio-group.test.js +6 -9
  211. package/src/components/radio/test/radio.test.js +3 -13
  212. package/src/components/scroll-top/ScrollTop.js +15 -5
  213. package/src/components/scroll-top/leu-scroll-top.js +1 -2
  214. package/src/components/select/Select.js +279 -175
  215. package/src/components/select/leu-select.js +1 -2
  216. package/src/components/select/select.css +20 -12
  217. package/src/components/select/stories/select.stories.js +16 -2
  218. package/src/components/select/test/select.test.js +191 -37
  219. package/src/components/table/Table.js +15 -9
  220. package/src/components/table/leu-table.js +1 -2
  221. package/src/components/table/table.css +3 -1
  222. package/src/components/visually-hidden/VisuallyHidden.js +6 -2
  223. package/src/components/visually-hidden/leu-visually-hidden.js +1 -2
  224. package/src/lib/LeuElement.js +23 -0
  225. package/src/lib/a11y.js +26 -0
  226. package/src/styles/custom-properties.css +2 -0
  227. package/web-test-runner.config.mjs +2 -0
  228. package/dist/Button-5326c982.d.ts.map +0 -1
  229. package/dist/_rollupPluginBabelHelpers-20f659f4.d.ts +0 -3
  230. package/dist/_rollupPluginBabelHelpers-20f659f4.d.ts.map +0 -1
  231. package/dist/defineElement-40372b4b.d.ts +0 -9
  232. package/dist/defineElement-40372b4b.d.ts.map +0 -1
  233. package/dist/defineElement-40372b4b.js +0 -15
  234. package/dist/icon-03e86700.d.ts +0 -11
  235. package/dist/icon-03e86700.d.ts.map +0 -1
  236. package/src/lib/defineElement.js +0 -13
@@ -1,17 +1,19 @@
1
- import { html, LitElement, nothing } from "lit"
1
+ import { html, nothing } from "lit"
2
2
  import { classMap } from "lit/directives/class-map.js"
3
3
  import { ifDefined } from "lit/directives/if-defined.js"
4
4
  import { live } from "lit/directives/live.js"
5
5
  import { createRef, ref } from "lit/directives/ref.js"
6
6
 
7
- import { Icon } from "../icon/icon.js"
7
+ import { LeuElement } from "../../lib/LeuElement.js"
8
+ import { LeuIcon } from "../icon/Icon.js"
8
9
 
10
+ // @ts-ignore
9
11
  import styles from "./input.css"
10
12
 
11
- export const SIZE_TYPES = {
13
+ export const SIZES = Object.freeze({
12
14
  SMALL: "small",
13
15
  REGULAR: "regular",
14
- }
16
+ })
15
17
 
16
18
  /**
17
19
  * TODO:
@@ -63,14 +65,18 @@ const VALIDATION_MESSAGES = {
63
65
  *
64
66
  * @tagname leu-input
65
67
  */
66
- export class LeuInput extends LitElement {
68
+ export class LeuInput extends LeuElement {
69
+ static dependencies = {
70
+ "leu-icon": LeuIcon,
71
+ }
72
+
67
73
  static styles = styles
68
74
 
69
75
  /**
70
76
  * @internal
71
77
  */
72
78
  static shadowRootOptions = {
73
- ...LitElement.shadowRootOptions,
79
+ ...LeuElement.shadowRootOptions,
74
80
  delegatesFocus: true,
75
81
  }
76
82
 
@@ -100,7 +106,6 @@ export class LeuInput extends LitElement {
100
106
  novalidate: { type: Boolean, reflect: true },
101
107
  step: { type: String, reflect: true },
102
108
 
103
- /** @type {ValidityState} */
104
109
  _validity: { state: true },
105
110
  }
106
111
 
@@ -119,20 +124,18 @@ export class LeuInput extends LitElement {
119
124
  this.required = false
120
125
  this.clearable = false
121
126
 
122
- /** @type {keyof typeof SIZE_TYPES} */
123
- this.size = SIZE_TYPES.REGULAR
127
+ /** @type {"small" | "regular"} */
128
+ this.size = SIZES.REGULAR
124
129
 
125
130
  this.type = "text"
126
131
  this._validity = null
127
132
  this.validationMessages = {}
128
133
  this.novalidate = false
134
+ this.value = ""
129
135
 
130
136
  /** @internal */
131
137
  this._identifier = ""
132
138
 
133
- /** @internal */
134
- this._clearIcon = Icon("clear")
135
-
136
139
  /**
137
140
  * @internal
138
141
  * @type {import("lit/directives/ref.js").Ref<HTMLInputElement>}
@@ -140,6 +143,13 @@ export class LeuInput extends LitElement {
140
143
  this._inputRef = createRef()
141
144
  }
142
145
 
146
+ get valueAsNumber() {
147
+ if (this.value === "") {
148
+ return NaN
149
+ }
150
+ return Number(this.value)
151
+ }
152
+
143
153
  /**
144
154
  * Method for handling the click event of the wrapper element.
145
155
  * Redirect every click on the wrapper to the input element.
@@ -306,10 +316,7 @@ export class LeuInput extends LitElement {
306
316
 
307
317
  /**
308
318
  * Creates an error list with an item for the given validity state.
309
- * @param {ValidityState} validityState
310
- * @param {Object} validationMessages
311
- * @param {String} idRef
312
- * @returns
319
+ * @returns {import("lit").TemplateResult | nothing}
313
320
  */
314
321
  renderErrorMessages() {
315
322
  if (!this.isInvalid()) {
@@ -341,11 +348,13 @@ export class LeuInput extends LitElement {
341
348
  * This can be either an icon, a clear button or an error indicator icon.
342
349
  *
343
350
  * @private
344
- * @returns {TemplateResult}
351
+ * @returns {import("lit").TemplateResult | nothing}
345
352
  */
346
353
  renderAfterContent() {
347
354
  if (this.isInvalid()) {
348
- return html`<div class="error-icon">${Icon("caution")}</div>`
355
+ return html`<div class="error-icon">
356
+ <leu-icon name="caution"></leu-icon>
357
+ </div>`
349
358
  }
350
359
 
351
360
  if (this.clearable && this.value) {
@@ -355,12 +364,14 @@ export class LeuInput extends LitElement {
355
364
  aria-label="Eingabefeld zurücksetzen"
356
365
  ?disabled=${this.disabled}
357
366
  >
358
- ${this._clearIcon}
367
+ <leu-icon name="clear"></leu-icon>
359
368
  </button>`
360
369
  }
361
370
 
362
371
  if (this.icon) {
363
- return html`<div class="icon">${Icon(this.icon)}</div>`
372
+ return html`<div class="icon">
373
+ <leu-icon name=${this.icon}></leu-icon>
374
+ </div>`
364
375
  }
365
376
 
366
377
  return nothing
@@ -154,7 +154,9 @@
154
154
  }
155
155
 
156
156
  /* is size small AND has no focus AND is empty */
157
- :host(:not(:focus-within)[size="small"]) .input-wrapper--empty .label {
157
+ :host(:not(:focus-within)[size="small"])
158
+ .input-wrapper--empty:not(.input-wrapper--invalid)
159
+ .label {
158
160
  top: calc(0.75rem - var(--input-border-width));
159
161
  opacity: 1;
160
162
  visibility: visible;
@@ -229,6 +231,6 @@
229
231
  color: var(--input-color-invalid);
230
232
  }
231
233
 
232
- :is(.icon, .error-icon) svg {
234
+ :is(.icon, .error-icon) leu-icon {
233
235
  display: block;
234
236
  }
@@ -1,6 +1,5 @@
1
- import { defineElement } from "../../lib/defineElement.js"
2
1
  import { LeuInput } from "./Input.js"
3
2
 
4
3
  export { LeuInput }
5
4
 
6
- defineElement("input", LeuInput)
5
+ LeuInput.define("leu-input")
@@ -3,8 +3,8 @@ import { ifDefined } from "lit/directives/if-defined.js"
3
3
 
4
4
  import "../leu-input.js"
5
5
 
6
- import { SIZE_TYPES } from "../Input.js"
7
- import { ICON_NAMES } from "../../icon/icon.js"
6
+ import { SIZES } from "../Input.js"
7
+ import { paths as iconPaths } from "../../icon/paths.js"
8
8
 
9
9
  export default {
10
10
  title: "Input",
@@ -15,9 +15,9 @@ export default {
15
15
  control: {
16
16
  type: "select",
17
17
  },
18
- options: Object.values(SIZE_TYPES),
18
+ options: Object.values(SIZES),
19
19
  },
20
- icon: { control: "select", options: ICON_NAMES },
20
+ icon: { control: "select", options: Object.keys(iconPaths) },
21
21
  },
22
22
  parameters: {
23
23
  design: {
@@ -165,7 +165,7 @@ export const Search = Template.bind({})
165
165
  Search.args = {
166
166
  label: "Suchen",
167
167
  clearable: true,
168
- size: SIZE_TYPES.SMALL,
168
+ size: SIZES.SMALL,
169
169
  icon: "search",
170
170
  novalidate: true,
171
171
  }
@@ -29,6 +29,8 @@ async function defaultFixture(args = {}) {
29
29
  ?novalidate=${args.novalidate}
30
30
  >
31
31
  </leu-input>
32
+ <!-- Firefox needs an other focusable element. Otherwise, sendKeys({press: "Tab"}) will have no effect -->
33
+ <div tabindex="0"></div>
32
34
  `)
33
35
  }
34
36
 
@@ -447,4 +449,24 @@ describe("LeuInput", () => {
447
449
  expect(getClearButton()).to.be.null
448
450
  expect(getIcon()).to.be.null
449
451
  })
452
+
453
+ it("returns the value as a number when it is possible", async () => {
454
+ const el = await defaultFixture({ label: "Länge", type: "number" })
455
+
456
+ expect(el.valueAsNumber).to.be.NaN
457
+
458
+ el.focus()
459
+
460
+ await sendKeys({ type: "123" })
461
+
462
+ expect(el.valueAsNumber).to.equal(123)
463
+
464
+ el.type = "text"
465
+ await elementUpdated(el)
466
+
467
+ el.focus()
468
+ await sendKeys({ type: "abc" })
469
+
470
+ expect(el.valueAsNumber).to.be.NaN
471
+ })
450
472
  })
@@ -1,12 +1,153 @@
1
- import { html, LitElement } from "lit"
1
+ import { html } from "lit"
2
+
3
+ import { LeuElement } from "../../lib/LeuElement.js"
4
+
5
+ import { LeuMenuItem } from "./MenuItem.js"
6
+
7
+ // @ts-ignore
2
8
  import styles from "./menu.css"
3
9
 
10
+ /**
11
+ * @typedef {'single' | 'multiple' | 'none'} SelectsType
12
+ */
13
+
4
14
  /**
5
15
  * @tagname leu-menu
16
+ * @property {SelectsType} selects - This has only an effect when the role is 'menu'. It defines which role the menu items will get. Default is 'none'.
6
17
  */
7
- export class LeuMenu extends LitElement {
18
+ export class LeuMenu extends LeuElement {
8
19
  static styles = styles
9
20
 
21
+ static shadowRootOptions = {
22
+ ...LeuElement.shadowRootOptions,
23
+ delegatesFocus: true,
24
+ }
25
+
26
+ static properties = {
27
+ selects: { type: String, reflect: true },
28
+ }
29
+
30
+ constructor() {
31
+ super()
32
+
33
+ /** @type {SelectsType} */
34
+ this.selects = "none"
35
+
36
+ this.value = undefined
37
+ }
38
+
39
+ connectedCallback() {
40
+ super.connectedCallback()
41
+
42
+ if (!this.getAttribute("role")) {
43
+ this.setAttribute("role", "menu")
44
+ }
45
+
46
+ this.addEventListener("keydown", this._handleKeyDown)
47
+ }
48
+
49
+ disconnectedCallback() {
50
+ super.disconnectedCallback()
51
+ this.removeEventListener("keydown", this._handleKeyDown)
52
+ }
53
+
54
+ _handleSlotChange() {
55
+ this.setCurrentItem(0)
56
+ this._setMenuItemRoles()
57
+ }
58
+
59
+ _setMenuItemRoles() {
60
+ const menuRole = this.getAttribute("role")
61
+ let menuItemRole
62
+
63
+ if (menuRole === "menu") {
64
+ if (this.selects === "multiple") {
65
+ menuItemRole = "menuitemcheckbox"
66
+ } else if (this.selects === "single") {
67
+ menuItemRole = "menuitemradio"
68
+ } else {
69
+ menuItemRole = "menuitem"
70
+ }
71
+ } else if (menuRole === "listbox") {
72
+ menuItemRole = "option"
73
+ }
74
+
75
+ if (menuItemRole) {
76
+ this.getMenuItems().forEach((menuItem) => {
77
+ menuItem.componentRole = menuItemRole // eslint-disable-line no-param-reassign
78
+ })
79
+ }
80
+ }
81
+
82
+ /**
83
+ *
84
+ * @returns {import("./MenuItem").LeuMenuItem[]}
85
+ */
86
+ getMenuItems() {
87
+ const slot = this.shadowRoot.querySelector("slot")
88
+ return slot
89
+ .assignedElements({ flatten: true })
90
+ .filter((el) => el instanceof LeuMenuItem)
91
+ }
92
+
93
+ getVisibleMenuItems() {
94
+ return this.getMenuItems().filter((menuItem) => !menuItem.hidden)
95
+ }
96
+
97
+ _handleKeyDown(event) {
98
+ if (["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key)) {
99
+ event.preventDefault()
100
+
101
+ const menuItems = this.getVisibleMenuItems()
102
+ let index = menuItems.findIndex((menuItem) => menuItem.tabbable)
103
+
104
+ if (event.key === "ArrowDown") {
105
+ index += 1
106
+ } else if (event.key === "ArrowUp") {
107
+ index -= 1
108
+ } else if (event.key === "Home") {
109
+ index = 0
110
+ } else if (event.key === "End") {
111
+ index = menuItems.length - 1
112
+ }
113
+
114
+ this.focusItem(index)
115
+ }
116
+ }
117
+
118
+ setCurrentItem(index) {
119
+ const menuItems = this.getVisibleMenuItems()
120
+ let currentItem = null
121
+
122
+ const currentItemIndex = (index + menuItems.length) % menuItems.length
123
+
124
+ menuItems.forEach((menuItem, i) => {
125
+ if (i === currentItemIndex) {
126
+ currentItem = menuItem
127
+ menuItem.tabbable = true // eslint-disable-line no-param-reassign
128
+ } else {
129
+ menuItem.tabbable = false // eslint-disable-line no-param-reassign
130
+ }
131
+ })
132
+
133
+ return currentItem
134
+ }
135
+
136
+ focusItem(index) {
137
+ const currentItem = this.setCurrentItem(index)
138
+ currentItem.focus()
139
+ }
140
+
141
+ firstUpdated() {
142
+ this.setCurrentItem(0)
143
+ }
144
+
145
+ updated(changedProperties) {
146
+ if (changedProperties.has("selects")) {
147
+ this._setMenuItemRoles()
148
+ }
149
+ }
150
+
10
151
  render() {
11
152
  return html`<slot></slot>`
12
153
  }
@@ -1,44 +1,48 @@
1
- import { LitElement, nothing } from "lit"
2
- import { html, unsafeStatic } from "lit/static-html.js"
1
+ import { html } from "lit"
3
2
  import { ifDefined } from "lit/directives/if-defined.js"
4
3
 
4
+ import { LeuElement } from "../../lib/LeuElement.js"
5
+ import { LeuIcon } from "../icon/Icon.js"
6
+
7
+ // @ts-ignore
5
8
  import styles from "./menu-item.css"
6
9
 
7
- import { Icon, ICON_NAMES } from "../icon/icon.js"
10
+ /**
11
+ * @typedef {'menuitem' | 'menuitemcheckbox' | 'menuitemradio' | 'option' | 'none'} MenuItemRole
12
+ */
8
13
 
9
14
  /**
10
15
  * @tagname leu-menu-item
11
16
  * @slot - The label of the menu item
17
+ * @property {boolean} active - Defines if the item is selected or checked
18
+ * @property {boolean} disabled - Disables the underlying button or link
19
+ * @property {string} value - The value of the item. It must not contain commas. See `getValue()`
20
+ * @property {string} href - The href of the underlying link
21
+ * @property {boolean} tabbable - If the item should be focusable. Will be reflected as `tabindex` to the underlying button or link
22
+ * @property {MenuItemRole} componentRole - The role of the item. This will be reflected as `role` to the underlying button or link. Default is `'menuitem'.`
12
23
  */
13
- export class LeuMenuItem extends LitElement {
24
+ export class LeuMenuItem extends LeuElement {
25
+ static dependencies = {
26
+ "leu-icon": LeuIcon,
27
+ }
28
+
14
29
  static styles = styles
15
30
 
16
31
  /**
17
32
  * @internal
18
33
  */
19
34
  static shadowRootOptions = {
20
- ...LitElement.shadowRootOptions,
35
+ ...LeuElement.shadowRootOptions,
21
36
  delegatesFocus: true,
22
37
  }
23
38
 
24
39
  static properties = {
25
- /**
26
- * Can be either an icon name or a text
27
- * If no icon with this value is found, it will be displayed as text.
28
- * If the value is "EMPTY", an empty placeholder with the size of an icon will be displayed.
29
- */
30
- before: { type: String, reflect: true },
31
- /**
32
- * Can be either an icon name or a text
33
- * If no icon with this value is found, it will be displayed as text
34
- * If the value is "EMPTY", an empty placeholder with the size of an icon will be displayed.
35
- */
36
- after: { type: String, reflect: true },
37
40
  active: { type: Boolean, reflect: true },
38
- highlighted: { type: Boolean, reflect: true },
39
41
  disabled: { type: Boolean, reflect: true },
40
- label: { type: String, reflect: true },
42
+ tabbable: { type: Boolean, reflect: true },
41
43
  href: { type: String, reflect: true },
44
+ value: { type: String, reflect: true },
45
+ componentRole: { type: String, reflect: true },
42
46
  }
43
47
 
44
48
  constructor() {
@@ -46,57 +50,105 @@ export class LeuMenuItem extends LitElement {
46
50
 
47
51
  this.active = false
48
52
  this.disabled = false
53
+ this.value = undefined
54
+ this.href = undefined
55
+ this.tabbable = undefined
49
56
 
50
- /**
51
- * A programmatic way to highlight the menu item like it is hovered.
52
- * This is just a visual effect and does not change the active state.
53
- */
54
- this.highlighted = false
57
+ /** @type {MenuItemRole} */
58
+ this.componentRole = "menuitem"
55
59
  }
56
60
 
57
- static getIconOrText(name) {
58
- if (ICON_NAMES.includes(name)) {
59
- return Icon(name)
60
- }
61
+ connectedCallback() {
62
+ super.connectedCallback()
63
+ this.addEventListener("click", this._handleClick, true)
64
+ }
65
+
66
+ disconnectedCallback() {
67
+ super.disconnectedCallback()
68
+ this.removeEventListener("click", this._handleClick, true)
69
+ }
61
70
 
62
- if (name === "EMPTY") {
63
- return html`<div class="icon-placeholder"></div>`
71
+ _handleClick(event) {
72
+ if (this.disabled) {
73
+ event.stopPropagation()
74
+ event.preventDefault()
64
75
  }
76
+ }
65
77
 
66
- return name
78
+ /**
79
+ * Returns the value of the item. If `value` is not set, it will return the inner text
80
+ * @returns {string}
81
+ */
82
+ getValue() {
83
+ return this.value || this.innerText
67
84
  }
68
85
 
69
- renderBefore() {
70
- if (this.before) {
71
- const content = LeuMenuItem.getIconOrText(this.before)
72
- return html`<span class="before">${content}</span>`
86
+ _getAria() {
87
+ const commonAttributes = {
88
+ disabled: this.disabled,
89
+ }
90
+
91
+ if (this.href) {
92
+ return commonAttributes
73
93
  }
74
94
 
75
- return nothing
95
+ return {
96
+ ...commonAttributes,
97
+ checked:
98
+ this.componentRole === "menuitemcheckbox" ||
99
+ this.componentRole === "menuitemradio"
100
+ ? this.active
101
+ : undefined,
102
+ selected: this.componentRole === "option" ? this.active : undefined,
103
+ role: this.componentRole === "none" ? undefined : this.componentRole,
104
+ }
76
105
  }
77
106
 
78
- renderAfter() {
79
- if (this.after) {
80
- const content = LeuMenuItem.getIconOrText(this.after)
81
- return html`<span class="after">${content}</span>`
107
+ _getTabIndex() {
108
+ if (typeof this.tabbable === "boolean") {
109
+ return this.tabbable ? 0 : -1
82
110
  }
83
111
 
84
- return nothing
112
+ return undefined
113
+ }
114
+
115
+ _renderLink(content) {
116
+ const aria = this._getAria()
117
+
118
+ return html`<a
119
+ class="button"
120
+ href=${this.href}
121
+ aria-disabled=${ifDefined(aria.disabled)}
122
+ aria-checked=${ifDefined(aria.checked)}
123
+ aria-selected=${ifDefined(aria.selected)}
124
+ role=${ifDefined(aria.role)}
125
+ tabindex=${ifDefined(this._getTabIndex())}
126
+ >${content}</a
127
+ >`
85
128
  }
86
129
 
87
- getTagName() {
88
- return this.href ? "a" : "button"
130
+ _renderButton(content) {
131
+ const aria = this._getAria()
132
+
133
+ return html`<button
134
+ class="button"
135
+ aria-disabled=${ifDefined(aria.disabled)}
136
+ aria-checked=${ifDefined(aria.checked)}
137
+ aria-selected=${ifDefined(aria.selected)}
138
+ role=${ifDefined(aria.role)}
139
+ tabindex=${ifDefined(this._getTabIndex())}
140
+ >
141
+ ${content}
142
+ </button>`
89
143
  }
90
144
 
91
145
  render() {
92
- /* The eslint rules don't recognize html import from lit/static-html.js */
93
- /* eslint-disable lit/binding-positions, lit/no-invalid-html */
94
- return html`<${unsafeStatic(
95
- this.getTagName()
96
- )} class="button" href=${ifDefined(this.href)} ?disabled=${this.disabled}>
97
- ${this.renderBefore()}<span class="label">${this.label}</span
98
- >${this.renderAfter()}
99
- </${unsafeStatic(this.getTagName())}>`
100
- /* eslint-enable lit/binding-positions, lit/no-invalid-html */
146
+ const content = html`
147
+ <slot class="before" name="before"></slot>
148
+ <span class="label"><slot></slot></span>
149
+ <slot class="after" name="after"></slot>
150
+ `
151
+
152
+ return this.href ? this._renderLink(content) : this._renderButton(content)
101
153
  }
102
154
  }
@@ -1,6 +1,5 @@
1
- import { defineElement } from "../../lib/defineElement.js"
2
1
  import { LeuMenuItem } from "./MenuItem.js"
3
2
 
4
3
  export { LeuMenuItem }
5
4
 
6
- defineElement("menu-item", LeuMenuItem)
5
+ LeuMenuItem.define("leu-menu-item")
@@ -1,6 +1,5 @@
1
- import { defineElement } from "../../lib/defineElement.js"
2
1
  import { LeuMenu } from "./Menu.js"
3
2
 
4
3
  export { LeuMenu }
5
4
 
6
- defineElement("menu", LeuMenu)
5
+ LeuMenu.define("leu-menu")
@@ -6,9 +6,10 @@
6
6
  :host {
7
7
  --background: var(--leu-color-black-0);
8
8
  --background-hover: var(--leu-color-black-10);
9
- --background-active: var(--leu-color-func-cyan);
9
+ --background-active: var(--leu-color-accent-blue);
10
10
  --background-disabled: var(--leu-color-black-black-0);
11
11
  --color: var(--leu-color-black-transp-60);
12
+ --color-active: var(--leu-color-black-0);
12
13
  --color-disabled: var(--leu-color-black-transp-20);
13
14
  --font-regular: var(--leu-font-family-regular);
14
15
  --font-black: var(--leu-font-family-black);
@@ -29,6 +30,7 @@
29
30
 
30
31
  padding: 0.75rem;
31
32
 
33
+ font-family: inherit;
32
34
  font-size: 1rem;
33
35
  line-height: 1.5;
34
36
  text-align: left;
@@ -43,21 +45,26 @@
43
45
  }
44
46
 
45
47
  .button:hover,
46
- :host([highlighted]) .button {
48
+ .button:focus-visible {
47
49
  --background: var(--background-hover);
48
50
  }
49
51
 
52
+ /*
53
+ * These colors do not match with the design system (yet).
54
+ * But at least they are compliant with the WCAG AA contrast ratio.
55
+ */
50
56
  :host([active]) .button {
51
57
  --background: var(--background-active);
58
+ --color: var(--color-active);
52
59
  }
53
60
 
54
61
  :host([disabled]) .button {
55
62
  --background: var(--background-disabled);
56
63
  --color: var(--color-disabled);
57
- cursor: default;
64
+ cursor: not-allowed;
58
65
  }
59
66
 
60
- :is(.before, .after) svg {
67
+ :is(.before, .after) leu-icon {
61
68
  display: block;
62
69
  }
63
70
 
@@ -2,6 +2,12 @@ import { html } from "lit"
2
2
  import { ifDefined } from "lit/directives/if-defined.js"
3
3
 
4
4
  import "../leu-menu-item.js"
5
+ import "../../icon/leu-icon.js"
6
+ import { paths as iconPaths } from "../../icon/paths.js"
7
+
8
+ function isIcon(name) {
9
+ return name === "EMPTY" || Object.keys(iconPaths).includes(name)
10
+ }
5
11
 
6
12
  export default {
7
13
  title: "Menu/Item",
@@ -20,13 +26,18 @@ export default {
20
26
  function Template(args) {
21
27
  return html`
22
28
  <leu-menu-item
23
- label=${args.label}
24
- before=${ifDefined(args.before)}
25
- after=${ifDefined(args.after)}
26
29
  href=${ifDefined(args.href)}
27
30
  ?active=${args.active}
28
31
  ?disabled=${args.disabled}
29
- ></leu-menu-item>
32
+ >
33
+ ${isIcon(args.before)
34
+ ? html`<leu-icon slot="before" name=${args.before}></leu-icon>`
35
+ : html`<span slot="before">${args.before}</span>`}
36
+ ${args.label}
37
+ ${isIcon(args.after)
38
+ ? html`<leu-icon slot="after" name=${args.after}></leu-icon>`
39
+ : html`<span slot="after">${args.after}</span>`}
40
+ </leu-menu-item>
30
41
  `
31
42
  }
32
43