@kubex/zinc 1.0.25 → 1.0.99

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 (199) hide show
  1. package/.claude/project.md +556 -0
  2. package/.github/workflows/js_build_and_deploy.yaml +9 -10
  3. package/dist/custom-elements.json +13575 -8856
  4. package/dist/vscode.html-custom-data.json +583 -48
  5. package/dist/web-types.json +1441 -181
  6. package/dist/zn.d.ts +1645 -660
  7. package/dist/zn.min.css +1 -1
  8. package/dist/zn.min.js +1412 -893
  9. package/docs/_utilities/code-previews.cjs +10 -1
  10. package/docs/assets/scripts/docs.js +42 -18
  11. package/docs/data/data-table.json +229 -66
  12. package/docs/data/empty.json +6 -0
  13. package/docs/data/products-table.json +41 -193
  14. package/docs/pages/components/action-bar.md +121 -6
  15. package/docs/pages/components/alert.md +118 -15
  16. package/docs/pages/components/animated-button.md +299 -0
  17. package/docs/pages/components/audio-select.md +534 -0
  18. package/docs/pages/components/bulk-actions.md +79 -5
  19. package/docs/pages/components/button-group.md +235 -27
  20. package/docs/pages/components/button.md +139 -1
  21. package/docs/pages/components/chart.md +365 -9
  22. package/docs/pages/components/checkbox.md +387 -58
  23. package/docs/pages/components/chip.md +253 -33
  24. package/docs/pages/components/collapsible.md +393 -46
  25. package/docs/pages/components/cols.md +152 -30
  26. package/docs/pages/components/confirm.md +182 -16
  27. package/docs/pages/components/copy-button.md +169 -6
  28. package/docs/pages/components/data-select.md +194 -15
  29. package/docs/pages/components/data-table-filter.md +96 -5
  30. package/docs/pages/components/data-table-search.md +451 -0
  31. package/docs/pages/components/data-table-sort.md +66 -5
  32. package/docs/pages/components/data-table.md +727 -66
  33. package/docs/pages/components/datepicker.md +402 -7
  34. package/docs/pages/components/defined-label.md +120 -5
  35. package/docs/pages/components/dialog.md +339 -59
  36. package/docs/pages/components/dropdown.md +366 -11
  37. package/docs/pages/components/editor.md +604 -7
  38. package/docs/pages/components/empty-state.md +315 -16
  39. package/docs/pages/components/expanding-action.md +212 -28
  40. package/docs/pages/components/file.md +421 -12
  41. package/docs/pages/components/filter-container.md +158 -5
  42. package/docs/pages/components/filter-wrapper.md +167 -6
  43. package/docs/pages/components/form-group.md +383 -6
  44. package/docs/pages/components/hover-container.md +237 -6
  45. package/docs/pages/components/icon-picker.md +121 -0
  46. package/docs/pages/components/icon.md +454 -48
  47. package/docs/pages/components/inline-edit.md +787 -19
  48. package/docs/pages/components/input-group.md +207 -13
  49. package/docs/pages/components/input.md +194 -0
  50. package/docs/pages/components/item.md +18 -0
  51. package/docs/pages/components/key-container.md +115 -0
  52. package/docs/pages/components/key.md +101 -0
  53. package/docs/pages/components/menu.md +512 -6
  54. package/docs/pages/components/note.md +239 -36
  55. package/docs/pages/components/opt-group.md +107 -0
  56. package/docs/pages/components/pagination.md +332 -6
  57. package/docs/pages/components/pane.md +222 -6
  58. package/docs/pages/components/panel.md +213 -26
  59. package/docs/pages/components/popup.md +1354 -6
  60. package/docs/pages/components/priority-list.md +233 -0
  61. package/docs/pages/components/progress-bar.md +387 -6
  62. package/docs/pages/components/radio-group.md +242 -5
  63. package/docs/pages/components/radio.md +474 -6
  64. package/docs/pages/components/rating.md +331 -7
  65. package/docs/pages/components/reveal.md +524 -6
  66. package/docs/pages/components/scroll-container.md +552 -6
  67. package/docs/pages/components/select.md +762 -160
  68. package/docs/pages/components/skeleton.md +473 -30
  69. package/docs/pages/components/slideout.md +261 -5
  70. package/docs/pages/components/sp.md +99 -41
  71. package/docs/pages/components/split-button.md +347 -9
  72. package/docs/pages/components/split-pane.md +445 -6
  73. package/docs/pages/components/stat.md +549 -9
  74. package/docs/pages/components/status-indicator.md +230 -1
  75. package/docs/pages/components/stepper.md +563 -6
  76. package/docs/pages/components/tabs.md +439 -13
  77. package/docs/pages/components/textarea.md +415 -17
  78. package/docs/pages/components/tile.md +385 -26
  79. package/docs/pages/components/toggle.md +424 -6
  80. package/docs/pages/components/tooltip.md +367 -0
  81. package/docs/pages/components/translation-group.md +210 -0
  82. package/docs/pages/components/translations.md +319 -19
  83. package/docs/pages/components/vertical-stepper.md +401 -8
  84. package/docs/pages/components/well.md +300 -3
  85. package/docs/pages/getting-started/form-controls.md +162 -0
  86. package/docs/pages/index.md +116 -15
  87. package/package.json +5 -6
  88. package/scss/_global-spacing.scss +4 -0
  89. package/scss/_root.scss +2 -2
  90. package/scss/boot.scss +19 -0
  91. package/scss/shared/layout.scss +5 -0
  92. package/scss/themes/_light.scss +3 -3
  93. package/src/components/alert/alert.scss +1 -0
  94. package/src/components/animated-button/README.md +306 -0
  95. package/src/components/animated-button/animated-button.component.ts +229 -0
  96. package/src/components/animated-button/animated-button.scss +228 -0
  97. package/src/components/animated-button/animated-button.test.ts +143 -0
  98. package/src/components/animated-button/index.ts +12 -0
  99. package/src/components/button/button.component.ts +5 -0
  100. package/src/components/button-group/button-group.component.ts +25 -13
  101. package/src/components/button-group/button-group.scss +6 -0
  102. package/src/components/checkbox/checkbox.component.ts +4 -0
  103. package/src/components/checkbox/checkbox.scss +7 -4
  104. package/src/components/checkbox-group/checkbox-group.scss +7 -0
  105. package/src/components/chip/chip.component.ts +7 -2
  106. package/src/components/chip/chip.scss +16 -2
  107. package/src/components/collapsible/collapsible.component.ts +27 -20
  108. package/src/components/cols/cols.component.ts +14 -2
  109. package/src/components/cols/cols.scss +6 -12
  110. package/src/components/confirm/confirm.component.ts +7 -0
  111. package/src/components/copy-button/copy-button.component.ts +12 -11
  112. package/src/components/data-select/data-select.component.ts +215 -44
  113. package/src/components/data-select/data-select.scss +66 -6
  114. package/src/components/data-select/providers/currency-data-provider.ts +20 -6
  115. package/src/components/data-select/providers/provider.ts +1 -0
  116. package/src/components/data-select/providers/us-state-data-provider.ts +68 -0
  117. package/src/components/data-table/data-table.component.ts +81 -22
  118. package/src/components/data-table-search/data-table-search.component.ts +184 -0
  119. package/src/components/data-table-search/data-table-search.scss +17 -0
  120. package/src/components/data-table-search/data-table-search.test.ts +11 -0
  121. package/src/components/data-table-search/index.ts +12 -0
  122. package/src/components/datepicker/datepicker.component.ts +347 -13
  123. package/src/components/dialog/dialog.component.ts +1 -1
  124. package/src/components/empty-state/empty-state.scss +6 -1
  125. package/src/components/header/header.scss +3 -3
  126. package/src/components/icon/icon.component.ts +23 -1
  127. package/src/components/icon/icon.scss +23 -0
  128. package/src/components/icon-picker/brand-icons.ts +24 -0
  129. package/src/components/icon-picker/icon-picker.component.ts +407 -0
  130. package/src/components/icon-picker/icon-picker.scss +205 -0
  131. package/src/components/icon-picker/index.ts +12 -0
  132. package/src/components/icon-picker/line-icons.ts +612 -0
  133. package/src/components/icon-picker/material-icons.ts +14543 -0
  134. package/src/components/inline-edit/inline-edit.component.ts +190 -46
  135. package/src/components/inline-edit/inline-edit.scss +29 -0
  136. package/src/components/inline-edit/inline-edit.test.ts +314 -1
  137. package/src/components/input/input.component.ts +324 -5
  138. package/src/components/input/input.scss +87 -0
  139. package/src/components/input-group/input-group.component.ts +28 -7
  140. package/src/components/input-group/input-group.scss +48 -10
  141. package/src/components/item/item.component.ts +24 -3
  142. package/src/components/item/item.scss +18 -0
  143. package/src/components/key/index.ts +12 -0
  144. package/src/components/key/key.component.ts +47 -0
  145. package/src/components/key/key.scss +38 -0
  146. package/src/components/key-container/index.ts +12 -0
  147. package/src/components/key-container/key-container.component.ts +142 -0
  148. package/src/components/key-container/key-container.scss +33 -0
  149. package/src/components/menu-item/menu-item.component.ts +20 -0
  150. package/src/components/menu-item/menu-item.scss +69 -2
  151. package/src/components/navbar/navbar.component.ts +103 -11
  152. package/src/components/navbar/navbar.scss +6 -1
  153. package/src/components/opt-group/index.ts +12 -0
  154. package/src/components/opt-group/opt-group.component.ts +74 -0
  155. package/src/components/opt-group/opt-group.scss +37 -0
  156. package/src/components/opt-group/opt-group.test.ts +27 -0
  157. package/src/components/option/option.component.ts +1 -16
  158. package/src/components/option/option.scss +4 -0
  159. package/src/components/panel/panel.component.ts +10 -8
  160. package/src/components/panel/panel.scss +20 -0
  161. package/src/components/priority-list/index.ts +12 -0
  162. package/src/components/priority-list/priority-list.component.ts +611 -0
  163. package/src/components/priority-list/priority-list.scss +185 -0
  164. package/src/components/priority-list/priority-list.test.ts +139 -0
  165. package/src/components/progress-bar/progress-bar.component.ts +22 -16
  166. package/src/components/radio/radio.scss +10 -3
  167. package/src/components/select/select.component.ts +812 -72
  168. package/src/components/select/select.scss +108 -2
  169. package/src/components/select/select.test.ts +147 -2
  170. package/src/components/settings-container/settings-container.component.ts +44 -19
  171. package/src/components/settings-container/settings-container.scss +1 -1
  172. package/src/components/skeleton/skeleton.component.ts +6 -1
  173. package/src/components/slideout/slideout.scss +8 -3
  174. package/src/components/sp/sp.component.ts +3 -1
  175. package/src/components/sp/sp.scss +19 -0
  176. package/src/components/stepper/stepper.component.ts +16 -10
  177. package/src/components/textarea/textarea.scss +0 -4
  178. package/src/components/tile/tile.component.ts +12 -3
  179. package/src/components/tile/tile.scss +1 -1
  180. package/src/components/timer/timer.component.ts +15 -2
  181. package/src/components/toggle/toggle.component.ts +29 -26
  182. package/src/components/toggle/toggle.scss +13 -1
  183. package/src/components/translation-group/index.ts +12 -0
  184. package/src/components/translation-group/translation-group.component.ts +207 -0
  185. package/src/components/translation-group/translation-group.scss +8 -0
  186. package/src/components/translations/translations.component.ts +90 -44
  187. package/src/components/translations/translations.scss +4 -0
  188. package/src/components/vertical-stepper/vertical-stepper.component.ts +14 -9
  189. package/src/events/events.ts +12 -7
  190. package/src/events/zn-language-change.ts +7 -0
  191. package/src/events/zn-purchase.ts +11 -0
  192. package/src/events/zn-redirect.ts +7 -0
  193. package/src/events/zn-reorder.ts +7 -0
  194. package/src/events/zn-search-change.ts +11 -0
  195. package/src/events/zn-submit.ts +1 -1
  196. package/src/form-control.scss +4 -0
  197. package/src/internal/form-navigation.ts +510 -0
  198. package/src/internal/form.ts +6 -0
  199. package/src/zinc.ts +91 -83
