@statistikzh/leu 0.5.1 → 0.6.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 +40 -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 -26
  10. package/dist/{Button-5326c982.d.ts → Button-9692e403.d.ts} +10 -11
  11. package/dist/Button-9692e403.d.ts.map +1 -0
  12. package/dist/{Button-5326c982.js → Button-9692e403.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 -19
  21. package/dist/CheckboxGroup.d.ts +2 -2
  22. package/dist/CheckboxGroup.d.ts.map +1 -1
  23. package/dist/CheckboxGroup.js +4 -6
  24. package/dist/Chip.d.ts +2 -2
  25. package/dist/Chip.d.ts.map +1 -1
  26. package/dist/Chip.js +6 -13
  27. package/dist/ChipGroup.d.ts +9 -7
  28. package/dist/ChipGroup.d.ts.map +1 -1
  29. package/dist/ChipGroup.js +8 -5
  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 +9 -1
  37. package/dist/ChipSelectable.d.ts.map +1 -1
  38. package/dist/ChipSelectable.js +12 -16
  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 +20 -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 +7 -16
  46. package/dist/Input.d.ts.map +1 -1
  47. package/dist/Input.js +24 -28
  48. package/dist/LeuElement-6de6f209.d.ts +7 -0
  49. package/dist/LeuElement-6de6f209.d.ts.map +1 -0
  50. package/dist/LeuElement-6de6f209.js +43 -0
  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 -19
  63. package/dist/Radio.d.ts +4 -2
  64. package/dist/Radio.d.ts.map +1 -1
  65. package/dist/Radio.js +9 -16
  66. package/dist/RadioGroup.d.ts +2 -2
  67. package/dist/RadioGroup.d.ts.map +1 -1
  68. package/dist/RadioGroup.js +4 -6
  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 -183
  75. package/dist/Table.d.ts +2 -6
  76. package/dist/Table.d.ts.map +1 -1
  77. package/dist/Table.js +17 -18
  78. package/dist/VisuallyHidden.d.ts +2 -2
  79. package/dist/VisuallyHidden.d.ts.map +1 -1
  80. package/dist/VisuallyHidden.js +5 -7
  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 +116 -79
  133. package/dist/vue/index.d.ts +80 -76
  134. package/dist/web-types.json +405 -270
  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 +10 -4
  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 +11 -17
  162. package/src/components/chip/chip.css +3 -4
  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-link.stories.js +3 -5
  168. package/src/components/chip/stories/chip-removable.stories.js +3 -4
  169. package/src/components/chip/stories/chip-selectable.stories.js +2 -2
  170. package/src/components/chip/test/chip-group.test.js +15 -30
  171. package/src/components/chip/test/chip-link.test.js +2 -6
  172. package/src/components/chip/test/chip-removable.test.js +4 -10
  173. package/src/components/chip/test/chip-selectable.test.js +3 -5
  174. package/src/components/dropdown/Dropdown.js +79 -26
  175. package/src/components/dropdown/leu-dropdown.js +1 -2
  176. package/src/components/dropdown/stories/dropdown.stories.js +30 -7
  177. package/src/components/dropdown/test/dropdown.test.js +5 -5
  178. package/src/components/icon/Icon.js +55 -0
  179. package/src/components/icon/icon.css +6 -0
  180. package/src/components/icon/leu-icon.js +5 -0
  181. package/src/components/icon/{icon.js → paths.js} +4 -37
  182. package/src/components/icon/stories/icon.stories.js +47 -0
  183. package/src/components/icon/test/icon.test.js +23 -40
  184. package/src/components/input/Input.js +21 -23
  185. package/src/components/input/input.css +4 -2
  186. package/src/components/input/leu-input.js +1 -2
  187. package/src/components/input/stories/input.stories.js +2 -2
  188. package/src/components/input/test/input.test.js +2 -0
  189. package/src/components/menu/Menu.js +143 -2
  190. package/src/components/menu/MenuItem.js +104 -52
  191. package/src/components/menu/leu-menu-item.js +1 -2
  192. package/src/components/menu/leu-menu.js +1 -2
  193. package/src/components/menu/menu-item.css +11 -4
  194. package/src/components/menu/stories/menu-item.stories.js +15 -4
  195. package/src/components/menu/stories/menu.stories.js +34 -7
  196. package/src/components/menu/test/menu-item.test.js +88 -82
  197. package/src/components/menu/test/menu.test.js +101 -8
  198. package/src/components/pagination/Pagination.js +27 -18
  199. package/src/components/pagination/leu-pagination.js +1 -2
  200. package/src/components/popup/Popup.js +39 -16
  201. package/src/components/popup/leu-popup.js +1 -2
  202. package/src/components/popup/popup.css +1 -0
  203. package/src/components/radio/Radio.js +12 -7
  204. package/src/components/radio/RadioGroup.js +6 -3
  205. package/src/components/radio/leu-radio-group.js +1 -2
  206. package/src/components/radio/leu-radio.js +1 -2
  207. package/src/components/radio/stories/radio-group.stories.js +5 -19
  208. package/src/components/radio/stories/radio.stories.js +2 -7
  209. package/src/components/radio/test/radio-group.test.js +6 -9
  210. package/src/components/radio/test/radio.test.js +3 -13
  211. package/src/components/scroll-top/ScrollTop.js +15 -5
  212. package/src/components/scroll-top/leu-scroll-top.js +1 -2
  213. package/src/components/select/Select.js +279 -175
  214. package/src/components/select/leu-select.js +1 -2
  215. package/src/components/select/select.css +20 -12
  216. package/src/components/select/stories/select.stories.js +16 -2
  217. package/src/components/select/test/select.test.js +191 -37
  218. package/src/components/table/Table.js +15 -9
  219. package/src/components/table/leu-table.js +1 -2
  220. package/src/components/table/table.css +3 -1
  221. package/src/components/visually-hidden/VisuallyHidden.js +6 -2
  222. package/src/components/visually-hidden/leu-visually-hidden.js +1 -2
  223. package/src/lib/LeuElement.js +23 -0
  224. package/src/lib/a11y.js +26 -0
  225. package/src/styles/custom-properties.css +2 -0
  226. package/web-test-runner.config.mjs +2 -0
  227. package/dist/Button-5326c982.d.ts.map +0 -1
  228. package/dist/_rollupPluginBabelHelpers-20f659f4.d.ts +0 -3
  229. package/dist/_rollupPluginBabelHelpers-20f659f4.d.ts.map +0 -1
  230. package/dist/_rollupPluginBabelHelpers-20f659f4.js +0 -30
  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,17 @@
1
- import { html, LitElement, nothing } from "lit"
1
+ import { html, nothing } from "lit"
2
2
  import { classMap } from "lit/directives/class-map.js"
3
-
4
- import { map } from "lit/directives/map.js"
5
- import { ifDefined } from "lit/directives/if-defined.js"
6
3
  import { createRef, ref } from "lit/directives/ref.js"
7
4
 
8
- import { Icon } from "../icon/icon.js"
5
+ import { ifDefined } from "lit/directives/if-defined.js"
6
+ import { LeuElement } from "../../lib/LeuElement.js"
9
7
  import { HasSlotController } from "../../lib/hasSlotController.js"
10
- import "../button/leu-button.js"
11
- import "../menu/leu-menu.js"
12
- import "../menu/leu-menu-item.js"
13
- import "../input/leu-input.js"
14
- import "../popup/leu-popup.js"
8
+
9
+ import { LeuButton } from "../button/Button.js"
10
+ import { LeuMenu } from "../menu/Menu.js"
11
+ import { LeuMenuItem } from "../menu/MenuItem.js"
12
+ import { LeuIcon } from "../icon/Icon.js"
13
+ import { LeuInput } from "../input/Input.js"
14
+ import { LeuPopup } from "../popup/Popup.js"
15
15
 
16
16
  // @ts-ignore
17
17
  import styles from "./select.css"
@@ -20,21 +20,51 @@ import styles from "./select.css"
20
20
  * @tagname leu-select
21
21
  * @slot before - Optional content the appears before the option list
22
22
  * @slot after - Optional content the appears after the option list
23
+ * @property {string} name - Reflects to the name attribute of the hidden input field that would be used in a form
24
+ * @property {boolean} open - The expanded state of the popup
25
+ * @property {string} label - The label of the select
26
+ * @property {array} value - List of selected values. If they're set from outside the component, the select element tries to find all the options with the given values and selects them.
27
+ * @property {boolean} clearable - Show a clearable button to reset the value
28
+ * @property {boolean} disabled - If the select should be disabled
29
+ * @property {boolean} filterable - Show an input field to filter the options inside the popup
30
+ * @property {boolean} multiple - Allow multiple selections
31
+ * @attribute {string} value - The selected values separated by commas.
23
32
  */
24
- export class LeuSelect extends LitElement {
33
+ export class LeuSelect extends LeuElement {
34
+ static dependencies = {
35
+ "leu-button": LeuButton,
36
+ "leu-menu": LeuMenu,
37
+ "leu-menu-item": LeuMenuItem,
38
+ "leu-icon": LeuIcon,
39
+ "leu-input": LeuInput,
40
+ "leu-popup": LeuPopup,
41
+ }
42
+
25
43
  static styles = styles
26
44
 
27
45
  static get properties() {
28
46
  return {
47
+ name: { type: String, reflect: true },
29
48
  open: { type: Boolean, reflect: true },
30
49
  label: { type: String, reflect: true },
31
- options: { type: Array },
32
- value: { type: Array },
50
+ value: {
51
+ type: Array,
52
+ converter: {
53
+ fromAttribute(value) {
54
+ if (value) {
55
+ return value.split(",").map((v) => v.trim())
56
+ }
57
+ return value
58
+ },
59
+ },
60
+ },
33
61
  clearable: { type: Boolean, reflect: true },
34
62
  disabled: { type: Boolean, reflect: true },
35
63
  filterable: { type: Boolean, reflect: true },
36
64
  multiple: { type: Boolean, reflect: true },
37
- optionFilter: { state: true },
65
+ _optionFilter: { state: true },
66
+ _hasFilterResults: { state: true },
67
+ _displayValue: { state: true },
38
68
  }
39
69
  }
40
70
 
@@ -59,54 +89,111 @@ export class LeuSelect extends LitElement {
59
89
  this.clearable = false
60
90
  this.filterable = false
61
91
  this.value = []
62
- this.options = []
63
92
  this.label = ""
93
+ this.name = ""
64
94
 
65
95
  /** @internal */
66
- this._arrowIcon = Icon("angleDropDown")
96
+ this._optionFilter = ""
67
97
 
68
98
  /** @internal */
69
- this._clearIcon = Icon("clear")
99
+ this._hasFilterResults = true
70
100
 
71
101
  /** @internal */
72
- this.optionFilter = ""
102
+ this._deferedChangeEvent = false
73
103
 
74
104
  /** @internal */
75
- this.deferedChangeEvent = false
105
+ this._displayValue = ""
76
106
 
77
- /**
78
- * @type {import("lit/directives/ref").Ref<import("../menu/Menu").LeuMenu>}
79
- */
80
- this.menuRef = createRef()
81
107
  /**
82
108
  * @type {import("lit/directives/ref").Ref<import("../input/Input").LeuInput>}
83
109
  */
84
- this.optionFilterRef = createRef()
110
+ this._optionFilterRef = createRef()
85
111
  /**
86
112
  * @type {import("lit/directives/ref").Ref<HTMLButtonElement>}
87
113
  */
88
- this.toggleButtonRef = createRef()
114
+ this._toggleButtonRef = createRef()
115
+
116
+ /**
117
+ * @type {import("lit/directives/ref").Ref<import("../menu/Menu").LeuMenu>}
118
+ */
119
+ this._menuRef = createRef()
89
120
  }
90
121
 
91
122
  connectedCallback() {
92
123
  super.connectedCallback()
93
- document.addEventListener("click", this.handleDocumentClick)
124
+ document.addEventListener("click", this._handleDocumentClick)
94
125
  }
95
126
 
96
127
  disconnectedCallback() {
97
128
  super.disconnectedCallback()
98
- document.removeEventListener("click", this.handleDocumentClick)
129
+ document.removeEventListener("click", this._handleDocumentClick)
99
130
  }
100
131
 
101
132
  updated(changedProperties) {
102
133
  if (changedProperties.has("open") && this.open) {
103
134
  if (this.filterable) {
104
- this.optionFilterRef.value.focus()
135
+ this._optionFilterRef.value.focus()
105
136
  } else {
106
- this.menuRef.value.focus()
137
+ this._menuRef.value.focusItem(0)
107
138
  }
108
139
  } else if (changedProperties.has("open") && !this.open) {
109
- this.toggleButtonRef.value.focus()
140
+ // TODO: Check if the ref is guaranteed to be set
141
+ // in the updated method.
142
+ // According to the lit documentation, a ref callback
143
+ // CAN be called with undefined.
144
+ this._toggleButtonRef.value?.focus()
145
+ }
146
+
147
+ if (
148
+ changedProperties.has("value") ||
149
+ changedProperties.has("_optionFilter")
150
+ ) {
151
+ this._updateMenuItems({
152
+ value: changedProperties.has("value"),
153
+ optionFilter: changedProperties.has("_optionFilter"),
154
+ })
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Apply the current state to the menu items.
160
+ * - Set the active property when the value property has changed.
161
+ * - Hide menu items that do not match the filter.
162
+ */
163
+ async _updateMenuItems(changed) {
164
+ /** @type {LeuMenu} */
165
+ const menu = this._menuRef.value
166
+
167
+ await menu.updateComplete
168
+
169
+ const menuItems = menu.getMenuItems()
170
+ let hasFilterResults = false
171
+
172
+ /* eslint-disable no-param-reassign */
173
+ menuItems.forEach((menuItem) => {
174
+ if (changed.optionFilter) {
175
+ menuItem.hidden =
176
+ this._optionFilter !== "" &&
177
+ !menuItem.textContent
178
+ .toLowerCase()
179
+ .includes(this._optionFilter.toLowerCase())
180
+
181
+ hasFilterResults = hasFilterResults || !menuItem.hidden
182
+ }
183
+
184
+ if (changed.value) {
185
+ menuItem.active = this._isSelected(menuItem.getValue())
186
+
187
+ if (!this.multiple && menuItem.active) {
188
+ this._displayValue = menuItem.textContent
189
+ }
190
+ }
191
+ })
192
+ /* eslint-enable no-param-reassign */
193
+
194
+ if (changed.optionFilter) {
195
+ this._hasFilterResults = hasFilterResults
196
+ menu.setCurrentItem(0)
110
197
  }
111
198
  }
112
199
 
@@ -115,49 +202,72 @@ export class LeuSelect extends LitElement {
115
202
  * @internal
116
203
  * @param {MouseEvent} event
117
204
  */
118
- handleDocumentClick = (event) => {
205
+ _handleDocumentClick = (event) => {
119
206
  if (
120
207
  event.target instanceof Node &&
121
208
  !this.contains(event.target) &&
122
209
  this.open
123
210
  ) {
124
- this.closeDropdown()
211
+ this._closeDropdown()
125
212
  }
126
213
  }
127
214
 
128
215
  /**
129
216
  * @internal
130
- * @param {KeyboardEvent} e
217
+ * @param {KeyboardEvent} event
131
218
  */
132
- handleKeyDown = (event) => {
219
+ _handleKeyDown = (event) => {
133
220
  if (event.key === "Escape") {
134
- this.closeDropdown()
221
+ this._closeDropdown()
135
222
  }
136
223
  }
137
224
 
138
- getDisplayValue(value) {
139
- if (this.multiple) {
140
- return value.length === 0 ? `` : `${value.length} gewählt`
141
- }
225
+ /**
226
+ * @internal
227
+ * @param {KeyboardEvent} event
228
+ */
229
+ async _handleToggleKeyDown(event) {
230
+ if (["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key)) {
231
+ event.preventDefault()
232
+
233
+ const menu = this._menuRef.value
234
+
235
+ this.open = true
236
+ await this.updateComplete
142
237
 
143
- return LeuSelect.getOptionLabel(value[0])
238
+ if (event.key === "ArrowDown" || event.key === "Home") {
239
+ menu.focusItem(0)
240
+ } else if (event.key === "ArrowUp" || event.key === "End") {
241
+ menu.focusItem(-1)
242
+ }
243
+ }
144
244
  }
145
245
 
146
- getFilteredOptions() {
147
- return this.filterable && this.optionFilter.length > 0
148
- ? this.options.filter((option) => {
149
- const label = LeuSelect.getOptionLabel(option)
150
- return label.toLowerCase().includes(this.optionFilter.toLowerCase())
151
- })
152
- : this.options
246
+ /**
247
+ * @internal
248
+ * @param {KeyboardEvent} event
249
+ */
250
+ _handleFilterInputKeyDown(event) {
251
+ if (event.key === "ArrowDown") {
252
+ this._menuRef.value.focusItem(0)
253
+ } else if (event.key === "ArrowUp") {
254
+ this._menuRef.value.focusItem(-1)
255
+ }
153
256
  }
154
257
 
155
- emitUpdateEvents() {
156
- this.emitInputEvent()
157
- this.emitChangeEvent()
258
+ /**
259
+ * Determines the value or label that should be displayed inside the toggle button.
260
+ * @returns {String | nothing}
261
+ */
262
+ _getDisplayValue() {
263
+ if (this.multiple) {
264
+ return this.value.length === 0 ? `` : `${this.value.length} gewählt`
265
+ }
266
+
267
+ return this._displayValue ?? nothing
158
268
  }
159
269
 
160
- emitInputEvent() {
270
+ _emitInputEvent() {
161
271
  const inputevent = new CustomEvent("input", {
162
272
  composed: true,
163
273
  bubbles: true,
@@ -165,7 +275,7 @@ export class LeuSelect extends LitElement {
165
275
  this.dispatchEvent(inputevent)
166
276
  }
167
277
 
168
- emitChangeEvent() {
278
+ _emitChangeEvent() {
169
279
  const changeevent = new CustomEvent("change", {
170
280
  composed: true,
171
281
  bubbles: true,
@@ -173,126 +283,94 @@ export class LeuSelect extends LitElement {
173
283
  this.dispatchEvent(changeevent)
174
284
  }
175
285
 
176
- clearValue(event) {
286
+ _clearValue(event) {
177
287
  if (!this.disabled) {
178
288
  event.stopPropagation()
179
289
  this.value = []
180
290
  }
181
291
 
182
- this.emitUpdateEvents()
292
+ this._emitInputEvent()
293
+ this._emitChangeEvent()
183
294
  }
184
295
 
185
- toggleDropdown() {
296
+ _toggleDropdown() {
186
297
  if (!this.disabled) {
187
298
  this.open = !this.open
188
299
  }
189
300
  }
190
301
 
191
- openDropdown() {
192
- this.open = true
193
- }
194
-
195
- closeDropdown() {
302
+ _closeDropdown() {
196
303
  this.open = false
197
304
 
198
- if (this.deferedChangeEvent) {
199
- this.emitChangeEvent()
200
- this.deferedChangeEvent = false
305
+ if (this._deferedChangeEvent) {
306
+ this._emitChangeEvent()
307
+ this._deferedChangeEvent = false
201
308
  }
202
309
  }
203
310
 
311
+ _handleFilterInput(event) {
312
+ this._optionFilter = event.target.value
313
+ }
314
+
204
315
  /**
205
- * Adds or replaces the given option in the options array.
206
- *
207
- * @param {*} option
316
+ * Checks if the given value is selected.
317
+ * @param {String} menuItemValue
318
+ * @returns {Boolean}
208
319
  */
209
- selectOption(option) {
210
- const isSelected = this.isSelected(option)
320
+ _isSelected(menuItemValue) {
321
+ return this.value.includes(menuItemValue)
322
+ }
323
+
324
+ _handleMenuItemClick(event) {
325
+ if (!(event.target instanceof LeuMenuItem) || event.target.disabled) {
326
+ return
327
+ }
328
+
329
+ /** @type {LeuMenuItem} */
330
+ const menuItem = event.target
331
+
332
+ const value = menuItem.getValue()
333
+ const isSelected = this._isSelected(value)
211
334
 
212
335
  if (this.multiple) {
213
336
  this.value = isSelected
214
- ? this.value.filter((v) => v !== option)
215
- : this.value.concat(option)
337
+ ? this.value.filter((v) => v !== value)
338
+ : this.value.concat(value)
216
339
 
217
- this.deferedChangeEvent = true
340
+ this._deferedChangeEvent = true
218
341
  } else {
219
- this.value = isSelected ? [] : [option]
342
+ this.value = isSelected ? [] : [value]
343
+ this._displayValue = isSelected ? "" : menuItem.textContent
220
344
  }
221
345
 
222
- this.emitInputEvent()
346
+ this._emitInputEvent()
223
347
 
224
348
  if (!this.multiple) {
225
- this.closeDropdown()
349
+ this._closeDropdown()
226
350
  }
227
351
  }
228
352
 
229
- handleApplyClick() {
230
- this.closeDropdown()
231
- }
232
-
233
- handleFilterInput(event) {
234
- this.optionFilter = event.target.value
235
- }
236
-
237
- isSelected(option) {
238
- return this.value.includes(option)
239
- }
240
-
241
- renderMenu() {
242
- const menuClasses = {
243
- "select-menu": true,
244
- multiple: this.multiple,
353
+ /**
354
+ * Close the dropdown if the focus moves outside the component.
355
+ */
356
+ _handlePopupFocusOut(event) {
357
+ if (
358
+ !this.contains(event.relatedTarget) &&
359
+ !this.shadowRoot.contains(event.relatedTarget)
360
+ ) {
361
+ this._closeDropdown()
245
362
  }
246
-
247
- const filteredOptions = this.getFilteredOptions()
248
-
249
- return html`
250
- <leu-menu
251
- role="listbox"
252
- class=${classMap(menuClasses)}
253
- aria-multiselectable="${this.multiple}"
254
- aria-labelledby="select-label"
255
- ref=${ref(this.menuRef)}
256
- >
257
- ${filteredOptions.length > 0
258
- ? map(this.getFilteredOptions(), (option) => {
259
- const isSelected = this.isSelected(option)
260
- let beforeIcon
261
-
262
- if (this.multiple && isSelected) {
263
- beforeIcon = "check"
264
- } else if (this.multiple) {
265
- beforeIcon = "EMPTY"
266
- }
267
-
268
- return html`<leu-menu-item
269
- before=${ifDefined(beforeIcon)}
270
- @click=${() => this.selectOption(option)}
271
- role="option"
272
- label=${LeuSelect.getOptionLabel(option)}
273
- ?active=${isSelected}
274
- aria-selected=${isSelected}
275
- >
276
- </leu-menu-item>`
277
- })
278
- : html`<leu-menu-item
279
- label=${this.optionFilter === ""
280
- ? "Keine Optionen"
281
- : "Keine Resultate"}
282
- disabled
283
- ></leu-menu-item>`}
284
- </leu-menu>
285
- `
286
363
  }
287
364
 
288
- renderFilterInput() {
365
+ _renderFilterInput() {
289
366
  if (this.filterable) {
290
367
  return html` <leu-input
291
368
  class="select-search"
292
369
  size="small"
293
- @input=${this.handleFilterInput}
370
+ @input=${this._handleFilterInput}
371
+ @keydown=${this._handleFilterInputKeyDown}
294
372
  clearable
295
- ref=${ref(this.optionFilterRef)}
373
+ ref=${ref(this._optionFilterRef)}
296
374
  label="Nach Stichwort filtern"
297
375
  ></leu-input>`
298
376
  }
@@ -300,23 +378,25 @@ export class LeuSelect extends LitElement {
300
378
  return nothing
301
379
  }
302
380
 
303
- renderApplyButton() {
381
+ _renderApplyButton() {
304
382
  if (this.multiple) {
305
383
  return html`
306
- <leu-button
307
- type="button"
308
- class="apply-button"
309
- @click=${this.handleApplyClick}
310
- fluid
311
- >Anwenden</leu-button
312
- >
384
+ <div class="apply-button-wrapper">
385
+ <leu-button
386
+ type="button"
387
+ class="apply-button"
388
+ @click=${this._closeDropdown}
389
+ fluid
390
+ >Anwenden</leu-button
391
+ >
392
+ </div>
313
393
  `
314
394
  }
315
395
 
316
396
  return nothing
317
397
  }
318
398
 
319
- renderToggleButton() {
399
+ _renderToggleButton() {
320
400
  const toggleClasses = {
321
401
  "select-toggle": true,
322
402
  open: this.open,
@@ -325,28 +405,32 @@ export class LeuSelect extends LitElement {
325
405
  }
326
406
 
327
407
  return html`<button
408
+ ${ref(this._toggleButtonRef)}
328
409
  type="button"
329
410
  class=${classMap(toggleClasses)}
330
- @click=${this.toggleDropdown}
331
- aria-controls="select-dialog"
332
- aria-haspopup="dialog"
411
+ @click=${this._toggleDropdown}
412
+ @keydown=${this._handleToggleKeyDown}
413
+ ?disabled=${this.disabled}
414
+ aria-controls="select-popup"
333
415
  aria-expanded="${this.open}"
416
+ aria-labelledby="select-label"
334
417
  role="combobox"
335
- ref=${ref(this.toggleButtonRef)}
336
418
  slot="anchor"
337
419
  >
338
420
  <span class="label" id="select-label">${this.label}</span>
339
- <span class="value"> ${this.getDisplayValue(this.value)} </span>
340
- <span class="arrow-icon"> ${this._arrowIcon} </span>
421
+ <span class="value"> ${this._getDisplayValue()} </span>
422
+ <span class="arrow-icon">
423
+ <leu-icon name="angleDropDown"></leu-icon>
424
+ </span>
341
425
  ${this.clearable && this.value.length !== 0
342
426
  ? html`<button
343
427
  type="button"
344
428
  class="clear-button"
345
- @click=${this.clearValue}
429
+ @click=${this._clearValue}
346
430
  aria-label=${`${this.label} zurücksetzen`}
347
431
  ?disabled=${this.disabled}
348
432
  >
349
- ${this._clearIcon}
433
+ <leu-icon name="clear"></leu-icon>
350
434
  </button>`
351
435
  : nothing}
352
436
  </button>`
@@ -359,33 +443,53 @@ export class LeuSelect extends LitElement {
359
443
  "select--has-after": this.hasSlotController.test("after"),
360
444
  }
361
445
 
446
+ /*
447
+ * We use the click event listener with the event delegation pattern
448
+ * so this is not a violation of the rule.
449
+ */
450
+ /* eslint-disable lit-a11y/click-events-have-key-events */
362
451
  return html`<div
363
- class=${classMap(selectClasses)}
364
- ?disabled=${this.disabled}
365
- aria-readonly="${this.disabled}"
366
- aria-labelledby="select-label"
367
- @keydown=${this.handleKeyDown}
368
- >
369
- <leu-popup
370
- ?active=${this.open}
371
- placement="bottom-start"
372
- flip
373
- matchSize="width"
374
- autoSize="height"
375
- autoSizePadding="8"
452
+ class=${classMap(selectClasses)}
453
+ @keydown=${this._handleKeyDown}
376
454
  >
377
- ${this.renderToggleButton()}
378
- <dialog
379
- id="select-dialog"
380
- class="select-menu-container"
381
- ?open=${this.open}
455
+ <leu-popup
456
+ ?active=${this.open}
457
+ placement="bottom-start"
458
+ flip
459
+ matchSize="width"
460
+ autoSize="height"
461
+ autoSizePadding="8"
382
462
  >
383
- <slot name="before" class="before"></slot>
384
- ${this.renderFilterInput()} ${this.renderMenu()}
385
- ${this.renderApplyButton()}
386
- <slot name="after" class="after"></slot>
387
- </dialog>
388
- </leu-popup>
389
- </div> `
463
+ ${this._renderToggleButton()}
464
+ <div
465
+ id="select-popup"
466
+ class="select-menu-container"
467
+ @focusout=${this._handlePopupFocusOut}
468
+ >
469
+ <slot name="before" class="before"></slot>
470
+ ${this._renderFilterInput()}
471
+ <leu-menu
472
+ ref=${ref(this._menuRef)}
473
+ role="listbox"
474
+ aria-multiselectable=${ifDefined(
475
+ this.multiple ? "true" : undefined
476
+ )}
477
+ class="menu"
478
+ @click=${this._handleMenuItemClick}
479
+ >
480
+ <slot></slot>
481
+ </leu-menu>
482
+ ${this._hasFilterResults
483
+ ? nothing
484
+ : html` <p class="filter-message-empty" aria-live="polite">
485
+ Keine Resultate
486
+ </p>`}
487
+ ${this._renderApplyButton()}
488
+ <slot name="after" class="after"></slot>
489
+ </div>
490
+ </leu-popup>
491
+ </div>
492
+ <input type="hidden" name=${this.name} .value=${this.value.join(",")} />`
493
+ /* eslint-enable lit-a11y/click-events-have-key-events */
390
494
  }
391
495
  }
@@ -1,6 +1,5 @@
1
- import { defineElement } from "../../lib/defineElement.js"
2
1
  import { LeuSelect } from "./Select.js"
3
2
 
4
3
  export { LeuSelect }
5
4
 
6
- defineElement("select", LeuSelect)
5
+ LeuSelect.define("leu-select")