@redvars/peacock 3.4.0 → 3.5.1

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 (258) hide show
  1. package/dist/BaseButton-DuASuVth.js +219 -0
  2. package/dist/BaseButton-DuASuVth.js.map +1 -0
  3. package/dist/BaseHyperlinkMixin-BNuwbiEf.js +65 -0
  4. package/dist/BaseHyperlinkMixin-BNuwbiEf.js.map +1 -0
  5. package/dist/assets/components.css +1 -1
  6. package/dist/assets/components.css.map +1 -1
  7. package/dist/assets/styles.css +1 -1
  8. package/dist/assets/styles.css.map +1 -1
  9. package/dist/banner.js +187 -0
  10. package/dist/banner.js.map +1 -0
  11. package/dist/bottom-sheet.js +2 -2
  12. package/dist/{button-COYCtuA8.js → button-DouvOfEU.js} +92 -283
  13. package/dist/button-DouvOfEU.js.map +1 -0
  14. package/dist/{button-group-DsXquZQn.js → button-group-CEdMwvJJ.js} +72 -48
  15. package/dist/button-group-CEdMwvJJ.js.map +1 -0
  16. package/dist/button-group.js +8 -5
  17. package/dist/button-group.js.map +1 -1
  18. package/dist/button.js +7 -4
  19. package/dist/button.js.map +1 -1
  20. package/dist/card.js +29 -74
  21. package/dist/card.js.map +1 -1
  22. package/dist/chart-bar.js +2 -2
  23. package/dist/chart-bar.js.map +1 -1
  24. package/dist/chart-doughnut.js +2 -2
  25. package/dist/chart-doughnut.js.map +1 -1
  26. package/dist/chart-pie.js +2 -2
  27. package/dist/chart-pie.js.map +1 -1
  28. package/dist/chart-stacked-bar.js +2 -2
  29. package/dist/chart-stacked-bar.js.map +1 -1
  30. package/dist/{class-map-3TAnCMAX.js → class-map-YU7g0o3B.js} +2 -2
  31. package/dist/{class-map-3TAnCMAX.js.map → class-map-YU7g0o3B.js.map} +1 -1
  32. package/dist/clock.js.map +1 -1
  33. package/dist/code-editor.js +4 -4
  34. package/dist/code-editor.js.map +1 -1
  35. package/dist/code-highlighter.js +5 -4
  36. package/dist/code-highlighter.js.map +1 -1
  37. package/dist/custom-elements-jsdocs.json +6627 -3477
  38. package/dist/custom-elements.json +10954 -7810
  39. package/dist/directive-ZPhl09Yt.js +9 -0
  40. package/dist/directive-ZPhl09Yt.js.map +1 -0
  41. package/dist/dispatch-event-utils-CuEqjlPT.js +127 -0
  42. package/dist/dispatch-event-utils-CuEqjlPT.js.map +1 -0
  43. package/dist/fab.js +423 -0
  44. package/dist/fab.js.map +1 -0
  45. package/dist/index.js +17 -9
  46. package/dist/index.js.map +1 -1
  47. package/dist/{observe-theme-change-DKAIv5BB.js → is-dark-mode-DicqGkCJ.js} +6 -2
  48. package/dist/is-dark-mode-DicqGkCJ.js.map +1 -0
  49. package/dist/{select-C3XAzenC.js → navigation-rail-Lxetd5-Z.js} +2426 -898
  50. package/dist/navigation-rail-Lxetd5-Z.js.map +1 -0
  51. package/dist/notification.js +418 -0
  52. package/dist/notification.js.map +1 -0
  53. package/dist/number-counter.js +2 -2
  54. package/dist/number-counter.js.map +1 -1
  55. package/dist/observe-slot-change-BGJfgg2E.js +31 -0
  56. package/dist/observe-slot-change-BGJfgg2E.js.map +1 -0
  57. package/dist/peacock-loader.js +48 -13
  58. package/dist/peacock-loader.js.map +1 -1
  59. package/dist/search.js +456 -0
  60. package/dist/search.js.map +1 -0
  61. package/dist/side-sheet.js +2 -2
  62. package/dist/src/__mixins/BaseButtonMixin.d.ts +20 -0
  63. package/dist/src/__mixins/BaseHyperlinkMixin.d.ts +18 -0
  64. package/dist/src/__mixins/MixinConstructor.d.ts +1 -0
  65. package/dist/src/__utils/cache-fetch.d.ts +1 -0
  66. package/dist/src/__utils/is-dark-mode.d.ts +1 -0
  67. package/dist/src/__utils/is-in-viewport.d.ts +1 -0
  68. package/dist/src/__utils/observe-slot-change.d.ts +1 -0
  69. package/dist/src/__utils/sanitize-svg.d.ts +1 -0
  70. package/dist/src/__utils/throttle.d.ts +4 -0
  71. package/dist/src/accordion/accordion-item.d.ts +33 -9
  72. package/dist/src/accordion/accordion.d.ts +21 -5
  73. package/dist/src/banner/banner.d.ts +43 -0
  74. package/dist/src/banner/index.d.ts +1 -0
  75. package/dist/src/button/BaseButton.d.ts +7 -57
  76. package/dist/src/button/button/button.d.ts +32 -3
  77. package/dist/src/button/button-group/button-group.d.ts +4 -4
  78. package/dist/src/button/icon-button/icon-button.d.ts +33 -8
  79. package/dist/src/card/card.d.ts +4 -15
  80. package/dist/src/empty-state/empty-state.d.ts +1 -1
  81. package/dist/src/fab/fab.d.ts +80 -0
  82. package/dist/src/fab/index.d.ts +1 -0
  83. package/dist/src/focus-ring/focus-ring.d.ts +11 -5
  84. package/dist/src/index.d.ts +8 -1
  85. package/dist/src/link/link.d.ts +3 -10
  86. package/dist/src/menu/menu/menu.d.ts +3 -2
  87. package/dist/src/menu/sub-menu/sub-menu.d.ts +1 -0
  88. package/dist/src/navigation-rail/index.d.ts +2 -0
  89. package/dist/src/navigation-rail/navigation-rail-item.d.ts +55 -0
  90. package/dist/src/navigation-rail/navigation-rail.d.ts +71 -0
  91. package/dist/src/notification/index.d.ts +1 -0
  92. package/dist/src/notification/notification.d.ts +69 -0
  93. package/dist/src/pagination/pagination.d.ts +8 -1
  94. package/dist/src/search/index.d.ts +1 -0
  95. package/dist/src/search/search.d.ts +76 -0
  96. package/dist/src/select/select.d.ts +3 -5
  97. package/dist/src/sidebar-menu/index.d.ts +3 -0
  98. package/dist/src/sidebar-menu/sidebar-menu-item.d.ts +58 -0
  99. package/dist/src/sidebar-menu/sidebar-menu.d.ts +38 -0
  100. package/dist/src/sidebar-menu/sidebar-sub-menu.d.ts +35 -0
  101. package/dist/src/slider/slider.d.ts +4 -0
  102. package/dist/src/snackbar/snackbar.d.ts +14 -1
  103. package/dist/src/toolbar/index.d.ts +1 -0
  104. package/dist/src/toolbar/toolbar.d.ts +86 -0
  105. package/dist/src/tooltip/tooltip.d.ts +3 -0
  106. package/dist/src/url-field/index.d.ts +1 -0
  107. package/dist/src/url-field/url-field.d.ts +48 -0
  108. package/dist/{style-map-CRFEoCEg.js → style-map-DVmWOuYy.js} +2 -2
  109. package/dist/{style-map-CRFEoCEg.js.map → style-map-DVmWOuYy.js.map} +1 -1
  110. package/dist/test/banner.test.d.ts +1 -0
  111. package/dist/test/search.test.d.ts +1 -0
  112. package/dist/test/sidebar-menu.test.d.ts +1 -0
  113. package/dist/test/toolbar.test.d.ts +1 -0
  114. package/dist/toolbar.js +306 -0
  115. package/dist/toolbar.js.map +1 -0
  116. package/dist/tsconfig.tsbuildinfo +1 -1
  117. package/dist/{unsafe-html-D3GHRaGQ.js → unsafe-html-BsGUjx94.js} +2 -2
  118. package/dist/{unsafe-html-D3GHRaGQ.js.map → unsafe-html-BsGUjx94.js.map} +1 -1
  119. package/package.json +1 -1
  120. package/readme.md +73 -65
  121. package/scss/mixin.scss +16 -0
  122. package/scss/styles.scss +4 -0
  123. package/src/__mixins/BaseButtonMixin.ts +83 -0
  124. package/src/__mixins/BaseHyperlinkMixin.ts +68 -0
  125. package/src/__mixins/MixinConstructor.ts +1 -0
  126. package/src/__mixins/README.md +19 -0
  127. package/src/__utils/cache-fetch.ts +65 -0
  128. package/src/__utils/is-dark-mode.ts +3 -0
  129. package/src/__utils/is-in-viewport.ts +6 -0
  130. package/src/__utils/observe-slot-change.ts +38 -0
  131. package/src/__utils/sanitize-svg.ts +27 -0
  132. package/src/__utils/throttle.ts +27 -0
  133. package/src/accordion/accordion-item.scss +136 -65
  134. package/src/accordion/accordion-item.ts +117 -44
  135. package/src/accordion/accordion.scss +24 -5
  136. package/src/accordion/accordion.ts +29 -23
  137. package/src/accordion/demo/index.html +74 -35
  138. package/src/banner/banner.scss +83 -0
  139. package/src/banner/banner.ts +101 -0
  140. package/src/banner/index.ts +1 -0
  141. package/src/button/BaseButton.ts +13 -115
  142. package/src/button/button/button-colors.scss +14 -14
  143. package/src/button/button/button-sizes.scss +4 -2
  144. package/src/button/button/button.ts +80 -26
  145. package/src/button/button-group/button-group.ts +5 -5
  146. package/src/button/icon-button/icon-button.ts +79 -44
  147. package/src/card/card.ts +50 -100
  148. package/src/chart-bar/chart-bar.ts +10 -15
  149. package/src/chart-bar/chart-stacked-bar.ts +15 -19
  150. package/src/chart-doughnut/chart-doughnut.ts +24 -28
  151. package/src/chart-pie/chart-pie.ts +20 -24
  152. package/src/checkbox/checkbox.scss +17 -34
  153. package/src/checkbox/checkbox.ts +4 -2
  154. package/src/clock/clock.ts +1 -1
  155. package/src/code-editor/code-editor.ts +4 -4
  156. package/src/code-highlighter/code-highlighter.scss +1 -0
  157. package/src/code-highlighter/code-highlighter.ts +3 -3
  158. package/src/date-picker/date-picker.ts +6 -3
  159. package/src/divider/divider.ts +3 -1
  160. package/src/elevation/elevation.scss +5 -5
  161. package/src/empty-state/empty-state.scss +7 -9
  162. package/src/empty-state/empty-state.ts +1 -1
  163. package/src/fab/fab-colors.scss +49 -0
  164. package/src/fab/fab-sizes.scss +47 -0
  165. package/src/fab/fab.scss +137 -0
  166. package/src/fab/fab.ts +214 -0
  167. package/src/fab/index.ts +1 -0
  168. package/src/field/field.ts +3 -1
  169. package/src/focus-ring/focus-ring.ts +47 -40
  170. package/src/icon/datasource.ts +1 -1
  171. package/src/icon/icon.ts +3 -1
  172. package/src/image/image.ts +3 -2
  173. package/src/index.ts +8 -1
  174. package/src/input/input.ts +8 -3
  175. package/src/link/link.ts +2 -15
  176. package/src/menu/menu/menu.scss +7 -0
  177. package/src/menu/menu/menu.ts +7 -4
  178. package/src/menu/menu-item/menu-item.ts +3 -1
  179. package/src/menu/sub-menu/sub-menu.ts +1 -0
  180. package/src/navigation-rail/index.ts +2 -0
  181. package/src/navigation-rail/navigation-rail-item.scss +216 -0
  182. package/src/navigation-rail/navigation-rail-item.ts +223 -0
  183. package/src/navigation-rail/navigation-rail.scss +72 -0
  184. package/src/navigation-rail/navigation-rail.ts +149 -0
  185. package/src/notification/index.ts +1 -0
  186. package/src/notification/notification.scss +201 -0
  187. package/src/notification/notification.ts +207 -0
  188. package/src/number-counter/number-counter.ts +3 -1
  189. package/src/number-field/number-field.ts +10 -6
  190. package/src/pagination/pagination.scss +33 -24
  191. package/src/pagination/pagination.ts +115 -60
  192. package/src/peacock-loader.ts +42 -5
  193. package/src/radio/radio.ts +3 -1
  194. package/src/search/index.ts +1 -0
  195. package/src/search/search-colors.scss +14 -0
  196. package/src/search/search.scss +204 -0
  197. package/src/search/search.ts +244 -0
  198. package/src/select/option.ts +1 -1
  199. package/src/select/select.scss +5 -0
  200. package/src/select/select.ts +71 -37
  201. package/src/sidebar-menu/demo/index.html +68 -0
  202. package/src/sidebar-menu/index.ts +3 -0
  203. package/src/sidebar-menu/sidebar-menu-item.scss +102 -0
  204. package/src/sidebar-menu/sidebar-menu-item.ts +151 -0
  205. package/src/{tree-view/tree-view.scss → sidebar-menu/sidebar-menu.scss} +1 -1
  206. package/src/sidebar-menu/sidebar-menu.ts +182 -0
  207. package/src/sidebar-menu/sidebar-sub-menu.scss +130 -0
  208. package/src/sidebar-menu/sidebar-sub-menu.ts +160 -0
  209. package/src/skeleton/skeleton.scss +18 -24
  210. package/src/slider/slider.scss +19 -0
  211. package/src/slider/slider.ts +30 -19
  212. package/src/snackbar/snackbar.scss +62 -31
  213. package/src/snackbar/snackbar.ts +91 -11
  214. package/src/switch/switch.ts +3 -1
  215. package/src/table/table.ts +3 -1
  216. package/src/tabs/tab.ts +10 -6
  217. package/src/text/text.css-component.scss +7 -1
  218. package/src/textarea/textarea.ts +4 -2
  219. package/src/time-picker/time-picker.ts +5 -3
  220. package/src/toolbar/index.ts +1 -0
  221. package/src/toolbar/toolbar-colors.scss +16 -0
  222. package/src/toolbar/toolbar.scss +165 -0
  223. package/src/toolbar/toolbar.ts +137 -0
  224. package/src/tooltip/tooltip.ts +24 -0
  225. package/src/url-field/index.ts +1 -0
  226. package/src/url-field/url-field.scss +50 -0
  227. package/src/url-field/url-field.ts +239 -0
  228. package/dist/button-COYCtuA8.js.map +0 -1
  229. package/dist/button-group-DsXquZQn.js.map +0 -1
  230. package/dist/directive-Cuw6h7YA.js +0 -9
  231. package/dist/directive-Cuw6h7YA.js.map +0 -1
  232. package/dist/dispatch-event-utils-B4odODQf.js +0 -277
  233. package/dist/dispatch-event-utils-B4odODQf.js.map +0 -1
  234. package/dist/observe-theme-change-DKAIv5BB.js.map +0 -1
  235. package/dist/select-C3XAzenC.js.map +0 -1
  236. package/dist/src/styleMixins.css.d.ts +0 -9
  237. package/dist/src/tree-view/index.d.ts +0 -2
  238. package/dist/src/tree-view/tree-node.d.ts +0 -69
  239. package/dist/src/tree-view/tree-view.d.ts +0 -40
  240. package/dist/src/tree-view/wc-tree-view.d.ts +0 -6
  241. package/dist/src/utils.d.ts +0 -9
  242. package/dist/test/tree-view.test.d.ts +0 -1
  243. package/src/styleMixins.css.ts +0 -55
  244. package/src/tree-view/demo/index.html +0 -57
  245. package/src/tree-view/index.ts +0 -2
  246. package/src/tree-view/tree-node.scss +0 -101
  247. package/src/tree-view/tree-node.ts +0 -268
  248. package/src/tree-view/tree-view.ts +0 -182
  249. package/src/tree-view/wc-tree-view.ts +0 -9
  250. package/src/utils.ts +0 -193
  251. /package/dist/src/{spread.d.ts → __directive/spread.d.ts} +0 -0
  252. /package/dist/src/{utils → __utils}/copy-to-clipboard.d.ts +0 -0
  253. /package/dist/src/{utils → __utils}/dispatch-event-utils.d.ts +0 -0
  254. /package/dist/src/{utils → __utils}/observe-theme-change.d.ts +0 -0
  255. /package/src/{spread.ts → __directive/spread.ts} +0 -0
  256. /package/src/{utils → __utils}/copy-to-clipboard.ts +0 -0
  257. /package/src/{utils → __utils}/dispatch-event-utils.ts +0 -0
  258. /package/src/{utils → __utils}/observe-theme-change.ts +0 -0