@@ -10,75 +10,48 @@ unusedProperties: |
10
10
 
11
11
  ## Examples
12
12
 
13
- ### Basic Select with Label
13
+ ### Basic Select
14
14
 
15
- Use the `label` attribute to give the select an accessible label. For labels that contain HTML, use the `label` slot
16
- instead.
15
+ Use the `label` attribute to give the select an accessible label. For labels that contain HTML, use the `label` slot instead.
17
16
 
18
17
  ```html:preview
19
-
20
- <form method="get" action="#">
21
- <zn-select name="something" label="Select one option">
22
- <zn-option value="0">Option 0</zn-option>
23
- <zn-option value="1">Option 1</zn-option>
24
- <zn-option value="2" selected>Option 2</zn-option>
25
- <zn-option value="3">Option 3</zn-option>
26
- <zn-option value="4">Option 4</zn-option>
27
- <zn-option value="5">Option 5</zn-option>
28
- <zn-option value="6">Option 6</zn-option>
29
- </zn-select>
30
-
31
- <br>
32
-
33
- <zn-button type="submit">Submit</zn-button>
34
- </form>
18
+ <zn-select label="Select one option">
19
+ <zn-option value="option-1">Option 1</zn-option>
20
+ <zn-option value="option-2">Option 2</zn-option>
21
+ <zn-option value="option-3">Option 3</zn-option>
22
+ <zn-option value="option-4">Option 4</zn-option>
23
+ <zn-option value="option-5">Option 5</zn-option>
24
+ <zn-option value="option-6">Option 6</zn-option>
25
+ </zn-select>
35
26
  ```
36
27
 
37
28
  :::tip
38
- This component works with standard `<form>` elements. Please refer to the section
39
- on [form controls](/getting-started/form-controls) to learn more about form submission and client-side validation.
29
+ This component works with standard `<form>` elements. Please refer to the section on [form controls](/getting-started/form-controls) to learn more about form submission and client-side validation.
40
30
  :::
41
31
 
42
- ### Distinct Selects
43
-
44
- ```html:preview
45
-
46
- <div class="form-spacing">
47
- <zn-select provider="country" multiple name="country-one" id="country-one" distinct="country-two">
48
- <zn-option value="0">Option 0</zn-option>
49
- <zn-option value="1">Option 1</zn-option>
50
- <zn-option value="2">Option 2</zn-option>
51
- </zn-select>
52
- <zn-select provider="country" multiple name="country-two" id="country-two" distinct="country-one">
53
- <zn-option value="0">Option 0</zn-option>
54
- <zn-option value="1">Option 1</zn-option>
55
- <zn-option value="2">Option 2</zn-option>
56
- </zn-select>
57
- </div>
58
- ```
32
+ ### Initial Values
59
33
 
60
- ### Conditional Selects
34
+ Use the `value` attribute to set the initial selection. You can also use the `selected` attribute on individual options.
61
35
 
62
36
  ```html:preview
63
-
64
- <div class="form-spacing">
65
- <zn-select provider="conditional" multiple name="conditional-one" id="conditional-one" conditional="conditional-two">
66
- <zn-option value="0">Option 0</zn-option>
67
- <zn-option value="1">Option 1</zn-option>
68
- <zn-option value="2">Option 2</zn-option>
69
- </zn-select>
70
- <zn-select provider="conditional" multiple name="conditional-two" id="conditional-two" conditional="conditional-one">
71
- <zn-option value="0">Option 0</zn-option>
72
- <zn-option value="1">Option 1</zn-option>
73
- <zn-option value="2">Option 2</zn-option>
74
- </zn-select>
75
- </div>
37
+ <zn-select label="Select with initial value" value="option-2">
38
+ <zn-option value="option-1">Option 1</zn-option>
39
+ <zn-option value="option-2">Option 2</zn-option>
40
+ <zn-option value="option-3">Option 3</zn-option>
41
+ </zn-select>
42
+ <br />
43
+ <zn-select label="Using selected attribute">
44
+ <zn-option value="option-1">Option 1</zn-option>
45
+ <zn-option value="option-2" selected>Option 2</zn-option>
46
+ <zn-option value="option-3">Option 3</zn-option>
47
+ </zn-select>
76
48
  ```
