@statistikzh/leu 0.26.0 → 0.28.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 (209) hide show
  1. package/.release-please-manifest.json +1 -1
  2. package/.storybook/main.ts +1 -1
  3. package/.storybook/preview.ts +6 -4
  4. package/.storybook/static/global.css +5 -0
  5. package/CHANGELOG.md +44 -0
  6. package/dist/{Accordion-B04QkmHz.js → Accordion-D9kLsiBW.js} +1 -1
  7. package/dist/Accordion.d.ts +1 -1
  8. package/dist/Accordion.js +2 -2
  9. package/dist/{Button-BgNUxmo_.d.ts → Button-DcuvEVkC.d.ts} +14 -12
  10. package/dist/{Button-BkhqVjug.js → Button-DyNVOHCd.js} +90 -82
  11. package/dist/Button.d.ts +1 -1
  12. package/dist/Button.js +4 -4
  13. package/dist/{ButtonGroup-B8U9fDvM.js → ButtonGroup-MEh4vb5a.js} +2 -2
  14. package/dist/ButtonGroup.d.ts +1 -1
  15. package/dist/ButtonGroup.js +5 -5
  16. package/dist/{ChartWrapper-CSMFwz9e.js → ChartWrapper-DAl91BIN.js} +2 -2
  17. package/dist/ChartWrapper.d.ts +3 -3
  18. package/dist/ChartWrapper.js +3 -3
  19. package/dist/{Checkbox-Dd1QLpfn.js → Checkbox-CGGyUW9U.js} +2 -2
  20. package/dist/Checkbox.d.ts +3 -3
  21. package/dist/Checkbox.js +3 -3
  22. package/dist/{CheckboxGroup-Bz2eWEFL.js → CheckboxGroup-DXt5iMdj.js} +2 -2
  23. package/dist/CheckboxGroup.d.ts +1 -1
  24. package/dist/CheckboxGroup.js +4 -4
  25. package/dist/{Chip-XAQIIsXq.js → Chip-BGs71WGH.js} +1 -1
  26. package/dist/{Chip-DLKM9P7v.d.ts → Chip-DVGjEPJE.d.ts} +1 -1
  27. package/dist/Chip.d.ts +1 -1
  28. package/dist/Chip.js +2 -2
  29. package/dist/{ChipGroup-Ta8Ht4jc.d.ts → ChipGroup-BK5BYM0X.d.ts} +2 -2
  30. package/dist/{ChipGroup-DLqfK2kn.js → ChipGroup-BcGyusP-.js} +1 -1
  31. package/dist/ChipGroup.d.ts +1 -1
  32. package/dist/ChipGroup.js +3 -3
  33. package/dist/{ChipLink-BAxyQO2M.d.ts → ChipLink-BdG2o-nk.d.ts} +1 -1
  34. package/dist/ChipLink.d.ts +1 -1
  35. package/dist/ChipLink.js +2 -2
  36. package/dist/{ChipRemovable-DBjwt0CH.d.ts → ChipRemovable-CCwSQTAL.d.ts} +2 -2
  37. package/dist/ChipRemovable.d.ts +1 -1
  38. package/dist/ChipRemovable.js +3 -3
  39. package/dist/{ChipSelectable-CMJNcE4U.d.ts → ChipSelectable-BQ3VLVi5.d.ts} +1 -1
  40. package/dist/ChipSelectable.d.ts +1 -1
  41. package/dist/ChipSelectable.js +2 -2
  42. package/dist/{Dialog-DHuXR_oo.js → Dialog-BzuyL1U3.js} +2 -2
  43. package/dist/Dialog.d.ts +3 -3
  44. package/dist/Dialog.js +3 -3
  45. package/dist/{Dropdown-DtFTePbc.js → Dropdown-plyBTM15.js} +5 -5
  46. package/dist/Dropdown.d.ts +6 -6
  47. package/dist/Dropdown.js +8 -8
  48. package/dist/{FileInput-b8sbLDPI.js → FileInput-BT3Fe-0J.js} +22 -7
  49. package/dist/FileInput.d.ts +16 -5
  50. package/dist/FileInput.js +6 -6
  51. package/dist/{Icon-Op80LrrO.d.ts → Icon-CUfh3eb3.d.ts} +1 -1
  52. package/dist/{Icon-C_yYuynf.js → Icon-D83qesg5.js} +1 -1
  53. package/dist/Icon.d.ts +1 -1
  54. package/dist/Icon.js +2 -2
  55. package/dist/{Input-DEOVocTa.js → Input-D7zS50oz.js} +32 -11
  56. package/dist/{Input-D2THgo7c.d.ts → Input-fEiQvGDF.d.ts} +9 -5
  57. package/dist/Input.d.ts +1 -1
  58. package/dist/Input.js +3 -3
  59. package/dist/{LeuElement-BeFrgKes.js → LeuElement-DQI8cqZV.js} +1 -1
  60. package/dist/{Menu-BeqqtCw6.js → Menu-DRU1LiMM.js} +2 -2
  61. package/dist/{Menu-CQdx1ef3.d.ts → Menu-Z2b7dsB5.d.ts} +2 -2
  62. package/dist/Menu.d.ts +1 -1
  63. package/dist/Menu.js +4 -4
  64. package/dist/{MenuItem-DVg8-1Bq.js → MenuItem-DCttylRO.js} +2 -2
  65. package/dist/{MenuItem-QcgnRk_7.d.ts → MenuItem-LY4TRIho.d.ts} +2 -2
  66. package/dist/MenuItem.d.ts +1 -1
  67. package/dist/MenuItem.js +3 -3
  68. package/dist/{Message-BhknWvAF.js → Message-0NxnKEqw.js} +2 -2
  69. package/dist/Message.d.ts +2 -2
  70. package/dist/Message.js +3 -3
  71. package/dist/{Pagination-CqkHh-Vd.d.ts → Pagination-9eZ8WMvR.d.ts} +4 -4
  72. package/dist/{Pagination-DJI5MIi_.js → Pagination-CIy7YvWE.js} +4 -4
  73. package/dist/Pagination.d.ts +1 -1
  74. package/dist/Pagination.js +6 -6
  75. package/dist/{Placeholder-BJybFwSg.js → Placeholder-Dol_X5Hp.js} +1 -1
  76. package/dist/Placeholder.d.ts +1 -1
  77. package/dist/Placeholder.js +2 -2
  78. package/dist/{Popup-Btgm2a3D.d.ts → Popup-B5iDSLIO.d.ts} +1 -1
  79. package/dist/{Popup-DNlm_9AA.js → Popup-nJrJHGSy.js} +1 -1
  80. package/dist/Popup.d.ts +1 -1
  81. package/dist/Popup.js +2 -2
  82. package/dist/{ProgressBar-B0wYj1KF.js → ProgressBar-Dmq9veqU.js} +1 -1
  83. package/dist/ProgressBar.d.ts +1 -1
  84. package/dist/ProgressBar.js +2 -2
  85. package/dist/{Radio-DMCL8c4D.js → Radio-W5ck_IJI.js} +1 -1
  86. package/dist/Radio.d.ts +1 -1
  87. package/dist/Radio.js +2 -2
  88. package/dist/{RadioGroup-CM6IyBlq.js → RadioGroup-De5x2YCO.js} +2 -2
  89. package/dist/RadioGroup.d.ts +1 -1
  90. package/dist/RadioGroup.js +3 -3
  91. package/dist/{Range-B72rtfln.js → Range-NCdfDkeD.js} +1 -1
  92. package/dist/Range.d.ts +1 -1
  93. package/dist/Range.js +2 -2
  94. package/dist/{ScrollTop-BFAqBVDR.js → ScrollTop-DwcNIKmN.js} +20 -20
  95. package/dist/ScrollTop.d.ts +8 -8
  96. package/dist/ScrollTop.js +5 -5
  97. package/dist/{Select-vxl3BvD4.js → Select-Bpicra9q.js} +153 -134
  98. package/dist/Select.d.ts +80 -78
  99. package/dist/Select.js +9 -9
  100. package/dist/{Spinner-DDTqijTO.js → Spinner-BBiVZxFH.js} +1 -1
  101. package/dist/{Spinner-CrM1enM0.d.ts → Spinner-DHYaX6-Y.d.ts} +1 -1
  102. package/dist/Spinner.d.ts +1 -1
  103. package/dist/Spinner.js +2 -2
  104. package/dist/Tab-CN97q0nj.d.ts +30 -0
  105. package/dist/Tab-Ce9nrDok.js +117 -0
  106. package/dist/Tab.d.ts +2 -0
  107. package/dist/Tab.js +3 -0
  108. package/dist/TabGroup-C-cd4Wcx.js +248 -0
  109. package/dist/TabGroup.d.ts +64 -0
  110. package/dist/TabGroup.js +5 -0
  111. package/dist/TabPanel-BW1ydVBT.js +65 -0
  112. package/dist/TabPanel-DQgWP7LO.d.ts +26 -0
  113. package/dist/TabPanel.d.ts +2 -0
  114. package/dist/TabPanel.js +3 -0
  115. package/dist/{Table-BgCxfBcm.js → Table-DiYqIzBu.js} +3 -3
  116. package/dist/Table.d.ts +3 -3
  117. package/dist/Table.js +7 -7
  118. package/dist/{Tag-DK2KkPIQ.js → Tag-Ct8Hhv7W.js} +1 -1
  119. package/dist/Tag.d.ts +1 -1
  120. package/dist/Tag.js +2 -2
  121. package/dist/{VisuallyHidden-OeQvhxYn.d.ts → VisuallyHidden-CB7aRJzF.d.ts} +1 -1
  122. package/dist/{VisuallyHidden-pll3amXE.js → VisuallyHidden-CpYXyuC7.js} +1 -1
  123. package/dist/VisuallyHidden.d.ts +1 -1
  124. package/dist/VisuallyHidden.js +2 -2
  125. package/dist/index.d.ts +14 -14
  126. package/dist/index.js +30 -30
  127. package/dist/leu-accordion.js +2 -2
  128. package/dist/leu-button-group.js +5 -5
  129. package/dist/leu-button.d.ts +1 -1
  130. package/dist/leu-button.js +4 -4
  131. package/dist/leu-chart-wrapper.js +3 -3
  132. package/dist/leu-checkbox-group.js +4 -4
  133. package/dist/leu-checkbox.js +3 -3
  134. package/dist/leu-chip-group.d.ts +1 -1
  135. package/dist/leu-chip-group.js +3 -3
  136. package/dist/leu-chip-link.d.ts +1 -1
  137. package/dist/leu-chip-link.js +2 -2
  138. package/dist/leu-chip-removable.d.ts +1 -1
  139. package/dist/leu-chip-removable.js +3 -3
  140. package/dist/leu-chip-selectable.d.ts +1 -1
  141. package/dist/leu-chip-selectable.js +2 -2
  142. package/dist/leu-dialog.js +3 -3
  143. package/dist/leu-dropdown.js +8 -8
  144. package/dist/leu-file-input.js +6 -6
  145. package/dist/leu-icon.d.ts +1 -1
  146. package/dist/leu-icon.js +2 -2
  147. package/dist/leu-input.d.ts +1 -1
  148. package/dist/leu-input.js +3 -3
  149. package/dist/leu-menu-item.d.ts +1 -1
  150. package/dist/leu-menu-item.js +3 -3
  151. package/dist/leu-menu.d.ts +1 -1
  152. package/dist/leu-menu.js +4 -4
  153. package/dist/leu-message.js +3 -3
  154. package/dist/leu-pagination.d.ts +1 -1
  155. package/dist/leu-pagination.js +6 -6
  156. package/dist/leu-placeholder.js +2 -2
  157. package/dist/leu-popup.d.ts +1 -1
  158. package/dist/leu-popup.js +2 -2
  159. package/dist/leu-progress-bar.js +2 -2
  160. package/dist/leu-radio-group.js +3 -3
  161. package/dist/leu-radio.js +2 -2
  162. package/dist/leu-range.js +2 -2
  163. package/dist/leu-scroll-top.js +5 -5
  164. package/dist/leu-select.js +9 -9
  165. package/dist/leu-spinner.d.ts +1 -1
  166. package/dist/leu-spinner.js +2 -2
  167. package/dist/leu-tab-group.d.ts +10 -0
  168. package/dist/leu-tab-group.js +8 -0
  169. package/dist/leu-tab-panel.d.ts +10 -0
  170. package/dist/leu-tab-panel.js +6 -0
  171. package/dist/leu-tab.d.ts +10 -0
  172. package/dist/leu-tab.js +6 -0
  173. package/dist/leu-table.js +7 -7
  174. package/dist/leu-tag.js +2 -2
  175. package/dist/leu-visually-hidden.d.ts +1 -1
  176. package/dist/leu-visually-hidden.js +2 -2
  177. package/dist/vscode.html-custom-data.json +90 -35
  178. package/dist/vue/index.d.ts +89 -29
  179. package/dist/web-types.json +182 -76
  180. package/package.json +1 -2
  181. package/src/components/button/Button.ts +45 -30
  182. package/src/components/button/button.css +55 -54
  183. package/src/components/button/stories/button.stories.ts +17 -20
  184. package/src/components/button/test/button.test.ts +46 -0
  185. package/src/components/file-input/FileInput.ts +24 -5
  186. package/src/components/input/Input.ts +43 -8
  187. package/src/components/input/test/input.test.ts +106 -1
  188. package/src/components/scroll-top/ScrollTop.ts +18 -16
  189. package/src/components/select/Select.ts +198 -125
  190. package/src/components/select/select.css +4 -0
  191. package/src/components/select/stories/select.stories.ts +10 -0
  192. package/src/components/select/test/select.test.ts +440 -35
  193. package/src/components/tab/Tab.ts +72 -0
  194. package/src/components/tab/TabGroup.ts +267 -0
  195. package/src/components/tab/TabPanel.ts +59 -0
  196. package/src/components/tab/leu-tab-group.ts +11 -0
  197. package/src/components/tab/leu-tab-panel.ts +11 -0
  198. package/src/components/tab/leu-tab.ts +11 -0
  199. package/src/components/tab/stories/tab.stories.ts +97 -0
  200. package/src/components/tab/tab-group.css +63 -0
  201. package/src/components/tab/tab-panel.css +10 -0
  202. package/src/components/tab/tab.css +54 -0
  203. package/src/components/tab/test/tab-group.test.ts +426 -0
  204. package/src/components/tab/test/tab-panel.test.ts +102 -0
  205. package/src/components/tab/test/tab.test.ts +139 -0
  206. package/tsconfig.json +1 -0
  207. /package/dist/{FormAssociatedMixin-Cc74LjbC.d.ts → FormAssociatedMixin-Cw7LsSUE.d.ts} +0 -0
  208. /package/dist/{LeuElement-pJFU18Xm.d.ts → LeuElement-DK1jntuu.d.ts} +0 -0
  209. /package/dist/{hasSlotController-DWPyZ52b.d.ts → hasSlotController-BjKyhJm-.d.ts} +0 -0
