@synergy-design-system/mcp 2.1.0 → 2.3.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/CHANGELOG.md +26 -0
- package/metadata/checksum.txt +1 -1
- package/metadata/packages/components/components/syn-alert/component.styles.ts +0 -7
- package/metadata/packages/components/components/syn-alert/component.ts +0 -7
- package/metadata/packages/components/components/syn-badge/component.styles.ts +0 -7
- package/metadata/packages/components/components/syn-badge/component.ts +0 -7
- package/metadata/packages/components/components/syn-breadcrumb/component.styles.ts +0 -7
- package/metadata/packages/components/components/syn-breadcrumb/component.ts +0 -7
- package/metadata/packages/components/components/syn-breadcrumb-item/component.styles.ts +0 -7
- package/metadata/packages/components/components/syn-breadcrumb-item/component.ts +0 -7
- package/metadata/packages/components/components/syn-button/component.styles.ts +0 -7
- package/metadata/packages/components/components/syn-button/component.ts +0 -7
- package/metadata/packages/components/components/syn-button-group/component.styles.ts +0 -7
- package/metadata/packages/components/components/syn-button-group/component.ts +0 -7
- package/metadata/packages/components/components/syn-card/component.styles.ts +0 -7
- package/metadata/packages/components/components/syn-card/component.ts +0 -7
- package/metadata/packages/components/components/syn-checkbox/component.styles.ts +0 -7
- package/metadata/packages/components/components/syn-checkbox/component.ts +0 -7
- package/metadata/packages/components/components/syn-combobox/component.angular.ts +84 -13
- package/metadata/packages/components/components/syn-combobox/component.custom.styles.ts +87 -0
- package/metadata/packages/components/components/syn-combobox/component.react.ts +11 -2
- package/metadata/packages/components/components/syn-combobox/component.styles.ts +0 -35
- package/metadata/packages/components/components/syn-combobox/component.ts +682 -163
- package/metadata/packages/components/components/syn-combobox/component.vue +50 -7
- package/metadata/packages/components/components/syn-details/component.styles.ts +0 -7
- package/metadata/packages/components/components/syn-details/component.ts +0 -7
- package/metadata/packages/components/components/syn-dialog/component.styles.ts +0 -7
- package/metadata/packages/components/components/syn-dialog/component.ts +0 -7
- package/metadata/packages/components/components/syn-divider/component.styles.ts +0 -7
- package/metadata/packages/components/components/syn-divider/component.ts +0 -7
- package/metadata/packages/components/components/syn-drawer/component.styles.ts +0 -7
- package/metadata/packages/components/components/syn-drawer/component.ts +0 -7
- package/metadata/packages/components/components/syn-dropdown/component.styles.ts +0 -7
- package/metadata/packages/components/components/syn-dropdown/component.ts +0 -7
- package/metadata/packages/components/components/syn-icon/component.styles.ts +0 -7
- package/metadata/packages/components/components/syn-icon/component.ts +0 -7
- package/metadata/packages/components/components/syn-icon-button/component.styles.ts +0 -7
- package/metadata/packages/components/components/syn-icon-button/component.ts +0 -7
- package/metadata/packages/components/components/syn-input/component.styles.ts +0 -7
- package/metadata/packages/components/components/syn-input/component.ts +0 -7
- package/metadata/packages/components/components/syn-menu/component.styles.ts +0 -7
- package/metadata/packages/components/components/syn-menu/component.ts +0 -7
- package/metadata/packages/components/components/syn-menu-item/component.styles.ts +0 -7
- package/metadata/packages/components/components/syn-menu-item/component.ts +0 -7
- package/metadata/packages/components/components/syn-menu-label/component.styles.ts +0 -7
- package/metadata/packages/components/components/syn-menu-label/component.ts +0 -7
- package/metadata/packages/components/components/syn-option/component.styles.ts +0 -7
- package/metadata/packages/components/components/syn-option/component.ts +2 -7
- package/metadata/packages/components/components/syn-popup/component.styles.ts +0 -7
- package/metadata/packages/components/components/syn-popup/component.ts +0 -7
- package/metadata/packages/components/components/syn-progress-bar/component.styles.ts +0 -7
- package/metadata/packages/components/components/syn-progress-bar/component.ts +0 -7
- package/metadata/packages/components/components/syn-progress-ring/component.styles.ts +0 -7
- package/metadata/packages/components/components/syn-progress-ring/component.ts +0 -7
- package/metadata/packages/components/components/syn-radio/component.styles.ts +0 -7
- package/metadata/packages/components/components/syn-radio/component.ts +0 -7
- package/metadata/packages/components/components/syn-radio-button/component.styles.ts +0 -7
- package/metadata/packages/components/components/syn-radio-button/component.ts +0 -7
- package/metadata/packages/components/components/syn-radio-group/component.styles.ts +0 -7
- package/metadata/packages/components/components/syn-radio-group/component.ts +0 -7
- package/metadata/packages/components/components/syn-resize-observer/component.styles.ts +0 -7
- package/metadata/packages/components/components/syn-resize-observer/component.ts +0 -7
- package/metadata/packages/components/components/syn-select/component.styles.ts +0 -7
- package/metadata/packages/components/components/syn-select/component.ts +0 -7
- package/metadata/packages/components/components/syn-spinner/component.styles.ts +0 -7
- package/metadata/packages/components/components/syn-spinner/component.ts +0 -7
- package/metadata/packages/components/components/syn-switch/component.styles.ts +0 -7
- package/metadata/packages/components/components/syn-switch/component.ts +0 -7
- package/metadata/packages/components/components/syn-tab/component.styles.ts +0 -7
- package/metadata/packages/components/components/syn-tab/component.ts +0 -7
- package/metadata/packages/components/components/syn-tab-group/component.styles.ts +0 -7
- package/metadata/packages/components/components/syn-tab-group/component.ts +0 -7
- package/metadata/packages/components/components/syn-tab-panel/component.styles.ts +0 -7
- package/metadata/packages/components/components/syn-tab-panel/component.ts +0 -7
- package/metadata/packages/components/components/syn-tag/component.styles.ts +0 -7
- package/metadata/packages/components/components/syn-tag/component.ts +0 -7
- package/metadata/packages/components/components/syn-textarea/component.styles.ts +0 -7
- package/metadata/packages/components/components/syn-textarea/component.ts +0 -7
- package/metadata/packages/components/components/syn-tooltip/component.styles.ts +0 -7
- package/metadata/packages/components/components/syn-tooltip/component.ts +0 -7
- package/metadata/packages/components/static/CHANGELOG.md +21 -0
- package/metadata/packages/components/static/LIMITATIONS.md +58 -0
- package/metadata/packages/components/static/README.md +0 -61
- package/metadata/packages/tokens/CHANGELOG.md +2 -0
- package/metadata/packages/tokens/dark.css +1 -1
- package/metadata/packages/tokens/index.js +1 -1
- package/metadata/packages/tokens/light.css +1 -1
- package/metadata/packages/tokens/sick2018_dark.css +1 -1
- package/metadata/packages/tokens/sick2018_light.css +1 -1
- package/metadata/packages/tokens/sick2025_dark.css +1 -1
- package/metadata/packages/tokens/sick2025_light.css +1 -1
- package/metadata/static/components/syn-combobox/docs.md +135 -1
- package/package.json +5 -5
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
/* eslint-disable no-param-reassign */
|
|
2
|
+
/* eslint-disable no-underscore-dangle */
|
|
3
|
+
import type { CSSResultGroup, PropertyValues, TemplateResult } from 'lit';
|
|
2
4
|
import { classMap } from 'lit/directives/class-map.js';
|
|
3
5
|
import { html } from 'lit';
|
|
4
6
|
import { property, query, state } from 'lit/decorators.js';
|
|
7
|
+
import { unsafeHTML } from 'lit/directives/unsafe-html.js';
|
|
5
8
|
import { animateTo, stopAnimations } from '../../internal/animate.js';
|
|
6
|
-
import { defaultValue } from '../../internal/default-value.js';
|
|
7
9
|
import { FormControlController } from '../../internal/form.js';
|
|
8
10
|
import { getAnimation, setDefaultAnimation } from '../../utilities/animation-registry.js';
|
|
9
11
|
import { HasSlotController } from '../../internal/slot.js';
|
|
@@ -19,24 +21,30 @@ import SynPopup from '../popup/popup.component.js';
|
|
|
19
21
|
import type { SynergyFormControl } from '../../internal/synergy-element.js';
|
|
20
22
|
import SynOption from '../option/option.component.js';
|
|
21
23
|
import type SynOptGroup from '../optgroup/optgroup.js';
|
|
24
|
+
import SynTag from '../tag/tag.component.js';
|
|
22
25
|
import styles from './combobox.styles.js';
|
|
23
26
|
import customStyles from './combobox.custom.styles.js';
|
|
24
27
|
import {
|
|
25
28
|
checkValueBelongsToOption,
|
|
26
29
|
createOptionFromDifferentTypes, filterOnlyOptgroups, getAllOptions, getAssignedElementsForSlot,
|
|
27
|
-
getValueFromOption, normalizeString,
|
|
30
|
+
getValueFromOption, getValuesFromOptions, normalizeString,
|
|
28
31
|
} from './utils.js';
|
|
29
32
|
import { scrollIntoView } from '../../internal/scroll.js';
|
|
30
33
|
import { type OptionRenderer, defaultOptionRenderer } from './option-renderer.js';
|
|
31
34
|
import { enableDefaultSettings } from '../../utilities/defaultSettings/decorator.js';
|
|
35
|
+
import type { SynRemoveEvent } from '../../events/events.js';
|
|
36
|
+
import { compareValues, isAllowedValue } from '../select/utility.js';
|
|
32
37
|
|
|
33
38
|
/**
|
|
34
|
-
* @summary
|
|
39
|
+
* @summary A combobox component that combines the functionality of a text input with a dropdown listbox,
|
|
40
|
+
* allowing users to either select from predefined options or enter custom values (when not restricted).
|
|
41
|
+
*
|
|
35
42
|
* @documentation https://synergy-design-system.github.io/?path=/docs/components-syn-combobox--docs
|
|
36
43
|
* @status stable
|
|
37
44
|
*
|
|
38
45
|
* @dependency syn-icon
|
|
39
46
|
* @dependency syn-popup
|
|
47
|
+
* @dependency syn-tag
|
|
40
48
|
*
|
|
41
49
|
* @slot - The listbox options. Must be `<syn-option>` elements.
|
|
42
50
|
* You can use `<syn-optgroup>`'s to group items visually.
|
|
@@ -66,7 +74,7 @@ import { enableDefaultSettings } from '../../utilities/defaultSettings/decorator
|
|
|
66
74
|
* @csspart form-control-label - The label's wrapper.
|
|
67
75
|
* @csspart form-control-input - The combobox's wrapper.
|
|
68
76
|
* @csspart form-control-help-text - The help text's wrapper.
|
|
69
|
-
* @csspart combobox - The container
|
|
77
|
+
* @csspart combobox - The container that wraps the prefix, combobox, clear icon, and expand button.
|
|
70
78
|
* @csspart prefix - The container that wraps the prefix slot.
|
|
71
79
|
* @csspart suffix - The container that wraps the suffix slot.
|
|
72
80
|
* @csspart display-input - The element that displays the selected option's label,
|
|
@@ -79,6 +87,12 @@ import { enableDefaultSettings } from '../../utilities/defaultSettings/decorator
|
|
|
79
87
|
* @csspart popup - The popup's exported `popup` part.
|
|
80
88
|
* Use this to target the tooltip's popup container.
|
|
81
89
|
* @csspart no-results - The container that wraps the "no results" message.
|
|
90
|
+
* @csspart tags - The container that houses option tags when `multiple` is used.
|
|
91
|
+
* @csspart tag - The individual tags that represent each selected option in `multiple`.
|
|
92
|
+
* @csspart tag__base - The tag's base part.
|
|
93
|
+
* @csspart tag__content - The tag's content part.
|
|
94
|
+
* @csspart tag__remove-button - The tag's remove button.
|
|
95
|
+
* @csspart tag__remove-button__base - The tag's remove button base part.
|
|
82
96
|
*
|
|
83
97
|
* @animation combobox.show - The animation to use when showing the combobox.
|
|
84
98
|
* @animation combobox.hide - The animation to use when hiding the combobox.
|
|
@@ -96,6 +110,7 @@ export default class SynCombobox extends SynergyElement implements SynergyFormCo
|
|
|
96
110
|
static dependencies = {
|
|
97
111
|
'syn-icon': SynIcon,
|
|
98
112
|
'syn-popup': SynPopup,
|
|
113
|
+
'syn-tag': SynTag,
|
|
99
114
|
};
|
|
100
115
|
|
|
101
116
|
private readonly formControlController = new FormControlController(this, {
|
|
@@ -108,13 +123,24 @@ export default class SynCombobox extends SynergyElement implements SynergyFormCo
|
|
|
108
123
|
|
|
109
124
|
private closeWatcher: CloseWatcher | null;
|
|
110
125
|
|
|
111
|
-
/**
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
126
|
+
/**
|
|
127
|
+
* Cache of the last syn-options that were selected by user interaction (click or keyboard navigation).
|
|
128
|
+
* Used to track user selections and maintain selection state during value changes.
|
|
129
|
+
*/
|
|
130
|
+
private lastOptions: SynOption[] = [];
|
|
115
131
|
|
|
116
132
|
private isInitialized: boolean = false;
|
|
117
133
|
|
|
134
|
+
/**
|
|
135
|
+
* Flag to prevent infinite loops when the option renderer programmatically updates options.
|
|
136
|
+
* Set to true during option rendering to ignore slot change events triggered by our own updates.
|
|
137
|
+
*/
|
|
138
|
+
private isOptionRendererTriggered: boolean = false;
|
|
139
|
+
|
|
140
|
+
private resizeObserver: ResizeObserver;
|
|
141
|
+
|
|
142
|
+
private mutationObserver: MutationObserver;
|
|
143
|
+
|
|
118
144
|
@query('.combobox') popup: SynPopup;
|
|
119
145
|
|
|
120
146
|
@query('.combobox__inputs') combobox: HTMLSlotElement;
|
|
@@ -127,28 +153,58 @@ export default class SynCombobox extends SynergyElement implements SynergyFormCo
|
|
|
127
153
|
|
|
128
154
|
@query('slot:not([name])') private defaultSlot: HTMLSlotElement;
|
|
129
155
|
|
|
156
|
+
@query('.combobox__tags') tagContainer: HTMLDivElement;
|
|
157
|
+
|
|
130
158
|
@state() private hasFocus = false;
|
|
131
159
|
|
|
132
160
|
@state() private isUserInput = false;
|
|
133
161
|
|
|
134
162
|
@state() displayLabel = '';
|
|
135
163
|
|
|
136
|
-
@state()
|
|
164
|
+
@state() selectedOptions: SynOption[] = [];
|
|
137
165
|
|
|
138
166
|
@state() numberFilteredOptions = 0;
|
|
139
167
|
|
|
140
|
-
@state() cachedOptions: SynOption
|
|
168
|
+
@state() cachedOptions: SynOption[] = [];
|
|
169
|
+
|
|
170
|
+
@state() private valueHasChanged: boolean = false;
|
|
171
|
+
|
|
172
|
+
@state() private hideOptions = false;
|
|
141
173
|
|
|
142
174
|
/** The name of the combobox, submitted as a name/value pair with form data. */
|
|
143
175
|
@property() name = '';
|
|
144
176
|
|
|
177
|
+
private _value: string | number | Array<string | number> = '';
|
|
178
|
+
|
|
179
|
+
get value() {
|
|
180
|
+
return this._value;
|
|
181
|
+
}
|
|
182
|
+
|
|
145
183
|
/**
|
|
146
|
-
* The current value of the combobox, submitted as a name/value pair with form data.
|
|
184
|
+
* The current value of the combobox, submitted as a name/value pair with form data. When `multiple` is enabled, the
|
|
185
|
+
* value attribute will be a list of values separated by the delimiter, based on the options selected, and the value property will
|
|
186
|
+
* be an array. **For this reason, values must not contain the delimiter character.**
|
|
147
187
|
*/
|
|
148
|
-
@
|
|
188
|
+
@state()
|
|
189
|
+
set value(val: string | number | Array<string | number>) {
|
|
190
|
+
if (this.multiple) {
|
|
191
|
+
if (!Array.isArray(val)) {
|
|
192
|
+
val = typeof val === 'string' ? val.split(this.delimiter) : [val].filter(isAllowedValue);
|
|
193
|
+
}
|
|
194
|
+
} else {
|
|
195
|
+
val = Array.isArray(val) ? val.join(this.delimiter) : val;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (compareValues(this._value, val)) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
this.valueHasChanged = true;
|
|
203
|
+
this._value = val;
|
|
204
|
+
}
|
|
149
205
|
|
|
150
206
|
/** The default value of the form control. Primarily used for resetting the form control. */
|
|
151
|
-
@
|
|
207
|
+
@property({ attribute: 'value' }) defaultValue: string | number | Array<string | number> = '';
|
|
152
208
|
|
|
153
209
|
/** The combobox's size. */
|
|
154
210
|
@property({ reflect: true }) size: 'small' | 'medium' | 'large' = 'medium';
|
|
@@ -195,9 +251,16 @@ export default class SynCombobox extends SynergyElement implements SynergyFormCo
|
|
|
195
251
|
/**
|
|
196
252
|
* When set to `true`, restricts the combobox to only allow selection from the available options.
|
|
197
253
|
* Users will not be able to enter custom values that are not present in the list.
|
|
254
|
+
* This will always be true, if `multiple` is active.
|
|
198
255
|
*/
|
|
199
256
|
@property({ reflect: true, type: Boolean }) restricted = false;
|
|
200
257
|
|
|
258
|
+
/**
|
|
259
|
+
* Allows more than one option to be selected.
|
|
260
|
+
* If `multiple` is set, the combobox will always be `restricted` to the available options
|
|
261
|
+
* */
|
|
262
|
+
@property({ reflect: true, type: Boolean }) multiple = false;
|
|
263
|
+
|
|
201
264
|
/**
|
|
202
265
|
* A function that customizes the rendered option. The first argument is the option, the second
|
|
203
266
|
* is the query string, which is typed into the combobox.
|
|
@@ -228,9 +291,44 @@ export default class SynCombobox extends SynergyElement implements SynergyFormCo
|
|
|
228
291
|
return true;
|
|
229
292
|
}
|
|
230
293
|
|
|
231
|
-
return option?.value === queryStr;
|
|
294
|
+
return option?.value?.toString() === queryStr;
|
|
232
295
|
};
|
|
233
296
|
|
|
297
|
+
/**
|
|
298
|
+
* The delimiter to use when setting the value when `multiple` is enabled.
|
|
299
|
+
* The default is a space ' ', but you can set it to a comma or other character(s).
|
|
300
|
+
* @example <syn-combobox delimiter="~" value="option-1~option-2"></syn-combobox>
|
|
301
|
+
*/
|
|
302
|
+
@property() delimiter = ' ';
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* The maximum number of selected options to show when `multiple` is true. After the maximum, "+n" will be shown to
|
|
306
|
+
* indicate the number of additional items that are selected. Set to 0 to remove the limit.
|
|
307
|
+
*/
|
|
308
|
+
@property({ attribute: 'max-options-visible', type: Number }) maxOptionsVisible = 3;
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* A function that customizes the tags to be rendered when `multiple` is true. The first argument is the option, the second
|
|
312
|
+
* is the current tag's index. The function should return either a Lit TemplateResult or a string containing trusted HTML of the symbol to render at
|
|
313
|
+
* the specified value.
|
|
314
|
+
*/
|
|
315
|
+
@property() getTag: (option: SynOption, index: number) => TemplateResult | string | HTMLElement = option => html`
|
|
316
|
+
<syn-tag
|
|
317
|
+
part="tag"
|
|
318
|
+
exportparts="
|
|
319
|
+
base:tag__base,
|
|
320
|
+
content:tag__content,
|
|
321
|
+
remove-button:tag__remove-button,
|
|
322
|
+
remove-button__base:tag__remove-button__base
|
|
323
|
+
"
|
|
324
|
+
size=${this.size}
|
|
325
|
+
removable
|
|
326
|
+
@syn-remove=${(event: SynRemoveEvent) => this.handleTagRemove(event, option)}
|
|
327
|
+
>
|
|
328
|
+
${option.getTextLabel()}
|
|
329
|
+
</syn-tag>
|
|
330
|
+
`;
|
|
331
|
+
|
|
234
332
|
/** Gets the validity state object */
|
|
235
333
|
get validity() {
|
|
236
334
|
return this.valueInput.validity;
|
|
@@ -241,29 +339,159 @@ export default class SynCombobox extends SynergyElement implements SynergyFormCo
|
|
|
241
339
|
return this.valueInput.validationMessage;
|
|
242
340
|
}
|
|
243
341
|
|
|
342
|
+
private calculateTagMaxWidth = (entries: ResizeObserverEntry[]) => {
|
|
343
|
+
const input = entries.at(0);
|
|
344
|
+
if (!input || !this.tagContainer) return;
|
|
345
|
+
|
|
346
|
+
const inputWidth = input.contentRect.width;
|
|
347
|
+
const tagsWidth = this.tagContainer.getBoundingClientRect().width;
|
|
348
|
+
|
|
349
|
+
// The min-width of the input is 48px, this should stay available for the input
|
|
350
|
+
// The min-width of the tags is 85px, so we should not go below that
|
|
351
|
+
const availableTagSpace = Math.max(85, tagsWidth + inputWidth - 48);
|
|
352
|
+
|
|
353
|
+
this.tagContainer.style.setProperty('--syn-select-tag-max-width', `${availableTagSpace}px`);
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
private enableResizeObserver() {
|
|
357
|
+
if (!this.multiple) return;
|
|
358
|
+
|
|
359
|
+
if (!this.resizeObserver) {
|
|
360
|
+
this.resizeObserver = new ResizeObserver(this.calculateTagMaxWidth);
|
|
361
|
+
}
|
|
362
|
+
// We use the `displayInput`, as the observer is fired for the initial state if someone is selecting an option
|
|
363
|
+
// and when the combobox size changes e.g. via window resize
|
|
364
|
+
this.resizeObserver.observe(this.displayInput);
|
|
365
|
+
}
|
|
366
|
+
|
|
244
367
|
connectedCallback() {
|
|
245
368
|
super.connectedCallback();
|
|
246
369
|
|
|
370
|
+
this.mutationObserver = new MutationObserver((entries) => {
|
|
371
|
+
// Only process attribute mutations on SynOption instances for the 'value' attribute. This is needed for changing of "delimiter"
|
|
372
|
+
const hasRelevantValueChange = entries.some(entry => {
|
|
373
|
+
// Check if the target is a SynOption instance
|
|
374
|
+
if (!(entry.target instanceof SynOption)) {
|
|
375
|
+
return false;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Check if it's an attribute mutation for the 'value' attribute
|
|
379
|
+
if (entry.type !== 'attributes' || entry.attributeName !== 'value') {
|
|
380
|
+
return false;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Check if the value actually changed and is not nullish
|
|
384
|
+
const currentValue = (entry.target as HTMLElement).getAttribute('value');
|
|
385
|
+
return entry.oldValue !== currentValue && !!currentValue;
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
if (hasRelevantValueChange) {
|
|
389
|
+
this.handleSlotContentChange();
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
this.mutationObserver.observe(this, {
|
|
394
|
+
attributeFilter: ['value'],
|
|
395
|
+
attributeOldValue: true,
|
|
396
|
+
attributes: true,
|
|
397
|
+
childList: true,
|
|
398
|
+
subtree: true,
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
setTimeout(() => {
|
|
402
|
+
// #813 needed to catch initial value via property binding
|
|
403
|
+
this.handleSlotContentChange();
|
|
404
|
+
});
|
|
405
|
+
|
|
247
406
|
// Because this is a form control, it shouldn't be opened initially
|
|
248
407
|
this.open = false;
|
|
249
408
|
}
|
|
250
409
|
|
|
410
|
+
disconnectedCallback() {
|
|
411
|
+
super.disconnectedCallback();
|
|
412
|
+
|
|
413
|
+
this.resizeObserver?.disconnect();
|
|
414
|
+
this.mutationObserver?.disconnect();
|
|
415
|
+
this.removeOpenListeners();
|
|
416
|
+
}
|
|
417
|
+
|
|
251
418
|
firstUpdated() {
|
|
252
419
|
this.isInitialized = true;
|
|
253
420
|
this.formControlController.updateValidity();
|
|
254
421
|
}
|
|
255
422
|
|
|
423
|
+
protected updated(changedProperties: PropertyValues<this>) {
|
|
424
|
+
super.updated(changedProperties);
|
|
425
|
+
if (changedProperties.has('multiple')) {
|
|
426
|
+
if (!this.multiple) {
|
|
427
|
+
this.resizeObserver?.disconnect();
|
|
428
|
+
} else {
|
|
429
|
+
this.enableResizeObserver();
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// eslint-disable-next-line complexity
|
|
256
435
|
protected override willUpdate(changedProperties: PropertyValues) {
|
|
257
436
|
super.willUpdate(changedProperties);
|
|
258
437
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
438
|
+
// Check for defaultValue if it is not undefined, null, empty string or empty array
|
|
439
|
+
const isDefaultValueEmpty = this.defaultValue == null
|
|
440
|
+
|| this.defaultValue === ''
|
|
441
|
+
|| (Array.isArray(this.defaultValue) && this.defaultValue.length === 0);
|
|
442
|
+
|
|
443
|
+
if (changedProperties.has('value') && isDefaultValueEmpty && this.value && !this.isUserInput) {
|
|
444
|
+
// If the value was set initially via property binding instead of attribute, we need to set the defaultValue manually
|
|
445
|
+
// to be able to reset forms and the dynamic loading of options are working correctly.
|
|
446
|
+
if (this.multiple && Array.isArray(this.value)) {
|
|
447
|
+
this.defaultValue = this.value.join(this.delimiter);
|
|
448
|
+
} else {
|
|
449
|
+
this.defaultValue = this.value;
|
|
450
|
+
}
|
|
451
|
+
this.valueHasChanged = false;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// This is needed, as the result otherwise is different on the attribute order of "multiple", "delimiter" and "value".
|
|
455
|
+
if (!this.isInitialized && changedProperties.has('value') && this.value !== undefined && changedProperties.has('multiple') && this.multiple) {
|
|
456
|
+
if (!Array.isArray(this.defaultValue)) {
|
|
457
|
+
const cachedValueHasChanged = this.valueHasChanged;
|
|
458
|
+
this.value = typeof this.defaultValue === 'string' ? this.defaultValue.split(this.delimiter) : [this.defaultValue].filter(isAllowedValue);
|
|
459
|
+
|
|
460
|
+
// Set it back to false since this isn't an interaction.
|
|
461
|
+
this.valueHasChanged = cachedValueHasChanged;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
attributeChangedCallback(name: string, oldVal: string | null, newVal: string | null) {
|
|
467
|
+
super.attributeChangedCallback(name, oldVal, newVal);
|
|
468
|
+
|
|
469
|
+
/** This is a backwards compatibility call. In a new major version we should make a clean separation between "value" the attribute mapping to "defaultValue" property and "value" the property not reflecting. */
|
|
470
|
+
if (name === 'value') {
|
|
471
|
+
const cachedValueHasChanged = this.valueHasChanged;
|
|
472
|
+
this.value = this.defaultValue;
|
|
473
|
+
|
|
474
|
+
// Set it back to false since this isn't an interaction.
|
|
475
|
+
this.valueHasChanged = cachedValueHasChanged;
|
|
264
476
|
}
|
|
265
477
|
}
|
|
266
478
|
|
|
479
|
+
protected get tags() {
|
|
480
|
+
return this.selectedOptions.map((option, index) => {
|
|
481
|
+
if (index < this.maxOptionsVisible || this.maxOptionsVisible <= 0) {
|
|
482
|
+
const tag = this.getTag(option, index);
|
|
483
|
+
// Wrap so we can handle the remove
|
|
484
|
+
return html`<div @syn-remove=${(e: SynRemoveEvent) => this.handleTagRemove(e, option)}>
|
|
485
|
+
${typeof tag === 'string' ? unsafeHTML(tag) : tag}
|
|
486
|
+
</div>`;
|
|
487
|
+
} if (index === this.maxOptionsVisible) {
|
|
488
|
+
// Hit tag limit
|
|
489
|
+
return html`<syn-tag size=${this.size}>+${this.selectedOptions.length - index}</syn-tag>`;
|
|
490
|
+
}
|
|
491
|
+
return html``;
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
|
|
267
495
|
private addOpenListeners() {
|
|
268
496
|
//
|
|
269
497
|
// Listen on the root node instead of the document in case the elements are inside a shadow root
|
|
@@ -271,7 +499,6 @@ export default class SynCombobox extends SynergyElement implements SynergyFormCo
|
|
|
271
499
|
// https://github.com/synergy-design-system/synergy/issues/1763
|
|
272
500
|
//
|
|
273
501
|
document.addEventListener('focusin', this.handleDocumentFocusIn);
|
|
274
|
-
document.addEventListener('keydown', this.handleDocumentKeyDown);
|
|
275
502
|
document.addEventListener('mousedown', this.handleDocumentMouseDown);
|
|
276
503
|
|
|
277
504
|
// If the component is rendered in a shadow root,
|
|
@@ -295,7 +522,6 @@ export default class SynCombobox extends SynergyElement implements SynergyFormCo
|
|
|
295
522
|
|
|
296
523
|
private removeOpenListeners() {
|
|
297
524
|
document.removeEventListener('focusin', this.handleDocumentFocusIn);
|
|
298
|
-
document.removeEventListener('keydown', this.handleDocumentKeyDown);
|
|
299
525
|
document.removeEventListener('mousedown', this.handleDocumentMouseDown);
|
|
300
526
|
|
|
301
527
|
if (this.getRootNode() !== document) {
|
|
@@ -342,11 +568,17 @@ export default class SynCombobox extends SynergyElement implements SynergyFormCo
|
|
|
342
568
|
this.hide();
|
|
343
569
|
this.displayInput.focus({ preventScroll: true });
|
|
344
570
|
} else if (!this.open) {
|
|
345
|
-
this.
|
|
571
|
+
if (this.multiple) {
|
|
572
|
+
// In multiple mode, only clear the input field but preserve selected options
|
|
573
|
+
this.clearInputField();
|
|
574
|
+
} else {
|
|
575
|
+
// In single mode, clear everything as before
|
|
576
|
+
this.clearCombobox();
|
|
577
|
+
}
|
|
346
578
|
}
|
|
347
579
|
}
|
|
348
580
|
|
|
349
|
-
// Handle enter
|
|
581
|
+
// Handle enter - either select option or submit form
|
|
350
582
|
if (event.key === 'Enter') {
|
|
351
583
|
const currentOption = this.getCurrentOption();
|
|
352
584
|
|
|
@@ -368,11 +600,26 @@ export default class SynCombobox extends SynergyElement implements SynergyFormCo
|
|
|
368
600
|
|
|
369
601
|
// Update the value based on the current selection and close it
|
|
370
602
|
if (currentOption) {
|
|
371
|
-
|
|
372
|
-
this.
|
|
603
|
+
this.isUserInput = true;
|
|
604
|
+
this.valueHasChanged = true;
|
|
605
|
+
const oldValue = this.lastOptions ? getValuesFromOptions(this.lastOptions) : [];
|
|
606
|
+
|
|
607
|
+
if (this.multiple) {
|
|
608
|
+
this.toggleOptionSelection(currentOption);
|
|
609
|
+
} else {
|
|
610
|
+
this.setSelectedOptions(currentOption);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
this.selectionChanged();
|
|
373
614
|
|
|
374
|
-
|
|
375
|
-
|
|
615
|
+
const value = Array.isArray(this.value) ? this.value : [this.value];
|
|
616
|
+
|
|
617
|
+
// Make reset in forms work correctly via isUserInput flag
|
|
618
|
+
this.updateComplete.then(() => {
|
|
619
|
+
this.isUserInput = false;
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
if (!compareValues(oldValue, value)) {
|
|
376
623
|
// Emit after updating
|
|
377
624
|
this.updateComplete.then(() => {
|
|
378
625
|
this.emit('syn-input');
|
|
@@ -381,7 +628,9 @@ export default class SynCombobox extends SynergyElement implements SynergyFormCo
|
|
|
381
628
|
}
|
|
382
629
|
}
|
|
383
630
|
|
|
384
|
-
this.
|
|
631
|
+
if (!this.multiple) {
|
|
632
|
+
this.hide();
|
|
633
|
+
}
|
|
385
634
|
this.displayInput.focus({ preventScroll: true });
|
|
386
635
|
return;
|
|
387
636
|
}
|
|
@@ -408,8 +657,8 @@ export default class SynCombobox extends SynergyElement implements SynergyFormCo
|
|
|
408
657
|
this.displayInput.setSelectionRange(0, 0);
|
|
409
658
|
} else if (event.key === 'End') {
|
|
410
659
|
this.displayInput.setSelectionRange(
|
|
411
|
-
this.
|
|
412
|
-
this.
|
|
660
|
+
this.displayLabel.length,
|
|
661
|
+
this.displayLabel.length,
|
|
413
662
|
);
|
|
414
663
|
}
|
|
415
664
|
}
|
|
@@ -429,9 +678,30 @@ export default class SynCombobox extends SynergyElement implements SynergyFormCo
|
|
|
429
678
|
this.displayInput.focus();
|
|
430
679
|
}
|
|
431
680
|
|
|
681
|
+
private handleTagRemove(event: SynRemoveEvent, option: SynOption) {
|
|
682
|
+
event.stopPropagation();
|
|
683
|
+
|
|
684
|
+
this.valueHasChanged = true;
|
|
685
|
+
|
|
686
|
+
if (!this.disabled) {
|
|
687
|
+
this.toggleOptionSelection(option, false);
|
|
688
|
+
this.selectionChanged();
|
|
689
|
+
|
|
690
|
+
// Emit after updating
|
|
691
|
+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
692
|
+
this.updateComplete.then(() => {
|
|
693
|
+
this.emit('syn-input');
|
|
694
|
+
this.emit('syn-change');
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
432
699
|
private handleComboboxMouseDown(event: MouseEvent) {
|
|
433
|
-
|
|
434
|
-
|
|
700
|
+
const path = event.composedPath();
|
|
701
|
+
const isIconButton = path.some(el => el instanceof Element && el.tagName.toLowerCase() === 'syn-icon-button');
|
|
702
|
+
|
|
703
|
+
// Ignore disabled controls and clicks on tags (remove buttons)
|
|
704
|
+
if (this.disabled || isIconButton) {
|
|
435
705
|
return;
|
|
436
706
|
}
|
|
437
707
|
|
|
@@ -457,14 +727,30 @@ export default class SynCombobox extends SynergyElement implements SynergyFormCo
|
|
|
457
727
|
this.clearCombobox();
|
|
458
728
|
}
|
|
459
729
|
|
|
730
|
+
private clearInputField() {
|
|
731
|
+
if (this.displayLabel !== '') {
|
|
732
|
+
const cachedValueHasChanged = this.valueHasChanged;
|
|
733
|
+
// remove the text from the input field only by reset value to selected options
|
|
734
|
+
this.value = getValuesFromOptions(this.selectedOptions);
|
|
735
|
+
this.valueHasChanged = cachedValueHasChanged;
|
|
736
|
+
this.displayInput.focus({ preventScroll: true });
|
|
737
|
+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
738
|
+
this.updateComplete.then(() => {
|
|
739
|
+
this.emit('syn-input');
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
460
744
|
private clearCombobox() {
|
|
745
|
+
this.valueHasChanged = true;
|
|
746
|
+
|
|
461
747
|
if (this.value !== '') {
|
|
462
748
|
this.value = '';
|
|
463
|
-
this.
|
|
464
|
-
this.
|
|
465
|
-
this.
|
|
749
|
+
this.displayLabel = '';
|
|
750
|
+
this.lastOptions = [];
|
|
751
|
+
this.setSelectedOptions([]);
|
|
752
|
+
this.selectionChanged();
|
|
466
753
|
this.displayInput.focus({ preventScroll: true });
|
|
467
|
-
this.setSelectedOptionToSelected();
|
|
468
754
|
|
|
469
755
|
// Emit after update
|
|
470
756
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
@@ -487,15 +773,28 @@ export default class SynCombobox extends SynergyElement implements SynergyFormCo
|
|
|
487
773
|
private handleOptionClick(event: MouseEvent) {
|
|
488
774
|
const target = event.target as HTMLElement;
|
|
489
775
|
const option = target.closest('syn-option');
|
|
490
|
-
const oldValue = this.
|
|
776
|
+
const oldValue = this.lastOptions ? getValuesFromOptions(this.lastOptions) : [];
|
|
491
777
|
if (option && !option.disabled) {
|
|
492
|
-
this.
|
|
778
|
+
this.isUserInput = true;
|
|
779
|
+
|
|
780
|
+
this.valueHasChanged = true;
|
|
781
|
+
if (this.multiple) {
|
|
782
|
+
this.toggleOptionSelection(option);
|
|
783
|
+
} else {
|
|
784
|
+
this.setSelectedOptions(option);
|
|
785
|
+
}
|
|
786
|
+
this.selectionChanged();
|
|
493
787
|
|
|
494
788
|
// Set focus after updating so the value is announced by screen readers
|
|
495
|
-
|
|
789
|
+
// and make the reset in forms work correctly via isUserInput flag
|
|
790
|
+
this.updateComplete.then(() => {
|
|
791
|
+
this.displayInput.focus({ preventScroll: true });
|
|
792
|
+
this.isUserInput = false;
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
const value = Array.isArray(this.value) ? this.value : [this.value];
|
|
496
796
|
|
|
497
|
-
if (
|
|
498
|
-
this.setSelectedOptionToSelected();
|
|
797
|
+
if (!compareValues(oldValue, value)) {
|
|
499
798
|
// Emit after updating
|
|
500
799
|
this.updateComplete.then(() => {
|
|
501
800
|
this.emit('syn-input');
|
|
@@ -503,8 +802,10 @@ export default class SynCombobox extends SynergyElement implements SynergyFormCo
|
|
|
503
802
|
});
|
|
504
803
|
}
|
|
505
804
|
|
|
506
|
-
this.
|
|
507
|
-
|
|
805
|
+
if (!this.multiple) {
|
|
806
|
+
this.hide();
|
|
807
|
+
this.displayInput.focus({ preventScroll: true });
|
|
808
|
+
}
|
|
508
809
|
}
|
|
509
810
|
}
|
|
510
811
|
/* eslint-enable @typescript-eslint/no-floating-promises */
|
|
@@ -537,6 +838,44 @@ export default class SynCombobox extends SynergyElement implements SynergyFormCo
|
|
|
537
838
|
scrollIntoView(this.getCurrentOption()!, this.listbox, 'vertical', 'auto');
|
|
538
839
|
}
|
|
539
840
|
|
|
841
|
+
// Toggles an option's selected state
|
|
842
|
+
// eslint-disable-next-line class-methods-use-this
|
|
843
|
+
private toggleOptionSelection(option: SynOption, force?: boolean) {
|
|
844
|
+
if (force === true || force === false) {
|
|
845
|
+
option.selected = force;
|
|
846
|
+
} else {
|
|
847
|
+
option.selected = !option.selected;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// This is needed so the highlight option renderer is working correctly for `restricted` and `multiple` comboboxes
|
|
851
|
+
const cachedOption = this.cachedOptions.find(opt => opt.id === option.id);
|
|
852
|
+
if (cachedOption) {
|
|
853
|
+
cachedOption.selected = option.selected;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// Sets the selected option(s)
|
|
858
|
+
private setSelectedOptions(option: SynOption | SynOption[]) {
|
|
859
|
+
const newSelectedOptions = Array.isArray(option) ? option : [option];
|
|
860
|
+
// In single-select mode, if multiple options are provided, keep only the first one to select
|
|
861
|
+
if (!this.multiple && newSelectedOptions.length > 1) {
|
|
862
|
+
newSelectedOptions.splice(1);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
const slottedOptions = this.getSlottedOptions();
|
|
866
|
+
slottedOptions.forEach((opt) => {
|
|
867
|
+
opt.selected = newSelectedOptions.some(
|
|
868
|
+
selectedOpt => selectedOpt.id === opt.id,
|
|
869
|
+
);
|
|
870
|
+
});
|
|
871
|
+
// This is needed so the highlight option renderer is working correctly for `restricted` and `multiple` comboboxes
|
|
872
|
+
this.cachedOptions.forEach((opt) => {
|
|
873
|
+
opt.selected = newSelectedOptions.some(
|
|
874
|
+
selectedOpt => selectedOpt.id === opt.id,
|
|
875
|
+
);
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
|
|
540
879
|
private getAllFilteredOptions() {
|
|
541
880
|
return this.getSlottedOptions().filter(option => !option.hidden);
|
|
542
881
|
}
|
|
@@ -569,47 +908,68 @@ export default class SynCombobox extends SynergyElement implements SynergyFormCo
|
|
|
569
908
|
}
|
|
570
909
|
|
|
571
910
|
/**
|
|
572
|
-
* Updates the
|
|
911
|
+
* Updates the component state after selection changes.
|
|
912
|
+
*
|
|
913
|
+
* This method synchronizes:
|
|
914
|
+
* 1. The selectedOptions cache with currently selected options
|
|
915
|
+
* 2. The component's value property (string or array)
|
|
916
|
+
* 3. The display label shown in the input
|
|
917
|
+
* 4. Form validation state
|
|
918
|
+
*
|
|
919
|
+
* **Validation Logic:**
|
|
920
|
+
* - In restricted mode, invalid values trigger a reset to last valid state
|
|
921
|
+
* - Multiple mode requires all values to correspond to existing options
|
|
922
|
+
* - Single mode allows free text input when not restricted
|
|
573
923
|
*/
|
|
574
924
|
// eslint-disable-next-line complexity
|
|
575
|
-
private
|
|
576
|
-
|
|
925
|
+
private selectionChanged() {
|
|
926
|
+
const options = this.getSlottedOptions();
|
|
927
|
+
this.selectedOptions = options.filter(opt => opt.selected);
|
|
928
|
+
|
|
929
|
+
// This is needed if there are no valid options and therefore just the typed in value should be displayed
|
|
930
|
+
if (this.selectedOptions.length === 0) {
|
|
931
|
+
this.displayLabel = Array.isArray(this.value) ? this.value.join(', ') : String(this.value);
|
|
932
|
+
}
|
|
577
933
|
|
|
578
934
|
let optionValue;
|
|
579
935
|
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
936
|
+
// Keep a reference to the previous `valueHasChanged`. Changes made here don't count has changing the value.
|
|
937
|
+
const cachedValueHasChanged = this.valueHasChanged;
|
|
938
|
+
|
|
939
|
+
if (this.multiple) {
|
|
940
|
+
this.value = this.selectedOptions.map(opt => getValueFromOption(opt));
|
|
941
|
+
if (this.value.length === 0 && this.selectedOptions.length !== 0) {
|
|
942
|
+
this.valueHasChanged = cachedValueHasChanged;
|
|
943
|
+
this.resetToLastValidValue();
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
946
|
+
} else {
|
|
947
|
+
if (this.selectedOptions.length !== 0) {
|
|
948
|
+
// This is only for non multiple
|
|
949
|
+
optionValue = getValueFromOption(this.selectedOptions[0]);
|
|
950
|
+
} else if (this.restricted && !this.isValidValue(this.displayLabel) && this.displayLabel !== '' && !this.isUserInput) {
|
|
951
|
+
// if an invalid value was set via property binding for `restricted`comboboxes,
|
|
952
|
+
// reset to last valid value
|
|
953
|
+
this.resetToLastValidValue();
|
|
954
|
+
this.valueHasChanged = cachedValueHasChanged;
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
this.value = optionValue ?? this.displayLabel;
|
|
588
958
|
}
|
|
589
959
|
|
|
590
|
-
|
|
591
|
-
|
|
960
|
+
this.valueHasChanged = cachedValueHasChanged;
|
|
961
|
+
|
|
962
|
+
// Store the new last selected options
|
|
963
|
+
this.lastOptions = [...this.selectedOptions];
|
|
592
964
|
|
|
593
965
|
// Update validity and display label
|
|
594
966
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
595
967
|
this.updateComplete.then(() => {
|
|
596
|
-
this.displayLabel = this.
|
|
968
|
+
this.displayLabel = this.multiple ? '' : this.selectedOptions[0]?.getTextLabel() ?? this.displayLabel;
|
|
597
969
|
this.formControlController.updateValidity();
|
|
598
970
|
});
|
|
599
971
|
}
|
|
600
972
|
|
|
601
|
-
private setSelectedOptionToSelected() {
|
|
602
|
-
const slottedOptions = this.getSlottedOptions();
|
|
603
|
-
slottedOptions.forEach((opt) => {
|
|
604
|
-
// eslint-disable-next-line no-param-reassign
|
|
605
|
-
opt.selected = false;
|
|
606
|
-
});
|
|
607
|
-
|
|
608
|
-
if (this.selectedOption) {
|
|
609
|
-
this.selectedOption.selected = true;
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
|
|
613
973
|
private handleInvalid(event: Event) {
|
|
614
974
|
this.formControlController.setValidity(false);
|
|
615
975
|
this.formControlController.emitInvalidEvent(event);
|
|
@@ -617,7 +977,18 @@ export default class SynCombobox extends SynergyElement implements SynergyFormCo
|
|
|
617
977
|
|
|
618
978
|
@watch(['filter', 'getOption'], { waitUntilFirstUpdate: true })
|
|
619
979
|
handlePropertiesChange() {
|
|
620
|
-
this.createComboboxOptionsFromQuery(this.
|
|
980
|
+
this.createComboboxOptionsFromQuery(this.displayLabel);
|
|
981
|
+
if (this.open) {
|
|
982
|
+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
983
|
+
this.updateComplete.then(() => {
|
|
984
|
+
this.open = this.multiple || this.restricted || this.numberFilteredOptions > 0;
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
@watch('displayLabel', { waitUntilFirstUpdate: true })
|
|
990
|
+
handleDisplayInputValueChange() {
|
|
991
|
+
this.createComboboxOptionsFromQuery(this.displayLabel);
|
|
621
992
|
}
|
|
622
993
|
|
|
623
994
|
@watch('disabled', { waitUntilFirstUpdate: true })
|
|
@@ -633,17 +1004,31 @@ export default class SynCombobox extends SynergyElement implements SynergyFormCo
|
|
|
633
1004
|
}
|
|
634
1005
|
}
|
|
635
1006
|
|
|
636
|
-
@watch('
|
|
1007
|
+
@watch('delimiter')
|
|
1008
|
+
handleDelimiterChange() {
|
|
1009
|
+
this.getSlottedOptions().forEach(option => {
|
|
1010
|
+
option.delimiter = this.delimiter;
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
@watch(['defaultValue', 'value', 'delimiter', 'multiple', 'restricted'], { waitUntilFirstUpdate: true })
|
|
637
1015
|
handleValueChange() {
|
|
1016
|
+
if (!this.valueHasChanged) {
|
|
1017
|
+
const cachedValueHasChanged = this.valueHasChanged;
|
|
1018
|
+
this.value = this.defaultValue;
|
|
1019
|
+
|
|
1020
|
+
// Set it back to false since this isn't an interaction.
|
|
1021
|
+
this.valueHasChanged = cachedValueHasChanged;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
638
1024
|
this.updateSelectedOptionFromValue();
|
|
639
|
-
this.setCurrentOption(null);
|
|
640
1025
|
}
|
|
641
1026
|
|
|
642
1027
|
@watch('open', { waitUntilFirstUpdate: true })
|
|
643
1028
|
async handleOpenChange() {
|
|
644
1029
|
if (this.open && !this.disabled) {
|
|
645
|
-
if (this.numberFilteredOptions === 0 && !this.restricted) {
|
|
646
|
-
// Don't open the listbox if there are no options and it is not restricted
|
|
1030
|
+
if (this.numberFilteredOptions === 0 && !this.restricted && !this.multiple) {
|
|
1031
|
+
// Don't open the listbox if there are no options and it is not restricted or multiple
|
|
647
1032
|
this.open = false;
|
|
648
1033
|
this.emit('syn-error');
|
|
649
1034
|
return;
|
|
@@ -738,32 +1123,53 @@ export default class SynCombobox extends SynergyElement implements SynergyFormCo
|
|
|
738
1123
|
this.displayInput.blur();
|
|
739
1124
|
}
|
|
740
1125
|
|
|
1126
|
+
/**
|
|
1127
|
+
* Updates the visibility and rendering of options based on the current query string.
|
|
1128
|
+
*
|
|
1129
|
+
* This method performs several critical tasks:
|
|
1130
|
+
* 1. **Option Filtering**: Uses the `filter` function to determine which options should be visible
|
|
1131
|
+
* 2. **Custom Rendering**: Applies the `getOption` function to customize option appearance
|
|
1132
|
+
* 3. **Optgroup Management**: Shows/hides optgroups based on their visible children
|
|
1133
|
+
* 4. **Counts visible options**: Tracks the number of visible options for UI logic
|
|
1134
|
+
*
|
|
1135
|
+
* **Performance Considerations:**
|
|
1136
|
+
* - Uses cached options to avoid repeated DOM queries
|
|
1137
|
+
* - Prevents infinite loops during option updates with `isOptionRendererTriggered`
|
|
1138
|
+
*
|
|
1139
|
+
* @param queryString - The current user input to filter and highlight options with
|
|
1140
|
+
*/
|
|
741
1141
|
/* eslint-disable no-param-reassign, @typescript-eslint/no-floating-promises */
|
|
742
1142
|
private createComboboxOptionsFromQuery(queryString: string) {
|
|
743
1143
|
this.numberFilteredOptions = 0;
|
|
1144
|
+
|
|
1145
|
+
// Prevent slot change events during option updates to avoid infinite loops
|
|
744
1146
|
this.isOptionRendererTriggered = true;
|
|
745
1147
|
|
|
746
|
-
// This is needed for angular. For some reason the
|
|
1148
|
+
// This is needed for angular. For some reason the handleSlotContentChange is not triggered
|
|
747
1149
|
// initially and therefore the cachedOptions are not set.
|
|
748
1150
|
if (this.cachedOptions.length === 0) {
|
|
749
1151
|
this.cacheSlottedOptionsAndOptgroups();
|
|
750
1152
|
}
|
|
751
1153
|
|
|
752
|
-
// Update
|
|
1154
|
+
// Update each syn-option based on the query string and custom getOption renderer
|
|
753
1155
|
this.getSlottedOptions()
|
|
754
1156
|
.forEach(option => {
|
|
755
1157
|
// Use the original cached option, to do changes on it
|
|
756
1158
|
const cachedOption = this.cachedOptions.find(o => o.id === option.id) || option;
|
|
1159
|
+
|
|
1160
|
+
// Apply custom option rendering
|
|
757
1161
|
const optionResult = this.getOption(cachedOption, queryString);
|
|
758
1162
|
let updatedOption = createOptionFromDifferentTypes(optionResult);
|
|
759
1163
|
|
|
760
|
-
//
|
|
1164
|
+
// Fall back to original option if rendering fails
|
|
761
1165
|
if (!updatedOption) {
|
|
762
1166
|
updatedOption = cachedOption;
|
|
763
1167
|
}
|
|
764
1168
|
|
|
1169
|
+
// Apply filtering logic to determine visibility
|
|
765
1170
|
const hideOption = !(this.filter(updatedOption, queryString) || queryString === '');
|
|
766
1171
|
updatedOption.hidden = hideOption;
|
|
1172
|
+
|
|
767
1173
|
option.replaceWith(updatedOption);
|
|
768
1174
|
|
|
769
1175
|
if (!hideOption) {
|
|
@@ -771,7 +1177,7 @@ export default class SynCombobox extends SynergyElement implements SynergyFormCo
|
|
|
771
1177
|
}
|
|
772
1178
|
});
|
|
773
1179
|
|
|
774
|
-
// Update
|
|
1180
|
+
// Update optgroup visibility based on their children
|
|
775
1181
|
const visibleOptgroups = this.getSlottedOptGroups().filter(optgroup => {
|
|
776
1182
|
const options = getAllOptions(Array.from(optgroup.children) as HTMLElement[]).flat();
|
|
777
1183
|
const isVisible = options.some(option => !option.hidden);
|
|
@@ -782,45 +1188,75 @@ export default class SynCombobox extends SynergyElement implements SynergyFormCo
|
|
|
782
1188
|
// Hide the divider of the first visible optgroup, as it can unfortunately not be hidden via css
|
|
783
1189
|
visibleOptgroups[0]?.style.setProperty('--display-divider', 'none');
|
|
784
1190
|
|
|
785
|
-
//
|
|
786
|
-
// because the slot change event is endlessly retriggered otherwise.
|
|
1191
|
+
// Reset flag after updates are complete to re-enable slot change handling
|
|
787
1192
|
setTimeout(() => {
|
|
788
1193
|
this.isOptionRendererTriggered = false;
|
|
789
|
-
}
|
|
1194
|
+
});
|
|
790
1195
|
}
|
|
791
1196
|
/* eslint-enable no-param-reassign, @typescript-eslint/no-floating-promises */
|
|
792
1197
|
|
|
1198
|
+
// eslint-disable-next-line complexity
|
|
793
1199
|
private async handleInput() {
|
|
794
1200
|
const inputValue = this.displayInput.value;
|
|
795
|
-
|
|
1201
|
+
this.displayLabel = inputValue;
|
|
1202
|
+
const cachedLastOption = this.lastOptions;
|
|
796
1203
|
this.isUserInput = true;
|
|
797
1204
|
|
|
798
|
-
|
|
1205
|
+
// Do the reset of the selected options before the value setting, as otherwise we are getting endless default slot triggering in safari
|
|
1206
|
+
if (!this.multiple) {
|
|
1207
|
+
this.selectedOptions = [];
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
if (this.multiple) {
|
|
1211
|
+
// In multiple mode, combine selected option values with current input
|
|
1212
|
+
const validValues = getValuesFromOptions(this.selectedOptions);
|
|
1213
|
+
this.value = [...validValues, inputValue];
|
|
1214
|
+
} else {
|
|
1215
|
+
// In single mode, replace value with current input
|
|
1216
|
+
this.value = inputValue;
|
|
1217
|
+
}
|
|
1218
|
+
|
|
799
1219
|
await this.updateComplete;
|
|
800
1220
|
this.isUserInput = false;
|
|
801
|
-
this.
|
|
802
|
-
this.open = this.restricted || this.numberFilteredOptions > 0;
|
|
803
|
-
|
|
1221
|
+
this.lastOptions = cachedLastOption;
|
|
1222
|
+
this.open = this.multiple || this.restricted || this.numberFilteredOptions > 0;
|
|
1223
|
+
|
|
804
1224
|
this.formControlController.updateValidity();
|
|
805
1225
|
this.emit('syn-input');
|
|
806
1226
|
}
|
|
807
1227
|
|
|
808
1228
|
/**
|
|
809
|
-
* Checks if the
|
|
1229
|
+
* Checks if the value is available in the options list.
|
|
810
1230
|
* This is used to determine if the value is valid when the combobox is restricted.
|
|
811
|
-
*
|
|
1231
|
+
* @param value - The value to check for validity.
|
|
812
1232
|
* @returns `true` if the current value is available in the options list,
|
|
813
1233
|
* otherwise `false`.
|
|
814
1234
|
*/
|
|
815
|
-
private isValidValue(): boolean {
|
|
1235
|
+
private isValidValue(value: string | number): boolean {
|
|
816
1236
|
const isValid = this.cachedOptions.some(
|
|
817
|
-
option =>
|
|
1237
|
+
option => checkValueBelongsToOption(value, option),
|
|
818
1238
|
);
|
|
819
1239
|
return isValid;
|
|
820
1240
|
}
|
|
821
1241
|
|
|
822
|
-
private
|
|
823
|
-
|
|
1242
|
+
private getOptionsFromValue(): SynOption[] {
|
|
1243
|
+
// Use defaultValue only if the value has not been changed by the user
|
|
1244
|
+
const value = this.valueHasChanged ? this.value : this.defaultValue;
|
|
1245
|
+
// #845: if value is undefined return empty array
|
|
1246
|
+
let convertedValue: (string | number)[];
|
|
1247
|
+
if (Array.isArray(value)) {
|
|
1248
|
+
convertedValue = value;
|
|
1249
|
+
} else if (value === undefined || value == null) {
|
|
1250
|
+
convertedValue = [];
|
|
1251
|
+
} else if (this.multiple && typeof value === 'string') {
|
|
1252
|
+
convertedValue = value.split(this.delimiter);
|
|
1253
|
+
} else {
|
|
1254
|
+
convertedValue = [value];
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
return convertedValue
|
|
1258
|
+
.map((val) => this.cachedOptions.find(option => checkValueBelongsToOption(val, option)))
|
|
1259
|
+
.filter((opt) => opt !== undefined);
|
|
824
1260
|
}
|
|
825
1261
|
|
|
826
1262
|
/**
|
|
@@ -828,11 +1264,14 @@ export default class SynCombobox extends SynergyElement implements SynergyFormCo
|
|
|
828
1264
|
*/
|
|
829
1265
|
private resetToLastValidValue() {
|
|
830
1266
|
let label = '';
|
|
831
|
-
let value =
|
|
1267
|
+
let value: Array<string | number> = [];
|
|
832
1268
|
|
|
833
|
-
if (this.
|
|
834
|
-
value =
|
|
835
|
-
|
|
1269
|
+
if (this.lastOptions.length !== 0) {
|
|
1270
|
+
value = getValuesFromOptions(this.lastOptions);
|
|
1271
|
+
|
|
1272
|
+
if (!this.multiple) {
|
|
1273
|
+
label = this.lastOptions[0].getTextLabel();
|
|
1274
|
+
}
|
|
836
1275
|
}
|
|
837
1276
|
|
|
838
1277
|
// Wait for the popup close animation to be finished before updating the value.
|
|
@@ -847,40 +1286,68 @@ export default class SynCombobox extends SynergyElement implements SynergyFormCo
|
|
|
847
1286
|
)
|
|
848
1287
|
: Promise.resolve();
|
|
849
1288
|
|
|
1289
|
+
this.hideOptions = true;
|
|
1290
|
+
|
|
850
1291
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
851
1292
|
waitForAnimations.then(() => {
|
|
852
|
-
|
|
853
|
-
this.
|
|
854
|
-
this.formControlController.updateValidity();
|
|
1293
|
+
// restore options after the animation
|
|
1294
|
+
this.hideOptions = false;
|
|
855
1295
|
});
|
|
1296
|
+
// Keep a reference to the previous `valueHasChanged`. Changes made here don't count has changing the value.
|
|
1297
|
+
const cachedValueHasChanged = this.valueHasChanged;
|
|
1298
|
+
this.value = value;
|
|
1299
|
+
this.displayLabel = label;
|
|
1300
|
+
this.formControlController.updateValidity();
|
|
1301
|
+
this.valueHasChanged = cachedValueHasChanged;
|
|
856
1302
|
}
|
|
857
1303
|
|
|
1304
|
+
// eslint-disable-next-line complexity
|
|
858
1305
|
private handleChange() {
|
|
859
1306
|
// Only update the value and emit the event, if the change event occurred by
|
|
860
1307
|
// the user typing something in and removing focus of the combobox
|
|
861
|
-
|
|
1308
|
+
const isSameSelection = (this.selectedOptions.length !== 0 && this.selectedOptions.length === this.getOptionsFromValue().length);
|
|
1309
|
+
let values: (string | number)[];
|
|
1310
|
+
if (Array.isArray(this.value)) {
|
|
1311
|
+
values = this.value;
|
|
1312
|
+
} else if (typeof this.value === 'string') {
|
|
1313
|
+
values = this.value.split(this.delimiter);
|
|
1314
|
+
} else {
|
|
1315
|
+
values = [this.value];
|
|
1316
|
+
}
|
|
1317
|
+
const allValuesValid = values.every((val) => this.isValidValue(val));
|
|
1318
|
+
const isMultipleAndUserInput = this.multiple && isSameSelection && allValuesValid;
|
|
1319
|
+
const isNonMultipleAndUserInput = !this.multiple && this.selectedOptions.length > 0;
|
|
1320
|
+
if (isNonMultipleAndUserInput || isMultipleAndUserInput) {
|
|
862
1321
|
return;
|
|
863
1322
|
}
|
|
864
1323
|
|
|
1324
|
+
const oldValue = this.lastOptions ? getValuesFromOptions(this.lastOptions) : [];
|
|
1325
|
+
|
|
865
1326
|
// If the value is not valid, we need to reset the value to the last valid value
|
|
866
|
-
if (this.restricted && !this.isValidValue() && this.
|
|
1327
|
+
if ((this.restricted || this.multiple) && !this.isValidValue(this.displayLabel) && this.displayLabel !== '') {
|
|
867
1328
|
this.resetToLastValidValue();
|
|
868
1329
|
return;
|
|
869
1330
|
}
|
|
1331
|
+
const options = this.getOptionsFromValue();
|
|
870
1332
|
|
|
871
|
-
|
|
872
|
-
this.
|
|
873
|
-
this.
|
|
874
|
-
this.selectedOption = this.getOptionFromValue();
|
|
1333
|
+
this.setSelectedOptions(options);
|
|
1334
|
+
this.selectionChanged();
|
|
1335
|
+
this.lastOptions = [...options];
|
|
875
1336
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
876
1337
|
this.updateComplete.then(() => {
|
|
877
1338
|
this.formControlController.updateValidity();
|
|
878
1339
|
});
|
|
879
|
-
|
|
880
|
-
this.
|
|
1340
|
+
|
|
1341
|
+
const value = Array.isArray(this.value) ? this.value : [this.value];
|
|
1342
|
+
if (!compareValues(oldValue, value)) {
|
|
1343
|
+
this.emit('syn-change');
|
|
1344
|
+
}
|
|
881
1345
|
}
|
|
882
1346
|
|
|
883
1347
|
private getSlottedOptions() {
|
|
1348
|
+
if (!this.defaultSlot) {
|
|
1349
|
+
return [];
|
|
1350
|
+
}
|
|
884
1351
|
return getAllOptions(getAssignedElementsForSlot(this.defaultSlot)).flat();
|
|
885
1352
|
}
|
|
886
1353
|
|
|
@@ -907,44 +1374,81 @@ export default class SynCombobox extends SynergyElement implements SynergyFormCo
|
|
|
907
1374
|
/* eslint-enable no-param-reassign */
|
|
908
1375
|
|
|
909
1376
|
private updateSelectedOptionFromValue(): void {
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
1377
|
+
if (!this.isUserInput) {
|
|
1378
|
+
// check if the value has corresponding options via value or text content
|
|
1379
|
+
// for empty values use the text content, as then the values of the options are not set
|
|
1380
|
+
const options = this.getOptionsFromValue();
|
|
913
1381
|
|
|
914
|
-
|
|
915
|
-
this.
|
|
1382
|
+
this.setSelectedOptions(options);
|
|
1383
|
+
this.selectionChanged();
|
|
916
1384
|
}
|
|
917
1385
|
|
|
918
|
-
|
|
919
|
-
|
|
1386
|
+
let queryString = '';
|
|
1387
|
+
if (this.multiple) {
|
|
1388
|
+
queryString = this.displayLabel;
|
|
1389
|
+
} else {
|
|
1390
|
+
queryString = Array.isArray(this.value) ? this.value.join(', ') : String(this.value);
|
|
1391
|
+
}
|
|
1392
|
+
this.createComboboxOptionsFromQuery(queryString);
|
|
920
1393
|
}
|
|
921
1394
|
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
1395
|
+
/**
|
|
1396
|
+
* Synchronizes the internal component state with changes to slotted options.
|
|
1397
|
+
*
|
|
1398
|
+
* This method is automatically triggered by:
|
|
1399
|
+
* - MutationObserver when option 'value' attributes change
|
|
1400
|
+
* - Initial component setup during connectedCallback (after timeout)
|
|
1401
|
+
* - Default slot changes (via handleDefaultSlotChange)
|
|
1402
|
+
* - Custom element registration completion for syn-option (deferred execution)
|
|
1403
|
+
*
|
|
1404
|
+
* The synchronization process:
|
|
1405
|
+
* 1. Waits for syn-option custom elements to be registered before processing
|
|
1406
|
+
* 2. Updates delimiter settings on all slotted options
|
|
1407
|
+
* 3. Refreshes the internal cache of options and optgroups
|
|
1408
|
+
* 4. Synchronizes selected options based on current component value
|
|
1409
|
+
* 5. Auto-opens listbox if component has focus, has value, and is currently closed
|
|
1410
|
+
*
|
|
1411
|
+
* This ensures the component's internal state stays consistent with the slotted
|
|
1412
|
+
* DOM content after options are added, removed, or their values change.
|
|
1413
|
+
*
|
|
1414
|
+
*/
|
|
1415
|
+
/* eslint-disable @typescript-eslint/no-floating-promises */
|
|
1416
|
+
private handleSlotContentChange() {
|
|
1417
|
+
// Rerun this handler when <syn-option> is registered
|
|
1418
|
+
if (!customElements.get('syn-option')) {
|
|
1419
|
+
customElements.whenDefined('syn-option').then(() => this.handleSlotContentChange());
|
|
1420
|
+
return;
|
|
1421
|
+
}
|
|
930
1422
|
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
if (!customElements.get('syn-option')) {
|
|
934
|
-
customElements.whenDefined('syn-option').then(() => this.handleDefaultSlotChange());
|
|
935
|
-
return;
|
|
936
|
-
}
|
|
1423
|
+
this.handleDelimiterChange();
|
|
1424
|
+
this.cacheSlottedOptionsAndOptgroups();
|
|
937
1425
|
|
|
938
|
-
|
|
1426
|
+
this.updateSelectedOptionFromValue();
|
|
939
1427
|
|
|
940
|
-
|
|
1428
|
+
// Auto-open listbox for better UX when new options are added during interaction
|
|
1429
|
+
let hasValue: boolean;
|
|
1430
|
+
if (Array.isArray(this.value)) {
|
|
1431
|
+
hasValue = this.value.length > 0;
|
|
1432
|
+
} else if (typeof this.value === 'string') {
|
|
1433
|
+
hasValue = this.value.length > 0;
|
|
1434
|
+
} else {
|
|
1435
|
+
hasValue = this.value !== undefined && this.value !== null;
|
|
1436
|
+
}
|
|
941
1437
|
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
1438
|
+
if (this.hasFocus && hasValue && !this.open) {
|
|
1439
|
+
this.show();
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
/* eslint-enable @typescript-eslint/no-floating-promises */
|
|
1443
|
+
|
|
1444
|
+
public handleDefaultSlotChange() {
|
|
1445
|
+
// Ignore slot changes triggered by our own option updates
|
|
1446
|
+
if (this.isOptionRendererTriggered) {
|
|
1447
|
+
return;
|
|
945
1448
|
}
|
|
1449
|
+
|
|
1450
|
+
this.handleSlotContentChange();
|
|
946
1451
|
}
|
|
947
|
-
/* eslint-enable @typescript-eslint/no-floating-promises, complexity */
|
|
948
1452
|
|
|
949
1453
|
/* eslint-disable @typescript-eslint/unbound-method */
|
|
950
1454
|
// eslint-disable-next-line complexity
|
|
@@ -953,20 +1457,30 @@ export default class SynCombobox extends SynergyElement implements SynergyFormCo
|
|
|
953
1457
|
const hasHelpTextSlot = this.hasSlotController.test('help-text');
|
|
954
1458
|
const hasLabel = this.label ? true : !!hasLabelSlot;
|
|
955
1459
|
const hasHelpText = this.helpText ? true : !!hasHelpTextSlot;
|
|
956
|
-
|
|
957
|
-
|
|
1460
|
+
let hasValue: boolean;
|
|
1461
|
+
if (Array.isArray(this.value)) {
|
|
1462
|
+
hasValue = this.value.length > 0;
|
|
1463
|
+
} else if (typeof this.value === 'string') {
|
|
1464
|
+
hasValue = this.value.length > 0;
|
|
1465
|
+
} else {
|
|
1466
|
+
hasValue = this.value !== undefined && this.value !== null && typeof this.value === 'number';
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
const hasClearIcon = this.clearable && !this.disabled && hasValue;
|
|
1470
|
+
const isPlaceholderVisible = this.placeholder && !hasValue;
|
|
1471
|
+
const tagsVisible = this.multiple && this.selectedOptions.length > 0;
|
|
958
1472
|
|
|
959
1473
|
return html`
|
|
960
1474
|
<div
|
|
961
1475
|
part="form-control"
|
|
962
1476
|
class=${classMap({
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
1477
|
+
'form-control': true,
|
|
1478
|
+
'form-control--has-help-text': hasHelpText,
|
|
1479
|
+
'form-control--has-label': hasLabel,
|
|
1480
|
+
'form-control--large': this.size === 'large',
|
|
1481
|
+
'form-control--medium': this.size === 'medium',
|
|
1482
|
+
'form-control--small': this.size === 'small',
|
|
1483
|
+
})}
|
|
970
1484
|
>
|
|
971
1485
|
<label
|
|
972
1486
|
id="label"
|
|
@@ -981,18 +1495,20 @@ export default class SynCombobox extends SynergyElement implements SynergyFormCo
|
|
|
981
1495
|
<div part="form-control-input" class="form-control-input">
|
|
982
1496
|
<syn-popup
|
|
983
1497
|
class=${classMap({
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
1498
|
+
combobox: true,
|
|
1499
|
+
'combobox--bottom': this.placement === 'bottom',
|
|
1500
|
+
'combobox--disabled': this.disabled,
|
|
1501
|
+
'combobox--focused': this.hasFocus,
|
|
1502
|
+
'combobox--large': this.size === 'large',
|
|
1503
|
+
'combobox--medium': this.size === 'medium',
|
|
1504
|
+
'combobox--multiple': this.multiple,
|
|
1505
|
+
'combobox--open': this.open,
|
|
1506
|
+
'combobox--placeholder-visible': isPlaceholderVisible,
|
|
1507
|
+
'combobox--small': this.size === 'small',
|
|
1508
|
+
'combobox--standard': true,
|
|
1509
|
+
'combobox--tags-visible': tagsVisible,
|
|
1510
|
+
'combobox--top': this.placement === 'top',
|
|
1511
|
+
})}
|
|
996
1512
|
placement=${`${this.placement}-start`}
|
|
997
1513
|
flip
|
|
998
1514
|
shift
|
|
@@ -1010,6 +1526,8 @@ export default class SynCombobox extends SynergyElement implements SynergyFormCo
|
|
|
1010
1526
|
>
|
|
1011
1527
|
<slot part="prefix" name="prefix" class="combobox__prefix"></slot>
|
|
1012
1528
|
|
|
1529
|
+
${this.multiple ? html`<div part="tags" class="combobox__tags">${this.tags}</div>` : ''}
|
|
1530
|
+
|
|
1013
1531
|
<input
|
|
1014
1532
|
part="display-input"
|
|
1015
1533
|
class="combobox__display-input"
|
|
@@ -1042,7 +1560,7 @@ export default class SynCombobox extends SynergyElement implements SynergyFormCo
|
|
|
1042
1560
|
type="text"
|
|
1043
1561
|
?disabled=${this.disabled}
|
|
1044
1562
|
?required=${this.required}
|
|
1045
|
-
.value=${this.value}
|
|
1563
|
+
.value=${Array.isArray(this.value) ? this.value.join(', ') : this.value?.toString()}
|
|
1046
1564
|
tabindex="-1"
|
|
1047
1565
|
aria-hidden="true"
|
|
1048
1566
|
@focus=${() => this.focus()}
|
|
@@ -1050,7 +1568,7 @@ export default class SynCombobox extends SynergyElement implements SynergyFormCo
|
|
|
1050
1568
|
/>
|
|
1051
1569
|
|
|
1052
1570
|
${hasClearIcon
|
|
1053
|
-
|
|
1571
|
+
? html`
|
|
1054
1572
|
<button
|
|
1055
1573
|
part="clear-button"
|
|
1056
1574
|
class="combobox__clear"
|
|
@@ -1065,7 +1583,7 @@ export default class SynCombobox extends SynergyElement implements SynergyFormCo
|
|
|
1065
1583
|
</slot>
|
|
1066
1584
|
</button>
|
|
1067
1585
|
`
|
|
1068
|
-
|
|
1586
|
+
: ''}
|
|
1069
1587
|
|
|
1070
1588
|
<slot name="suffix" part="suffix" class="combobox__suffix"></slot>
|
|
1071
1589
|
|
|
@@ -1079,6 +1597,7 @@ export default class SynCombobox extends SynergyElement implements SynergyFormCo
|
|
|
1079
1597
|
role="listbox"
|
|
1080
1598
|
aria-expanded=${this.open ? 'true' : 'false'}
|
|
1081
1599
|
aria-labelledby="label"
|
|
1600
|
+
aria-multiselectable=${this.multiple ? 'true' : 'false'}
|
|
1082
1601
|
part="listbox"
|
|
1083
1602
|
class="combobox__listbox"
|
|
1084
1603
|
tabindex="-1"
|
|
@@ -1086,15 +1605,15 @@ export default class SynCombobox extends SynergyElement implements SynergyFormCo
|
|
|
1086
1605
|
@mouseup=${this.handleOptionClick}
|
|
1087
1606
|
>
|
|
1088
1607
|
<div class="listbox__options" part="filtered-listbox">
|
|
1089
|
-
${this.numberFilteredOptions === 0
|
|
1090
|
-
|
|
1608
|
+
${this.hideOptions || this.numberFilteredOptions === 0
|
|
1609
|
+
? html`<span
|
|
1091
1610
|
class="listbox__no-results"
|
|
1092
1611
|
aria-hidden="true"
|
|
1093
1612
|
part="no-results"
|
|
1094
1613
|
>${this.localize.term('noResults')}</span
|
|
1095
1614
|
>`
|
|
1096
|
-
|
|
1097
|
-
<slot @slotchange=${this.handleDefaultSlotChange}></slot>
|
|
1615
|
+
: ''}
|
|
1616
|
+
<slot class=${classMap({ options__hide: this.hideOptions })} @slotchange=${this.handleDefaultSlotChange}></slot>
|
|
1098
1617
|
</div>
|
|
1099
1618
|
</div>
|
|
1100
1619
|
</syn-popup>
|