@redvars/peacock 3.4.0 → 3.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (200) hide show
  1. package/dist/assets/styles.css +1 -1
  2. package/dist/assets/styles.css.map +1 -1
  3. package/dist/banner.js +202 -0
  4. package/dist/banner.js.map +1 -0
  5. package/dist/bottom-sheet.js +2 -2
  6. package/dist/{button-COYCtuA8.js → button-DMN1dPAg.js} +58 -75
  7. package/dist/button-DMN1dPAg.js.map +1 -0
  8. package/dist/{button-group-DsXquZQn.js → button-group-CX9CUUXk.js} +9 -14
  9. package/dist/button-group-CX9CUUXk.js.map +1 -0
  10. package/dist/button-group.js +8 -5
  11. package/dist/button-group.js.map +1 -1
  12. package/dist/button.js +7 -4
  13. package/dist/button.js.map +1 -1
  14. package/dist/card.js +15 -5
  15. package/dist/card.js.map +1 -1
  16. package/dist/chart-bar.js +2 -2
  17. package/dist/chart-bar.js.map +1 -1
  18. package/dist/chart-doughnut.js.map +1 -1
  19. package/dist/chart-pie.js.map +1 -1
  20. package/dist/chart-stacked-bar.js +2 -2
  21. package/dist/chart-stacked-bar.js.map +1 -1
  22. package/dist/{class-map-3TAnCMAX.js → class-map-YU7g0o3B.js} +2 -2
  23. package/dist/{class-map-3TAnCMAX.js.map → class-map-YU7g0o3B.js.map} +1 -1
  24. package/dist/clock.js.map +1 -1
  25. package/dist/code-editor.js +4 -4
  26. package/dist/code-editor.js.map +1 -1
  27. package/dist/code-highlighter.js +3 -3
  28. package/dist/code-highlighter.js.map +1 -1
  29. package/dist/custom-elements-jsdocs.json +2918 -1379
  30. package/dist/custom-elements.json +2783 -1054
  31. package/dist/directive-ZPhl09Yt.js +9 -0
  32. package/dist/directive-ZPhl09Yt.js.map +1 -0
  33. package/dist/dispatch-event-utils-CuEqjlPT.js +127 -0
  34. package/dist/dispatch-event-utils-CuEqjlPT.js.map +1 -0
  35. package/dist/fab-C5Nzxk0E.js +497 -0
  36. package/dist/fab-C5Nzxk0E.js.map +1 -0
  37. package/dist/fab.js +11 -0
  38. package/dist/fab.js.map +1 -0
  39. package/dist/index.js +17 -9
  40. package/dist/index.js.map +1 -1
  41. package/dist/{observe-theme-change-DKAIv5BB.js → is-dark-mode-DicqGkCJ.js} +6 -2
  42. package/dist/is-dark-mode-DicqGkCJ.js.map +1 -0
  43. package/dist/notification.js +417 -0
  44. package/dist/notification.js.map +1 -0
  45. package/dist/number-counter.js +2 -2
  46. package/dist/number-counter.js.map +1 -1
  47. package/dist/observe-slot-change-BGJfgg2E.js +31 -0
  48. package/dist/observe-slot-change-BGJfgg2E.js.map +1 -0
  49. package/dist/peacock-loader.js +32 -9
  50. package/dist/peacock-loader.js.map +1 -1
  51. package/dist/search.js +452 -0
  52. package/dist/search.js.map +1 -0
  53. package/dist/{select-C3XAzenC.js → select-4pl4XBj7.js} +778 -374
  54. package/dist/select-4pl4XBj7.js.map +1 -0
  55. package/dist/side-sheet.js +2 -2
  56. package/dist/spread-B5cgadZl.js +32 -0
  57. package/dist/spread-B5cgadZl.js.map +1 -0
  58. package/dist/src/__base_element/BaseHyperlink.d.ts +20 -0
  59. package/dist/src/__utils/cache-fetch.d.ts +1 -0
  60. package/dist/src/__utils/is-dark-mode.d.ts +1 -0
  61. package/dist/src/__utils/is-in-viewport.d.ts +1 -0
  62. package/dist/src/__utils/observe-slot-change.d.ts +1 -0
  63. package/dist/src/__utils/sanitize-svg.d.ts +1 -0
  64. package/dist/src/__utils/throttle.d.ts +4 -0
  65. package/dist/src/accordion/accordion-item.d.ts +33 -9
  66. package/dist/src/accordion/accordion.d.ts +21 -5
  67. package/dist/src/banner/banner.d.ts +47 -0
  68. package/dist/src/banner/index.d.ts +1 -0
  69. package/dist/src/button/BaseButton.d.ts +6 -13
  70. package/dist/src/button/button-group/button-group.d.ts +2 -2
  71. package/dist/src/empty-state/empty-state.d.ts +1 -1
  72. package/dist/src/fab/fab.d.ts +111 -0
  73. package/dist/src/fab/index.d.ts +1 -0
  74. package/dist/src/index.d.ts +5 -0
  75. package/dist/src/link/link.d.ts +3 -10
  76. package/dist/src/menu/menu/menu.d.ts +3 -2
  77. package/dist/src/menu/sub-menu/sub-menu.d.ts +1 -0
  78. package/dist/src/notification/index.d.ts +1 -0
  79. package/dist/src/notification/notification.d.ts +69 -0
  80. package/dist/src/pagination/pagination.d.ts +8 -1
  81. package/dist/src/search/index.d.ts +1 -0
  82. package/dist/src/search/search.d.ts +76 -0
  83. package/dist/src/select/select.d.ts +3 -5
  84. package/dist/src/slider/slider.d.ts +4 -0
  85. package/dist/src/snackbar/snackbar.d.ts +14 -1
  86. package/dist/src/toolbar/index.d.ts +1 -0
  87. package/dist/src/toolbar/toolbar.d.ts +86 -0
  88. package/dist/{style-map-CRFEoCEg.js → style-map-DVmWOuYy.js} +2 -2
  89. package/dist/{style-map-CRFEoCEg.js.map → style-map-DVmWOuYy.js.map} +1 -1
  90. package/dist/test/banner.test.d.ts +1 -0
  91. package/dist/test/search.test.d.ts +1 -0
  92. package/dist/test/toolbar.test.d.ts +1 -0
  93. package/dist/throttle-C7ZAPqtu.js +24 -0
  94. package/dist/throttle-C7ZAPqtu.js.map +1 -0
  95. package/dist/toolbar.js +306 -0
  96. package/dist/toolbar.js.map +1 -0
  97. package/dist/tsconfig.tsbuildinfo +1 -1
  98. package/dist/{unsafe-html-D3GHRaGQ.js → unsafe-html-BsGUjx94.js} +2 -2
  99. package/dist/{unsafe-html-D3GHRaGQ.js.map → unsafe-html-BsGUjx94.js.map} +1 -1
  100. package/package.json +1 -1
  101. package/readme.md +2 -2
  102. package/scss/styles.scss +4 -0
  103. package/src/__base_element/BaseHyperlink.ts +42 -0
  104. package/src/__base_element/README.md +19 -0
  105. package/src/__utils/cache-fetch.ts +65 -0
  106. package/src/__utils/is-dark-mode.ts +3 -0
  107. package/src/__utils/is-in-viewport.ts +6 -0
  108. package/src/__utils/observe-slot-change.ts +38 -0
  109. package/src/__utils/sanitize-svg.ts +27 -0
  110. package/src/__utils/throttle.ts +27 -0
  111. package/src/accordion/accordion-item.scss +136 -65
  112. package/src/accordion/accordion-item.ts +117 -44
  113. package/src/accordion/accordion.scss +24 -5
  114. package/src/accordion/accordion.ts +29 -23
  115. package/src/accordion/demo/index.html +74 -35
  116. package/src/banner/banner.scss +87 -0
  117. package/src/banner/banner.ts +107 -0
  118. package/src/banner/index.ts +1 -0
  119. package/src/button/BaseButton.ts +14 -27
  120. package/src/button/button/button-colors.scss +14 -14
  121. package/src/button/button/button.ts +6 -5
  122. package/src/button/button-group/button-group.ts +3 -3
  123. package/src/button/icon-button/icon-button.ts +4 -11
  124. package/src/card/card.ts +41 -31
  125. package/src/chart-bar/chart-bar.ts +1 -1
  126. package/src/chart-bar/chart-stacked-bar.ts +3 -1
  127. package/src/chart-doughnut/chart-doughnut.ts +1 -1
  128. package/src/chart-pie/chart-pie.ts +1 -1
  129. package/src/checkbox/checkbox.ts +1 -1
  130. package/src/clock/clock.ts +1 -1
  131. package/src/code-editor/code-editor.ts +4 -4
  132. package/src/code-highlighter/code-highlighter.ts +2 -2
  133. package/src/date-picker/date-picker.ts +5 -2
  134. package/src/divider/divider.ts +3 -1
  135. package/src/empty-state/empty-state.scss +7 -9
  136. package/src/empty-state/empty-state.ts +1 -1
  137. package/src/fab/fab-colors.scss +49 -0
  138. package/src/fab/fab-sizes.scss +47 -0
  139. package/src/fab/fab.scss +137 -0
  140. package/src/fab/fab.ts +285 -0
  141. package/src/fab/index.ts +1 -0
  142. package/src/field/field.ts +3 -1
  143. package/src/icon/datasource.ts +1 -1
  144. package/src/icon/icon.ts +3 -1
  145. package/src/image/image.ts +3 -2
  146. package/src/index.ts +5 -0
  147. package/src/input/input.ts +5 -2
  148. package/src/link/link.ts +2 -15
  149. package/src/menu/menu/menu.scss +7 -0
  150. package/src/menu/menu/menu.ts +7 -4
  151. package/src/menu/sub-menu/sub-menu.ts +1 -0
  152. package/src/notification/index.ts +1 -0
  153. package/src/notification/notification.scss +201 -0
  154. package/src/notification/notification.ts +206 -0
  155. package/src/number-counter/number-counter.ts +3 -1
  156. package/src/number-field/number-field.ts +4 -2
  157. package/src/pagination/pagination.scss +33 -24
  158. package/src/pagination/pagination.ts +113 -60
  159. package/src/peacock-loader.ts +20 -0
  160. package/src/radio/radio.ts +3 -1
  161. package/src/search/index.ts +1 -0
  162. package/src/search/search-colors.scss +14 -0
  163. package/src/search/search.scss +204 -0
  164. package/src/search/search.ts +240 -0
  165. package/src/select/option.ts +1 -1
  166. package/src/select/select.scss +5 -0
  167. package/src/select/select.ts +71 -37
  168. package/src/slider/slider.scss +19 -0
  169. package/src/slider/slider.ts +30 -19
  170. package/src/snackbar/snackbar.scss +62 -31
  171. package/src/snackbar/snackbar.ts +92 -12
  172. package/src/switch/switch.ts +3 -1
  173. package/src/table/table.ts +3 -1
  174. package/src/tabs/tab.ts +6 -3
  175. package/src/textarea/textarea.ts +4 -2
  176. package/src/time-picker/time-picker.ts +4 -2
  177. package/src/toolbar/index.ts +1 -0
  178. package/src/toolbar/toolbar-colors.scss +16 -0
  179. package/src/toolbar/toolbar.scss +165 -0
  180. package/src/toolbar/toolbar.ts +137 -0
  181. package/dist/button-COYCtuA8.js.map +0 -1
  182. package/dist/button-group-DsXquZQn.js.map +0 -1
  183. package/dist/directive-Cuw6h7YA.js +0 -9
  184. package/dist/directive-Cuw6h7YA.js.map +0 -1
  185. package/dist/dispatch-event-utils-B4odODQf.js +0 -277
  186. package/dist/dispatch-event-utils-B4odODQf.js.map +0 -1
  187. package/dist/observe-theme-change-DKAIv5BB.js.map +0 -1
  188. package/dist/select-C3XAzenC.js.map +0 -1
  189. package/dist/src/styleMixins.css.d.ts +0 -9
  190. package/dist/src/utils.d.ts +0 -9
  191. package/src/styleMixins.css.ts +0 -55
  192. package/src/utils.ts +0 -193
  193. /package/dist/src/{spread.d.ts → __directive/spread.d.ts} +0 -0
  194. /package/dist/src/{utils → __utils}/copy-to-clipboard.d.ts +0 -0
  195. /package/dist/src/{utils → __utils}/dispatch-event-utils.d.ts +0 -0
  196. /package/dist/src/{utils → __utils}/observe-theme-change.d.ts +0 -0
  197. /package/src/{spread.ts → __directive/spread.ts} +0 -0
  198. /package/src/{utils → __utils}/copy-to-clipboard.ts +0 -0
  199. /package/src/{utils → __utils}/dispatch-event-utils.ts +0 -0
  200. /package/src/{utils → __utils}/observe-theme-change.ts +0 -0