@@ -5,9 +5,10 @@ import { sendKeys } from "@web/test-runner-commands"
5
5
  import { spy } from "sinon"
6
6
 
7
7
  import "../leu-input.js"
8
+ import { LeuInput } from "../leu-input.js"
8
9
 
9
10
  async function defaultFixture(args = {}) {
10
- return fixture(html`
11
+ return fixture<LeuInput>(html`
11
12
  <leu-input
12
13
  value=${ifDefined(args.value)}
13
14
  error=${ifDefined(args.error)}
@@ -495,4 +496,108 @@ describe("LeuInput", () => {
495
496
  expect(error).to.be.null
496
497
  }
497
498
  })
499
+
500
+ it("returns the defaultValue when no value has been explicitly set", async () => {
501
+ const el = await defaultFixture({ value: "John" })
502
+
503
+ expect(el.defaultValue).to.equal("John")
504
+ expect(el.value).to.equal("John")
505
+ })
506
+
507
+ it("value property overrides the defaultValue", async () => {
508
+ const el = await defaultFixture({ value: "John" })
509
+
510
+ el.value = "Jane"
511
+ await elementUpdated(el)
512
+
513
+ expect(el.defaultValue).to.equal("John")
514
+ expect(el.value).to.equal("Jane")
515
+ })
516
+
517
+ it("resets to the defaultValue when the form is reset", async () => {
518
+ const form = await fixture<HTMLFormElement>(html`
519
+ <form>
520
+ <leu-input name="name" value="John" label="Name"></leu-input>
521
+ </form>
522
+ `)
523
+
524
+ const input = form.querySelector<LeuInput>("leu-input")
525
+ input.value = "Jane"
526
+ await elementUpdated(input)
527
+
528
+ expect(input.value).to.equal("Jane")
529
+
530
+ form.reset()
531
+ await elementUpdated(input)
532
+
533
+ expect(input.value).to.equal("John")
534
+ })
535
+
536
+ it("updates the form data when the defaultValue changes before any interaction", async () => {
537
+ const form = await fixture<HTMLFormElement>(html`
538
+ <form>
539
+ <leu-input name="name" value="John" label="Name"></leu-input>
540
+ </form>
541
+ `)
542
+
543
+ const input = form.querySelector<LeuInput>("leu-input")
544
+
545
+ let formData = new FormData(form)
546
+ expect(formData.get("name")).to.equal("John")
547
+
548
+ // Changing defaultValue before interaction should update the value
549
+ input.defaultValue = "Jane"
550
+ await elementUpdated(input)
551
+
552
+ formData = new FormData(form)
553
+ expect(formData.get("name")).to.equal("Jane")
554
+ })
555
+
556
+ it("does not update the value when the defaultValue changes after interaction", async () => {
557
+ const form = await fixture<HTMLFormElement>(html`
558
+ <form>
559
+ <leu-input name="name" label="Name"></leu-input>
560
+ <div tabindex="0"></div>
561
+ </form>
562
+ `)
563
+
564
+ const input = form.querySelector<LeuInput>("leu-input")
565
+ input.focus()
566
+ await sendKeys({ type: "John" })
567
+ await elementUpdated(input)
568
+
569
+ // User has interacted, changing defaultValue should NOT override the typed value
570
+ input.defaultValue = "Jane"
571
+ await elementUpdated(input)
572
+
573
+ expect(input.value).to.equal("John")
574
+
575
+ const formData = new FormData(form)
576
+ expect(formData.get("name")).to.equal("John")
577
+ })
578
+
579
+ it("updates the form data when the value or disabled state changes", async () => {
580
+ const form = await fixture<HTMLFormElement>(html`
581
+ <form>
582
+ <leu-input name="name" value="John" label="Name"></leu-input>
583
+ </form>
584
+ `)
585
+
586
+ const input = form.querySelector<LeuInput>("leu-input")
587
+
588
+ let formData = new FormData(form)
589
+ expect(formData.get("name")).to.equal("John")
590
+
591
+ input.value = "Jane"
592
+ await elementUpdated(input)
593
+
594
+ formData = new FormData(form)
595
+ expect(formData.get("name")).to.equal("Jane")
596
+
597
+ input.disabled = true
598
+ await elementUpdated(input)
599
+
600
+ formData = new FormData(form)
601
+ expect(formData.get("name")).to.be.null
602
+ })
498
603
  })