77
49
 
50
+ When using `multiple`, the `value` attribute uses space-delimited values to select more than one option. Because of this, `<zn-option>` values cannot contain spaces. If you're accessing the `value` property through JavaScript, it will be an array.
51
+
78
52
  ### Help Text
79
53
 
80
- Add descriptive help text to a select with the `help-text` attribute. For help texts that contain HTML, use the
81
- `help-text` slot instead.
54
+ Add descriptive help text to a select with the `help-text` attribute. For help texts that contain HTML, use the `help-text` slot instead.
82
55
 
83
56
  ```html:preview
84
57
  <zn-select label="Skill level" help-text="Select one option that best describes your current skill level">
@@ -102,9 +75,7 @@ Add descriptive help text to a select with the `help-text` attribute. For help t
102
75
  Use the `label-tooltip` attribute to add text that appears in a tooltip triggered by an info icon next to the label.
103
76
 
104
77
  :::tip
105
- **Usage:** Use a **label tooltip** to provide helpful but non-essential instructions or examples to guide people when
106
- selecting an option. Use **help text** to communicate instructions or requirements for choosing an option without
107
- errors.
78
+ **Usage:** Use a **label tooltip** to provide helpful but non-essential instructions or examples to guide people when selecting an option. Use **help text** to communicate instructions or requirements for choosing an option without errors.
108
79
  :::
109
80
 
110
81
  ```html:preview
@@ -118,14 +89,10 @@ errors.
118
89
 
119
90
  ### Label with Context Note
120
91
 
121
- Use the `context-note` attribute to add text that provides additional context or reference. For text that contains HTML,
122
- use the `context-note` slot. **Note:** On small screens the context note will wrap below the label if there isn't enough
123
- room next to the label.
92
+ Use the `context-note` attribute to add text that provides additional context or reference. For text that contains HTML, use the `context-note` slot. **Note:** On small screens the context note will wrap below the label if there isn't enough room next to the label.
124
93
 
125
94
  :::tip
126
- **Usage:** Use a **context note** to provide secondary contextual data, especially dynamic data, that would help people
127
- when choosing an option. Use **help text** to communicate instructions or requirements for choosing an option without
128
- errors.
95
+ **Usage:** Use a **context note** to provide secondary contextual data, especially dynamic data, that would help people when choosing an option. Use **help text** to communicate instructions or requirements for choosing an option without errors.
129
96
  :::
130
97
 
131
98
  ```html:preview
@@ -155,8 +122,7 @@ Use the `placeholder` attribute to add a placeholder.
155
122
  Use the `clearable` attribute to make the control clearable. The clear button only appears when an option is selected.
156
123
 
157
124
  :::tip
158
- **Usage:** Add a clear button only when **multiple** options can be selected. For the default single-choice use case (
159
- the most common for selects), include an empty option that people can select to "clear" the current selection.
125
+ **Usage:** Add a clear button only when **multiple** options can be selected. For the default single-choice use case (the most common for selects), include an empty option that people can select to "clear" the current selection.
160
126
  :::
161
127
 
162
128
  ```html:preview
@@ -180,39 +146,322 @@ the most common for selects), include an empty option that people can select to
180
146
  </zn-select>
181
147
  ```
182
148
 
183
- ### Pill
149
+ ### Search
184
150
 
185
- Use the `pill` attribute to give selects rounded edges.
186
-
187
- :::warning
188
- **Note:** Pill-shaped selects are not a standard pattern in our Design System, and there is no Figma component for this
189
- option. Please check with the design team before using this option.
190
- :::
151
+ Use the `search` attribute to allow users to type into the select to filter the list of options. When the dropdown opens, the input becomes editable and options are filtered as you type using case-insensitive substring matching against labels and values.
191
152
 
192
153
  ```html:preview
193
- <zn-select label="Medium pill" pill>
194
- <zn-option value="option-1">Option 1</zn-option>
195
- <zn-option value="option-2">Option 2</zn-option>
196
- <zn-option value="option-3">Option 3</zn-option>
197
- <zn-option value="option-4">Option 4</zn-option>
198
- <zn-option value="option-5">Option 5</zn-option>
199
- <zn-option value="option-6">Option 6</zn-option>
154
+ <zn-select label="Search for a country" search placeholder="Type to search...">
155
+ <zn-option value="au">Australia</zn-option>
156
+ <zn-option value="br">Brazil</zn-option>
157
+ <zn-option value="ca">Canada</zn-option>
158
+ <zn-option value="cn">China</zn-option>
159
+ <zn-option value="fr">France</zn-option>
160
+ <zn-option value="de">Germany</zn-option>
161
+ <zn-option value="in">India</zn-option>
162
+ <zn-option value="jp">Japan</zn-option>
163
+ <zn-option value="mx">Mexico</zn-option>
164
+ <zn-option value="uk">United Kingdom</zn-option>
165
+ <zn-option value="us">United States</zn-option>
200
166
  </zn-select>
201
- <br />
202
- <zn-select label="Large pill" size="large" pill>
203
- <zn-option value="option-1">Option 1</zn-option>
204
- <zn-option value="option-2">Option 2</zn-option>
205
- <zn-option value="option-3">Option 3</zn-option>
206
- <zn-option value="option-4">Option 4</zn-option>
207
- <zn-option value="option-5">Option 5</zn-option>
208
- <zn-option value="option-6">Option 6</zn-option>
167
+ ```
168
+
169
+ ### Search with Multiple Selection
170
+
171
+ Combine `search` with `multiple` to allow users to search and select multiple options. The search clears automatically after each selection so you can continue searching.
172
+
173
+ ```html:preview
174
+ <zn-select label="Select your skills" search multiple clearable placeholder="Search skills...">
175
+ <zn-option value="html">HTML</zn-option>
176
+ <zn-option value="css">CSS</zn-option>
177
+ <zn-option value="js">JavaScript</zn-option>
178
+ <zn-option value="ts">TypeScript</zn-option>
179
+ <zn-option value="react">React</zn-option>
180
+ <zn-option value="vue">Vue</zn-option>
181
+ <zn-option value="angular">Angular</zn-option>
182
+ <zn-option value="node">Node.js</zn-option>
183
+ <zn-option value="python">Python</zn-option>
184
+ <zn-option value="go">Go</zn-option>
209
185
  </zn-select>