@@ -0,0 +1,244 @@
1
+ import { LitElement, html, nothing } from 'lit';
2
+ import { property, query, state } from 'lit/decorators.js';
3
+ import { classMap } from 'lit/directives/class-map.js';
4
+ import { live } from 'lit/directives/live.js';
5
+ import IndividualComponent from '@/IndividualComponent.js';
6
+ import styles from './search.scss';
7
+ import colorStyles from './search-colors.scss';
8
+ import { observerSlotChangesWithCallback } from '@/__utils/observe-slot-change.js';
9
+
10
+ /**
11
+ * @label Search
12
+ * @tag wc-search
13
+ * @rawTag search
14
+ *
15
+ * @summary A Material 3 search bar for filtering and finding content.
16
+ * @overview
17
+ * <p>The search component provides a text input designed for search interactions.
18
+ * It supports outlined and filled variants, an optional clear button, and leading/trailing icon slots.</p>
19
+ *
20
+ * @cssprop --search-container-shape - Border radius of the search bar. Defaults to full (pill shape).
21
+ * @cssprop --search-container-color - Background color of the search container.
22
+ * @cssprop --search-input-text-color - Color of the input text.
23
+ * @cssprop --search-placeholder-color - Color of the placeholder text.
24
+ * @cssprop --search-icon-color - Color of the leading and trailing icons.
25
+ * @cssprop --search-outline-color - Border color for the outlined variant.
26
+ * @cssprop --search-outline-width - Border width for the outlined variant.
27
+ *
28
+ * @fires {CustomEvent} input - Dispatched when the search value changes.
29
+ * @fires {CustomEvent} change - Dispatched when the search input loses focus or Enter is pressed.
30
+ * @fires {CustomEvent} clear - Dispatched when the clear button is activated.
31
+ * @fires {CustomEvent} search - Dispatched when the user submits the search (presses Enter).
32
+ *
33
+ * @example
34
+ * ```html
35
+ * <wc-search placeholder="Search..."></wc-search>
36
+ * ```
37
+ * @tags form
38
+ */
39
+ @IndividualComponent
40
+ export class Search extends LitElement {
41
+ static styles = [styles, colorStyles];
42
+
43
+ /**
44
+ * Visual style variant.
45
+ * Possible values: `"outlined"`, `"filled"`. Defaults to `"filled"`.
46
+ */
47
+ @property({ type: String, reflect: true })
48
+ variant: 'outlined' | 'filled' = 'filled';
49
+
50
+ /**
51
+ * Placeholder text shown when the input is empty.
52
+ */
53
+ @property({ type: String })
54
+ placeholder: string = 'Search';
55
+
56
+ /**
57
+ * Current search value.
58
+ */
59
+ @property({ type: String })
60
+ value: string = '';
61
+
62
+ /**
63
+ * Whether the search bar is disabled.
64
+ */
65
+ @property({ type: Boolean, reflect: true })
66
+ disabled: boolean = false;
67
+
68
+ /**
69
+ * Whether a clear button is shown when the input has a value.
70
+ */
71
+ @property({ type: Boolean })
72
+ clearable: boolean = true;
73
+
74
+ /**
75
+ * Size of the search bar.
76
+ * Possible values: `"sm"`, `"md"`, `"lg"`. Defaults to `"md"`.
77
+ */
78
+ @property({ type: String, reflect: true })
79
+ size: 'sm' | 'md' | 'lg' = 'md';
80
+
81
+ @state()
82
+ private focused: boolean = false;
83
+
84
+ @state()
85
+ private leadingSlotHasContent: boolean = false;
86
+
87
+ @query('.search-input')
88
+ private inputElement?: HTMLInputElement;
89
+
90
+ override firstUpdated() {
91
+ observerSlotChangesWithCallback(
92
+ this.renderRoot.querySelector('slot[name="leading"]'),
93
+ hasContent => {
94
+ this.leadingSlotHasContent = hasContent;
95
+ this.requestUpdate();
96
+ },
97
+ );
98
+ }
99
+
100
+ /** Focuses the internal input element. */
101
+ override focus() {
102
+ this.inputElement?.focus();
103
+ }
104
+
105
+ /** Blurs the internal input element. */
106
+ override blur() {
107
+ this.inputElement?.blur();
108
+ }
109
+
110
+ private __handleInput(event: InputEvent) {
111
+ const input = event.target as HTMLInputElement;
112
+ this.value = input.value;
113
+ // Prevent the native input event from escaping in addition to our API event.
114
+ event.stopPropagation();
115
+ this.dispatchEvent(
116
+ new CustomEvent('input', {
117
+ detail: { value: this.value },
118
+ bubbles: true,
119
+ composed: true,
120
+ }),
121
+ );
122
+ }
123
+
124
+ private __handleChange(event: Event) {
125
+ const input = event.target as HTMLInputElement;
126
+ this.value = input.value;
127
+ // Prevent the native change event from escaping in addition to our API event.
128
+ event.stopPropagation();
129
+ this.dispatchEvent(
130
+ new CustomEvent('change', {
131
+ detail: { value: this.value },
132
+ bubbles: true,
133
+ composed: true,
134
+ }),
135
+ );
136
+ }
137
+
138
+ private __handleKeydown(event: KeyboardEvent) {
139
+ if (event.key === 'Enter') {
140
+ this.dispatchEvent(
141
+ new CustomEvent('search', {
142
+ detail: { value: this.value },
143
+ bubbles: true,
144
+ composed: true,
145
+ }),
146
+ );
147
+ }
148
+ if (event.key === 'Escape') {
149
+ this.__clearValue();
150
+ }
151
+ }
152
+
153
+ private __handleFocus() {
154
+ this.focused = true;
155
+ }
156
+
157
+ private __handleBlur() {
158
+ this.focused = false;
159
+ }
160
+
161
+ private __clearValue() {
162
+ this.value = '';
163
+ this.inputElement?.focus();
164
+ this.dispatchEvent(
165
+ new CustomEvent('clear', {
166
+ bubbles: true,
167
+ composed: true,
168
+ }),
169
+ );
170
+ this.dispatchEvent(
171
+ new CustomEvent('input', {
172
+ detail: { value: '' },
173
+ bubbles: true,
174
+ composed: true,
175
+ }),
176
+ );
177
+ }
178
+
179
+ private __renderClearButton() {
180
+ if (!this.clearable || !this.value) return nothing;
181
+
182
+ return html`
183
+ <button
184
+ class="clear-button"
185
+ aria-label="Clear search"
186
+ tabindex="-1"
187
+ @click=${this.__clearValue}
188
+ ?disabled=${this.disabled}
189
+ >
190
+ <wc-icon name="close"></wc-icon>
191
+ </button>
192
+ `;
193
+ }
194
+
195
+ private __renderLeadingIcon() {
196
+ return html`
197
+ <div class="leading-icon ${this.leadingSlotHasContent ? 'has-slot' : ''}">
198
+ <slot name="leading">
199
+ <wc-icon name="search"></wc-icon>
200
+ </slot>
201
+ </div>
202
+ `;
203
+ }
204
+
205
+ override render() {
206
+ const cssClasses = {
207
+ search: true,
208
+ [`variant-${this.variant}`]: true,
209
+ [`size-${this.size}`]: true,
210
+ focused: this.focused,
211
+ disabled: this.disabled,
212
+ 'has-value': !!this.value,
213
+ };
214
+
215
+ return html`
216
+ <div class=${classMap(cssClasses)} role="search">
217
+ <div class="background"></div>
218
+ <div class="outline"></div>
219
+
220
+ ${this.__renderLeadingIcon()}
221
+
222
+ <input
223
+ class="search-input"
224
+ type="search"
225
+ role="searchbox"
226
+ .value=${live(this.value)}
227
+ placeholder=${this.placeholder}
228
+ ?disabled=${this.disabled}
229
+ aria-label=${this.placeholder}
230
+ @input=${this.__handleInput}
231
+ @change=${this.__handleChange}
232
+ @keydown=${this.__handleKeydown}
233
+ @focus=${this.__handleFocus}
234
+ @blur=${this.__handleBlur}
235
+ />
236
+
237
+ <div class="trailing-actions">
238
+ ${this.__renderClearButton()}
239
+ <slot name="trailing"></slot>
240
+ </div>
241
+ </div>
242
+ `;
243
+ }
244
+ }
@@ -96,7 +96,7 @@ export class SelectOptionElement extends LitElement {
96
96
  ${this.icon
97
97
  ? html`<wc-icon name=${this.icon} slot="leading-icon"></wc-icon>`
98
98
  : nothing}
99
- <slot></slot>
99
+ <slot>${this.value === '' ? 'None' : ''}</slot>
100
100
  ${this.selected && this.keepOpen
101
101
  ? html`<wc-icon
102
102
  name="check"
@@ -73,6 +73,11 @@
73
73
  gap: var(--spacing-050);
74
74
  }
75
75
 
76
+ .select-empty-state {
77
+ display: block;
78
+ inline-size: min(22rem, 100%);
79
+ }
80
+
76
81
  /* Dropdown chevron icon */
77
82
  .dropdown-icon {
78
83
  --icon-size: 1.5rem;
@@ -38,6 +38,8 @@ export interface SelectOption {
38
38
  export class Select extends BaseInput {
39
39
  static styles = [styles];
40
40
 
41
+ private readonly _menuId = `wc-select-menu-${Math.random().toString(36).slice(2, 9)}`;
42
+
41
43
  /**
42
44
  * Array of options to display in the dropdown.
43
45
  * Setting this property creates matching `<wc-option>` children automatically.
@@ -77,12 +79,6 @@ export class Select extends BaseInput {
77
79
  @property({ type: String })
78
80
  label: string = '';
79
81
 
80
- /**
81
- * Show a clear button in single-select mode when a value is selected.
82
- */
83
- @property({ type: Boolean })
84
- clearable: boolean = false;
85
-
86
82
  /**
87
83
  * Visual variant of the field.
88
84
  */
@@ -160,7 +156,7 @@ export class Select extends BaseInput {
160
156
  const el = new SelectOptionElement();
161
157
  el.value = opt.value;
162
158
  if (opt.icon) el.icon = opt.icon;
163
- el.textContent = opt.label;
159
+ el.textContent = opt.label || (opt.value === '' ? 'None' : '');
164
160
  el.dataset.generated = '';
165
161
  this.appendChild(el);
166
162
  }
@@ -184,13 +180,13 @@ export class Select extends BaseInput {
184
180
  const q = this._searchQuery.toLowerCase();
185
181
  const label = opt.textContent?.trim() ?? '';
186
182
  opt.filtered = !label.toLowerCase().includes(q);
187
- if (!opt.filtered) visibleCount++;
183
+ if (!opt.filtered) visibleCount += 1;
188
184
  } else {
189
185
  opt.filtered = false;
190
- visibleCount++;
186
+ visibleCount += 1;
191
187
  }
192
188
  }
193
- this._noOptionsVisible = optEls.length > 0 && visibleCount === 0;
189
+ this._noOptionsVisible = visibleCount === 0;
194
190
  }
195
191
 
196
192
  // ── Helpers ────────────────────────────────────────────────────────────────
@@ -204,16 +200,25 @@ export class Select extends BaseInput {
204
200
  }
205
201
 
206
202
  private _isSelected(value: string): boolean {
203
+ if (!this.multiple) {
204
+ return this.value === value;
205
+ }
207
206
  return this._selectedValues.includes(value);
208
207
  }
209
208
 
210
209
  /** Returns the display label for a given option value. */
211
210
  private _getLabelForValue(val: string): string {
212
211
  for (const opt of this.querySelectorAll<SelectOptionElement>('wc-option')) {
213
- if (opt.value === val) return opt.textContent?.trim() ?? val;
212
+ if (opt.value === val) {
213
+ const label = opt.textContent?.trim();
214
+ if (label) return label;
215
+ return val === '' ? 'None' : val;
216
+ }
214
217
  }
215
218
  // Fallback to options array (before wc-option children are created)
216
- return this.options.find(o => o.value === val)?.label ?? val;
219
+ const programmaticLabel = this.options.find(o => o.value === val)?.label;
220
+ if (programmaticLabel) return programmaticLabel;
221
+ return val === '' ? 'None' : val;
217
222
  }
218
223
 
219
224
  private get _displayLabel(): string {
@@ -233,9 +238,16 @@ export class Select extends BaseInput {
233
238
  if (this.disabled || this.readonly) return;
234
239
  this._open = true;
235
240
  this._focused = true;
241
+ this._triggerEl?.focus();
236
242
  const menu = this._menu;
237
243
  if (menu && this._triggerEl) {
238
244
  menu.anchorElement = this._triggerEl;
245
+ const triggerWidth = this._triggerEl.getBoundingClientRect().width;
246
+ if (triggerWidth < 240) {
247
+ menu.style.setProperty('--menu-width', '240px');
248
+ } else {
249
+ menu.style.setProperty('--menu-width', `${Math.ceil(triggerWidth)}px`);
250
+ }
239
251
  menu.show();
240
252
  }
241
253
  if (this.search) {
@@ -261,6 +273,7 @@ export class Select extends BaseInput {
261
273
  // ── Event handlers ─────────────────────────────────────────────────────────
262
274
 
263
275
  private _handleTriggerClick(event: MouseEvent) {
276
+ event.stopPropagation();
264
277
  // Ignore clicks that originated inside the search input — those should not
265
278
  // toggle the menu (the input needs to stay open so the user can type).
266
279
  if (event.target instanceof HTMLInputElement) {
@@ -273,6 +286,29 @@ export class Select extends BaseInput {
273
286
  }
274
287
  }
275
288
 
289
+ private _handleFieldClick(event: MouseEvent) {
290
+ const eventPath = event.composedPath();
291
+
292
+ if (
293
+ eventPath.includes(this._triggerEl as EventTarget) ||
294
+ eventPath.some(
295
+ target =>
296
+ target instanceof HTMLInputElement ||
297
+ (target instanceof HTMLElement &&
298
+ (target.closest('.clear-btn') != null ||
299
+ target.matches('wc-icon-button'))),
300
+ )
301
+ ) {
302
+ return;
303
+ }
304
+
305
+ if (this._open) {
306
+ this._closeMenu();
307
+ } else {
308
+ this._openMenu();
309
+ }
310
+ }
311
+
276
312
  private _handleTriggerKeyDown(event: KeyboardEvent) {
277
313
  // When the typeahead search input is active, let the input handle its own
278
314
  // keys (Space, Enter, etc.). Only intercept Escape to close the menu.
@@ -312,9 +348,11 @@ export class Select extends BaseInput {
312
348
  if (!item) return;
313
349
 
314
350
  const val = item.value;
315
- if (!val) return;
351
+
352
+ if (val === undefined) return;
316
353
 
317
354
  if (this.multiple) {
355
+ if (val === '') return;
318
356
  const values = this._selectedValues;
319
357
  const idx = values.indexOf(val);
320
358
  if (idx >= 0) {
@@ -354,12 +392,6 @@ export class Select extends BaseInput {
354
392
  }
355
393
  }
356
394
 
357
- private _handleClear(event: MouseEvent) {
358
- event.stopPropagation();
359
- this.value = '';
360
- this._dispatchChange();
361
- }
362
-
363
395
  private _handleChipDismiss(event: CustomEvent, chipValue: string) {
364
396
  event.stopPropagation();
365
397
  const values = this._selectedValues.filter(v => v !== chipValue);
@@ -367,6 +399,21 @@ export class Select extends BaseInput {
367
399
  this._dispatchChange();
368
400
  }
369
401
 
402
+ private _renderEmptyState() {
403
+ const hasSearchQuery = this._searchQuery.trim().length > 0;
404
+
405
+ return html`
406
+ <wc-empty-state
407
+ class="select-empty-state content-center"
408
+ illustration="no-document"
409
+ headline=${hasSearchQuery ? 'No results found' : 'No options available'}
410
+ description=${hasSearchQuery
411
+ ? 'Try a different search term.'
412
+ : 'There is nothing to select right now.'}
413
+ ></wc-empty-state>
414
+ `;
415
+ }
416
+
370
417
  // ── Render helpers ─────────────────────────────────────────────────────────
371
418
 
372
419
  private _renderTriggerContent() {
@@ -406,22 +453,7 @@ export class Select extends BaseInput {
406
453
  }
407
454
 
408
455
  private _renderFieldEnd() {
409
- const showClear =
410
- this.clearable &&
411
- !this.multiple &&
412
- !!this.value &&
413
- !this.disabled &&
414
- !this.readonly;
415
456
  return html`
416
- ${showClear
417
- ? html`<wc-icon-button
418
- class="clear-btn"
419
- variant="text"
420
- size="sm"
421
- name="close"
422
- @click=${this._handleClear}
423
- ></wc-icon-button>`
424
- : nothing}
425
457
  <wc-icon
426
458
  class=${classMap({
427
459
  'dropdown-icon': true,
@@ -450,11 +482,14 @@ export class Select extends BaseInput {
450
482
  ?focused=${this._focused}
451
483
  .host=${this}
452
484
  class="select-field"
485
+ @click=${this._handleFieldClick}
453
486
  >
454
487
  <div
455
488
  class="select-trigger"
456
489
  tabindex=${this.disabled ? -1 : 0}
457
490
  role="combobox"
491
+ aria-label=${this.label || this.placeholder || 'Select option'}
492
+ aria-controls=${this._menuId}
458
493
  aria-expanded=${String(this._open)}
459
494
  aria-haspopup="listbox"
460
495
  @click=${this._handleTriggerClick}
@@ -469,6 +504,7 @@ export class Select extends BaseInput {
469
504
  </wc-field>
470
505
 
471
506
  <wc-menu
507
+ id=${this._menuId}
472
508
  placement="bottom-start"
473
509
  aria-label=${this.label || 'Options'}
474
510
  @closed=${this._handleMenuClosed}
@@ -476,9 +512,7 @@ export class Select extends BaseInput {
476
512
  this._handleMenuItemActivate(e)}
477
513
  >
478
514
  <slot></slot>
479
- ${this._noOptionsVisible
480
- ? html`<wc-menu-item disabled>No options</wc-menu-item>`
481
- : nothing}
515
+ ${this._noOptionsVisible ? this._renderEmptyState() : nothing}
482
516
  </wc-menu>
483
517
  `;
484
518
  }
@@ -0,0 +1,68 @@
1
+ <!doctype html>
2
+ <html lang='en-GB'>
3
+ <head>
4
+ <meta charset='utf-8'>
5
+ <meta name='viewport' content='width=device-width, initial-scale=1.0, viewport-fit=cover' />
6
+ <link rel='stylesheet' href='/dist/assets/styles/tokens.css' />
7
+ <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+Mono:wght@100..900&family=Noto+Sans:ital,wght@0,100..900;1,100..900&display=swap" rel="stylesheet">
8
+
9
+ <style>
10
+ body {
11
+ background: #fafafa;
12
+ padding: 2rem;
13
+ font-family: 'Noto Sans', sans-serif;
14
+ }
15
+ h2 {
16
+ margin-top: 2rem;
17
+ margin-bottom: 0.5rem;
18
+ }
19
+ </style>
20
+ </head>
21
+ <body>
22
+
23
+ <h2>Basic Sidebar Menu</h2>
24
+ <wc-sidebar-menu>
25
+ <wc-sidebar-sub-menu label="Documents" icon="folder" expanded>
26
+ <wc-sidebar-sub-menu label="Work" icon="folder" expanded>
27
+ <wc-sidebar-menu-item label="Project A" icon="description"></wc-sidebar-menu-item>
28
+ <wc-sidebar-menu-item label="Project B" icon="description"></wc-sidebar-menu-item>
29
+ </wc-sidebar-sub-menu>
30
+ <wc-sidebar-menu-item label="Personal" icon="folder"></wc-sidebar-menu-item>
31
+ </wc-sidebar-sub-menu>
32
+ <wc-sidebar-sub-menu label="Pictures" icon="image" expanded>
33
+ <wc-sidebar-menu-item label="Vacation 2024" icon="image"></wc-sidebar-menu-item>
34
+ <wc-sidebar-menu-item label="Family" icon="image"></wc-sidebar-menu-item>
35
+ </wc-sidebar-sub-menu>
36
+ </wc-sidebar-menu>
37
+
38
+ <h2>Sidebar Menu without icons</h2>
39
+ <wc-sidebar-menu>
40
+ <wc-sidebar-sub-menu label="Category 1" expanded>
41
+ <wc-sidebar-menu-item label="Subcategory 1.1"></wc-sidebar-menu-item>
42
+ <wc-sidebar-menu-item label="Subcategory 1.2"></wc-sidebar-menu-item>
43
+ </wc-sidebar-sub-menu>
44
+ <wc-sidebar-menu-item label="Category 2"></wc-sidebar-menu-item>
45
+ </wc-sidebar-menu>
46
+
47
+ <h2>Disabled items</h2>
48
+ <wc-sidebar-menu>
49
+ <wc-sidebar-sub-menu label="Enabled" expanded>
50
+ <wc-sidebar-menu-item label="Child 1"></wc-sidebar-menu-item>
51
+ <wc-sidebar-menu-item label="Child 2" disabled></wc-sidebar-menu-item>
52
+ </wc-sidebar-sub-menu>
53
+ <wc-sidebar-menu-item label="Disabled" disabled></wc-sidebar-menu-item>
54
+ </wc-sidebar-menu>
55
+
56
+ <script type='module'>
57
+ import '/dist/index.es2017.js';
58
+
59
+ const menu = document.querySelector('wc-sidebar-menu');
60
+ if (menu) {
61
+ menu.addEventListener('sidebar-menu:change', (e) => {
62
+ console.log('Selected item:', e.detail);
63
+ });
64
+ }
65
+ </script>
66
+
67
+ </body>
68
+ </html>
@@ -0,0 +1,3 @@
1
+ export { SidebarMenuItem } from './sidebar-menu-item.js';
2
+ export { SidebarSubMenu } from './sidebar-sub-menu.js';
3
+ export { SidebarMenu } from './sidebar-menu.js';
@@ -0,0 +1,102 @@
1
+ @use '../../scss/mixin';
2
+
3
+ @include mixin.base-styles;
4
+
5
+ :host {
6
+ display: block;
7
+
8
+ // M3 sidebar menu item sizing
9
+ --sidebar-menu-item-height: 3rem;
10
+ --sidebar-menu-item-icon-size: 1.25rem;
11
+ --sidebar-menu-item-border-radius: var(--global-shape-corner-full, 9999px);
12
+
13
+ // M3 color tokens
14
+ --sidebar-menu-item-label-color: var(--color-on-surface);
15
+ --sidebar-menu-item-icon-color: var(--color-on-surface-variant);
16
+ --sidebar-menu-item-selected-background: var(--color-secondary-container);
17
+ --sidebar-menu-item-selected-color: var(--color-on-secondary-container);
18
+ --sidebar-menu-item-focus-ring-color: var(--color-primary);
19
+
20
+ // M3 animation timing
21
+ --sidebar-menu-item-transition-duration: 200ms;
22
+ --sidebar-menu-item-transition-easing: cubic-bezier(0.4, 0, 0.2, 1);
23
+ }
24
+
25
+ .sidebar-menu-item {
26
+ position: relative;
27
+ display: flex;
28
+ align-items: center;
29
+ min-height: var(--sidebar-menu-item-height);
30
+ border-radius: var(--sidebar-menu-item-border-radius);
31
+ color: var(--_label-color);
32
+ cursor: pointer;
33
+ user-select: none;
34
+ text-decoration: none;
35
+ outline: none;
36
+
37
+ --_container-color: transparent;
38
+ --_container-opacity: 1;
39
+ --_label-color: var(--sidebar-menu-item-label-color);
40
+
41
+ @include mixin.get-typography(label-large);
42
+
43
+ &.selected {
44
+ --_container-color: var(--sidebar-menu-item-selected-background);
45
+ --_label-color: var(--sidebar-menu-item-selected-color);
46
+ }
47
+
48
+ &.disabled {
49
+ cursor: not-allowed;
50
+ opacity: 0.6;
51
+
52
+ .ripple {
53
+ display: none;
54
+ }
55
+ }
56
+ }
57
+
58
+ .sidebar-menu-item-content {
59
+ display: flex;
60
+ align-items: center;
61
+ gap: 0.25rem;
62
+ flex: 1;
63
+ min-height: var(--sidebar-menu-item-height);
64
+ padding-inline: 0.75rem;
65
+ color: inherit;
66
+ z-index: 1;
67
+ }
68
+
69
+ .background {
70
+ position: absolute;
71
+ inset: 0;
72
+ background-color: var(--_container-color);
73
+ opacity: var(--_container-opacity);
74
+ border-radius: inherit;
75
+ pointer-events: none;
76
+ }
77
+
78
+ .focus-ring {
79
+ z-index: 2;
80
+ --focus-ring-color: var(--sidebar-menu-item-focus-ring-color);
81
+ --focus-ring-container-shape-start-start: var(--sidebar-menu-item-border-radius);
82
+ --focus-ring-container-shape-start-end: var(--sidebar-menu-item-border-radius);
83
+ --focus-ring-container-shape-end-start: var(--sidebar-menu-item-border-radius);
84
+ --focus-ring-container-shape-end-end: var(--sidebar-menu-item-border-radius);
85
+ }
86
+
87
+ .ripple {
88
+ --ripple-pressed-color: var(--color-on-surface);
89
+ border-radius: inherit;
90
+ }
91
+
92
+ .sidebar-menu-item.selected .ripple {
93
+ --ripple-pressed-color: var(--sidebar-menu-item-selected-color);
94
+ }
95
+
96
+ .sidebar-menu-item-label {
97
+ flex: 1;
98
+ overflow: hidden;
99
+ text-overflow: ellipsis;
100
+ white-space: nowrap;
101
+ }
102
+