@@ -0,0 +1,240 @@
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
+ this.dispatchEvent(
114
+ new CustomEvent('input', {
115
+ detail: { value: this.value },
116
+ bubbles: true,
117
+ composed: true,
118
+ }),
119
+ );
120
+ }
121
+
122
+ private __handleChange(event: Event) {
123
+ const input = event.target as HTMLInputElement;
124
+ this.value = input.value;
125
+ this.dispatchEvent(
126
+ new CustomEvent('change', {
127
+ detail: { value: this.value },
128
+ bubbles: true,
129
+ composed: true,
130
+ }),
131
+ );
132
+ }
133
+
134
+ private __handleKeydown(event: KeyboardEvent) {
135
+ if (event.key === 'Enter') {
136
+ this.dispatchEvent(
137
+ new CustomEvent('search', {
138
+ detail: { value: this.value },
139
+ bubbles: true,
140
+ composed: true,
141
+ }),
142
+ );
143
+ }
144
+ if (event.key === 'Escape') {
145
+ this.__clearValue();
146
+ }
147
+ }
148
+
149
+ private __handleFocus() {
150
+ this.focused = true;
151
+ }
152
+
153
+ private __handleBlur() {
154
+ this.focused = false;
155
+ }
156
+
157
+ private __clearValue() {
158
+ this.value = '';
159
+ this.inputElement?.focus();
160
+ this.dispatchEvent(
161
+ new CustomEvent('clear', {
162
+ bubbles: true,
163
+ composed: true,
164
+ }),
165
+ );
166
+ this.dispatchEvent(
167
+ new CustomEvent('input', {
168
+ detail: { value: '' },
169
+ bubbles: true,
170
+ composed: true,
171
+ }),
172
+ );
173
+ }
174
+
175
+ private __renderClearButton() {
176
+ if (!this.clearable || !this.value) return nothing;
177
+
178
+ return html`
179
+ <button
180
+ class="clear-button"
181
+ aria-label="Clear search"
182
+ tabindex="-1"
183
+ @click=${this.__clearValue}
184
+ ?disabled=${this.disabled}
185
+ >
186
+ <wc-icon name="close"></wc-icon>
187
+ </button>
188
+ `;
189
+ }
190
+
191
+ private __renderLeadingIcon() {
192
+ return html`
193
+ <div class="leading-icon ${this.leadingSlotHasContent ? 'has-slot' : ''}">
194
+ <slot name="leading">
195
+ <wc-icon name="search"></wc-icon>
196
+ </slot>
197
+ </div>
198
+ `;
199
+ }
200
+
201
+ override render() {
202
+ const cssClasses = {
203
+ search: true,
204
+ [`variant-${this.variant}`]: true,
205
+ [`size-${this.size}`]: true,
206
+ focused: this.focused,
207
+ disabled: this.disabled,
208
+ 'has-value': !!this.value,
209
+ };
210
+
211
+ return html`
212
+ <div class=${classMap(cssClasses)} role="search">
213
+ <div class="background"></div>
214
+ <div class="outline"></div>
215
+
216
+ ${this.__renderLeadingIcon()}
217
+
218
+ <input
219
+ class="search-input"
220
+ type="search"
221
+ role="searchbox"
222
+ .value=${live(this.value)}
223
+ placeholder=${this.placeholder}
224
+ ?disabled=${this.disabled}
225
+ aria-label=${this.placeholder}
226
+ @input=${this.__handleInput}
227
+ @change=${this.__handleChange}
228
+ @keydown=${this.__handleKeydown}
229
+ @focus=${this.__handleFocus}
230
+ @blur=${this.__handleBlur}
231
+ />
232
+
233
+ <div class="trailing-actions">
234
+ ${this.__renderClearButton()}
235
+ <slot name="trailing"></slot>
236
+ </div>
237
+ </div>
238
+ `;
239
+ }
240
+ }
@@ -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
  }