210
186
  ```
211
187
 
188
+ ### Remote Search
189
+
190
+ When `search` and `data-uri` are used together, typing in the select sends debounced requests to the server instead of filtering locally. The search term is appended as a query parameter (default `q`). Use `search-param` to customise the parameter name, `search-debounce` to control the delay, and `max-results` to cap displayed results.
191
+
192
+ If the server returns fewer results than `max-results`, further refinements of the same query are filtered client-side automatically — no extra requests are made. When results are truncated, a message at the bottom of the dropdown shows how many results are displayed out of the total, prompting the user to refine their search.
193
+
194
+ ```html:preview
195
+ <div class="remote-search-demo">
196
+ <zn-select
197
+ id="remote-search-example"
198
+ label="Search users"
199
+ search
200
+ search-debounce="300"
201
+ max-results="5"
202
+ placeholder="Type a name to search..."
203
+ help-text="Try typing 'a' then narrowing to 'al' — the second query filters locally."
204
+ ></zn-select>
205
+
206
+ <div id="remote-search-log" style="margin-top: 1rem; padding: 0.75rem 1rem; background: var(--zn-color-neutral-100); border-radius: var(--zn-border-radius-medium); font-family: var(--zn-font-mono); font-size: var(--zn-font-size-small); max-height: 8rem; overflow-y: auto;">
207
+ <strong>Request log:</strong>
208
+ </div>
209
+ </div>
210
+
211
+ <script type="module">
212
+ // -- Mock user database --
213
+ const allUsers = [
214
+ { key: 'alice', value: 'Alice Johnson' },
215
+ { key: 'alex', value: 'Alex Thompson' },
216
+ { key: 'amanda', value: 'Amanda Garcia' },
217
+ { key: 'bob', value: 'Bob Williams' },
218
+ { key: 'beth', value: 'Beth Davis' },
219
+ { key: 'charlie', value: 'Charlie Brown' },
220
+ { key: 'carol', value: 'Carol Martinez' },
221
+ { key: 'david', value: 'David Lee' },
222
+ { key: 'diana', value: 'Diana Wilson' },
223
+ { key: 'emma', value: 'Emma Taylor' },
224
+ { key: 'elena', value: 'Elena Rodriguez' },
225
+ { key: 'frank', value: 'Frank Anderson' },
226
+ { key: 'fiona', value: 'Fiona Clark' },
227
+ { key: 'george', value: 'George Hall' },
228
+ { key: 'grace', value: 'Grace Lewis' },
229
+ ];
230
+
231
+ // -- Intercept fetch for the demo endpoint --
232
+ const realFetch = window.fetch;
233
+ const DEMO_URL = '/_demo/api/users';
234
+ const log = document.getElementById('remote-search-log');
235
+
236
+ window.fetch = function(url, opts) {
237
+ const urlStr = typeof url === 'string' ? url : url.toString();
238
+ if (!urlStr.startsWith(DEMO_URL)) {
239
+ return realFetch.call(this, url, opts);
240
+ }
241
+
242
+ const params = new URL(urlStr, location.origin).searchParams;
243
+ const q = (params.get('q') || '').toLowerCase();
244
+ const time = new Date().toLocaleTimeString();
245
+ log.innerHTML += `<br>[${time}] GET ${urlStr.replace(location.origin, '')}`;
246
+ log.scrollTop = log.scrollHeight;
247
+
248
+ // Simulate network latency
249
+ return new Promise(resolve => {
250
+ setTimeout(() => {
251
+ const filtered = allUsers.filter(u =>
252
+ u.value.toLowerCase().includes(q) || u.key.includes(q)
253
+ );
254
+ resolve(new Response(JSON.stringify(filtered), {
255
+ status: 200,
256
+ headers: { 'Content-Type': 'application/json' }
257
+ }));
258
+ }, 200 + Math.random() * 300);
259
+ });
260
+ };
261
+
262
+ // Wire up the select
263
+ document.getElementById('remote-search-example').setAttribute('data-uri', DEMO_URL);
264
+ </script>
265
+ ```
266
+
267
+ ### Remote Search with Custom Parameter
268
+
269
+ Use `search-param` to change the query parameter name sent to the server.
270
+
271
+ ```html:preview
272
+ <zn-select
273
+ id="custom-param-example"
274
+ label="Search products"
275
+ search
276
+ search-param="filter"
277
+ search-debounce="500"
278
+ placeholder="Search with custom param..."
279
+ help-text='Sends requests like /api?filter=term instead of /api?q=term'
280
+ ></zn-select>
281
+
282
+ <script type="module">
283
+ const products = [
284
+ { key: 'laptop', value: 'Laptop' },
285
+ { key: 'phone', value: 'Phone' },
286
+ { key: 'tablet', value: 'Tablet' },
287
+ { key: 'monitor', value: 'Monitor' },
288
+ { key: 'keyboard', value: 'Keyboard' },
289
+ { key: 'mouse', value: 'Mouse' },
290
+ { key: 'headset', value: 'Headset' },
291
+ { key: 'webcam', value: 'Webcam' },
292
+ ];
293
+
294
+ const realFetch = window.fetch;
295
+ const DEMO_URL = '/_demo/api/products';
296
+
297
+ window.fetch = function(url, opts) {
298
+ const urlStr = typeof url === 'string' ? url : url.toString();
299
+ if (!urlStr.startsWith(DEMO_URL)) {
300
+ return realFetch.call(this, url, opts);
301
+ }
302
+
303
+ const params = new URL(urlStr, location.origin).searchParams;
304
+ const q = (params.get('filter') || '').toLowerCase();
305
+
306
+ return new Promise(resolve => {
307
+ setTimeout(() => {
308
+ const filtered = products.filter(p =>
309
+ p.value.toLowerCase().includes(q) || p.key.includes(q)
310
+ );
311
+ resolve(new Response(JSON.stringify(filtered), {
312
+ status: 200,
313
+ headers: { 'Content-Type': 'application/json' }
314
+ }));
315
+ }, 150 + Math.random() * 200);
316
+ });
317
+ };
318
+
319
+ document.getElementById('custom-param-example').setAttribute('data-uri', DEMO_URL);
320
+ </script>
321
+ ```
322
+
323
+ ### Remote Search with Multiple Selection
324
+
325
+ Combine remote search with `multiple` to search and pick several results.
326
+
327
+ ```html:preview
328
+ <div class="remote-search-multi-demo">
329
+ <zn-select
330
+ id="remote-search-multi"
331
+ label="Assign team members"
332
+ search
333
+ multiple
334
+ clearable
335
+ search-debounce="300"
336
+ max-results="8"
337
+ max-options-visible="3"
338
+ placeholder="Search team members..."
339
+ ></zn-select>
340
+ </div>
341
+
342
+ <script type="module">
343
+ const allUsers = [
344
+ { key: 'alice', value: 'Alice Johnson' },
345
+ { key: 'alex', value: 'Alex Thompson' },
346
+ { key: 'amanda', value: 'Amanda Garcia' },
347
+ { key: 'bob', value: 'Bob Williams' },
348
+ { key: 'beth', value: 'Beth Davis' },
349
+ { key: 'charlie', value: 'Charlie Brown' },
350
+ { key: 'carol', value: 'Carol Martinez' },
351
+ { key: 'david', value: 'David Lee' },
352
+ { key: 'diana', value: 'Diana Wilson' },
353
+ { key: 'emma', value: 'Emma Taylor' },
354
+ ];
355
+
356
+ const realFetch = window.fetch;
357
+ const DEMO_URL = '/_demo/api/team';
358
+
359
+ window.fetch = function(url, opts) {
360
+ const urlStr = typeof url === 'string' ? url : url.toString();
361
+ if (!urlStr.startsWith(DEMO_URL)) {
362
+ return realFetch.call(this, url, opts);
363
+ }
364
+
365
+ const q = new URL(urlStr, location.origin).searchParams.get('q') || '';
366
+ return new Promise(resolve => {
367
+ setTimeout(() => {
368
+ const filtered = allUsers.filter(u =>
369
+ u.value.toLowerCase().includes(q.toLowerCase()) || u.key.includes(q.toLowerCase())
370
+ );
371
+ resolve(new Response(JSON.stringify(filtered), {
372
+ status: 200,
373
+ headers: { 'Content-Type': 'application/json' }
374
+ }));
375
+ }, 150 + Math.random() * 200);
376
+ });
377
+ };
378
+
379
+ document.getElementById('remote-search-multi').setAttribute('data-uri', DEMO_URL);
380
+ </script>
381
+ ```
382
+
383
+ ### Search Only (No Initial Load)
384
+
385
+ Use `search-only` with `search` and `data-uri` to defer all fetching until the user types. No request is made on page load — the dropdown shows a "Type to search..." prompt until a query is entered. This is ideal for large datasets where preloading all options isn't practical.
386
+
387
+ ```html:preview
388
+ <div class="search-only-demo">
389
+ <zn-select
390
+ id="search-only-example"
391
+ label="Find a city"
392
+ search
393
+ search-only
394
+ search-debounce="300"
395
+ max-results="10"
396
+ placeholder="Start typing a city name..."
397
+ ></zn-select>
398
+
399
+ <div id="search-only-log" style="margin-top: 1rem; padding: 0.75rem 1rem; background: var(--zn-color-neutral-100); border-radius: var(--zn-border-radius-medium); font-family: var(--zn-font-mono); font-size: var(--zn-font-size-small); max-height: 8rem; overflow-y: auto;">
400
+ <strong>Request log:</strong>
401
+ </div>
402
+ </div>
403
+
404
+ <script type="module">
405
+ const cities = [
406
+ { key: 'london', value: 'London' },
407
+ { key: 'los-angeles', value: 'Los Angeles' },
408
+ { key: 'lisbon', value: 'Lisbon' },
409
+ { key: 'lima', value: 'Lima' },
410
+ { key: 'lagos', value: 'Lagos' },
411
+ { key: 'lyon', value: 'Lyon' },
412
+ { key: 'new-york', value: 'New York' },
413
+ { key: 'nairobi', value: 'Nairobi' },
414
+ { key: 'nice', value: 'Nice' },
415
+ { key: 'paris', value: 'Paris' },
416
+ { key: 'prague', value: 'Prague' },
417
+ { key: 'porto', value: 'Porto' },
418
+ { key: 'perth', value: 'Perth' },
419
+ { key: 'sydney', value: 'Sydney' },
420
+ { key: 'seoul', value: 'Seoul' },
421
+ { key: 'shanghai', value: 'Shanghai' },
422
+ { key: 'singapore', value: 'Singapore' },
423
+ { key: 'stockholm', value: 'Stockholm' },
424
+ { key: 'tokyo', value: 'Tokyo' },
425
+ { key: 'toronto', value: 'Toronto' },
426
+ { key: 'taipei', value: 'Taipei' },
427
+ ];
428
+
429
+ const realFetch = window.fetch;
430
+ const DEMO_URL = '/_demo/api/cities';
431
+ const log = document.getElementById('search-only-log');
432
+
433
+ window.fetch = function(url, opts) {
434
+ const urlStr = typeof url === 'string' ? url : url.toString();
435
+ if (!urlStr.startsWith(DEMO_URL)) {
436
+ return realFetch.call(this, url, opts);
437
+ }
438
+
439
+ const params = new URL(urlStr, location.origin).searchParams;
440
+ const q = (params.get('q') || '').toLowerCase();
441
+ const time = new Date().toLocaleTimeString();
442
+ log.innerHTML += `<br>[${time}] GET ...?q=${q}`;
443
+ log.scrollTop = log.scrollHeight;
444
+
445
+ return new Promise(resolve => {
446
+ setTimeout(() => {
447
+ const filtered = cities.filter(c =>
448
+ c.value.toLowerCase().includes(q) || c.key.includes(q)
449
+ );
450
+ resolve(new Response(JSON.stringify(filtered), {
451
+ status: 200,
452
+ headers: { 'Content-Type': 'application/json' }
453
+ }));
454
+ }, 200 + Math.random() * 300);
455
+ });
456
+ };
457
+
458
+ document.getElementById('search-only-example').setAttribute('data-uri', DEMO_URL);
459
+ </script>
460
+ ```
461
+
212
462
  ### Disabled
213
463
 
214
- Use the `disabled` attribute to disable the entire select. To disable just one option, put `disabled` on the
215
- `zn-option`.
464
+ Use the `disabled` attribute to disable the entire select. To disable just one option, put `disabled` on the `zn-option`.
216
465
 
217
466
  ```html:preview
