@redvars/peacock 3.3.2 → 3.4.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.
- package/dist/IndividualComponent-DUINtMGK.js +67 -0
- package/dist/IndividualComponent-DUINtMGK.js.map +1 -0
- package/dist/assets/images/empty-state/no-document.svg +11 -12
- package/dist/assets/images/empty-state/page.svg +15 -9
- package/dist/bottom-sheet.js +238 -0
- package/dist/bottom-sheet.js.map +1 -0
- package/dist/{button-ClzS8JLq.js → button-COYCtuA8.js} +306 -149
- package/dist/button-COYCtuA8.js.map +1 -0
- package/dist/button-group-DsXquZQn.js +440 -0
- package/dist/button-group-DsXquZQn.js.map +1 -0
- package/dist/button-group.js +6 -4
- package/dist/button-group.js.map +1 -1
- package/dist/button.js +5 -3
- package/dist/button.js.map +1 -1
- package/dist/card-content.js +29 -0
- package/dist/card-content.js.map +1 -0
- package/dist/card.js +418 -44
- package/dist/card.js.map +1 -1
- package/dist/{chart-bar-DbnXQgvS.js → chart-bar-cn6rrna-.js} +2 -2
- package/dist/{chart-bar-DbnXQgvS.js.map → chart-bar-cn6rrna-.js.map} +1 -1
- package/dist/chart-bar.js +4 -3
- package/dist/chart-bar.js.map +1 -1
- package/dist/chart-doughnut.js +2 -1
- package/dist/chart-doughnut.js.map +1 -1
- package/dist/chart-pie.js +2 -1
- package/dist/chart-pie.js.map +1 -1
- package/dist/chart-stacked-bar.js +4 -3
- package/dist/chart-stacked-bar.js.map +1 -1
- package/dist/{class-map-59YGWLnx.js → class-map-3TAnCMAX.js} +3 -9
- package/dist/class-map-3TAnCMAX.js.map +1 -0
- package/dist/clock.js +2 -1
- package/dist/clock.js.map +1 -1
- package/dist/code-editor.js +6 -4
- package/dist/code-editor.js.map +1 -1
- package/dist/code-highlighter.js +5 -3
- package/dist/code-highlighter.js.map +1 -1
- package/dist/custom-elements-jsdocs.json +2458 -2753
- package/dist/custom-elements.json +3185 -777
- package/dist/dispatch-event-utils-B4odODQf.js.map +1 -1
- package/dist/index.js +14 -10
- package/dist/index.js.map +1 -1
- package/dist/number-counter.js +3 -2
- package/dist/number-counter.js.map +1 -1
- package/dist/{observe-theme-change-pALI5fmV.js → observe-theme-change-DKAIv5BB.js} +3 -2
- package/dist/observe-theme-change-DKAIv5BB.js.map +1 -0
- package/dist/peacock-loader.js +37 -8
- package/dist/peacock-loader.js.map +1 -1
- package/dist/property-1psGvXOq.js +10 -0
- package/dist/property-1psGvXOq.js.map +1 -0
- package/dist/{snackbar-74YCdMPL.js → select-C3XAzenC.js} +2158 -191
- package/dist/select-C3XAzenC.js.map +1 -0
- package/dist/side-sheet.js +186 -0
- package/dist/side-sheet.js.map +1 -0
- package/dist/src/bottom-sheet/bottom-sheet.d.ts +42 -0
- package/dist/src/bottom-sheet/index.d.ts +1 -0
- package/dist/src/button/BaseButton.d.ts +4 -3
- package/dist/src/button/button/button.d.ts +4 -0
- package/dist/src/button/button-group/button-group.d.ts +32 -3
- package/dist/src/button/icon-button/icon-button.d.ts +4 -0
- package/dist/src/card/card-content.d.ts +15 -0
- package/dist/src/card/card.d.ts +37 -3
- package/dist/src/card/index.d.ts +1 -0
- package/dist/src/container/container.d.ts +1 -1
- package/dist/src/empty-state/empty-state.d.ts +1 -1
- package/dist/src/focus-ring/focus-ring.d.ts +4 -1
- package/dist/src/index.d.ts +7 -1
- package/dist/src/menu/menu/menu.d.ts +1 -0
- package/dist/src/menu/menu-item/menu-item.d.ts +0 -1
- package/dist/src/radio/index.d.ts +1 -0
- package/dist/src/radio/radio.d.ts +73 -0
- package/dist/src/ripple/ripple.d.ts +19 -3
- package/dist/src/segmented-button/index.d.ts +2 -0
- package/dist/src/segmented-button/segmented-button-group.d.ts +46 -0
- package/dist/src/segmented-button/segmented-button.d.ts +65 -0
- package/dist/src/select/index.d.ts +3 -0
- package/dist/src/select/option.d.ts +55 -0
- package/dist/src/select/select.d.ts +116 -0
- package/dist/src/side-sheet/index.d.ts +1 -0
- package/dist/src/side-sheet/side-sheet.d.ts +41 -0
- package/dist/src/tabs/tab-group.d.ts +0 -1
- package/dist/src/tabs/tab.d.ts +8 -2
- package/dist/src/tabs/tabs.d.ts +13 -1
- package/dist/state-DwbEjqVk.js +10 -0
- package/dist/state-DwbEjqVk.js.map +1 -0
- package/dist/{style-map-DcB52w-l.js → style-map-CRFEoCEg.js} +2 -2
- package/dist/{style-map-DcB52w-l.js.map → style-map-CRFEoCEg.js.map} +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/{unsafe-html-C2r3PyzF.js → unsafe-html-D3GHRaGQ.js} +2 -2
- package/dist/{unsafe-html-C2r3PyzF.js.map → unsafe-html-D3GHRaGQ.js.map} +1 -1
- package/package.json +1 -1
- package/readme.md +2 -2
- package/src/bottom-sheet/bottom-sheet.scss +88 -0
- package/src/bottom-sheet/bottom-sheet.ts +135 -0
- package/src/bottom-sheet/index.ts +1 -0
- package/src/button/BaseButton.ts +16 -7
- package/src/button/button/button-colors.scss +76 -5
- package/src/button/button/button-sizes.scss +39 -19
- package/src/button/button/button.scss +117 -116
- package/src/button/button/button.ts +23 -1
- package/src/button/button-group/button-group.scss +25 -22
- package/src/button/button-group/button-group.ts +121 -4
- package/src/button/icon-button/icon-button-sizes.scss +35 -15
- package/src/button/icon-button/icon-button.ts +21 -1
- package/src/card/card-colors.scss +10 -0
- package/src/card/card-content.ts +26 -0
- package/src/card/card.scss +221 -41
- package/src/card/card.ts +240 -7
- package/src/card/index.ts +1 -0
- package/src/code-editor/code-editor.ts +1 -1
- package/src/container/container.ts +1 -1
- package/src/empty-state/empty-state.scss +8 -0
- package/src/empty-state/empty-state.ts +2 -2
- package/src/focus-ring/focus-ring.ts +37 -19
- package/src/index.ts +8 -1
- package/src/menu/menu/menu.scss +24 -3
- package/src/menu/menu/menu.ts +23 -2
- package/src/menu/menu-item/menu-item.scss +1 -0
- package/src/menu/menu-item/menu-item.ts +1 -9
- package/src/peacock-loader.ts +32 -0
- package/src/radio/index.ts +1 -0
- package/src/radio/radio.scss +181 -0
- package/src/radio/radio.ts +362 -0
- package/src/ripple/ripple.ts +19 -3
- package/src/segmented-button/index.ts +2 -0
- package/src/segmented-button/segmented-button-group.scss +21 -0
- package/src/segmented-button/segmented-button-group.ts +110 -0
- package/src/segmented-button/segmented-button.scss +115 -0
- package/src/segmented-button/segmented-button.ts +175 -0
- package/src/select/index.ts +3 -0
- package/src/select/option.ts +109 -0
- package/src/select/select.scss +120 -0
- package/src/select/select.ts +486 -0
- package/src/side-sheet/index.ts +1 -0
- package/src/side-sheet/side-sheet.scss +79 -0
- package/src/side-sheet/side-sheet.ts +100 -0
- package/src/slider/slider.scss +0 -1
- package/src/tabs/demo/index.html +90 -0
- package/src/tabs/tab-group.ts +0 -3
- package/src/tabs/tab.scss +237 -25
- package/src/tabs/tab.ts +86 -12
- package/src/tabs/tabs.scss +37 -3
- package/src/tabs/tabs.ts +118 -2
- package/src/utils/dispatch-event-utils.ts +1 -0
- package/dist/IndividualComponent-Dt5xirYG.js +0 -73
- package/dist/IndividualComponent-Dt5xirYG.js.map +0 -1
- package/dist/button-ClzS8JLq.js.map +0 -1
- package/dist/button-group-BMS5WvaF.js +0 -292
- package/dist/button-group-BMS5WvaF.js.map +0 -1
- package/dist/chart-donut.js +0 -309
- package/dist/chart-donut.js.map +0 -1
- package/dist/class-map-59YGWLnx.js.map +0 -1
- package/dist/observe-theme-change-pALI5fmV.js.map +0 -1
- package/dist/snackbar-74YCdMPL.js.map +0 -1
- package/dist/src/chart-donut/chart-donut.d.ts +0 -53
- package/dist/src/chart-donut/index.d.ts +0 -1
- package/dist/test/card.test.d.ts +0 -1
- package/src/chart-donut/chart-donut.scss +0 -37
- package/src/chart-donut/chart-donut.ts +0 -287
- package/src/chart-donut/demo/index.html +0 -51
- package/src/chart-donut/index.ts +0 -1
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
import { html, nothing } from 'lit';
|
|
2
|
+
import { property, query, state } from 'lit/decorators.js';
|
|
3
|
+
import { classMap } from 'lit/directives/class-map.js';
|
|
4
|
+
import BaseInput from '../input/BaseInput.js';
|
|
5
|
+
import styles from './select.scss';
|
|
6
|
+
import type { Menu } from '../menu/menu/menu.js';
|
|
7
|
+
import { SelectOptionElement } from './option.js';
|
|
8
|
+
|
|
9
|
+
export interface SelectOption {
|
|
10
|
+
label: string;
|
|
11
|
+
value: string;
|
|
12
|
+
icon?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @label Select
|
|
17
|
+
* @tag wc-select
|
|
18
|
+
* @rawTag select
|
|
19
|
+
*
|
|
20
|
+
* @summary A dropdown select component supporting single and multi-select with optional typeahead search.
|
|
21
|
+
* @overview
|
|
22
|
+
* Select builds on wc-field and wc-menu to provide a fully-featured dropdown picker.
|
|
23
|
+
*
|
|
24
|
+
* - Single and multi-select modes
|
|
25
|
+
* - Client-side typeahead with `search="contains"`
|
|
26
|
+
* - Server-side typeahead with `search="managed"`
|
|
27
|
+
* - Multi-select chips display
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```html
|
|
31
|
+
* <wc-select label="Fruit" placeholder="Pick a fruit...">
|
|
32
|
+
* <wc-option value="apple">Apple</wc-option>
|
|
33
|
+
* <wc-option value="banana">Banana</wc-option>
|
|
34
|
+
* </wc-select>
|
|
35
|
+
* ```
|
|
36
|
+
* @tags form
|
|
37
|
+
*/
|
|
38
|
+
export class Select extends BaseInput {
|
|
39
|
+
static styles = [styles];
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Array of options to display in the dropdown.
|
|
43
|
+
* Setting this property creates matching `<wc-option>` children automatically.
|
|
44
|
+
*/
|
|
45
|
+
@property({ type: Array })
|
|
46
|
+
options: SelectOption[] = [];
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The selected value. For multi-select, a comma-separated list of values.
|
|
50
|
+
*/
|
|
51
|
+
@property({ type: String, reflect: true })
|
|
52
|
+
value: string = '';
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Enable multi-select mode.
|
|
56
|
+
*/
|
|
57
|
+
@property({ type: Boolean, reflect: true })
|
|
58
|
+
multiple: boolean = false;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Enable typeahead search.
|
|
62
|
+
* - `'contains'`: filters options client-side.
|
|
63
|
+
* - `'managed'`: emits a `select-search` event for server-controlled filtering.
|
|
64
|
+
*/
|
|
65
|
+
@property({ type: String })
|
|
66
|
+
search: '' | 'contains' | 'managed' = '';
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Placeholder text shown when no value is selected.
|
|
70
|
+
*/
|
|
71
|
+
@property({ type: String })
|
|
72
|
+
placeholder: string = '';
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Label displayed above the field.
|
|
76
|
+
*/
|
|
77
|
+
@property({ type: String })
|
|
78
|
+
label: string = '';
|
|
79
|
+
|
|
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
|
+
/**
|
|
87
|
+
* Visual variant of the field.
|
|
88
|
+
*/
|
|
89
|
+
@property({ type: String })
|
|
90
|
+
variant: 'filled' | 'outlined' | 'default' = 'default';
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Helper text displayed below the field.
|
|
94
|
+
*/
|
|
95
|
+
@property({ type: String, attribute: 'helper-text' })
|
|
96
|
+
helperText: string = '';
|
|
97
|
+
|
|
98
|
+
@property({ type: Boolean })
|
|
99
|
+
error: boolean = false;
|
|
100
|
+
|
|
101
|
+
@property({ type: String, attribute: 'error-text' })
|
|
102
|
+
errorText: string = '';
|
|
103
|
+
|
|
104
|
+
@property({ type: Boolean })
|
|
105
|
+
warning: boolean = false;
|
|
106
|
+
|
|
107
|
+
@property({ type: String, attribute: 'warning-text' })
|
|
108
|
+
warningText: string = '';
|
|
109
|
+
|
|
110
|
+
@state()
|
|
111
|
+
private _open: boolean = false;
|
|
112
|
+
|
|
113
|
+
@state()
|
|
114
|
+
private _focused: boolean = false;
|
|
115
|
+
|
|
116
|
+
@state()
|
|
117
|
+
private _searchQuery: string = '';
|
|
118
|
+
|
|
119
|
+
/** True when all options are filtered out by the current search query. */
|
|
120
|
+
@state()
|
|
121
|
+
private _noOptionsVisible: boolean = false;
|
|
122
|
+
|
|
123
|
+
@query('.select-trigger')
|
|
124
|
+
private _triggerEl?: HTMLElement;
|
|
125
|
+
|
|
126
|
+
private get _menu(): Menu | null {
|
|
127
|
+
return (this.renderRoot?.querySelector('wc-menu') as unknown as Menu) ?? null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
@query('.search-input')
|
|
131
|
+
private _searchInputEl?: HTMLInputElement;
|
|
132
|
+
|
|
133
|
+
override focus() {
|
|
134
|
+
this._triggerEl?.focus();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
override blur() {
|
|
138
|
+
this._triggerEl?.blur();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
protected override updated(changedProperties: Map<string, unknown>) {
|
|
144
|
+
if (changedProperties.has('options')) {
|
|
145
|
+
this._syncProgrammaticOptions();
|
|
146
|
+
}
|
|
147
|
+
this._syncOptionStates();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ── Programmatic options ───────────────────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Reconciles the `options` property with auto-generated `<wc-option>` light-DOM
|
|
154
|
+
* children (marked `data-generated`). Declarative children placed by the
|
|
155
|
+
* consumer are left untouched.
|
|
156
|
+
*/
|
|
157
|
+
private _syncProgrammaticOptions() {
|
|
158
|
+
this.querySelectorAll('wc-option[data-generated]').forEach(el => el.remove());
|
|
159
|
+
for (const opt of this.options) {
|
|
160
|
+
const el = new SelectOptionElement();
|
|
161
|
+
el.value = opt.value;
|
|
162
|
+
if (opt.icon) el.icon = opt.icon;
|
|
163
|
+
el.textContent = opt.label;
|
|
164
|
+
el.dataset.generated = '';
|
|
165
|
+
this.appendChild(el);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ── Option state sync ──────────────────────────────────────────────────────
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Pushes `selected`, `keepOpen`, and `filtered` state onto every `<wc-option>`
|
|
173
|
+
* child element so each one can render itself correctly.
|
|
174
|
+
*/
|
|
175
|
+
private _syncOptionStates() {
|
|
176
|
+
const optEls = Array.from(
|
|
177
|
+
this.querySelectorAll<SelectOptionElement>('wc-option'),
|
|
178
|
+
);
|
|
179
|
+
let visibleCount = 0;
|
|
180
|
+
for (const opt of optEls) {
|
|
181
|
+
opt.selected = this._isSelected(opt.value);
|
|
182
|
+
opt.keepOpen = this.multiple;
|
|
183
|
+
if (this.search && this.search !== 'managed' && this._searchQuery) {
|
|
184
|
+
const q = this._searchQuery.toLowerCase();
|
|
185
|
+
const label = opt.textContent?.trim() ?? '';
|
|
186
|
+
opt.filtered = !label.toLowerCase().includes(q);
|
|
187
|
+
if (!opt.filtered) visibleCount++;
|
|
188
|
+
} else {
|
|
189
|
+
opt.filtered = false;
|
|
190
|
+
visibleCount++;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
this._noOptionsVisible = optEls.length > 0 && visibleCount === 0;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
197
|
+
|
|
198
|
+
private get _selectedValues(): string[] {
|
|
199
|
+
if (!this.value) return [];
|
|
200
|
+
return this.value
|
|
201
|
+
.split(',')
|
|
202
|
+
.map(v => v.trim())
|
|
203
|
+
.filter(Boolean);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
private _isSelected(value: string): boolean {
|
|
207
|
+
return this._selectedValues.includes(value);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Returns the display label for a given option value. */
|
|
211
|
+
private _getLabelForValue(val: string): string {
|
|
212
|
+
for (const opt of this.querySelectorAll<SelectOptionElement>('wc-option')) {
|
|
213
|
+
if (opt.value === val) return opt.textContent?.trim() ?? val;
|
|
214
|
+
}
|
|
215
|
+
// Fallback to options array (before wc-option children are created)
|
|
216
|
+
return this.options.find(o => o.value === val)?.label ?? val;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private get _displayLabel(): string {
|
|
220
|
+
if (!this.value) return '';
|
|
221
|
+
const firstValue = this._selectedValues[0];
|
|
222
|
+
if (!firstValue) return '';
|
|
223
|
+
return this._getLabelForValue(firstValue);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
private get _isPopulated(): boolean {
|
|
227
|
+
return !!this.value;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ── Menu open/close ────────────────────────────────────────────────────────
|
|
231
|
+
|
|
232
|
+
private _openMenu() {
|
|
233
|
+
if (this.disabled || this.readonly) return;
|
|
234
|
+
this._open = true;
|
|
235
|
+
this._focused = true;
|
|
236
|
+
const menu = this._menu;
|
|
237
|
+
if (menu && this._triggerEl) {
|
|
238
|
+
menu.anchorElement = this._triggerEl;
|
|
239
|
+
menu.show();
|
|
240
|
+
}
|
|
241
|
+
if (this.search) {
|
|
242
|
+
this._searchQuery = '';
|
|
243
|
+
// Use rAF so that the search input receives focus *after* wc-menu has
|
|
244
|
+
// finished showing and potentially focused a menu item.
|
|
245
|
+
this.updateComplete.then(() => {
|
|
246
|
+
requestAnimationFrame(() => {
|
|
247
|
+
this._searchInputEl?.focus();
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private _closeMenu() {
|
|
254
|
+
if (!this._open) return;
|
|
255
|
+
this._open = false;
|
|
256
|
+
this._focused = false;
|
|
257
|
+
this._searchQuery = '';
|
|
258
|
+
this._menu?.close();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ── Event handlers ─────────────────────────────────────────────────────────
|
|
262
|
+
|
|
263
|
+
private _handleTriggerClick(event: MouseEvent) {
|
|
264
|
+
// Ignore clicks that originated inside the search input — those should not
|
|
265
|
+
// toggle the menu (the input needs to stay open so the user can type).
|
|
266
|
+
if (event.target instanceof HTMLInputElement) {
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
if (this._open) {
|
|
270
|
+
this._closeMenu();
|
|
271
|
+
} else {
|
|
272
|
+
this._openMenu();
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
private _handleTriggerKeyDown(event: KeyboardEvent) {
|
|
277
|
+
// When the typeahead search input is active, let the input handle its own
|
|
278
|
+
// keys (Space, Enter, etc.). Only intercept Escape to close the menu.
|
|
279
|
+
if (event.target instanceof HTMLInputElement) {
|
|
280
|
+
if (event.key === 'Escape') {
|
|
281
|
+
event.preventDefault();
|
|
282
|
+
this._closeMenu();
|
|
283
|
+
}
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
switch (event.key) {
|
|
287
|
+
case 'Enter':
|
|
288
|
+
case ' ':
|
|
289
|
+
case 'ArrowDown':
|
|
290
|
+
event.preventDefault();
|
|
291
|
+
if (!this._open) this._openMenu();
|
|
292
|
+
break;
|
|
293
|
+
case 'Escape':
|
|
294
|
+
if (this._open) {
|
|
295
|
+
event.preventDefault();
|
|
296
|
+
this._closeMenu();
|
|
297
|
+
}
|
|
298
|
+
break;
|
|
299
|
+
default:
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
private _handleMenuClosed() {
|
|
305
|
+
this._open = false;
|
|
306
|
+
this._focused = false;
|
|
307
|
+
this._searchQuery = '';
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
private _handleMenuItemActivate(event: CustomEvent) {
|
|
311
|
+
const item = event.detail?.item as { value: string } | undefined;
|
|
312
|
+
if (!item) return;
|
|
313
|
+
|
|
314
|
+
const val = item.value;
|
|
315
|
+
if (!val) return;
|
|
316
|
+
|
|
317
|
+
if (this.multiple) {
|
|
318
|
+
const values = this._selectedValues;
|
|
319
|
+
const idx = values.indexOf(val);
|
|
320
|
+
if (idx >= 0) {
|
|
321
|
+
values.splice(idx, 1);
|
|
322
|
+
} else {
|
|
323
|
+
values.push(val);
|
|
324
|
+
}
|
|
325
|
+
this.value = values.join(',');
|
|
326
|
+
} else {
|
|
327
|
+
this.value = val;
|
|
328
|
+
this._closeMenu();
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
this._dispatchChange();
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
private _dispatchChange() {
|
|
335
|
+
this.dispatchEvent(
|
|
336
|
+
new CustomEvent('change', {
|
|
337
|
+
detail: { value: this.value },
|
|
338
|
+
bubbles: true,
|
|
339
|
+
composed: true,
|
|
340
|
+
}),
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
private _handleSearchInput(event: InputEvent) {
|
|
345
|
+
this._searchQuery = (event.target as HTMLInputElement).value;
|
|
346
|
+
if (this.search === 'managed') {
|
|
347
|
+
this.dispatchEvent(
|
|
348
|
+
new CustomEvent('select-search', {
|
|
349
|
+
detail: { value: this._searchQuery },
|
|
350
|
+
bubbles: true,
|
|
351
|
+
composed: true,
|
|
352
|
+
}),
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
private _handleClear(event: MouseEvent) {
|
|
358
|
+
event.stopPropagation();
|
|
359
|
+
this.value = '';
|
|
360
|
+
this._dispatchChange();
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
private _handleChipDismiss(event: CustomEvent, chipValue: string) {
|
|
364
|
+
event.stopPropagation();
|
|
365
|
+
const values = this._selectedValues.filter(v => v !== chipValue);
|
|
366
|
+
this.value = values.join(',');
|
|
367
|
+
this._dispatchChange();
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// ── Render helpers ─────────────────────────────────────────────────────────
|
|
371
|
+
|
|
372
|
+
private _renderTriggerContent() {
|
|
373
|
+
// Typeahead: when open, show a text input for filtering
|
|
374
|
+
if (this.search && this._open) {
|
|
375
|
+
return html`<input
|
|
376
|
+
class="search-input"
|
|
377
|
+
.value=${this._searchQuery}
|
|
378
|
+
placeholder=${this._displayLabel || this.placeholder}
|
|
379
|
+
@input=${this._handleSearchInput}
|
|
380
|
+
/>`;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Multi-select: show chips for selected items
|
|
384
|
+
if (this.multiple && this._selectedValues.length > 0) {
|
|
385
|
+
return html`<div class="chips-container">
|
|
386
|
+
${this._selectedValues.map(
|
|
387
|
+
val => html`
|
|
388
|
+
<wc-chip
|
|
389
|
+
dismissible
|
|
390
|
+
value=${val}
|
|
391
|
+
@tag--dismiss=${(e: CustomEvent) =>
|
|
392
|
+
this._handleChipDismiss(e, val)}
|
|
393
|
+
>${this._getLabelForValue(val)}</wc-chip
|
|
394
|
+
>
|
|
395
|
+
`,
|
|
396
|
+
)}
|
|
397
|
+
</div>`;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Single select: show selected label or placeholder
|
|
401
|
+
const label = this._displayLabel;
|
|
402
|
+
if (!label) {
|
|
403
|
+
return html`<span class="placeholder">${this.placeholder}</span>`;
|
|
404
|
+
}
|
|
405
|
+
return html`<span class="display-value">${label}</span>`;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
private _renderFieldEnd() {
|
|
409
|
+
const showClear =
|
|
410
|
+
this.clearable &&
|
|
411
|
+
!this.multiple &&
|
|
412
|
+
!!this.value &&
|
|
413
|
+
!this.disabled &&
|
|
414
|
+
!this.readonly;
|
|
415
|
+
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
|
+
<wc-icon
|
|
426
|
+
class=${classMap({
|
|
427
|
+
'dropdown-icon': true,
|
|
428
|
+
'dropdown-icon--open': this._open,
|
|
429
|
+
})}
|
|
430
|
+
name="arrow_drop_down"
|
|
431
|
+
></wc-icon>
|
|
432
|
+
`;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
render() {
|
|
436
|
+
return html`
|
|
437
|
+
<wc-field
|
|
438
|
+
label=${this.label}
|
|
439
|
+
?required=${this.required}
|
|
440
|
+
?disabled=${this.disabled}
|
|
441
|
+
?readonly=${this.readonly}
|
|
442
|
+
?skeleton=${this.skeleton}
|
|
443
|
+
helper-text=${this.helperText}
|
|
444
|
+
?error=${this.error}
|
|
445
|
+
error-text=${this.errorText}
|
|
446
|
+
?warning=${this.warning}
|
|
447
|
+
warning-text=${this.warningText}
|
|
448
|
+
variant=${this.variant}
|
|
449
|
+
?populated=${this._isPopulated || this._open}
|
|
450
|
+
?focused=${this._focused}
|
|
451
|
+
.host=${this}
|
|
452
|
+
class="select-field"
|
|
453
|
+
>
|
|
454
|
+
<div
|
|
455
|
+
class="select-trigger"
|
|
456
|
+
tabindex=${this.disabled ? -1 : 0}
|
|
457
|
+
role="combobox"
|
|
458
|
+
aria-expanded=${String(this._open)}
|
|
459
|
+
aria-haspopup="listbox"
|
|
460
|
+
@click=${this._handleTriggerClick}
|
|
461
|
+
@keydown=${this._handleTriggerKeyDown}
|
|
462
|
+
>
|
|
463
|
+
${this._renderTriggerContent()}
|
|
464
|
+
</div>
|
|
465
|
+
|
|
466
|
+
<div slot="field-end" class="field-end-wrapper">
|
|
467
|
+
${this._renderFieldEnd()}
|
|
468
|
+
</div>
|
|
469
|
+
</wc-field>
|
|
470
|
+
|
|
471
|
+
<wc-menu
|
|
472
|
+
placement="bottom-start"
|
|
473
|
+
aria-label=${this.label || 'Options'}
|
|
474
|
+
@closed=${this._handleMenuClosed}
|
|
475
|
+
@menu-item-activate=${(e: CustomEvent) =>
|
|
476
|
+
this._handleMenuItemActivate(e)}
|
|
477
|
+
>
|
|
478
|
+
<slot></slot>
|
|
479
|
+
${this._noOptionsVisible
|
|
480
|
+
? html`<wc-menu-item disabled>No options</wc-menu-item>`
|
|
481
|
+
: nothing}
|
|
482
|
+
</wc-menu>
|
|
483
|
+
`;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { SideSheet } from './side-sheet.js';
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
@use "../../scss/mixin";
|
|
2
|
+
|
|
3
|
+
@include mixin.base-styles;
|
|
4
|
+
|
|
5
|
+
:host {
|
|
6
|
+
display: contents;
|
|
7
|
+
|
|
8
|
+
--side-sheet-container-color: var(--color-surface-container-low, #f7f2fa);
|
|
9
|
+
--side-sheet-scrim-color: rgba(0, 0, 0, 0.32);
|
|
10
|
+
--side-sheet-shape: var(--shape-corner-extra-large, 28px);
|
|
11
|
+
--side-sheet-width: 360px;
|
|
12
|
+
--side-sheet-transition-duration: var(--duration-medium2, 300ms);
|
|
13
|
+
--side-sheet-transition-easing: var(--easing-standard, cubic-bezier(0.2, 0, 0, 1));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.scrim {
|
|
17
|
+
inset: 0;
|
|
18
|
+
position: fixed;
|
|
19
|
+
background-color: var(--side-sheet-scrim-color);
|
|
20
|
+
opacity: 0;
|
|
21
|
+
pointer-events: none;
|
|
22
|
+
transition:
|
|
23
|
+
opacity var(--side-sheet-transition-duration) var(--side-sheet-transition-easing);
|
|
24
|
+
z-index: 1000;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.scrim.visible {
|
|
28
|
+
opacity: 1;
|
|
29
|
+
pointer-events: auto;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.sheet {
|
|
33
|
+
background-color: var(--side-sheet-container-color);
|
|
34
|
+
bottom: 0;
|
|
35
|
+
display: flex;
|
|
36
|
+
flex-direction: column;
|
|
37
|
+
max-width: 100%;
|
|
38
|
+
overflow: hidden;
|
|
39
|
+
position: fixed;
|
|
40
|
+
top: 0;
|
|
41
|
+
transition:
|
|
42
|
+
transform var(--side-sheet-transition-duration) var(--side-sheet-transition-easing);
|
|
43
|
+
width: var(--side-sheet-width);
|
|
44
|
+
will-change: transform;
|
|
45
|
+
z-index: 1001;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.sheet.variant-standard {
|
|
49
|
+
position: relative;
|
|
50
|
+
z-index: 1;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/* Position: right */
|
|
54
|
+
.sheet.position-right {
|
|
55
|
+
border-radius: var(--side-sheet-shape) 0 0 var(--side-sheet-shape);
|
|
56
|
+
right: 0;
|
|
57
|
+
transform: translateX(100%);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.sheet.position-right.open {
|
|
61
|
+
transform: translateX(0);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/* Position: left */
|
|
65
|
+
.sheet.position-left {
|
|
66
|
+
border-radius: 0 var(--side-sheet-shape) var(--side-sheet-shape) 0;
|
|
67
|
+
left: 0;
|
|
68
|
+
transform: translateX(-100%);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.sheet.position-left.open {
|
|
72
|
+
transform: translateX(0);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.content {
|
|
76
|
+
flex: 1 1 auto;
|
|
77
|
+
overflow-y: auto;
|
|
78
|
+
padding: var(--spacing-300, 1.5rem);
|
|
79
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { LitElement, html, nothing } from 'lit';
|
|
2
|
+
import { property } from 'lit/decorators.js';
|
|
3
|
+
import { classMap } from 'lit/directives/class-map.js';
|
|
4
|
+
import IndividualComponent from '../IndividualComponent.js';
|
|
5
|
+
import styles from './side-sheet.scss';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @label Side Sheet
|
|
9
|
+
* @tag wc-side-sheet
|
|
10
|
+
* @rawTag side-sheet
|
|
11
|
+
* @summary Side sheets slide in from the edge of the screen to reveal supplemental content. Supports standard and modal variants per Material Design 3.
|
|
12
|
+
*
|
|
13
|
+
* @cssprop --side-sheet-container-color - Background color of the sheet container.
|
|
14
|
+
* @cssprop --side-sheet-scrim-color - Color of the modal scrim overlay.
|
|
15
|
+
* @cssprop --side-sheet-shape - Corner radius of the leading edge.
|
|
16
|
+
* @cssprop --side-sheet-width - Width of the side sheet.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```html
|
|
20
|
+
* Side Sheet
|
|
21
|
+
|
|
22
|
+
* ```
|
|
23
|
+
* @tags navigation, overlay
|
|
24
|
+
*/
|
|
25
|
+
@IndividualComponent
|
|
26
|
+
export class SideSheet extends LitElement {
|
|
27
|
+
static styles = [styles];
|
|
28
|
+
|
|
29
|
+
/** Whether the sheet is visible. */
|
|
30
|
+
@property({ type: Boolean, reflect: true }) open = false;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Sheet variant.
|
|
34
|
+
* - `"standard"`: Coexists with page content; no scrim.
|
|
35
|
+
* - `"modal"`: Overlays page content with a scrim backdrop.
|
|
36
|
+
*/
|
|
37
|
+
@property({ type: String, reflect: true }) variant: 'standard' | 'modal' =
|
|
38
|
+
'modal';
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Side from which the sheet slides in.
|
|
42
|
+
* - `"left"`: Sheet opens from the left edge.
|
|
43
|
+
* - `"right"`: Sheet opens from the right edge.
|
|
44
|
+
*/
|
|
45
|
+
@property({ type: String, reflect: true }) position: 'left' | 'right' =
|
|
46
|
+
'right';
|
|
47
|
+
|
|
48
|
+
show() {
|
|
49
|
+
this.open = true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
hide() {
|
|
53
|
+
this._close('programmatic');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private _close(reason: string) {
|
|
57
|
+
if (!this.open) return;
|
|
58
|
+
this.open = false;
|
|
59
|
+
this.dispatchEvent(
|
|
60
|
+
new CustomEvent('side-sheet-close', {
|
|
61
|
+
detail: { reason },
|
|
62
|
+
bubbles: true,
|
|
63
|
+
composed: true,
|
|
64
|
+
}),
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private _handleScrimClick() {
|
|
69
|
+
if (this.variant === 'modal') {
|
|
70
|
+
this._close('scrim');
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
render() {
|
|
75
|
+
return html`
|
|
76
|
+
${this.variant === 'modal'
|
|
77
|
+
? html`<div
|
|
78
|
+
class=${classMap({ scrim: true, visible: this.open })}
|
|
79
|
+
@click=${this._handleScrimClick}
|
|
80
|
+
></div>`
|
|
81
|
+
: nothing}
|
|
82
|
+
|
|
83
|
+
<div
|
|
84
|
+
class=${classMap({
|
|
85
|
+
sheet: true,
|
|
86
|
+
open: this.open,
|
|
87
|
+
[`variant-${this.variant}`]: true,
|
|
88
|
+
[`position-${this.position}`]: true,
|
|
89
|
+
})}
|
|
90
|
+
role="dialog"
|
|
91
|
+
aria-modal=${this.variant === 'modal' ? 'true' : 'false'}
|
|
92
|
+
aria-hidden=${!this.open ? 'true' : 'false'}
|
|
93
|
+
>
|
|
94
|
+
<div class="content">
|
|
95
|
+
<slot></slot>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
`;
|
|
99
|
+
}
|
|
100
|
+
}
|