@@ -15,6 +15,17 @@
15
15
  touch-action: none; // Prevent scrolling while dragging
16
16
  }
17
17
 
18
+ .slider {
19
+ display: flex;
20
+ align-items: center;
21
+ gap: var(--spacing-100, 0.5rem);
22
+ width: 100%;
23
+ }
24
+
25
+ .slider.with-value .slider-container {
26
+ flex: 1;
27
+ }
28
+
18
29
  .slider-container {
19
30
  position: relative;
20
31
  display: flex;
@@ -29,6 +40,14 @@
29
40
  }
30
41
  }
31
42
 
43
+ .value-display {
44
+ min-width: 2.25rem;
45
+ text-align: end;
46
+ color: var(--color-on-surface-variant);
47
+ font-size: 0.875rem;
48
+ font-weight: 500;
49
+ }
50
+
32
51
  .track {
33
52
  position: absolute;
34
53
  width: 100%;
@@ -51,9 +51,15 @@ export class Slider extends LitElement {
51
51
  */
52
52
  @property({ type: Boolean }) labeled = true;
53
53
 
54
+ /**
55
+ * Whether to show the current value beside the slider.
56
+ */
57
+ @property({ type: Boolean, attribute: 'show-value' }) showValue = false;
58
+
54
59
  @state() private isDragging = false;
55
60
 
56
61
  @query('.slider-container') private container!: HTMLElement;
62
+
57
63
  @query('.thumb') private thumbElement!: HTMLElement;
58
64
 
59
65
  private handleInput(event: MouseEvent | TouchEvent) {
@@ -150,28 +156,33 @@ export class Slider extends LitElement {
150
156
  const percentage = ((this.value - this.min) / (this.max - this.min)) * 100;
151
157
 
152
158
  return html`
153
- <div
154
- class="slider-container ${this.disabled ? 'disabled' : ''}"
155
- @mousedown=${this.onMouseDown}
156
- @touchstart=${this.onMouseDown}
157
- >
158
- <div class="track">
159
- <div class="track-active" style=${styleMap({ width: `${percentage}%` })}></div>
160
- </div>
161
-
159
+ <div class="slider ${this.showValue ? 'with-value' : ''}">
162
160
  <div
163
- class="thumb"
164
- role="slider"
165
- tabindex="${this.disabled ? -1 : 0}"
166
- aria-valuemin=${this.min}
167
- aria-valuemax=${this.max}
168
- aria-valuenow=${this.value}
169
- aria-disabled=${this.disabled}
170
- style=${styleMap({ left: `calc(${percentage}% - var(--thumb-half))` })}
171
- @keydown=${this.handleKeyDown}
161
+ class="slider-container ${this.disabled ? 'disabled' : ''}"
162
+ @mousedown=${this.onMouseDown}
163
+ @touchstart=${this.onMouseDown}
172
164
  >
173
- ${this.labeled ? html`<div class="value-label">${this.value}</div>` : ''}
165
+ <div class="track">
166
+ <div class="track-active" style=${styleMap({ width: `${percentage}%` })}></div>
167
+ </div>
168
+
169
+ <div
170
+ class="thumb"
171
+ role="slider"
172
+ aria-label="Slider"
173
+ tabindex="${this.disabled ? -1 : 0}"
174
+ aria-valuemin=${this.min}
175
+ aria-valuemax=${this.max}
176
+ aria-valuenow=${this.value}
177
+ aria-disabled=${this.disabled}
178
+ style=${styleMap({ left: `calc(${percentage}% - var(--thumb-half))` })}
179
+ @keydown=${this.handleKeyDown}
180
+ >
181
+ ${this.labeled ? html`<div class="value-label">${this.value}</div>` : ''}
182
+ </div>
174
183
  </div>
184
+
185
+ ${this.showValue ? html`<output class="value-display" aria-live="polite">${this.value}</output>` : ''}
175
186
  </div>
176
187
  `;
177
188
  }