218
467
  <zn-select label="Disabled select" disabled>
@@ -235,14 +484,12 @@ Use the `disabled` attribute to disable the entire select. To disable just one o
235
484
 
236
485
  ```
237
486
 
238
- ### Multiple
487
+ ### Sizes
239
488
 
240
- To allow multiple options to be selected, use the `multiple` attribute. When this option is enabled, be sure to also add
241
- the `clearable` attribute to display a clear button. To set multiple values at once, set `value` to a space-delimited
242
- list of values.
489
+ Use the `size` attribute to change a select's size. Note that size does not apply to listbox options. Size `medium` is the selects default.
243
490
 
244
491
  ```html:preview
245
- <zn-select label="Select one or more" value="option-1 option-2 option-3" multiple clearable>
492
+ <zn-select label="Medium input">
246
493
  <zn-option value="option-1">Option 1</zn-option>
247
494
  <zn-option value="option-2">Option 2</zn-option>
248
495
  <zn-option value="option-3">Option 3</zn-option>
@@ -250,19 +497,8 @@ list of values.
250
497
  <zn-option value="option-5">Option 5</zn-option>
251
498
  <zn-option value="option-6">Option 6</zn-option>
252
499
  </zn-select>
253
- ```
254
-
255
- :::tip
256
- Note that multi-select options may wrap, causing the control to expand vertically. You can use the `max-options-visible`
257
- attribute to control the maximum number of selected options to show at once.
258
- :::
259
-
260
- ### Multiple with a limit
261
-
262
- To limit the number of options that can be selected, use the `max-options` attribute.
263
-
264
- ```html:preview
265
- <zn-select label="Select one or more" value="option-1 option-2" multiple clearable max-options="3">
500
+ <br />
501
+ <zn-select label="Medium input" size="medium">
266
502
  <zn-option value="option-1">Option 1</zn-option>
267
503
  <zn-option value="option-2">Option 2</zn-option>
268
504
  <zn-option value="option-3">Option 3</zn-option>
@@ -270,56 +506,51 @@ To limit the number of options that can be selected, use the `max-options` attri
270
506
  <zn-option value="option-5">Option 5</zn-option>
271
507
  <zn-option value="option-6">Option 6</zn-option>
272
508
  </zn-select>
273
- ```
274
-
275
- ### Setting Initial Values
276
-
277
- Use the `value` attribute to set the initial selection.
278
-
279
- When using `multiple`, the `value` _attribute_ uses space-delimited values to select more than one option. Because of
280
- this, `<zn-option>` values cannot contain spaces. If you're accessing the `value` _property_ through Javascript, it will
281
- be an array.
282
-
283
- ```html:preview
284
- <zn-select label="Select one or more" value="option-1 option-2" multiple clearable>
509
+ <br />
510
+ <zn-select label="Large input" size="large">
285
511
  <zn-option value="option-1">Option 1</zn-option>
286
512
  <zn-option value="option-2">Option 2</zn-option>
287
513
  <zn-option value="option-3">Option 3</zn-option>
288
514
  <zn-option value="option-4">Option 4</zn-option>
515
+ <zn-option value="option-5">Option 5</zn-option>
516
+ <zn-option value="option-6">Option 6</zn-option>
289
517
  </zn-select>
290
518
  ```
291
519
 
292
- ### Grouping Options
520
+ ### Pill
293
521
 
294
- Use `<zn-divider>` to group listbox items visually. You can also use `<small>` to provide labels for each group, but
295
- they won't be announced by most assistive devices.
522
+ Use the `pill` attribute to give selects rounded edges.
296
523
 
297
524
  :::warning
298
- **Note:** `ts_form_for` doesn't support grouping select options with labels and dividers.
525
+ **Note:** Pill-shaped selects are not a standard pattern in our Design System, and there is no Figma component for this option. Please check with the design team before using this option.
299
526
  :::
300
527
 
301
528
  ```html:preview
302
- <zn-select label="Select an option from one of the groups">
303
- <zn-option value=""></zn-option>
304
- <small>Section 1</small>
529
+ <zn-select label="Medium pill" pill>
530
+ <zn-option value="option-1">Option 1</zn-option>
531
+ <zn-option value="option-2">Option 2</zn-option>
532
+ <zn-option value="option-3">Option 3</zn-option>
533
+ <zn-option value="option-4">Option 4</zn-option>
534
+ <zn-option value="option-5">Option 5</zn-option>
535
+ <zn-option value="option-6">Option 6</zn-option>
536
+ </zn-select>
537
+ <br />
538
+ <zn-select label="Large pill" size="large" pill>
305
539
  <zn-option value="option-1">Option 1</zn-option>
306
540
  <zn-option value="option-2">Option 2</zn-option>
307
541
  <zn-option value="option-3">Option 3</zn-option>
308
- <zn-divider></zn-divider>
309
- <small>Section 2</small>
310
542
  <zn-option value="option-4">Option 4</zn-option>
311
543
  <zn-option value="option-5">Option 5</zn-option>
312
544
  <zn-option value="option-6">Option 6</zn-option>
313
545
  </zn-select>
314
546
  ```
315
547
 
316
- ### Sizes
548
+ ### Multiple Selection
317
549
 
318
- Use the `size` attribute to change a select's size. Note that size does not apply to listbox options. Size `medium` is
319
- the selects default.
550
+ To allow multiple options to be selected, use the `multiple` attribute. When this option is enabled, be sure to also add the `clearable` attribute to display a clear button. To set multiple values at once, set `value` to a space-delimited list of values.
320
551
 
321
552
  ```html:preview
