@kubex/zinc 1.0.113 → 1.0.114
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/.github/workflows/eleventy_build.yml +5 -5
- package/.github/workflows/js_build_and_deploy.yaml +1 -1
- package/dist/custom-elements.json +2636 -421
- package/dist/vscode.html-custom-data.json +96 -16
- package/dist/web-types.json +268 -31
- package/dist/zn.d.ts +517 -340
- package/dist/zn.min.css +1 -1
- package/dist/zn.min.js +942 -863
- package/docs/eleventy.config.cjs +4 -2
- package/docs/pages/components/header.md +13 -0
- package/docs/pages/components/inline-edit.md +20 -0
- package/docs/pages/components/item.md +12 -1
- package/docs/pages/components/page.md +149 -0
- package/docs/pages/components/progress-bar.md +16 -0
- package/docs/pages/components/select.md +25 -0
- package/docs/pages/components/split-pane.md +6 -7
- package/docs/pages/components/tabs.md +59 -9
- package/package.json +8 -5
- package/scripts/build.js +40 -19
- package/scss/_global-spacing.scss +12 -1
- package/scss/_root.scss +34 -2
- package/scss/shared/layout.scss +15 -2
- package/scss/themes/_light.scss +1 -1
- package/src/components/absolute-container/absolute-container.component.ts +9 -25
- package/src/components/animated-button/animated-button.test.ts +8 -8
- package/src/components/button/button.component.ts +3 -1
- package/src/components/button/button.scss +5 -0
- package/src/components/button-menu/button-menu.component.ts +36 -21
- package/src/components/button-menu/button-menu.test.ts +55 -1
- package/src/components/chart/chart.component.ts +7 -7
- package/src/components/collapsible/collapsible.component.ts +7 -14
- package/src/components/cols/cols.scss +1 -1
- package/src/components/content-block/content-block.component.ts +11 -33
- package/src/components/data-select/data-select.component.ts +7 -11
- package/src/components/data-table/data-table.component.ts +10 -14
- package/src/components/editor/modules/toolbar/toolbar.component.ts +5 -9
- package/src/components/expanding-action/expanding-action.component.ts +37 -43
- package/src/components/form-group/form-group.component.ts +8 -3
- package/src/components/header/header.component.ts +6 -5
- package/src/components/header/header.scss +31 -25
- package/src/components/inline-edit/inline-edit.component.ts +13 -1
- package/src/components/inline-edit/inline-edit.test.ts +50 -0
- package/src/components/input/input.test.ts +77 -0
- package/src/components/input-group/input-group.test.ts +2 -10
- package/src/components/item/item.component.ts +12 -6
- package/src/components/item/item.scss +1 -1
- package/src/components/item/item.test.ts +8 -0
- package/src/components/navbar/navbar.component.ts +222 -48
- package/src/components/navbar/navbar.scss +134 -21
- package/src/components/navbar/navbar.test.ts +114 -9
- package/src/components/option/option.scss +5 -7
- package/src/components/option/option.test.ts +30 -0
- package/src/components/page/index.ts +13 -0
- package/src/components/page/page.component.ts +406 -0
- package/src/components/page/page.scss +325 -0
- package/src/components/page/page.test.ts +397 -0
- package/src/components/page-nav/page-nav.scss +7 -5
- package/src/components/pane/pane.component.ts +2 -2
- package/src/components/pane/pane.scss +0 -1
- package/src/components/pane/pane.test.ts +31 -0
- package/src/components/panel/panel.scss +1 -0
- package/src/components/progress-bar/progress-bar.component.ts +5 -2
- package/src/components/progress-bar/progress-bar.scss +17 -1
- package/src/components/progress-bar/progress-bar.test.ts +12 -0
- package/src/components/scroll-container/scroll-container.component.ts +22 -21
- package/src/components/select/select.component.ts +240 -38
- package/src/components/select/select.scss +15 -0
- package/src/components/select/select.test.ts +477 -0
- package/src/components/settings-container/settings-container.component.ts +15 -19
- package/src/components/settings-container/settings-container.scss +8 -8
- package/src/components/sidebar/sidebar.component.ts +11 -11
- package/src/components/simple-chart/simple-chart.component.ts +6 -7
- package/src/components/sp/sp.scss +46 -15
- package/src/components/sp/sp.test.ts +15 -0
- package/src/components/split-pane/split-pane.component.ts +153 -26
- package/src/components/split-pane/split-pane.scss +89 -13
- package/src/components/split-pane/split-pane.test.ts +187 -0
- package/src/components/style/style.component.ts +8 -0
- package/src/components/tab/index.ts +13 -0
- package/src/components/tab/tab.component.ts +25 -0
- package/src/components/tab/tab.scss +7 -0
- package/src/components/table/table.component.ts +7 -2
- package/src/components/tabs/tabs.component.ts +75 -70
- package/src/components/tabs/tabs.scss +1 -1
- package/src/components/textarea/textarea.component.ts +5 -8
- package/src/components/translation-group/translation-group.component.ts +15 -14
- package/src/components/translations/translations.component.ts +18 -17
- package/src/components/translations/translations.scss +1 -0
- package/src/components/translations/translations.test.ts +3 -2
- package/src/internal/form.ts +234 -0
- package/src/internal/theme.ts +26 -46
- package/src/internal/zinc-element.ts +12 -7
- package/src/utilities/form.ts +1 -0
- package/src/wc.scss +1 -1
- package/src/zinc.ts +3 -0
|
@@ -113,12 +113,16 @@ export default class ZnSelect extends ZincElement implements ZincFormControl {
|
|
|
113
113
|
/** @internal - current search/filter text when search is enabled (lowercased for matching) */
|
|
114
114
|
private _searchQuery = '';
|
|
115
115
|
|
|
116
|
-
/** @internal - raw display value of the search input (preserves case for the input field)
|
|
117
|
-
|
|
116
|
+
/** @internal - raw display value of the search input (preserves case for the input field). Reactive so the
|
|
117
|
+
* free-text "Add" row re-renders on every keystroke, not just when the match/no-match state changes. */
|
|
118
|
+
@state() private _searchDisplayValue = '';
|
|
118
119
|
|
|
119
120
|
/** @internal - whether the "no matching options" empty state is visible */
|
|
120
121
|
@state() private _noResultsVisible = false;
|
|
121
122
|
|
|
123
|
+
/** @internal - whether the free-text "Add" row is the active keyboard-navigation target */
|
|
124
|
+
@state() private _addOptionActive = false;
|
|
125
|
+
|
|
122
126
|
/** @internal */
|
|
123
127
|
@state() private _fetchedOptions: ({ key: string; value: string; group?: undefined } | {
|
|
124
128
|
group: string;
|
|
@@ -275,6 +279,14 @@ export default class ZnSelect extends ZincElement implements ZincFormControl {
|
|
|
275
279
|
/** Enables search/filter functionality. When enabled, the user can type into the select to filter the visible options. */
|
|
276
280
|
@property({type: Boolean, reflect: true}) search = false;
|
|
277
281
|
|
|
282
|
+
/**
|
|
283
|
+
* Allows the user to enter values that are not in the options list ("free text"). Implies the editable,
|
|
284
|
+
* filtering input behavior of `search`. A typed value that doesn't match an existing option is committed
|
|
285
|
+
* by pressing Enter, clicking the "Add" row, or blurring the field, and becomes a selected option — a tag
|
|
286
|
+
* when `multiple` is enabled. The committed value is both the option's value and its label.
|
|
287
|
+
*/
|
|
288
|
+
@property({type: Boolean, reflect: true, attribute: 'free-text'}) freeText = false;
|
|
289
|
+
|
|
278
290
|
/** The select's label. If you need to display HTML, use the `label` slot instead. */
|
|
279
291
|
@property() label = '';
|
|
280
292
|
|
|
@@ -477,6 +489,13 @@ export default class ZnSelect extends ZincElement implements ZincFormControl {
|
|
|
477
489
|
return;
|
|
478
490
|
}
|
|
479
491
|
|
|
492
|
+
// Escape cancels uncommitted free-text rather than letting the close path commit it. Done here (before
|
|
493
|
+
// either the CloseWatcher or the branch below closes the dropdown) so it applies on every browser.
|
|
494
|
+
if (event.key === 'Escape' && this.open && this.freeText && this._searchDisplayValue.trim()) {
|
|
495
|
+
this.clearSearch();
|
|
496
|
+
this.displayInput.value = '';
|
|
497
|
+
}
|
|
498
|
+
|
|
480
499
|
// Close when pressing escape
|
|
481
500
|
if ((event.key === 'Escape' || event.key === 'Tab') && this.open && !this.closeWatcher) {
|
|
482
501
|
event.preventDefault();
|
|
@@ -494,7 +513,7 @@ export default class ZnSelect extends ZincElement implements ZincFormControl {
|
|
|
494
513
|
|
|
495
514
|
// Handle enter and space. When pressing space, we allow for type to select behaviors so if there's anything in the
|
|
496
515
|
// buffer we _don't_ close it. When search is enabled, space should type into the input, not toggle.
|
|
497
|
-
if (event.key === 'Enter' || (event.key === ' ' && !this.
|
|
516
|
+
if (event.key === 'Enter' || (event.key === ' ' && !this._isTypeable && this.typeToSelectString === '')) {
|
|
498
517
|
event.preventDefault();
|
|
499
518
|
event.stopImmediatePropagation();
|
|
500
519
|
|
|
@@ -504,6 +523,15 @@ export default class ZnSelect extends ZincElement implements ZincFormControl {
|
|
|
504
523
|
return;
|
|
505
524
|
}
|
|
506
525
|
|
|
526
|
+
// Free-text: commit when the Add row is the active target, or when no real option is highlighted
|
|
527
|
+
if (this.freeText) {
|
|
528
|
+
const addActive = this._addOptionActive && this._freeTextAddValue !== null;
|
|
529
|
+
const usableCurrent = !addActive && this.currentOption && !this.currentOption.disabled && !this.currentOption.hidden;
|
|
530
|
+
if (!usableCurrent && this.commitFreeText()) {
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
507
535
|
// If it is open, update the value based on the current selection and close it
|
|
508
536
|
if (this.currentOption && !this.currentOption.disabled) {
|
|
509
537
|
this.valueHasChanged = true;
|
|
@@ -514,7 +542,7 @@ export default class ZnSelect extends ZincElement implements ZincFormControl {
|
|
|
514
542
|
}
|
|
515
543
|
|
|
516
544
|
// Clear search after keyboard selection
|
|
517
|
-
if (this.
|
|
545
|
+
if (this._isTypeable) {
|
|
518
546
|
this.clearSearch();
|
|
519
547
|
if (this.multiple) {
|
|
520
548
|
this.displayInput.value = '';
|
|
@@ -530,8 +558,8 @@ export default class ZnSelect extends ZincElement implements ZincFormControl {
|
|
|
530
558
|
if (!this.multiple) {
|
|
531
559
|
this.hide().then();
|
|
532
560
|
this.displayInput.focus({preventScroll: true});
|
|
533
|
-
} else if (this.
|
|
534
|
-
// In multi-select +
|
|
561
|
+
} else if (this._isTypeable) {
|
|
562
|
+
// In multi-select + typeable, re-focus the search input after keyboard selection
|
|
535
563
|
this.updateComplete.then(() => {
|
|
536
564
|
this.displayInput.value = '';
|
|
537
565
|
this.displayInput.focus({preventScroll: true});
|
|
@@ -542,12 +570,8 @@ export default class ZnSelect extends ZincElement implements ZincFormControl {
|
|
|
542
570
|
return;
|
|
543
571
|
}
|
|
544
572
|
|
|
545
|
-
// Navigate options
|
|
573
|
+
// Navigate options (and the free-text "Add" row, when shown)
|
|
546
574
|
if (['ArrowUp', 'ArrowDown', 'Home', 'End'].includes(event.key)) {
|
|
547
|
-
const navOptions = this.search ? this.getVisibleOptions() : this.getAllOptions();
|
|
548
|
-
const currentIndex = navOptions.indexOf(this.currentOption);
|
|
549
|
-
let newIndex = Math.max(0, currentIndex);
|
|
550
|
-
|
|
551
575
|
// Prevent scrolling
|
|
552
576
|
event.preventDefault();
|
|
553
577
|
|
|
@@ -562,23 +586,43 @@ export default class ZnSelect extends ZincElement implements ZincFormControl {
|
|
|
562
586
|
}
|
|
563
587
|
}
|
|
564
588
|
|
|
589
|
+
const navOptions = this._isTypeable ? this.getVisibleOptions() : this.getAllOptions();
|
|
590
|
+
const hasAdd = this.freeText && this._freeTextAddValue !== null;
|
|
591
|
+
|
|
592
|
+
// The Add row, when shown, sits at the top of the listbox and is the first navigable target.
|
|
593
|
+
const ADD = 'add';
|
|
594
|
+
const targets: (ZnOption | typeof ADD)[] = hasAdd ? [ADD, ...navOptions] : [...navOptions];
|
|
595
|
+
if (targets.length === 0) return;
|
|
596
|
+
|
|
597
|
+
const currentTarget: ZnOption | typeof ADD | null =
|
|
598
|
+
this._addOptionActive && hasAdd ? ADD : (this.currentOption ?? null);
|
|
599
|
+
const currentIndex = currentTarget === null ? -1 : targets.indexOf(currentTarget);
|
|
600
|
+
let newIndex = Math.max(0, currentIndex);
|
|
601
|
+
|
|
565
602
|
if (event.key === 'ArrowDown') {
|
|
566
603
|
newIndex = currentIndex + 1;
|
|
567
|
-
if (newIndex >
|
|
604
|
+
if (newIndex > targets.length - 1) newIndex = 0;
|
|
568
605
|
} else if (event.key === 'ArrowUp') {
|
|
569
606
|
newIndex = currentIndex - 1;
|
|
570
|
-
if (newIndex < 0) newIndex =
|
|
607
|
+
if (newIndex < 0) newIndex = targets.length - 1;
|
|
571
608
|
} else if (event.key === 'Home') {
|
|
572
609
|
newIndex = 0;
|
|
573
610
|
} else if (event.key === 'End') {
|
|
574
|
-
newIndex =
|
|
611
|
+
newIndex = targets.length - 1;
|
|
575
612
|
}
|
|
576
613
|
|
|
577
|
-
|
|
614
|
+
const next = targets[newIndex];
|
|
615
|
+
if (next === ADD) {
|
|
616
|
+
this._addOptionActive = true;
|
|
617
|
+
this.setCurrentOption(null);
|
|
618
|
+
} else {
|
|
619
|
+
this._addOptionActive = false;
|
|
620
|
+
this.setCurrentOption(next);
|
|
621
|
+
}
|
|
578
622
|
}
|
|
579
623
|
|
|
580
|
-
// When
|
|
581
|
-
if (this.
|
|
624
|
+
// When typeable, let the input handle keystrokes natively (they go through handleSearchInput)
|
|
625
|
+
if (this._isTypeable) {
|
|
582
626
|
return;
|
|
583
627
|
}
|
|
584
628
|
|
|
@@ -648,8 +692,8 @@ export default class ZnSelect extends ZincElement implements ZincFormControl {
|
|
|
648
692
|
return;
|
|
649
693
|
}
|
|
650
694
|
|
|
651
|
-
// When
|
|
652
|
-
if (this.
|
|
695
|
+
// When typeable, clicking the input area should open (not toggle) so the user can type
|
|
696
|
+
if (this._isTypeable && this.open && !isExpandIcon) {
|
|
653
697
|
event.preventDefault();
|
|
654
698
|
this.displayInput.focus({preventScroll: true});
|
|
655
699
|
return;
|
|
@@ -679,7 +723,7 @@ export default class ZnSelect extends ZincElement implements ZincFormControl {
|
|
|
679
723
|
event.stopPropagation();
|
|
680
724
|
|
|
681
725
|
if (this.value !== '') {
|
|
682
|
-
if (this.
|
|
726
|
+
if (this._isTypeable) {
|
|
683
727
|
this.clearSearch();
|
|
684
728
|
this.displayInput.value = '';
|
|
685
729
|
}
|
|
@@ -711,9 +755,17 @@ export default class ZnSelect extends ZincElement implements ZincFormControl {
|
|
|
711
755
|
return this.search && !!this.dataUri;
|
|
712
756
|
}
|
|
713
757
|
|
|
758
|
+
/** Whether the display input is editable and filters options (search or free-text). */
|
|
759
|
+
private get _isTypeable() {
|
|
760
|
+
return this.search || this.freeText;
|
|
761
|
+
}
|
|
762
|
+
|
|
714
763
|
/** Handles text input on the display input for search/filter mode */
|
|
715
764
|
private handleSearchInput() {
|
|
716
|
-
if (!this.
|
|
765
|
+
if (!this._isTypeable) return;
|
|
766
|
+
|
|
767
|
+
// A new keystroke resets the Add-row keyboard highlight
|
|
768
|
+
this._addOptionActive = false;
|
|
717
769
|
|
|
718
770
|
this._searchDisplayValue = this.displayInput.value;
|
|
719
771
|
this._searchQuery = this._searchDisplayValue.toLowerCase();
|
|
@@ -807,6 +859,7 @@ export default class ZnSelect extends ZincElement implements ZincFormControl {
|
|
|
807
859
|
this._searchQuery = '';
|
|
808
860
|
this._searchDisplayValue = '';
|
|
809
861
|
this._noResultsVisible = false;
|
|
862
|
+
this._addOptionActive = false;
|
|
810
863
|
|
|
811
864
|
// Cancel any pending remote search
|
|
812
865
|
if (this._searchDebounceTimer !== null) {
|
|
@@ -825,8 +878,126 @@ export default class ZnSelect extends ZincElement implements ZincFormControl {
|
|
|
825
878
|
});
|
|
826
879
|
}
|
|
827
880
|
|
|
881
|
+
/**
|
|
882
|
+
* Commits the current typed text as a selected value when `free-text` is enabled. A selected `<zn-option>`
|
|
883
|
+
* (value = label = trimmed text) is created in the light DOM so the value flows through the existing
|
|
884
|
+
* tag/value/selection machinery and appears as a deselectable row in the listbox. Returns `true` when
|
|
885
|
+
* something was committed, `false` when there was nothing to commit.
|
|
886
|
+
*/
|
|
887
|
+
private commitFreeText(refocus = true): boolean {
|
|
888
|
+
if (!this.freeText) return false;
|
|
889
|
+
|
|
890
|
+
const text = (this.displayInput?.value ?? '').trim();
|
|
891
|
+
if (!text) return false;
|
|
892
|
+
|
|
893
|
+
this.valueHasChanged = true;
|
|
894
|
+
|
|
895
|
+
// If the text matches an existing option (label or value, case-insensitive), select it instead of
|
|
896
|
+
// creating a duplicate free-text option.
|
|
897
|
+
const lower = text.toLowerCase();
|
|
898
|
+
const existing = this.getAllOptions().find(
|
|
899
|
+
o => o.value.toLowerCase() === lower || o.getTextLabel().toLowerCase() === lower
|
|
900
|
+
);
|
|
901
|
+
|
|
902
|
+
const option = existing ?? this._createFreeTextOption(text);
|
|
903
|
+
|
|
904
|
+
if (this.multiple) {
|
|
905
|
+
// Add to the existing selection and keep the dropdown open for more entry
|
|
906
|
+
this.toggleOptionSelection(option, true);
|
|
907
|
+
} else {
|
|
908
|
+
this.setSelectedOptions(option);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
this.clearSearch();
|
|
912
|
+
|
|
913
|
+
this.updateComplete.then(() => {
|
|
914
|
+
this.emit('zn-input');
|
|
915
|
+
this.emit('zn-change');
|
|
916
|
+
});
|
|
917
|
+
|
|
918
|
+
// When committed via blur/close, the dropdown is already closing and focus has left — don't fight it.
|
|
919
|
+
if (!refocus) {
|
|
920
|
+
return true;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
if (this.multiple) {
|
|
924
|
+
// Clear the input so the user can type the next value
|
|
925
|
+
this.displayInput.value = '';
|
|
926
|
+
this.displayInput.focus({preventScroll: true});
|
|
927
|
+
} else {
|
|
928
|
+
this.hide().then();
|
|
929
|
+
this.displayInput.focus({preventScroll: true});
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
return true;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
/**
|
|
936
|
+
* The trimmed pending text to offer as a free-text "Add" row, or `null` when no Add row should be shown
|
|
937
|
+
* (free-text disabled, dropdown closed, input empty, or the text exactly matches an existing option).
|
|
938
|
+
*/
|
|
939
|
+
private get _freeTextAddValue(): string | null {
|
|
940
|
+
if (!this.freeText || !this.open) return null;
|
|
941
|
+
|
|
942
|
+
const text = this._searchDisplayValue.trim();
|
|
943
|
+
if (!text) return null;
|
|
944
|
+
|
|
945
|
+
const lower = text.toLowerCase();
|
|
946
|
+
const exact = this.getAllOptions().some(
|
|
947
|
+
o => o.value.toLowerCase() === lower || o.getTextLabel().toLowerCase() === lower
|
|
948
|
+
);
|
|
949
|
+
|
|
950
|
+
return exact ? null : text;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
/** Commits the typed value when the "Add" row is pressed, keeping focus on the input. */
|
|
954
|
+
private handleAddOptionMouseDown(event: MouseEvent) {
|
|
955
|
+
// Keep focus on the input but DON'T commit here. Committing on mousedown would clear the search and
|
|
956
|
+
// collapse the listbox, so the following mouseup would land on (and select) whichever option shifted
|
|
957
|
+
// under the cursor. Instead the commit happens on mouseup in handleOptionClick.
|
|
958
|
+
event.preventDefault();
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
/**
|
|
962
|
+
* In free-text mode, creates a hidden `<zn-option>` for any value that has no matching option, so values
|
|
963
|
+
* set via the `value`/`defaultValue` attribute (e.g. previously-saved custom entries) render as tags or
|
|
964
|
+
* the display value on load. No-op when free-text is disabled.
|
|
965
|
+
*/
|
|
966
|
+
private _materialiseFreeTextValues(values: string[]) {
|
|
967
|
+
if (!this.freeText) return;
|
|
968
|
+
|
|
969
|
+
const existingValues = new Set(this.getAllOptions().map(o => o.value));
|
|
970
|
+
values.forEach(value => {
|
|
971
|
+
if (value === '' || existingValues.has(value)) return;
|
|
972
|
+
this._createFreeTextOption(value);
|
|
973
|
+
existingValues.add(value);
|
|
974
|
+
});
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
/**
|
|
978
|
+
* Creates a free-text-marked `<zn-option>` in the light DOM (value = label) and returns it. The option is
|
|
979
|
+
* a normal, visible, selectable row so the user can deselect a custom value from the dropdown like any
|
|
980
|
+
* other option; the `data-free-text` marker lets us drop it from the DOM once it's no longer selected.
|
|
981
|
+
*/
|
|
982
|
+
private _createFreeTextOption(value: string): ZnOption {
|
|
983
|
+
const option = document.createElement('zn-option') as ZnOption;
|
|
984
|
+
option.value = value;
|
|
985
|
+
option.textContent = value;
|
|
986
|
+
option.setAttribute('data-free-text', '');
|
|
987
|
+
this.appendChild(option);
|
|
988
|
+
return option;
|
|
989
|
+
}
|
|
990
|
+
|
|
828
991
|
private handleOptionClick(event: MouseEvent) {
|
|
829
992
|
const target = event.target as HTMLElement;
|
|
993
|
+
|
|
994
|
+
// The free-text "Add" row lives in the listbox and commits on this same mouseup, so it can never
|
|
995
|
+
// also trigger an option selection.
|
|
996
|
+
if (target.closest('.select__add-option')) {
|
|
997
|
+
this.commitFreeText();
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
|
|
830
1001
|
const option = target.closest('zn-option');
|
|
831
1002
|
const oldValue = this.value;
|
|
832
1003
|
|
|
@@ -841,8 +1012,8 @@ export default class ZnSelect extends ZincElement implements ZincFormControl {
|
|
|
841
1012
|
this.setSelectedOptions(option);
|
|
842
1013
|
}
|
|
843
1014
|
|
|
844
|
-
// Clear search after selection when in search mode
|
|
845
|
-
if (this.
|
|
1015
|
+
// Clear search after selection when in search/free-text mode
|
|
1016
|
+
if (this._isTypeable) {
|
|
846
1017
|
this.clearSearch();
|
|
847
1018
|
if (this.multiple) {
|
|
848
1019
|
// Keep the search input clear for further typing
|
|
@@ -852,7 +1023,7 @@ export default class ZnSelect extends ZincElement implements ZincFormControl {
|
|
|
852
1023
|
|
|
853
1024
|
// Set focus after updating so the value is announced by screen readers
|
|
854
1025
|
this.updateComplete.then(() => {
|
|
855
|
-
if (this.multiple && this.
|
|
1026
|
+
if (this.multiple && this._isTypeable && this.open) {
|
|
856
1027
|
this.displayInput.value = '';
|
|
857
1028
|
}
|
|
858
1029
|
this.displayInput.focus({preventScroll: true});
|
|
@@ -891,8 +1062,11 @@ export default class ZnSelect extends ZincElement implements ZincFormControl {
|
|
|
891
1062
|
// Check for duplicate values in menu items
|
|
892
1063
|
allOptions.forEach(option => values.push(option.value));
|
|
893
1064
|
|
|
894
|
-
//
|
|
895
|
-
this.
|
|
1065
|
+
// In free-text mode, materialise any value with no matching option so it renders as a tag/value
|
|
1066
|
+
this._materialiseFreeTextValues(value);
|
|
1067
|
+
|
|
1068
|
+
// Select only the options that match the new value (re-query in case options were materialised)
|
|
1069
|
+
this.setSelectedOptions(this.getAllOptions().filter(el => value.includes(el.value)));
|
|
896
1070
|
|
|
897
1071
|
// of check if an option has selected attribute set initially
|
|
898
1072
|
if (!this.valueHasChanged) {
|
|
@@ -920,6 +1094,7 @@ export default class ZnSelect extends ZincElement implements ZincFormControl {
|
|
|
920
1094
|
event.stopImmediatePropagation();
|
|
921
1095
|
|
|
922
1096
|
if (!this.disabled) {
|
|
1097
|
+
// Deselecting drops the backing element for free-text values via selectionChanged's cleanup.
|
|
923
1098
|
this.toggleOptionSelection(option, false);
|
|
924
1099
|
|
|
925
1100
|
// Emit after updating
|
|
@@ -976,7 +1151,7 @@ export default class ZnSelect extends ZincElement implements ZincFormControl {
|
|
|
976
1151
|
option.tabIndex = 0;
|
|
977
1152
|
// When search is active and the display input has focus, don't steal focus to the option.
|
|
978
1153
|
// The user is typing in the search input and we only want to visually highlight the current option.
|
|
979
|
-
if (!(this.
|
|
1154
|
+
if (!(this._isTypeable && this.open && this.shadowRoot?.activeElement === this.displayInput)) {
|
|
980
1155
|
option.focus();
|
|
981
1156
|
}
|
|
982
1157
|
}
|
|
@@ -1016,6 +1191,13 @@ export default class ZnSelect extends ZincElement implements ZincFormControl {
|
|
|
1016
1191
|
// Update selected options cache
|
|
1017
1192
|
this.selectedOptions = this.getAllOptions().filter(el => el.selected);
|
|
1018
1193
|
|
|
1194
|
+
// Free-text options exist only to anchor a selected value. Drop any that are no longer selected — e.g.
|
|
1195
|
+
// when a single-select value is replaced or the selection is cleared, the previous custom entry should
|
|
1196
|
+
// disappear entirely rather than linger as an orphaned option.
|
|
1197
|
+
this.querySelectorAll<ZnOption>('[data-free-text]').forEach(option => {
|
|
1198
|
+
if (!option.selected) option.remove();
|
|
1199
|
+
});
|
|
1200
|
+
|
|
1019
1201
|
// In remote-search + multiple mode, track selected items so they persist across search result changes
|
|
1020
1202
|
if (this._isRemoteSearch && this.multiple) {
|
|
1021
1203
|
this._selectedRemoteItems = this.selectedOptions.map(el => ({
|
|
@@ -1408,8 +1590,8 @@ export default class ZnSelect extends ZincElement implements ZincFormControl {
|
|
|
1408
1590
|
// Reset the current option
|
|
1409
1591
|
this.setCurrentOption(null);
|
|
1410
1592
|
|
|
1411
|
-
// When
|
|
1412
|
-
if (this.
|
|
1593
|
+
// When typeable, clear the display input to allow typing and show all options
|
|
1594
|
+
if (this._isTypeable) {
|
|
1413
1595
|
this.clearSearch();
|
|
1414
1596
|
this.displayInput.value = '';
|
|
1415
1597
|
this.displayInput.readOnly = false;
|
|
@@ -1429,7 +1611,7 @@ export default class ZnSelect extends ZincElement implements ZincFormControl {
|
|
|
1429
1611
|
});
|
|
1430
1612
|
|
|
1431
1613
|
// Focus the search input so the user can start typing immediately
|
|
1432
|
-
if (this.
|
|
1614
|
+
if (this._isTypeable) {
|
|
1433
1615
|
requestAnimationFrame(() => {
|
|
1434
1616
|
this.displayInput.focus({preventScroll: true});
|
|
1435
1617
|
});
|
|
@@ -1449,8 +1631,13 @@ export default class ZnSelect extends ZincElement implements ZincFormControl {
|
|
|
1449
1631
|
this.emit('zn-hide');
|
|
1450
1632
|
this.removeOpenListeners();
|
|
1451
1633
|
|
|
1452
|
-
// When
|
|
1453
|
-
if (this.
|
|
1634
|
+
// When typeable, restore the display label and clear the search
|
|
1635
|
+
if (this._isTypeable) {
|
|
1636
|
+
// Commit any pending free-text before clearing it (blur/close-to-commit). Don't refocus —
|
|
1637
|
+
// the dropdown is closing because focus left the control.
|
|
1638
|
+
if (this.freeText && this._searchDisplayValue.trim()) {
|
|
1639
|
+
this.commitFreeText(false);
|
|
1640
|
+
}
|
|
1454
1641
|
this.clearSearch();
|
|
1455
1642
|
this.displayInput.readOnly = true;
|
|
1456
1643
|
// In multiple mode, tags show the selection so keep the input empty
|
|
@@ -1647,7 +1834,7 @@ export default class ZnSelect extends ZincElement implements ZincFormControl {
|
|
|
1647
1834
|
'select--medium': this.size === 'medium',
|
|
1648
1835
|
'select--large': this.size === 'large',
|
|
1649
1836
|
'select--has-input-prefix': this.inputPrefix,
|
|
1650
|
-
'select--search': this.
|
|
1837
|
+
'select--search': this._isTypeable
|
|
1651
1838
|
})}
|
|
1652
1839
|
placement=${this.placement}
|
|
1653
1840
|
strategy=${this.hoist ? 'fixed' : 'absolute'}
|
|
@@ -1671,20 +1858,20 @@ export default class ZnSelect extends ZincElement implements ZincFormControl {
|
|
|
1671
1858
|
part="display-input"
|
|
1672
1859
|
class="select__display-input"
|
|
1673
1860
|
type="text"
|
|
1674
|
-
placeholder=${this._fetchLoading ? 'Loading...' : this._fetchError ? this._fetchError : (this.
|
|
1861
|
+
placeholder=${this._fetchLoading ? 'Loading...' : this._fetchError ? this._fetchError : (this._isTypeable && this.open ? (this.multiple ? (this.selectedOptions.length ? '' : (this.placeholder || 'Search...')) : (this.displayLabel || this.placeholder || 'Search...')) : this.placeholder)}
|
|
1675
1862
|
.disabled=${isDisabled}
|
|
1676
|
-
.value=${isFetchDisabled ? '' : (this.
|
|
1863
|
+
.value=${isFetchDisabled ? '' : (this._isTypeable && this.open) ? this._searchDisplayValue : (this._isTypeable && this.multiple) ? '' : this.displayLabel}
|
|
1677
1864
|
autocomplete="off"
|
|
1678
1865
|
spellcheck="false"
|
|
1679
1866
|
autocapitalize="off"
|
|
1680
|
-
?readonly=${!(this.
|
|
1867
|
+
?readonly=${!(this._isTypeable && this.open)}
|
|
1681
1868
|
aria-controls="listbox"
|
|
1682
1869
|
aria-expanded=${this.open ? 'true' : 'false'}
|
|
1683
1870
|
aria-haspopup="listbox"
|
|
1684
1871
|
aria-labelledby="label"
|
|
1685
1872
|
aria-disabled=${isDisabled ? 'true' : 'false'}
|
|
1686
1873
|
aria-describedby="help-text"
|
|
1687
|
-
aria-autocomplete=${this.
|
|
1874
|
+
aria-autocomplete=${this._isTypeable ? 'list' : 'none'}
|
|
1688
1875
|
role="combobox"
|
|
1689
1876
|
tabindex="0"
|
|
1690
1877
|
@focus=${this.handleFocus}
|
|
@@ -1747,6 +1934,20 @@ export default class ZnSelect extends ZincElement implements ZincFormControl {
|
|
|
1747
1934
|
tabindex="-1"
|
|
1748
1935
|
@mouseup=${this.handleOptionClick}
|
|
1749
1936
|
@slotchange=${this.handleDefaultSlotChange}>
|
|
1937
|
+
${this._freeTextAddValue !== null
|
|
1938
|
+
? html`
|
|
1939
|
+
<div
|
|
1940
|
+
part="add-option"
|
|
1941
|
+
class=${classMap({
|
|
1942
|
+
'select__add-option': true,
|
|
1943
|
+
'select__add-option--current': this._addOptionActive
|
|
1944
|
+
})}
|
|
1945
|
+
role="option"
|
|
1946
|
+
aria-selected=${this._addOptionActive ? 'true' : 'false'}
|
|
1947
|
+
@mousedown=${this.handleAddOptionMouseDown}>
|
|
1948
|
+
Add "${this._freeTextAddValue}"
|
|
1949
|
+
</div>`
|
|
1950
|
+
: nothing}
|
|
1750
1951
|
<slot></slot>
|
|
1751
1952
|
${this._renderSelectedRemoteItems()}
|
|
1752
1953
|
${this._fetchedOptions.map(item =>
|
|
@@ -1780,7 +1981,8 @@ export default class ZnSelect extends ZincElement implements ZincFormControl {
|
|
|
1780
1981
|
Type to search...
|
|
1781
1982
|
</div>`
|
|
1782
1983
|
: html`
|
|
1783
|
-
<div part="empty-state" class="select__empty-state"
|
|
1984
|
+
<div part="empty-state" class="select__empty-state"
|
|
1985
|
+
?hidden=${!this._noResultsVisible || this._freeTextAddValue !== null}>
|
|
1784
1986
|
No matching options
|
|
1785
1987
|
</div>`
|
|
1786
1988
|
}
|
|
@@ -442,6 +442,21 @@
|
|
|
442
442
|
}
|
|
443
443
|
}
|
|
444
444
|
|
|
445
|
+
/* Free-text "Add" row */
|
|
446
|
+
.select__add-option {
|
|
447
|
+
display: flex;
|
|
448
|
+
align-items: center;
|
|
449
|
+
padding: var(--zn-spacing-x-small) var(--zn-spacing-small);
|
|
450
|
+
color: var(--zn-input-color);
|
|
451
|
+
font-size: var(--zn-font-size-small);
|
|
452
|
+
cursor: pointer;
|
|
453
|
+
|
|
454
|
+
&:hover,
|
|
455
|
+
&.select__add-option--current {
|
|
456
|
+
background-color: var(--zn-color-primary-100);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
445
460
|
/* Search mode */
|
|
446
461
|
.select--search .select__combobox {
|
|
447
462
|
cursor: text;
|