@@ -22,23 +22,23 @@ export class LeuScrollTop extends LeuElement {
22
22
  static styles = [LeuElement.styles, styles]
23
23
 
24
24
  @state()
25
- protected _showButton: boolean = false
25
+ protected showButton: boolean = false
26
26
 
27
- protected _prevYPos: number = 0
27
+ protected prevYPos: number = 0
28
28
 
29
- protected _scrollDown: boolean = false
29
+ protected hasScrolledDown: boolean = false
30
30
 
31
- protected _scrollListener: EventListener
31
+ protected scrollListener: EventListener
32
32
 
33
33
  scroll = () => {
34
- const delta = window.scrollY - this._prevYPos
34
+ const delta = window.scrollY - this.prevYPos
35
35
 
36
- if (this._scrollDown) {
36
+ if (this.hasScrolledDown) {
37
37
  if (delta < 0) {
38
- this._scrollDown = false
38
+ this.hasScrolledDown = false
39
39
  }
40
40
  } else if (delta > 0) {
41
- this._scrollDown = true
41
+ this.hasScrolledDown = true
42
42
  }
43
43
 
44
44
  /**
@@ -46,22 +46,24 @@ export class LeuScrollTop extends LeuElement {
46
46
  * ... the current scroll position is greater than the window height (below-the-fold) and when
47
47
  * ... scrolling up
48
48
  */
49
- this._showButton = window.scrollY > window.innerHeight && !this._scrollDown
50
- this._prevYPos = window.scrollY
49
+ this.showButton =
50
+ window.scrollY > window.innerHeight && !this.hasScrolledDown
51
+ this.prevYPos = window.scrollY
51
52
  }
52
53
 
53
54
  connectedCallback() {
54
55
  super.connectedCallback()
55
- this._scrollListener = throttle(this.scroll, 100)
56
- document.addEventListener("scroll", this._scrollListener, true)
56
+ this.scrollListener = throttle(this.scroll, 100)
57
+ document.addEventListener("scroll", this.scrollListener, true)
57
58
  }
58
59
 
59
60
  disconnectedCallback() {
60
- document.removeEventListener("scroll", this._scrollListener, true)
61
+ document.removeEventListener("scroll", this.scrollListener, true)
61
62
  super.disconnectedCallback()
62
63
  }
63
64
 
64
- static scrollToTop() {
65
+ // eslint-disable-next-line class-methods-use-this
66
+ scrollToTop() {
65
67
  window.scrollTo({
66
68
  top: 0,
67
69
  left: 0,
@@ -72,14 +74,14 @@ export class LeuScrollTop extends LeuElement {
72
74
  render() {
73
75
  const cssClasses = {
74
76
  "scroll-top": true,
75
- hide: !this._showButton,
77
+ hide: !this.showButton,
76
78
  }
77
79
  return html`
78
80
  <div class=${classMap(cssClasses)}>
79
81
  <leu-button
80
82
  label="Zum Seitenanfang"
81
83
  round
82
- @click="${() => LeuScrollTop.scrollToTop()}"
84
+ @click="${() => this.scrollToTop()}"
83
85
  >
84
86
  <leu-icon name="arrowUp"></leu-icon>
85
87
  </leu-button>
@@ -1,10 +1,12 @@
1
1
  import { html, nothing, PropertyValues } from "lit"
2
2
  import { classMap } from "lit/directives/class-map.js"
3
3
  import { createRef, ref } from "lit/directives/ref.js"
4
+ import { property, state } from "lit/decorators.js"
4
5
 
5
6
  import { ifDefined } from "lit/directives/if-defined.js"
6
7
  import { LeuElement } from "../../lib/LeuElement.js"
7
8
  import { HasSlotController } from "../../lib/hasSlotController.js"
9
+ import { FormAssociatedMixin } from "../../lib/mixins/FormAssociatedMixin.js"
8
10
 
9
11
  import { LeuButton } from "../button/Button.js"
10
12
  import { LeuMenu } from "../menu/Menu.js"
@@ -19,17 +21,9 @@ import styles from "./select.css?inline"
19
21
  * @tagname leu-select
20
22
  * @slot before - Optional content the appears before the option list
21
23
  * @slot after - Optional content the appears after the option list
22
- * @property {string} name - Reflects to the name attribute of the hidden input field that would be used in a form
23
- * @property {boolean} open - The expanded state of the popup
24
- * @property {string} label - The label of the select
25
- * @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.
26
- * @property {boolean} clearable - Show a clearable button to reset the value
27
- * @property {boolean} disabled - If the select should be disabled
28
- * @property {boolean} filterable - Show an input field to filter the options inside the popup
29
- * @property {boolean} multiple - Allow multiple selections
30
24
  * @attribute {string} value - The selected values separated by commas.
31
25
  */
32
- export class LeuSelect extends LeuElement {
26
+ export class LeuSelect extends FormAssociatedMixin(LeuElement) {
33
27
  static dependencies = {
34
28
  "leu-button": LeuButton,
35
29
  "leu-menu": LeuMenu,
@@ -41,32 +35,96 @@ export class LeuSelect extends LeuElement {
41
35
 
42
36
  static styles = [LeuElement.styles, styles]
43
37
 
44
- static get properties() {
45
- return {
46
- name: { type: String, reflect: true },
47
- open: { type: Boolean, reflect: true },
48
- label: { type: String, reflect: true },
49
- value: {
50
- type: Array,
51
- converter: {
52
- fromAttribute(value) {
53
- if (value) {
54
- return value.split(",").map((v) => v.trim())
55
- }
56
- return value
57
- },
58
- },
38
+ /**
39
+ * @internal
40
+ */
41
+ static shadowRootOptions = {
42
+ ...LeuElement.shadowRootOptions,
43
+ delegatesFocus: true,
44
+ }
45
+
46
+ /**
47
+ * The label of the select
48
+ */
49
+ @property({ type: String, reflect: true })
50
+ label: string = ""
51
+
52
+ /**
53
+ * The default value of the select. Corresponds to the `value` HTML attribute.
54
+ */
55
+ @property({
56
+ reflect: true,
57
+ attribute: "value",
58
+ converter: {
59
+ fromAttribute(value) {
60
+ if (value) {
61
+ return value.split(",").map((v) => v.trim())
62
+ }
63
+ return []
59
64
  },
60
- clearable: { type: Boolean, reflect: true },
61
- disabled: { type: Boolean, reflect: true },
62
- filterable: { type: Boolean, reflect: true },
63
- multiple: { type: Boolean, reflect: true },
64
- _optionFilter: { state: true },
65
- _hasFilterResults: { state: true },
66
- _displayValue: { state: true },
67
- }
65
+ toAttribute(value: Array<string>) {
66
+ return value.length > 0 ? value.join(",") : null
67
+ },
68
+ },
69
+ })
70
+ defaultValue: Array<string> = []
71
+
72
+ /** @internal */
73
+ protected _value: Array<string> | undefined
74
+
75
+ /**
76
+ * List of selected values. If they're set from outside the component, the select element
77
+ * finds all the options that match the given values and selects them.
78
+ */
79
+ @property({ type: Array, attribute: false })
80
+ set value(value: Array<string>) {
81
+ /**
82
+ * @todo Check if all of the value items are actually present in the options
83
+ */
84
+ this._value = value
85
+ }
86
+
87
+ get value(): Array<string> {
88
+ return this._value ?? this.defaultValue
68
89
  }
69
90
 
91
+ /**
92
+ * Show a clearable button to reset the value
93
+ */
94
+ @property({ type: Boolean, reflect: true })
95
+ clearable: boolean = false
96
+
97
+ /**
98
+ * Show an input field to filter the options inside the popup
99
+ */
100
+ @property({ type: Boolean, reflect: true })
101
+ filterable: boolean = false
102
+
103
+ /**
104
+ * Allow multiple selections
105
+ */
106
+ @property({ type: Boolean, reflect: true })
107
+ multiple: boolean = false
108
+
109
+ /** Marks the input element as required */
110
+ @property({ type: Boolean, reflect: true })
111
+ required: boolean = false
112
+
113
+ /**
114
+ * The expanded state of the popup
115
+ */
116
+ @state()
117
+ protected open: boolean = false
118
+
119
+ @state()
120
+ protected _optionFilter: string = ""
121
+
122
+ @state()
123
+ protected _hasFilterResults: boolean = true
124
+
125
+ @state()
126
+ protected _displayValue: string = ""
127
+
70
128
  static getOptionLabel(option) {
71
129
  if (typeof option === "object" && option !== null) {
72
130
  return option.label
@@ -74,48 +132,72 @@ export class LeuSelect extends LeuElement {
74
132
  return option
75
133
  }
76
134
 
135
+ /** @internal */
136
+ protected _deferedChangeEvent = false
137
+
138
+ /** @internal */
139
+ protected _optionFilterRef = createRef<LeuInput>()
140
+
141
+ /** @internal */
142
+ protected _toggleButtonRef = createRef<HTMLButtonElement>()
143
+
144
+ /** @internal */
145
+ protected _menuRef = createRef<LeuMenu>()
146
+
77
147
  /**
78
148
  * @internal
79
149
  */
80
150
  hasSlotController = new HasSlotController(this, ["before", "after"])
81
151
 
82
- constructor() {
83
- super()
84
- this.open = false
85
- this.disabled = false
86
- this.open = false
87
- this.multiple = false
88
- this.clearable = false
89
- this.filterable = false
90
- this.value = []
91
- this.label = ""
92
- this.name = ""
93
-
94
- /** @internal */
95
- this._optionFilter = ""
96
-
97
- /** @internal */
98
- this._hasFilterResults = true
152
+ protected setFormValue(): void {
153
+ const isEmpty = this.value.length === 0 || !this.value.some((v) => v !== "") // At least one value is not an empty string
154
+
155
+ if (isEmpty || this.disabled) {
156
+ this.internals.setFormValue(null)
157
+ } else if (this.multiple) {
158
+ const formData = new FormData()
159
+ this.value.forEach((v) => formData.append(this.name ?? "", v))
160
+ this.internals.setFormValue(formData)
161
+ } else {
162
+ this.internals.setFormValue(this.value[0])
163
+ }
99
164
 
100
- /** @internal */
101
- this._deferedChangeEvent = false
165
+ if (this.required && isEmpty) {
166
+ this.internals.setValidity(
167
+ { valueMissing: true },
168
+ "Bitte wählen Sie eine Option aus.",
169
+ )
170
+ } else {
171
+ this.internals.setValidity({})
172
+ }
173
+ }
102
174
 
103
- /** @internal */
175
+ public formResetCallback() {
176
+ super.formResetCallback()
177
+ this.value = this.defaultValue
104
178
  this._displayValue = ""
179
+ }
105
180
 
106
- /**
107
- * @type {import("lit/directives/ref").Ref<import("../input/Input").LeuInput>}
108
- */
109
- this._optionFilterRef = createRef()
110
- /**
111
- * @type {import("lit/directives/ref").Ref<HTMLButtonElement>}
112
- */
113
- this._toggleButtonRef = createRef()
181
+ protected willUpdate(changedProperties: PropertyValues<this>) {
182
+ super.willUpdate(changedProperties)
114
183
 
115
- /**
116
- * @type {import("lit/directives/ref").Ref<import("../menu/Menu").LeuMenu>}
117
- */
118
- this._menuRef = createRef()
184
+ if (
185
+ changedProperties.has("defaultValue") &&
186
+ !changedProperties.has("value") &&
187
+ !this.hasInteracted
188
+ ) {
189
+ this.value = this.defaultValue
190
+ }
191
+
192
+ if (
193
+ changedProperties.has("value") ||
194
+ changedProperties.has("defaultValue") ||
195
+ changedProperties.has("name") ||
196
+ changedProperties.has("disabled") ||
197
+ changedProperties.has("required")
198
+ ) {
199
+ this.setFormValue()
200
+ }
119
201
  }
120
202
 
121
203
  connectedCallback() {
@@ -156,13 +238,16 @@ export class LeuSelect extends LeuElement {
156
238
  }
157
239
  }
158
240
 
241
+ public click() {
242
+ this._toggleButtonRef.value?.click()
243
+ }
244
+
159
245
  /**
160
246
  * Apply the current state to the menu items.
161
247
  * - Set the active property when the value property has changed.
162
248
  * - Hide menu items that do not match the filter.
163
249
  */
164
250
  async _updateMenuItems(changed) {
165
- /** @type {LeuMenu} */
166
251
  const menu = this._menuRef.value
167
252
 
168
253
  await menu.updateComplete
@@ -221,9 +306,8 @@ export class LeuSelect extends LeuElement {
221
306
  /**
222
307
  * Handles clicks outside of the component to close the dropdown.
223
308
  * @internal
224
- * @param {MouseEvent} event
225
309
  */
226
- _handleDocumentClick = (event) => {
310
+ _handleDocumentClick = (event: MouseEvent) => {
227
311
  if (!event.composedPath().includes(this) && this.open) {
228
312
  this._closeDropdown()
229
313
  }
@@ -231,9 +315,8 @@ export class LeuSelect extends LeuElement {
231
315
 
232
316
  /**
233
317
  * @internal
234
- * @param {KeyboardEvent} event
235
318
  */
236
- _handleKeyDown = (event) => {
319
+ _handleKeyDown = (event: KeyboardEvent) => {
237
320
  if (event.key === "Escape") {
238
321
  this._closeDropdown()
239
322
  }
@@ -241,9 +324,8 @@ export class LeuSelect extends LeuElement {
241
324
 
242
325
  /**
243
326
  * @internal
244
- * @param {KeyboardEvent} event
245
327
  */
246
- async _handleToggleKeyDown(event) {
328
+ async _handleToggleKeyDown(event: KeyboardEvent) {
247
329
  if (["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key)) {
248
330
  event.preventDefault()
249
331
 
@@ -262,9 +344,8 @@ export class LeuSelect extends LeuElement {
262
344
 
263
345
  /**
264
346
  * @internal
265
- * @param {KeyboardEvent} event
266
347
  */
267
- _handleFilterInputKeyDown(event) {
348
+ _handleFilterInputKeyDown(event: KeyboardEvent) {
268
349
  if (event.key === "ArrowDown") {
269
350
  this._menuRef.value.focusItem(0)
270
351
  } else if (event.key === "ArrowUp") {
@@ -274,7 +355,6 @@ export class LeuSelect extends LeuElement {
274
355
 
275
356
  /**
276
357
  * Determines the value or label that should be displayed inside the toggle button.
277
- * @returns {String | nothing}
278
358
  */
279
359
  _getDisplayValue() {
280
360
  if (this.multiple) {
@@ -300,9 +380,10 @@ export class LeuSelect extends LeuElement {
300
380
  this.dispatchEvent(changeevent)
301
381
  }
302
382
 
303
- _clearValue(event) {
383
+ _clearValue(event: MouseEvent) {
304
384
  if (!this.disabled) {
305
385
  event.stopPropagation()
386
+ this.hasInteracted = true
306
387
  this.value = []
307
388
  }
308
389
 
@@ -325,30 +406,29 @@ export class LeuSelect extends LeuElement {
325
406
  }
326
407
  }
327
408
 
328
- _handleFilterInput(event) {
329
- this._optionFilter = event.target.value
409
+ _handleFilterInput(event: InputEvent) {
410
+ this._optionFilter = (event.target as HTMLInputElement).value
330
411
  }
331
412
 
332
413
  /**
333
414
  * Checks if the given value is selected.
334
- * @param {String} menuItemValue
335
- * @returns {Boolean}
336
415
  */
337
- _isSelected(menuItemValue) {
416
+ _isSelected(menuItemValue: string) {
338
417
  return this.value.includes(menuItemValue)
339
418
  }
340
419
 
341
- _handleMenuItemClick(event) {
420
+ _handleMenuItemClick(event: MouseEvent) {
342
421
  if (!(event.target instanceof LeuMenuItem) || event.target.disabled) {
343
422
  return
344
423
  }
345
424
 
346
- /** @type {LeuMenuItem} */
347
425
  const menuItem = event.target
348
426
 
349
427
  const value = menuItem.getValue()
350
428
  const isSelected = this._isSelected(value)
351
429
 
430
+ this.hasInteracted = true
431
+
352
432
  if (this.multiple) {
353
433
  this.value = isSelected
354
434
  ? this.value.filter((v) => v !== value)
@@ -392,7 +472,6 @@ export class LeuSelect extends LeuElement {
392
472
  type="button"
393
473
  class="apply-button"
394
474
  @click=${this._closeDropdown}
395
- fluid
396
475
  >Anwenden</leu-button
397
476
  >
398
477
  </div>
@@ -449,49 +528,43 @@ export class LeuSelect extends LeuElement {
449
528
  "select--has-after": this.hasSlotController.test("after"),
450
529
  }
451
530
 
452
- /*
453
- * We use the click event listener with the event delegation pattern
454
- * so this is not a violation of the rule.
455
- */
456
-
457
531
  return html`<div
458
- class=${classMap(selectClasses)}
459
- @keydown=${this._handleKeyDown}
532
+ class=${classMap(selectClasses)}
533
+ @keydown=${this._handleKeyDown}
534
+ >
535
+ <leu-popup
536
+ ?active=${this.open}
537
+ placement="bottom-start"
538
+ flip
539
+ matchSize="width"
540
+ autoSize="height"
541
+ autoSizePadding="8"
460
542
  >
461
- <leu-popup
462
- ?active=${this.open}
463
- placement="bottom-start"
464
- flip
465
- matchSize="width"
466
- autoSize="height"
467
- autoSizePadding="8"
468
- >
469
- ${this._renderToggleButton()}
470
- <div id="select-popup" class="select-menu-container">
471
- <slot name="before" class="before"></slot>
472
- ${this._renderFilterInput()}
473
- <leu-menu
474
- ref=${ref(this._menuRef)}
475
- role="listbox"
476
- aria-multiselectable=${ifDefined(
477
- this.multiple ? "true" : undefined,
478
- )}
479
- class="menu"
480
- @click=${this._handleMenuItemClick}
481
- aria-labelledby="select-label"
482
- >
483
- <slot @slotchange=${this._handleItemSlotChange}> </slot>
484
- </leu-menu>
485
- ${this._hasFilterResults || this._optionFilter === ""
486
- ? nothing
487
- : html` <p class="filter-message-empty" aria-live="polite">
488
- Keine Resultate
489
- </p>`}
490
- ${this._renderApplyButton()}
491
- <slot name="after" class="after"></slot>
492
- </div>
493
- </leu-popup>
494
- </div>
495
- <input type="hidden" name=${this.name} .value=${this.value.join(",")} />`
543
+ ${this._renderToggleButton()}
544
+ <div id="select-popup" class="select-menu-container">
545
+ <slot name="before" class="before"></slot>
546
+ ${this._renderFilterInput()}
547
+ <leu-menu
548
+ ref=${ref(this._menuRef)}
549
+ role="listbox"
550
+ aria-multiselectable=${ifDefined(
551
+ this.multiple ? "true" : undefined,
552
+ )}
553
+ class="menu"
554
+ @click=${this._handleMenuItemClick}
555
+ aria-labelledby="select-label"
556
+ >
557
+ <slot @slotchange=${this._handleItemSlotChange}> </slot>
558
+ </leu-menu>
559
+ ${this._hasFilterResults || this._optionFilter === ""
560
+ ? nothing
561
+ : html` <p class="filter-message-empty" aria-live="polite">
562
+ Keine Resultate
563
+ </p>`}
564
+ ${this._renderApplyButton()}
565
+ <slot name="after" class="after"></slot>
566
+ </div>
567
+ </leu-popup>
568
+ </div>`
496
569
  }
497
570
  }
@@ -90,6 +90,10 @@
90
90
  font-family: var(--select-font-regular);
91
91
  }
92
92
 
93
+ :host([required]) .label::after {
94
+ content: "*";
95
+ }
96
+
93
97
  .clear-button {
94
98
  --_length: 1.5rem;
95
99