322
- <zn-select label="Medium input">
553
+ <zn-select label="Select one or more" value="option-1 option-2 option-3" multiple clearable>
323
554
  <zn-option value="option-1">Option 1</zn-option>
324
555
  <zn-option value="option-2">Option 2</zn-option>
325
556
  <zn-option value="option-3">Option 3</zn-option>
@@ -327,8 +558,18 @@ the selects default.
327
558
  <zn-option value="option-5">Option 5</zn-option>
328
559
  <zn-option value="option-6">Option 6</zn-option>
329
560
  </zn-select>
330
- <br />
331
- <zn-select label="Medium input" size="medium">
561
+ ```
562
+
563
+ :::tip
564
+ Note that multi-select options may wrap, causing the control to expand vertically. You can use the `max-options-visible` attribute to control the maximum number of selected options to show at once.
565
+ :::
566
+
567
+ ### Multiple with Limit
568
+
569
+ To limit the number of options that can be selected, use the `max-options` attribute.
570
+
571
+ ```html:preview
572
+ <zn-select label="Select up to 3 options" value="option-1 option-2" multiple clearable max-options="3">
332
573
  <zn-option value="option-1">Option 1</zn-option>
333
574
  <zn-option value="option-2">Option 2</zn-option>
334
575
  <zn-option value="option-3">Option 3</zn-option>
@@ -336,8 +577,14 @@ the selects default.
336
577
  <zn-option value="option-5">Option 5</zn-option>
337
578
  <zn-option value="option-6">Option 6</zn-option>
338
579
  </zn-select>
339
- <br />
340
- <zn-select label="Large input" size="large">
580
+ ```
581
+
582
+ ### Maximum Visible Options
583
+
584
+ When using `multiple`, you can control how many selected options are displayed before showing a "+n" indicator using the `max-options-visible` attribute. Set to 0 to remove the limit.
585
+
586
+ ```html:preview
587
+ <zn-select label="Show max 2 options" value="option-1 option-2 option-3 option-4" multiple clearable max-options-visible="2">
341
588
  <zn-option value="option-1">Option 1</zn-option>
342
589
  <zn-option value="option-2">Option 2</zn-option>
343
590
  <zn-option value="option-3">Option 3</zn-option>
@@ -347,13 +594,90 @@ the selects default.
347
594
  </zn-select>
348
595
  ```
349
596
 
597
+ ### Grouping Options
598
+
599
+ Use `<zn-opt-group>` to group options under labeled headers, similar to `<optgroup>` in native HTML. Each group displays a non-selectable label above its options. Groups are also accessible, using `role="group"` and `aria-label`.
600
+
601
+ ```html:preview
602
+ <zn-select label="Select a fruit">
603
+ <zn-opt-group label="Citrus">
604
+ <zn-option value="orange">Orange</zn-option>
605
+ <zn-option value="lemon">Lemon</zn-option>
606
+ <zn-option value="lime">Lime</zn-option>
607
+ </zn-opt-group>
608
+ <zn-opt-group label="Berries">
609
+ <zn-option value="strawberry">Strawberry</zn-option>
610
+ <zn-option value="blueberry">Blueberry</zn-option>
611
+ <zn-option value="raspberry">Raspberry</zn-option>
612
+ </zn-opt-group>
613
+ </zn-select>
614
+ ```
615
+
616
+ ### Grouping with Search
617
+
618
+ When `search` is enabled, group headers automatically hide when all of their child options are filtered out.
619
+
620
+ ```html:preview
621
+ <zn-select label="Select a country" search placeholder="Type to search...">
622
+ <zn-opt-group label="North America">
623
+ <zn-option value="us">United States</zn-option>
624
+ <zn-option value="ca">Canada</zn-option>
625
+ <zn-option value="mx">Mexico</zn-option>
626
+ </zn-opt-group>
627
+ <zn-opt-group label="Europe">
628
+ <zn-option value="gb">United Kingdom</zn-option>
629
+ <zn-option value="de">Germany</zn-option>
630
+ <zn-option value="fr">France</zn-option>
631
+ </zn-opt-group>
632
+ <zn-opt-group label="Asia">
633
+ <zn-option value="jp">Japan</zn-option>
634
+ <zn-option value="cn">China</zn-option>
635
+ <zn-option value="in">India</zn-option>
636
+ </zn-opt-group>
637
+ </zn-select>
638
+ ```
639
+
640
+ ### Disabled Groups
641
+
642
+ Use the `disabled` attribute on `<zn-opt-group>` to disable all options within a group.
643
+
644
+ ```html:preview
645
+ <zn-select label="Select a plan">
646
+ <zn-opt-group label="Available">
647
+ <zn-option value="basic">Basic</zn-option>
648
+ <zn-option value="pro">Pro</zn-option>
649
+ </zn-opt-group>
650
+ <zn-opt-group label="Coming Soon" disabled>
651
+ <zn-option value="enterprise">Enterprise</zn-option>
652
+ <zn-option value="unlimited">Unlimited</zn-option>
653
+ </zn-opt-group>
654
+ </zn-select>
655
+ ```
656
+
657
+ ### Mixed Grouped and Ungrouped Options
658
+
659
+ You can mix ungrouped options alongside `<zn-opt-group>` elements.
660
+
661
+ ```html:preview
662
+ <zn-select label="Select an option">
663
+ <zn-option value="none">None</zn-option>
664
+ <zn-opt-group label="Group A">
665
+ <zn-option value="a1">Option A1</zn-option>
666
+ <zn-option value="a2">Option A2</zn-option>
667
+ </zn-opt-group>
668
+ <zn-opt-group label="Group B">
669
+ <zn-option value="b1">Option B1</zn-option>
670
+ <zn-option value="b2">Option B2</zn-option>
671
+ </zn-opt-group>
672
+ </zn-select>
673
+ ```
674
+
350
675
  ### Placement
351
676
 
352
- The preferred placement of the select's listbox can be set with the `placement` attribute. Note that the actual position
353
- may vary to ensure the panel remains in the viewport. Valid placements are `top` and `bottom`.
677
+ The preferred placement of the select's listbox can be set with the `placement` attribute. Note that the actual position may vary to ensure the panel remains in the viewport. Valid placements are `top` and `bottom`.
354
678
 
355
679
  ```html:preview
356
- <zn-select label="Select an option" placement="top" help-text="This selects panel of options will try to open on top first if there is room">
680
+ <zn-select label="Select an option" placement="top" help-text="This select's panel of options will try to open on top first if there is room">
357
681
  <zn-option value="option-1">Option 1</zn-option>
358
682
  <zn-option value="option-2">Option 2</zn-option>
359
683
  <zn-option value="option-3">Option 3</zn-option>
@@ -363,6 +687,23 @@ may vary to ensure the panel remains in the viewport. Valid placements are `top`
363
687
  </zn-select>
364
688
  ```
365
689
 
690
+ ### Hoisting
691
+
692
+ Enable this option to prevent the listbox from being clipped when the component is placed inside a container with `overflow: auto|scroll`. Hoisting uses a fixed positioning strategy that works in many, but not all, scenarios.
693
+
694
+ ```html:preview
695
+ <div style="overflow: auto; height: 150px; border: 1px solid var(--zn-color-neutral-200); padding: 1rem;">
696
+ <zn-select label="With hoisting enabled" hoist>
697
+ <zn-option value="option-1">Option 1</zn-option>
698
+ <zn-option value="option-2">Option 2</zn-option>
699
+ <zn-option value="option-3">Option 3</zn-option>
700
+ <zn-option value="option-4">Option 4</zn-option>
701
+ <zn-option value="option-5">Option 5</zn-option>
702
+ <zn-option value="option-6">Option 6</zn-option>
703
+ </zn-select>
704
+ </div>
705
+ ```
706
+
366
707
  ### Prefix Icons
367
708
 
368
709
  Use the `prefix` slot to prepend an icon to the select.
@@ -376,12 +717,10 @@ Follow these general guidelines when adding prefix icons to the select:
376
717
  - In general **don't** resize icons or change their color from the default already set by the `zn-select` component
377
718
 
378
719
  :::warning
379
- **Note:** If you find your use case requires a different size or color from the default, bring it up to the Design Team
380
- so that we can consider whether the pattern needs to be updated.
720
+ **Note:** If you find your use case requires a different size or color from the default, bring it up to the Design Team so that we can consider whether the pattern needs to be updated.
381
721
  :::
382
722
  :::warning
383
- **Note:** `ts_form_for` doesn't support slots. Prefix icons cannot be added when rendering `zn-select` with
384
- `ts_form_for`.
723
+ **Note:** `ts_form_for` doesn't support slots. Prefix icons cannot be added when rendering `zn-select` with `ts_form_for`.
385
724
  :::
386
725
 
387
726
  ```html:preview
@@ -423,8 +762,7 @@ so that we can consider whether the pattern needs to be updated.
423
762
 
424
763
  ### Checkbox Prefix
425
764
 
426
- You can place a checkbox in the `prefix` slot to visually join it to the select. The checkbox will match the height of
427
- the control and can be interacted with independently (clicking or toggling it won’t open the select).
765
+ You can place a checkbox in the `prefix` slot to visually join it to the select. The checkbox will match the height of the control and can be interacted with independently (clicking or toggling it won't open the select).
428
766
 
429
767
  ```html:preview
430
768
  <zn-select label="Notify me about" help-text="Use the checkbox to enable/disable notifications; choose a channel with the select">
@@ -446,23 +784,15 @@ the control and can be interacted with independently (clicking or toggling it wo
446
784
 
447
785
  ### Custom Tags
448
786
 
449
- When multiple options can be selected, you can provide custom tags by passing a function to the `getTag` property. Your
450
- function can return a string of HTML, a <a href="https://lit.dev/docs/templates/overview/">Lit Template</a>, or an [
451
- `HTMLElement`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement). The `getTag()` function will be called for
452
- each option. The first argument is an `<zn-option>` element and the second argument is the tag's index (its position in
453
- the tag list).
787
+ When multiple options can be selected, you can provide custom tags by passing a function to the `getTag` property. Your function can return a string of HTML, a <a href="https://lit.dev/docs/templates/overview/">Lit Template</a>, or an [`HTMLElement`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement). The `getTag()` function will be called for each option. The first argument is an `<zn-option>` element and the second argument is the tag's index (its position in the tag list).
454
788
 
455
- Remember that custom tags are rendered in a shadow root. To style them, you can use the `style` attribute in your
456
- template or you can add your own [parts](/getting-started/customizing/#css-parts) and target them with the [
457
- `::part()`](https://developer.mozilla.org/en-US/docs/Web/CSS/::part) selector.
789
+ Remember that custom tags are rendered in a shadow root. To style them, you can use the `style` attribute in your template or you can add your own [parts](/getting-started/customizing/#css-parts) and target them with the [`::part()`](https://developer.mozilla.org/en-US/docs/Web/CSS/::part) selector.
458
790
 
459
791
  :::warning
460
- **Note:** In general, you shouldn't need to do this. If you are working on a design that requires custom styling for the
461
- tag, please ensure that there's not a standard tag in the design system that would work instead.
792
+ **Note:** In general, you shouldn't need to do this. If you are working on a design that requires custom styling for the tag, please ensure that there's not a standard tag in the design system that would work instead.
462
793
  :::
463
794
  :::warning
464
- **Note:** `ts_form_for` doesn't support slots. Custom tags cannot be added when rendering `zn-select` with
465
- `ts_form_for`.
795
+ **Note:** `ts_form_for` doesn't support slots. Custom tags cannot be added when rendering `zn-select` with `ts_form_for`.
466
796
  :::
467
797
 
468
798
  ```html:preview
@@ -506,6 +836,278 @@ tag, please ensure that there's not a standard tag in the design system that wou
506
836
  ```
507
837
 
508
838
  :::warning
509
- Be sure you trust the content you are outputting! Passing unsanitized user input to `getTag()` can result in XSS
510
- vulnerabilities.
511
- :::
839
+ Be sure you trust the content you are outputting! Passing unsanitized user input to `getTag()` can result in XSS vulnerabilities.
840
+ :::
841
+
842
+ ### Form Integration
843
+
844
+ Select components work seamlessly with standard HTML forms. The select's `name` attribute determines the key in the form data.
845
+
846
+ ```html:preview
847
+ <form id="select-form">
848
+ <zn-select name="favoriteColor" label="Favorite color" required>
849
+ <zn-option value="red">Red</zn-option>
850
+ <zn-option value="blue">Blue</zn-option>
851
+ <zn-option value="green">Green</zn-option>
852
+ <zn-option value="yellow">Yellow</zn-option>
853
+ </zn-select>
854
+ <br />
855
+ <zn-select name="hobbies" label="Hobbies" multiple clearable>
856
+ <zn-option value="reading">Reading</zn-option>
857
+ <zn-option value="gaming">Gaming</zn-option>
858
+ <zn-option value="cooking">Cooking</zn-option>
859
+ <zn-option value="sports">Sports</zn-option>
860
+ </zn-select>
861
+ <br />
862
+ <zn-button type="submit" variant="primary">Submit</zn-button>
863
+ <zn-button type="reset">Reset</zn-button>
864
+ </form>
865
+
866
+ <script type="module">
867
+ const form = document.getElementById('select-form');
868
+
869
+ form.addEventListener('submit', (e) => {
870
+ e.preventDefault();
871
+ const formData = new FormData(form);
872
+ const data = Object.fromEntries(formData);
873
+ alert('Form submitted!\n\n' + JSON.stringify(data, null, 2));
874
+ });
875
+ </script>
876
+ ```
877
+
878
+ ### Validation
879
+
880
+ Select components support native HTML validation attributes like `required`. The component will show validation states and messages automatically.
881
+
882
+ ```html:preview
883
+ <form id="validation-form" class="form-spacing">
884
+ <zn-select name="country" label="Country" required help-text="This field is required">
885
+ <zn-option value=""></zn-option>
886
+ <zn-option value="us">United States</zn-option>
887
+ <zn-option value="ca">Canada</zn-option>
888
+ <zn-option value="uk">United Kingdom</zn-option>
889
+ <zn-option value="au">Australia</zn-option>
890
+ </zn-select>
891
+
892
+ <zn-select name="interests" label="Select at least one interest" multiple clearable required help-text="Please select at least one option">
893
+ <zn-option value="tech">Technology</zn-option>
894
+ <zn-option value="art">Art</zn-option>
895
+ <zn-option value="science">Science</zn-option>
896
+ <zn-option value="music">Music</zn-option>
897
+ </zn-select>
898
+
899
+ <zn-button type="submit" variant="primary">Submit</zn-button>
900
+ </form>
901
+
902
+ <script type="module">
903
+ const validationForm = document.getElementById('validation-form');
904
+
905
+ validationForm.addEventListener('submit', (e) => {
906
+ e.preventDefault();
907
+ const formData = new FormData(validationForm);
908
+ const data = Object.fromEntries(formData);
909
+ alert('Form is valid!\n\n' + JSON.stringify(data, null, 2));
910
+ });
911
+ </script>
912
+ ```
913
+
914
+ You can also use custom validation with the `setCustomValidity()` method:
915
+
916
+ ```html:preview
917
+ <form id="custom-validation-form">
918
+ <zn-select id="age-range" name="ageRange" label="Age range">
919
+ <zn-option value="under-18">Under 18</zn-option>
920
+ <zn-option value="18-25">18-25</zn-option>
921
+ <zn-option value="26-35">26-35</zn-option>
922
+ <zn-option value="over-35">Over 35</zn-option>
923
+ </zn-select>
924
+ <br />
925
+ <zn-button type="submit" variant="primary">Submit</zn-button>
926
+ </form>
927
+
928
+ <script type="module">
929
+ const customValidationForm = document.getElementById('custom-validation-form');
930
+ const ageRange = document.getElementById('age-range');
931
+
932
+ ageRange.addEventListener('zn-change', () => {
933
+ if (ageRange.value === 'under-18') {
934
+ ageRange.setCustomValidity('You must be 18 or older to continue');
935
+ } else {
936
+ ageRange.setCustomValidity('');
937
+ }
938
+ });
939
+
940
+ customValidationForm.addEventListener('submit', (e) => {
941
+ e.preventDefault();
942
+ if (customValidationForm.reportValidity()) {
943
+ alert('Form is valid!');
944
+ }
945
+ });
946
+ </script>
947
+ ```
948
+
949
+ ### Events
950
+
951
+ The select component emits several events you can listen to:
952
+
953
+ ```html:preview
954
+ <div class="form-spacing">
955
+ <zn-select id="event-example" label="Select an option" multiple clearable>
956
+ <zn-option value="option-1">Option 1</zn-option>
957
+ <zn-option value="option-2">Option 2</zn-option>
958
+ <zn-option value="option-3">Option 3</zn-option>
959
+ </zn-select>
960
+
961
+ <div style="margin-top: 1rem; padding: 1rem; background: var(--zn-color-neutral-100); border-radius: 4px;">
962
+ <strong>Event Log:</strong>
963
+ <div id="event-log" style="margin-top: 0.5rem; font-family: monospace; font-size: 0.875rem;"></div>
964
+ </div>
965
+ </div>
966
+
967
+ <script type="module">
968
+ const select = document.getElementById('event-example');
969
+ const eventLog = document.getElementById('event-log');
970
+
971
+ function logEvent(eventName, detail) {
972
+ const time = new Date().toLocaleTimeString();
973
+ const message = `[${time}] ${eventName}${detail ? ': ' + JSON.stringify(detail) : ''}`;
974
+ eventLog.innerHTML = message + '<br>' + eventLog.innerHTML;
975
+ // Keep only last 5 events
976
+ const lines = eventLog.innerHTML.split('<br>');
977
+ if (lines.length > 5) {
978
+ eventLog.innerHTML = lines.slice(0, 5).join('<br>');
979
+ }
980
+ }
981
+
982
+ select.addEventListener('zn-change', (e) => logEvent('zn-change', { value: e.target.value }));
983
+ select.addEventListener('zn-input', (e) => logEvent('zn-input'));
984
+ select.addEventListener('zn-focus', (e) => logEvent('zn-focus'));
985
+ select.addEventListener('zn-blur', (e) => logEvent('zn-blur'));
986
+ select.addEventListener('zn-clear', (e) => logEvent('zn-clear'));
987
+ select.addEventListener('zn-show', (e) => logEvent('zn-show'));
988
+ select.addEventListener('zn-hide', (e) => logEvent('zn-hide'));
989
+ </script>
990
+ ```
991
+
992
+ ### Distinct Selects
993
+
994
+ Use the `distinct` attribute to link two selects so that selected values in one are hidden from the other. This is useful for scenarios where you want to prevent duplicate selections across multiple select controls.
995
+
996
+ ```html:preview
997
+ <div class="form-spacing">
998
+ <zn-select label="First select" multiple name="country-one" id="country-one" distinct="country-two">
999
+ <zn-option value="option-1">Option 1</zn-option>
1000
+ <zn-option value="option-2">Option 2</zn-option>
1001
+ <zn-option value="option-3">Option 3</zn-option>
1002
+ <zn-option value="option-4">Option 4</zn-option>
1003
+ </zn-select>
1004
+ <zn-select label="Second select" multiple name="country-two" id="country-two" distinct="country-one">
1005
+ <zn-option value="option-1">Option 1</zn-option>
1006
+ <zn-option value="option-2">Option 2</zn-option>
1007
+ <zn-option value="option-3">Option 3</zn-option>
1008
+ <zn-option value="option-4">Option 4</zn-option>
1009
+ </zn-select>
1010
+ </div>
1011
+ ```
1012
+
1013
+ ### Conditional Selects
1014
+
1015
+ Use the `conditional` attribute to disable a select based on whether another select has a value. You can specify multiple select IDs separated by commas.
1016
+
1017
+ ```html:preview
1018
+ <div class="form-spacing">
1019
+ <zn-select label="Enable/disable the second select" multiple name="conditional-one" id="conditional-one" conditional="conditional-two">
1020
+ <zn-option value="option-1">Option 1</zn-option>
1021
+ <zn-option value="option-2">Option 2</zn-option>
1022
+ <zn-option value="option-3">Option 3</zn-option>
1023
+ </zn-select>
1024
+ <zn-select label="This is disabled when first has value" multiple name="conditional-two" id="conditional-two" conditional="conditional-one">
1025
+ <zn-option value="option-1">Option 1</zn-option>
1026
+ <zn-option value="option-2">Option 2</zn-option>
1027
+ <zn-option value="option-3">Option 3</zn-option>
1028
+ </zn-select>
1029
+ </div>
1030
+ ```
1031
+
1032
+ ### Programmatic Control
1033
+
1034
+ You can control the select programmatically using its methods:
1035
+
1036
+ ```html:preview
1037
+ <div class="form-spacing">
1038
+ <zn-select id="programmatic-select" label="Programmatic control" multiple clearable>
1039
+ <zn-option value="option-1">Option 1</zn-option>
1040
+ <zn-option value="option-2">Option 2</zn-option>
1041
+ <zn-option value="option-3">Option 3</zn-option>
1042
+ <zn-option value="option-4">Option 4</zn-option>
1043
+ </zn-select>
1044
+
1045
+ <div style="margin-top: 1rem; display: flex; gap: 0.5rem; flex-wrap: wrap;">
1046
+ <zn-button id="show-btn" size="small">Show</zn-button>
1047
+ <zn-button id="hide-btn" size="small">Hide</zn-button>
1048
+ <zn-button id="focus-btn" size="small">Focus</zn-button>
1049
+ <zn-button id="set-value-btn" size="small">Set Value</zn-button>
1050
+ <zn-button id="check-validity-btn" size="small">Check Validity</zn-button>
1051
+ </div>
1052
+ </div>
1053
+
1054
+ <script type="module">
1055
+ const select = document.getElementById('programmatic-select');
1056
+
1057
+ document.getElementById('show-btn').addEventListener('click', () => select.show());
1058
+ document.getElementById('hide-btn').addEventListener('click', () => select.hide());
1059
+ document.getElementById('focus-btn').addEventListener('click', () => select.focus());
1060
+ document.getElementById('set-value-btn').addEventListener('click', () => {
1061
+ select.value = ['option-2', 'option-3'];
1062
+ });
1063
+ document.getElementById('check-validity-btn').addEventListener('click', () => {
1064
+ alert('Valid: ' + select.checkValidity());
1065
+ });
1066
+ </script>
1067
+ ```
1068
+
1069
+ ### Keyboard Navigation
1070
+
1071
+ The select component supports comprehensive keyboard navigation:
1072
+
1073
+ - **Tab** - Move focus to/from the select
1074
+ - **Enter** or **Space** - Open the dropdown (when closed) or select the current option (when open)
1075
+ - **Escape** - Close the dropdown
1076
+ - **Arrow Up/Down** - Navigate through options
1077
+ - **Home/End** - Jump to first/last option
1078
+ - **Type characters** - Jump to options starting with typed characters (type-to-select)
1079
+
1080
+ When `search` is enabled, **Space** types into the input instead of toggling the dropdown, and **Arrow Up/Down** skips filtered (hidden) options.
1081
+
1082
+ ### Customizing Label Position
1083
+
1084
+ Use [CSS parts](#css-parts) to customize the way form controls are drawn. This example uses CSS grid to position the label to the left of the control.
1085
+
1086
+ ```html:preview
1087
+ <zn-select class="label-on-left" label="Country">
1088
+ <zn-option value="us">United States</zn-option>
1089
+ <zn-option value="ca">Canada</zn-option>
1090
+ <zn-option value="uk">United Kingdom</zn-option>
1091
+ </zn-select>
1092
+
1093
+ <style>
1094
+ .label-on-left {
1095
+ --label-width: 5rem;
1096
+ }
1097
+
1098
+ .label-on-left::part(form-control) {
1099
+ display: grid;
1100
+ grid: auto / var(--label-width) 1fr;
1101
+ gap: var(--zn-spacing-3x-small) var(--zn-spacing-medium);
1102
+ align-items: center;
1103
+ }
1104
+
1105
+ .label-on-left::part(form-control-label) {
1106
+ text-align: right;
1107
+ }
1108
+
1109
+ .label-on-left::part(form-control-help-text) {
1110
+ grid-column-start: 2;
1111
+ }
1112
+ </style>
1113
+ ```