@uni-design-system/uni-angular 7.3.0 → 8.0.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.
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
2
|
import { signal, computed, linkedSignal, resource, Injectable, inject, DestroyRef, InjectionToken, input, ChangeDetectionStrategy, Component, contentChildren, output, Renderer2, ElementRef, Directive, model, effect, viewChild, afterNextRender, ViewChild, afterRenderEffect, booleanAttribute, viewChildren } from '@angular/core';
|
|
3
3
|
import { injectGlobal, css, keyframes } from '@emotion/css';
|
|
4
|
-
import { UniThemes, LightTheme, toTypefaces, createThemeFromPalette, Z_INDEX, fadeIn, fadeOut, EXPAND_DEFAULT_SPEED, expandDuration, expandFadeIn, collapseFadeOut, removeInputPlatformStyling, ShapeRadii, generatePalette, emitThemeFile, emitDtcgTokens } from '@uni-design-system/uni-core';
|
|
4
|
+
import { UniThemes, LightTheme, toTypefaces, parseTheme, formatThemeIssues, hydrateTheme, createThemeFromPalette, Z_INDEX, fadeIn, fadeOut, EXPAND_DEFAULT_SPEED, expandDuration, expandFadeIn, collapseFadeOut, removeInputPlatformStyling, HOVER_OR_KEYBOARD_FOCUS, ShapeRadii, generatePalette, emitThemeFile, emitDtcgTokens } from '@uni-design-system/uni-core';
|
|
5
5
|
import { NgClass, NgTemplateOutlet, CommonModule } from '@angular/common';
|
|
6
6
|
|
|
7
7
|
let nextUniqueId = 0;
|
|
@@ -50,6 +50,112 @@ function motionSafe(styles) {
|
|
|
50
50
|
return { '@media (prefers-reduced-motion: no-preference)': styles };
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
/**
|
|
54
|
+
* The keyboard and ARIA bookkeeping shared by every combobox-style popup:
|
|
55
|
+
* open state, the active option index, and the `aria-activedescendant` id
|
|
56
|
+
* wiring. Deliberately owns no markup and no filtering — each control renders
|
|
57
|
+
* its own options and decides what a suggestion *is*.
|
|
58
|
+
*
|
|
59
|
+
* Extracted because three controls need the identical contract
|
|
60
|
+
* (`uni-search-input`, `uni-tag-input`, and the multi-select upgrade), and the
|
|
61
|
+
* parts that silently drift between hand-rolled copies all live here: the
|
|
62
|
+
* wrap-around arithmetic, Home/End, and keeping the active id in sync with the
|
|
63
|
+
* option list.
|
|
64
|
+
*
|
|
65
|
+
* `Enter` and `Escape` are deliberately *not* handled: what they mean depends
|
|
66
|
+
* on the control (submit a search, commit a typed token, clear the field), and
|
|
67
|
+
* `activeIndex()` is all a caller needs to decide.
|
|
68
|
+
*/
|
|
69
|
+
class ListboxNavigation {
|
|
70
|
+
config;
|
|
71
|
+
/** Id for the `role="listbox"` element; wire as `aria-controls`. */
|
|
72
|
+
listboxId;
|
|
73
|
+
_open = signal(false, ...(ngDevMode ? [{ debugName: "_open" }] : /* istanbul ignore next */ []));
|
|
74
|
+
_activeIndex = signal(-1, ...(ngDevMode ? [{ debugName: "_activeIndex" }] : /* istanbul ignore next */ []));
|
|
75
|
+
wrap;
|
|
76
|
+
/** Whether the popup is showing. Also false when there is nothing to show. */
|
|
77
|
+
open = computed(() => this._open() && this.config.count() > 0, ...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
|
|
78
|
+
/** Index of the highlighted option, or -1 when none is active. */
|
|
79
|
+
activeIndex = computed(() => {
|
|
80
|
+
const index = this._activeIndex();
|
|
81
|
+
// A shrinking option list must never leave the active id dangling.
|
|
82
|
+
return index < this.config.count() ? index : -1;
|
|
83
|
+
}, ...(ngDevMode ? [{ debugName: "activeIndex" }] : /* istanbul ignore next */ []));
|
|
84
|
+
/** Value for `aria-activedescendant`, or null when nothing is active. */
|
|
85
|
+
activeDescendantId = computed(() => {
|
|
86
|
+
const index = this.activeIndex();
|
|
87
|
+
return this.open() && index >= 0 ? this.optionId(index) : null;
|
|
88
|
+
}, ...(ngDevMode ? [{ debugName: "activeDescendantId" }] : /* istanbul ignore next */ []));
|
|
89
|
+
constructor(config) {
|
|
90
|
+
this.config = config;
|
|
91
|
+
this.listboxId = uniqueId(config.idPrefix ?? 'uni-listbox');
|
|
92
|
+
this.wrap = config.wrap ?? true;
|
|
93
|
+
}
|
|
94
|
+
/** Stable per-option id, for `role="option"` elements. */
|
|
95
|
+
optionId(index) {
|
|
96
|
+
return `${this.listboxId}-option-${index}`;
|
|
97
|
+
}
|
|
98
|
+
show() {
|
|
99
|
+
this._open.set(true);
|
|
100
|
+
}
|
|
101
|
+
/** Close and drop the active option, so reopening starts clean. */
|
|
102
|
+
hide() {
|
|
103
|
+
this._open.set(false);
|
|
104
|
+
this._activeIndex.set(-1);
|
|
105
|
+
}
|
|
106
|
+
setActive(index) {
|
|
107
|
+
this._activeIndex.set(index);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Handle ArrowDown / ArrowUp / Home / End, opening the popup if needed.
|
|
111
|
+
* Returns true when the key was consumed, so a caller can fall through to
|
|
112
|
+
* its own handling for everything else.
|
|
113
|
+
*/
|
|
114
|
+
navigate(event) {
|
|
115
|
+
const count = this.config.count();
|
|
116
|
+
if (count === 0)
|
|
117
|
+
return false;
|
|
118
|
+
const target = this.nextIndex(event.key, count);
|
|
119
|
+
if (target === null)
|
|
120
|
+
return false;
|
|
121
|
+
event.preventDefault();
|
|
122
|
+
this._open.set(true);
|
|
123
|
+
this._activeIndex.set(target);
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
nextIndex(key, count) {
|
|
127
|
+
const current = this.activeIndex();
|
|
128
|
+
switch (key) {
|
|
129
|
+
case 'ArrowDown':
|
|
130
|
+
return this.step(current, 1, count);
|
|
131
|
+
case 'ArrowUp':
|
|
132
|
+
// Opening with ArrowUp lands on the last option, matching menus.
|
|
133
|
+
return current < 0 ? count - 1 : this.step(current, -1, count);
|
|
134
|
+
case 'Home':
|
|
135
|
+
return 0;
|
|
136
|
+
case 'End':
|
|
137
|
+
return count - 1;
|
|
138
|
+
default:
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
step(current, delta, count) {
|
|
143
|
+
const next = current + delta;
|
|
144
|
+
if (this.wrap)
|
|
145
|
+
return (next + count) % count;
|
|
146
|
+
return Math.min(Math.max(next, 0), count - 1);
|
|
147
|
+
}
|
|
148
|
+
/** Close when focus leaves the control entirely (not on internal moves). */
|
|
149
|
+
closeOnFocusOut(event) {
|
|
150
|
+
const next = event.relatedTarget;
|
|
151
|
+
const container = event.currentTarget;
|
|
152
|
+
if (!next || !container?.contains(next))
|
|
153
|
+
this.hide();
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/** Factory mirroring the signal-API style used elsewhere in the CDK. */
|
|
157
|
+
const createListboxNavigation = (config) => new ListboxNavigation(config);
|
|
158
|
+
|
|
53
159
|
/** Returns a document-unique dashed-ident usable as a CSS `anchor-name`. */
|
|
54
160
|
function newAnchorName() {
|
|
55
161
|
return `--${uniqueId('uni-anchor')}`;
|
|
@@ -648,10 +754,19 @@ class ThemeService {
|
|
|
648
754
|
static CUSTOM_KEY = 'CustomTheme';
|
|
649
755
|
/** localStorage key holding the serialized brand palette config. */
|
|
650
756
|
static PALETTE_KEY = 'uni-custom-palette';
|
|
651
|
-
themes = inject(UNI_THEMES);
|
|
652
757
|
localStorage = inject(LocalStorageService);
|
|
653
|
-
|
|
654
|
-
|
|
758
|
+
/**
|
|
759
|
+
* The live theme registry: the injected {@link UNI_THEMES} map (validated
|
|
760
|
+
* at construction) plus anything added via {@link registerTheme} — including
|
|
761
|
+
* the custom brand theme, which registers under {@link CUSTOM_KEY} so it is
|
|
762
|
+
* an ordinary, selectable entry.
|
|
763
|
+
*/
|
|
764
|
+
registry = signal({}, ...(ngDevMode ? [{ debugName: "registry" }] : /* istanbul ignore next */ []));
|
|
765
|
+
_theme = signal(LightTheme, ...(ngDevMode ? [{ debugName: "_theme" }] : /* istanbul ignore next */ []));
|
|
766
|
+
/** The active theme. Write via {@link setTheme}/{@link selectTheme} — every write is validated. */
|
|
767
|
+
theme = this._theme.asReadonly();
|
|
768
|
+
/** Selectable themes, derived from the registry — reactive to registration. */
|
|
769
|
+
themeOptions = computed(() => Object.entries(this.registry()).map(([value, theme]) => ({ label: theme.name, value })), ...(ngDevMode ? [{ debugName: "themeOptions" }] : /* istanbul ignore next */ []));
|
|
655
770
|
components = computed(() => this.theme().components, ...(ngDevMode ? [{ debugName: "components" }] : /* istanbul ignore next */ []));
|
|
656
771
|
component = (componentName) => computed(() => this.components()[componentName] || {});
|
|
657
772
|
colors = computed(() => this.theme().colors, ...(ngDevMode ? [{ debugName: "colors" }] : /* istanbul ignore next */ []));
|
|
@@ -677,9 +792,21 @@ class ThemeService {
|
|
|
677
792
|
}
|
|
678
793
|
}
|
|
679
794
|
`;
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
795
|
+
// Seed the registry from the injected map, excluding (with reasons) any
|
|
796
|
+
// theme that fails the structural contract — a malformed theme would
|
|
797
|
+
// otherwise render as silent `undefined` CSS.
|
|
798
|
+
const injected = inject(UNI_THEMES);
|
|
799
|
+
const registry = {};
|
|
800
|
+
for (const [key, candidate] of Object.entries(injected)) {
|
|
801
|
+
const result = parseTheme(candidate);
|
|
802
|
+
if (result.success) {
|
|
803
|
+
registry[key] = result.theme;
|
|
804
|
+
}
|
|
805
|
+
else {
|
|
806
|
+
console.warn(`Uni theme '${key}' rejected: ${formatThemeIssues(result.issues)}`);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
this.registry.set(registry);
|
|
683
810
|
// Rehydrate a custom brand theme if one is active — this is what lets a
|
|
684
811
|
// palette built in one story reskin every other story: each story spins up
|
|
685
812
|
// a fresh ThemeService, and it reads the persisted config here on init.
|
|
@@ -689,14 +816,75 @@ class ThemeService {
|
|
|
689
816
|
this.applyPalette(savedPalette, false);
|
|
690
817
|
}
|
|
691
818
|
else {
|
|
692
|
-
this.selectTheme(savedTheme || Object.keys(
|
|
819
|
+
this.selectTheme(savedTheme || Object.keys(registry)[0] || 'base');
|
|
693
820
|
}
|
|
694
821
|
}
|
|
822
|
+
/**
|
|
823
|
+
* Activate a registered theme. Returns false — touching nothing, persisting
|
|
824
|
+
* nothing — when the name is unknown (previously this silently kept the old
|
|
825
|
+
* theme while still recording the bad key).
|
|
826
|
+
*/
|
|
695
827
|
selectTheme(themeName) {
|
|
828
|
+
const theme = this.registry()[themeName];
|
|
829
|
+
if (!theme)
|
|
830
|
+
return false;
|
|
831
|
+
// Registered themes were validated at registration; set directly.
|
|
832
|
+
this._theme.set(theme);
|
|
696
833
|
this.selectedThemeKey.set(themeName);
|
|
697
|
-
if (this.themes[themeName])
|
|
698
|
-
this.theme.set(this.themes[themeName]);
|
|
699
834
|
this.localStorage.setItem('theme', themeName);
|
|
835
|
+
return true;
|
|
836
|
+
}
|
|
837
|
+
/**
|
|
838
|
+
* Validate and activate a theme without registering it. The result carries
|
|
839
|
+
* acceptance or the complete list of rejection reasons; on rejection the
|
|
840
|
+
* active theme is unchanged.
|
|
841
|
+
*/
|
|
842
|
+
setTheme(input) {
|
|
843
|
+
const result = this.accept(input);
|
|
844
|
+
if (result.success) {
|
|
845
|
+
this._theme.set(result.theme);
|
|
846
|
+
this.selectedThemeKey.set(result.theme.id);
|
|
847
|
+
}
|
|
848
|
+
return result;
|
|
849
|
+
}
|
|
850
|
+
/**
|
|
851
|
+
* Validate and add a theme to the registry (keyed by its `id`), making it
|
|
852
|
+
* selectable and listed in {@link themeOptions}. On rejection the registry
|
|
853
|
+
* is unchanged and the result lists every reason.
|
|
854
|
+
*/
|
|
855
|
+
registerTheme(input, opts) {
|
|
856
|
+
const result = this.accept(input);
|
|
857
|
+
if (result.success) {
|
|
858
|
+
const theme = result.theme;
|
|
859
|
+
this.registry.update((themes) => ({ ...themes, [theme.id]: theme }));
|
|
860
|
+
if (opts?.select)
|
|
861
|
+
this.selectTheme(theme.id);
|
|
862
|
+
}
|
|
863
|
+
return result;
|
|
864
|
+
}
|
|
865
|
+
/**
|
|
866
|
+
* The gate every externally-supplied theme passes: validate, then restore
|
|
867
|
+
* any built-in icons the payload left out. Themes that travel as JSON (the
|
|
868
|
+
* MCP theme tools, a theme registry) omit the built-in icon set — it is
|
|
869
|
+
* ~71% of the bytes and every consumer already ships it — so hydration
|
|
870
|
+
* applies the same `{...BaseIcons, ...icons}` contract `createTheme` uses:
|
|
871
|
+
* the theme's own icons win, built-ins fill the rest.
|
|
872
|
+
*/
|
|
873
|
+
accept(input) {
|
|
874
|
+
const result = parseTheme(input);
|
|
875
|
+
return result.success ? { ...result, theme: hydrateTheme(result.theme) } : result;
|
|
876
|
+
}
|
|
877
|
+
/** Remove a registered theme; falls back to the first remaining theme if it was active. */
|
|
878
|
+
unregisterTheme(id) {
|
|
879
|
+
if (!(id in this.registry()))
|
|
880
|
+
return;
|
|
881
|
+
this.registry.update((themes) => {
|
|
882
|
+
const { [id]: _removed, ...rest } = themes;
|
|
883
|
+
return rest;
|
|
884
|
+
});
|
|
885
|
+
if (this.selectedThemeKey() === id) {
|
|
886
|
+
this.selectTheme(Object.keys(this.registry())[0] || 'base');
|
|
887
|
+
}
|
|
700
888
|
}
|
|
701
889
|
/** The live brand palette config, when a custom theme is active. */
|
|
702
890
|
customPalette = signal(null, ...(ngDevMode ? [{ debugName: "customPalette" }] : /* istanbul ignore next */ []));
|
|
@@ -708,18 +896,24 @@ class ThemeService {
|
|
|
708
896
|
*/
|
|
709
897
|
applyPalette(config, persist = true) {
|
|
710
898
|
this.customPalette.set(config);
|
|
711
|
-
|
|
899
|
+
// Registering (rather than only setting) makes 'Your Brand' an ordinary
|
|
900
|
+
// registry entry: it appears in themeOptions/uni-theme-switch and can be
|
|
901
|
+
// switched away from and back to.
|
|
902
|
+
const result = this.registerTheme(createThemeFromPalette({ ...config, id: ThemeService.CUSTOM_KEY, name: 'Your Brand' }));
|
|
903
|
+
if (!result.success)
|
|
904
|
+
return;
|
|
905
|
+
this._theme.set(result.theme);
|
|
712
906
|
this.selectedThemeKey.set(ThemeService.CUSTOM_KEY);
|
|
713
907
|
if (persist) {
|
|
714
908
|
this.localStorage.setItem(ThemeService.PALETTE_KEY, config);
|
|
715
909
|
this.localStorage.setItem('theme', ThemeService.CUSTOM_KEY);
|
|
716
910
|
}
|
|
717
911
|
}
|
|
718
|
-
/** Drop the custom brand theme and fall back to the first
|
|
912
|
+
/** Drop the custom brand theme and fall back to the first registered theme. */
|
|
719
913
|
clearCustomPalette() {
|
|
720
914
|
this.customPalette.set(null);
|
|
721
915
|
this.localStorage.removeItem(ThemeService.PALETTE_KEY);
|
|
722
|
-
this.
|
|
916
|
+
this.unregisterTheme(ThemeService.CUSTOM_KEY);
|
|
723
917
|
}
|
|
724
918
|
selectedThemeName = computed(() => this.theme().name, ...(ngDevMode ? [{ debugName: "selectedThemeName" }] : /* istanbul ignore next */ []));
|
|
725
919
|
selectedThemeKey = signal('', ...(ngDevMode ? [{ debugName: "selectedThemeKey" }] : /* istanbul ignore next */ []));
|
|
@@ -4202,8 +4396,13 @@ class UniMenuItemComponent {
|
|
|
4202
4396
|
...this.theme.radius(options.borderRadius),
|
|
4203
4397
|
...this.theme.typeface(options.typeface),
|
|
4204
4398
|
...this.theme.color(options.textColor),
|
|
4205
|
-
// Roving focus highlights items the same way hover does
|
|
4206
|
-
|
|
4399
|
+
// Roving focus highlights items the same way hover does — but only
|
|
4400
|
+
// when focus is keyboard-driven. `onOpened()` focuses an item on every
|
|
4401
|
+
// open, including pointer opens, and plain `:focus` would paint that
|
|
4402
|
+
// as a highlight the mouse user never asked for, reading as a
|
|
4403
|
+
// preselected item. `:focus-visible` excludes programmatic focus that
|
|
4404
|
+
// follows a click while still matching keyboard navigation.
|
|
4405
|
+
[HOVER_OR_KEYBOARD_FOCUS]: {
|
|
4207
4406
|
...this.theme.colorPair(this.hoverColor() ?? options.hoverColor),
|
|
4208
4407
|
outline: 'none',
|
|
4209
4408
|
},
|
|
@@ -4213,8 +4412,10 @@ class UniMenuItemComponent {
|
|
|
4213
4412
|
},
|
|
4214
4413
|
},
|
|
4215
4414
|
transitionSpeed > 0 && { transition: `all ${transitionSpeed}s ease` },
|
|
4216
|
-
// Variant tones override the base look
|
|
4217
|
-
//
|
|
4415
|
+
// Variant tones override the base look. A variant that restyles the
|
|
4416
|
+
// highlight must key it with HOVER_OR_KEYBOARD_FOCUS — Emotion merges by
|
|
4417
|
+
// exact selector text, so a variant spelling it `&:hover, &:focus`
|
|
4418
|
+
// would both miss the override and reintroduce the phantom highlight.
|
|
4218
4419
|
{ ...variantStyle },
|
|
4219
4420
|
]);
|
|
4220
4421
|
}, ...(ngDevMode ? [{ debugName: "menuItemClassName" }] : /* istanbul ignore next */ []));
|
|
@@ -4534,11 +4735,38 @@ class UniMultiSelectDropdownComponent extends BaseComponent {
|
|
|
4534
4735
|
// --- CONFIGURATION ---
|
|
4535
4736
|
options = input.required(...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
|
|
4536
4737
|
placeholder = input('', ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
|
|
4738
|
+
/**
|
|
4739
|
+
* What the field is for, e.g. "Fruits". Rendered visually hidden inside the
|
|
4740
|
+
* trigger so its accessible name reads "Fruits, Apple, Pear" — without it a
|
|
4741
|
+
* screen reader announces only the current selection, with no clue which
|
|
4742
|
+
* field it belongs to.
|
|
4743
|
+
*/
|
|
4744
|
+
label = input(...(ngDevMode ? [undefined, { debugName: "label" }] : /* istanbul ignore next */ []));
|
|
4745
|
+
/** Debounce for the filter box, matching the library's input debounce. */
|
|
4746
|
+
debounceTime = input(200, ...(ngDevMode ? [{ debugName: "debounceTime" }] : /* istanbul ignore next */ []));
|
|
4747
|
+
srOnly = css(visuallyHidden);
|
|
4537
4748
|
query = signal('', ...(ngDevMode ? [{ debugName: "query" }] : /* istanbul ignore next */ []));
|
|
4749
|
+
queryTimer;
|
|
4538
4750
|
filteredOptions = computed(() => {
|
|
4539
4751
|
const filterText = this.query().toLowerCase();
|
|
4540
4752
|
return this.options().filter((opt) => opt.label.toLowerCase().includes(filterText));
|
|
4541
4753
|
}, ...(ngDevMode ? [{ debugName: "filteredOptions" }] : /* istanbul ignore next */ []));
|
|
4754
|
+
optionRefs = viewChildren('optionRow', ...(ngDevMode ? [{ debugName: "optionRefs" }] : /* istanbul ignore next */ []));
|
|
4755
|
+
/**
|
|
4756
|
+
* Roving focus over the options. The shared helper owns the index
|
|
4757
|
+
* arithmetic — wrapping, Home/End, and never pointing past a list the
|
|
4758
|
+
* filter has narrowed — the same contract `uni-search-input` and
|
|
4759
|
+
* `uni-tag-input` use, so the keys behave identically across all three.
|
|
4760
|
+
*/
|
|
4761
|
+
list = createListboxNavigation({
|
|
4762
|
+
count: () => this.filteredOptions().length,
|
|
4763
|
+
idPrefix: 'uni-multi-select',
|
|
4764
|
+
});
|
|
4765
|
+
/** Announced with the selection so the count is not left to guesswork. */
|
|
4766
|
+
selectionSummary = computed(() => {
|
|
4767
|
+
const count = this.value().length;
|
|
4768
|
+
return count === 0 ? 'none selected' : `${count} selected`;
|
|
4769
|
+
}, ...(ngDevMode ? [{ debugName: "selectionSummary" }] : /* istanbul ignore next */ []));
|
|
4542
4770
|
// Derived display string
|
|
4543
4771
|
selectedLabelsText = computed(() => {
|
|
4544
4772
|
const selections = this.value();
|
|
@@ -4578,7 +4806,26 @@ class UniMultiSelectDropdownComponent extends BaseComponent {
|
|
|
4578
4806
|
},
|
|
4579
4807
|
}), ...(ngDevMode ? [{ debugName: "searchInputClass" }] : /* istanbul ignore next */ []));
|
|
4580
4808
|
handleQueryInput(event) {
|
|
4581
|
-
|
|
4809
|
+
const text = event.target.value;
|
|
4810
|
+
// Debounced so a long option list is not re-filtered on every keystroke.
|
|
4811
|
+
clearTimeout(this.queryTimer);
|
|
4812
|
+
this.queryTimer = setTimeout(() => this.query.set(text), this.debounceTime());
|
|
4813
|
+
}
|
|
4814
|
+
/**
|
|
4815
|
+
* Arrow keys walk the options from anywhere in the panel, including the
|
|
4816
|
+
* filter box — previously the only way in was to Tab through every
|
|
4817
|
+
* checkbox.
|
|
4818
|
+
*/
|
|
4819
|
+
onPanelKeydown(event) {
|
|
4820
|
+
if (!this.list.navigate(event))
|
|
4821
|
+
return;
|
|
4822
|
+
const row = this.optionRefs()[this.list.activeIndex()]?.nativeElement;
|
|
4823
|
+
row?.querySelector('input, [tabindex]')?.focus();
|
|
4824
|
+
}
|
|
4825
|
+
/** Keeps the active index in step when focus lands on a row by pointer. */
|
|
4826
|
+
onOptionFocus(index) {
|
|
4827
|
+
this.list.show();
|
|
4828
|
+
this.list.setActive(index);
|
|
4582
4829
|
}
|
|
4583
4830
|
selectAll() {
|
|
4584
4831
|
if (this.disabled())
|
|
@@ -4613,7 +4860,7 @@ class UniMultiSelectDropdownComponent extends BaseComponent {
|
|
|
4613
4860
|
});
|
|
4614
4861
|
}
|
|
4615
4862
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniMultiSelectDropdownComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
|
|
4616
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniMultiSelectDropdownComponent, isStandalone: true, selector: "uni-multi-select-dropdown", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, dirty: { classPropertyName: "dirty", publicName: "dirty", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, ariaDescribedBy: { classPropertyName: "ariaDescribedBy", publicName: "ariaDescribedBy", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: true, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touched: "touchedChange" }, host: { properties: { "class": "className" } }, providers: [{ provide: COMPONENT_NAME, useValue: 'multiSelectDropdown' }], usesInheritance: true, ngImport: i0, template: "<button\n [class]=\"triggerClass\"\n #trigger\n [style.cursor]=\"disabled() ? 'default' : 'pointer'\"\n [attr.aria-required]=\"required() ? true : null\"\n [attr.aria-invalid]=\"showError() ? true : null\"\n [attr.aria-describedby]=\"ariaDescribedBy() || null\"\n (click)=\"!disabled() && touched.set(true)\"\n>\n <uni-input-box [error]=\"showError()\" [disabled]=\"disabled()\">\n <div\n row-layout\n alignItems=\"center\"\n justifyContent=\"space-between\"\n [fullWidth]=\"true\"\n [minWidth]=\"0\"\n paddingLeft=\"sm\"\n >\n <span uni-text\n display=\"block\"\n [typeface]=\"componentOptions().textRole\"\n [color]=\"textColor()\"\n [ellipsis]=\"true\"\n [style.flex-grow]=\"1\"\n >{{ selectedLabelsText() }}</span>\n <uni-symbol name=\"keyboard_arrow_down\" [style.flex-shrink]=\"0\"></uni-symbol>\n </div>\n </uni-input-box>\n</button>\n<uni-dropdown\n #dropdown\n [trigger]=\"trigger\"\n ariaHasPopup=\"dialog\"\n (dropdownShowing)=\"$event && searchInput.focus()\"\n (dropdownHiding)=\"$event && trigger.focus()\"\n>\n <div box-layout gap=\"xs\" padding=\"xs\">\n <input\n #searchInput\n type=\"text\"\n [class]=\"searchInputClass()\"\n [value]=\"query()\"\n (input)=\"handleQueryInput($event)\"\n placeholder=\"Search...\"\n aria-label=\"Filter options\"\n (click)=\"$event.stopPropagation()\"\n [disabled]=\"disabled()\"\n />\n </div>\n\n <uni-divider [border]=\"componentOptions().dividerBorder\" />\n\n <div
|
|
4863
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniMultiSelectDropdownComponent, isStandalone: true, selector: "uni-multi-select-dropdown", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, dirty: { classPropertyName: "dirty", publicName: "dirty", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, ariaDescribedBy: { classPropertyName: "ariaDescribedBy", publicName: "ariaDescribedBy", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: true, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, debounceTime: { classPropertyName: "debounceTime", publicName: "debounceTime", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touched: "touchedChange" }, host: { properties: { "class": "className" } }, providers: [{ provide: COMPONENT_NAME, useValue: 'multiSelectDropdown' }], viewQueries: [{ propertyName: "optionRefs", predicate: ["optionRow"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<button\n [class]=\"triggerClass\"\n #trigger\n [style.cursor]=\"disabled() ? 'default' : 'pointer'\"\n [attr.aria-required]=\"required() ? true : null\"\n [attr.aria-invalid]=\"showError() ? true : null\"\n [attr.aria-describedby]=\"ariaDescribedBy() || null\"\n (click)=\"!disabled() && touched.set(true)\"\n>\n @if (label(); as fieldLabel) {\n <span [class]=\"srOnly\">{{ fieldLabel }},</span>\n }\n <span [class]=\"srOnly\">{{ selectionSummary() }}</span>\n <uni-input-box [error]=\"showError()\" [disabled]=\"disabled()\">\n <div\n row-layout\n alignItems=\"center\"\n justifyContent=\"space-between\"\n [fullWidth]=\"true\"\n [minWidth]=\"0\"\n paddingLeft=\"sm\"\n >\n <span uni-text\n display=\"block\"\n [typeface]=\"componentOptions().textRole\"\n [color]=\"textColor()\"\n [ellipsis]=\"true\"\n [style.flex-grow]=\"1\"\n >{{ selectedLabelsText() }}</span>\n <uni-symbol name=\"keyboard_arrow_down\" [style.flex-shrink]=\"0\"></uni-symbol>\n </div>\n </uni-input-box>\n</button>\n<uni-dropdown\n #dropdown\n [trigger]=\"trigger\"\n ariaHasPopup=\"dialog\"\n (dropdownShowing)=\"$event && searchInput.focus()\"\n (dropdownHiding)=\"$event && trigger.focus()\"\n>\n <!-- eslint-disable-next-line @angular-eslint/template/interactive-supports-focus -->\n <div (keydown)=\"onPanelKeydown($event)\">\n <div box-layout gap=\"xs\" padding=\"xs\">\n <input\n #searchInput\n type=\"text\"\n [class]=\"searchInputClass()\"\n [value]=\"query()\"\n (input)=\"handleQueryInput($event)\"\n placeholder=\"Search...\"\n aria-label=\"Filter options\"\n (click)=\"$event.stopPropagation()\"\n [disabled]=\"disabled()\"\n />\n </div>\n\n <uni-divider [border]=\"componentOptions().dividerBorder\" />\n\n <div\n stack-layout\n gap=\"xs\"\n padding=\"xs\"\n role=\"group\"\n [attr.aria-label]=\"label() ? label() + ' options' : 'Options'\"\n >\n @for (option of filteredOptions(); track option.value; let i = $index) {\n <div #optionRow (focusin)=\"onOptionFocus(i)\">\n <uni-checkbox\n [label]=\"option.label\"\n [checked]=\"isOptionSelected(option)()\"\n (checkedChange)=\"toggleOption(option, $event)\"\n variant=\"primary\"\n [disabled]=\"disabled()\"\n >\n </uni-checkbox>\n </div>\n }\n @if (filteredOptions().length === 0) {\n <span uni-text=\"label\" role=\"status\">No options match.</span>\n }\n </div>\n\n <uni-divider [border]=\"componentOptions().dividerBorder\" />\n\n <div row-layout gap=\"xs\" justifyContent=\"space-around\" padding=\"xs\">\n <button text-button (click)=\"dropdown.hideDropdown()\" size=\"sm\" variant=\"ghost\">Done</button>\n </div>\n </div>\n</uni-dropdown>\n", dependencies: [{ kind: "component", type: UniCheckboxComponent, selector: "uni-checkbox", inputs: ["checked", "disabled", "touched", "invalid", "dirty", "required", "ariaDescribedBy", "label", "indeterminate"], outputs: ["checkedChange", "touchedChange", "indeterminateChange"] }, { kind: "component", type: UniBoxComponent, selector: "[uni-box-layout], [box-layout]", inputs: ["color", "backgroundColor", "borderRadius", "borderRadiusLeft", "borderRadiusRight", "borderRadiusTop", "borderRadiusBottom", "padding", "paddingHorizontal", "paddingVertical", "paddingLeft", "paddingRight", "paddingTop", "paddingBottom", "border", "borderTop", "borderBottom", "borderLeft", "borderRight", "dashBorder", "alignSelf", "alignItems", "alignContent", "justifyContent", "grow", "display", "position", "inset", "height", "minHeight", "maxHeight", "width", "minWidth", "maxWidth", "ignoreDir", "gridArea", "gridColumn", "gridRow", "overflow", "elevation", "shadow", "gap", "fullWidth", "fullHeight", "flexDirection", "textAlign", "wrapItems", "zIndex"] }, { kind: "component", type: UniDropdownComponent, selector: "uni-dropdown", inputs: ["trigger", "placement", "offset", "ariaHasPopup", "paddingVertical", "paddingHorizontal", "border", "borderRadius", "shadow", "color"], outputs: ["dropdownShowing", "dropdownHiding"] }, { kind: "component", type: UniStackComponent, selector: "[uni-stack-layout], [stack-layout]", inputs: ["display", "flexDirection", "minHeight"] }, { kind: "component", type: UniDividerComponent, selector: "uni-divider", inputs: ["orientation", "border"] }, { kind: "component", type: UniButtonComponent, selector: "button[uni-text-button], button[text-button]", inputs: ["disable", "loading", "fullWidth", "symbolLeft", "symbolRight"] }, { kind: "component", type: UniTextComponent, selector: "[uni-text]", inputs: ["uni-text", "typeface", "color", "display", "align", "nowrap", "maxWidth", "ellipsis"] }, { kind: "component", type: UniSymbolComponent, selector: "uni-symbol", inputs: ["name", "fill", "weight", "grade", "opticalSize"] }, { kind: "component", type: UniRowComponent, selector: "[uni-row-layout], [row-layout]", inputs: ["display", "flexDirection", "minWidth"] }, { kind: "component", type: UniInputBoxComponent, selector: "uni-input-box", inputs: ["disabled", "error", "minWidth", "height"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
4617
4864
|
}
|
|
4618
4865
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniMultiSelectDropdownComponent, decorators: [{
|
|
4619
4866
|
type: Component,
|
|
@@ -4628,8 +4875,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
|
|
|
4628
4875
|
UniSymbolComponent,
|
|
4629
4876
|
UniRowComponent,
|
|
4630
4877
|
UniInputBoxComponent,
|
|
4631
|
-
], providers: [{ provide: COMPONENT_NAME, useValue: 'multiSelectDropdown' }], host: { '[class]': 'className' }, changeDetection: ChangeDetectionStrategy.OnPush, template: "<button\n [class]=\"triggerClass\"\n #trigger\n [style.cursor]=\"disabled() ? 'default' : 'pointer'\"\n [attr.aria-required]=\"required() ? true : null\"\n [attr.aria-invalid]=\"showError() ? true : null\"\n [attr.aria-describedby]=\"ariaDescribedBy() || null\"\n (click)=\"!disabled() && touched.set(true)\"\n>\n <uni-input-box [error]=\"showError()\" [disabled]=\"disabled()\">\n <div\n row-layout\n alignItems=\"center\"\n justifyContent=\"space-between\"\n [fullWidth]=\"true\"\n [minWidth]=\"0\"\n paddingLeft=\"sm\"\n >\n <span uni-text\n display=\"block\"\n [typeface]=\"componentOptions().textRole\"\n [color]=\"textColor()\"\n [ellipsis]=\"true\"\n [style.flex-grow]=\"1\"\n >{{ selectedLabelsText() }}</span>\n <uni-symbol name=\"keyboard_arrow_down\" [style.flex-shrink]=\"0\"></uni-symbol>\n </div>\n </uni-input-box>\n</button>\n<uni-dropdown\n #dropdown\n [trigger]=\"trigger\"\n ariaHasPopup=\"dialog\"\n (dropdownShowing)=\"$event && searchInput.focus()\"\n (dropdownHiding)=\"$event && trigger.focus()\"\n>\n <div box-layout gap=\"xs\" padding=\"xs\">\n <input\n #searchInput\n type=\"text\"\n [class]=\"searchInputClass()\"\n [value]=\"query()\"\n (input)=\"handleQueryInput($event)\"\n placeholder=\"Search...\"\n aria-label=\"Filter options\"\n (click)=\"$event.stopPropagation()\"\n [disabled]=\"disabled()\"\n />\n </div>\n\n <uni-divider [border]=\"componentOptions().dividerBorder\" />\n\n <div
|
|
4632
|
-
}], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }, { type: i0.Output, args: ["touchedChange"] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], dirty: [{ type: i0.Input, args: [{ isSignal: true, alias: "dirty", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], ariaDescribedBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaDescribedBy", required: false }] }], options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: true }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }] } });
|
|
4878
|
+
], providers: [{ provide: COMPONENT_NAME, useValue: 'multiSelectDropdown' }], host: { '[class]': 'className' }, changeDetection: ChangeDetectionStrategy.OnPush, template: "<button\n [class]=\"triggerClass\"\n #trigger\n [style.cursor]=\"disabled() ? 'default' : 'pointer'\"\n [attr.aria-required]=\"required() ? true : null\"\n [attr.aria-invalid]=\"showError() ? true : null\"\n [attr.aria-describedby]=\"ariaDescribedBy() || null\"\n (click)=\"!disabled() && touched.set(true)\"\n>\n @if (label(); as fieldLabel) {\n <span [class]=\"srOnly\">{{ fieldLabel }},</span>\n }\n <span [class]=\"srOnly\">{{ selectionSummary() }}</span>\n <uni-input-box [error]=\"showError()\" [disabled]=\"disabled()\">\n <div\n row-layout\n alignItems=\"center\"\n justifyContent=\"space-between\"\n [fullWidth]=\"true\"\n [minWidth]=\"0\"\n paddingLeft=\"sm\"\n >\n <span uni-text\n display=\"block\"\n [typeface]=\"componentOptions().textRole\"\n [color]=\"textColor()\"\n [ellipsis]=\"true\"\n [style.flex-grow]=\"1\"\n >{{ selectedLabelsText() }}</span>\n <uni-symbol name=\"keyboard_arrow_down\" [style.flex-shrink]=\"0\"></uni-symbol>\n </div>\n </uni-input-box>\n</button>\n<uni-dropdown\n #dropdown\n [trigger]=\"trigger\"\n ariaHasPopup=\"dialog\"\n (dropdownShowing)=\"$event && searchInput.focus()\"\n (dropdownHiding)=\"$event && trigger.focus()\"\n>\n <!-- eslint-disable-next-line @angular-eslint/template/interactive-supports-focus -->\n <div (keydown)=\"onPanelKeydown($event)\">\n <div box-layout gap=\"xs\" padding=\"xs\">\n <input\n #searchInput\n type=\"text\"\n [class]=\"searchInputClass()\"\n [value]=\"query()\"\n (input)=\"handleQueryInput($event)\"\n placeholder=\"Search...\"\n aria-label=\"Filter options\"\n (click)=\"$event.stopPropagation()\"\n [disabled]=\"disabled()\"\n />\n </div>\n\n <uni-divider [border]=\"componentOptions().dividerBorder\" />\n\n <div\n stack-layout\n gap=\"xs\"\n padding=\"xs\"\n role=\"group\"\n [attr.aria-label]=\"label() ? label() + ' options' : 'Options'\"\n >\n @for (option of filteredOptions(); track option.value; let i = $index) {\n <div #optionRow (focusin)=\"onOptionFocus(i)\">\n <uni-checkbox\n [label]=\"option.label\"\n [checked]=\"isOptionSelected(option)()\"\n (checkedChange)=\"toggleOption(option, $event)\"\n variant=\"primary\"\n [disabled]=\"disabled()\"\n >\n </uni-checkbox>\n </div>\n }\n @if (filteredOptions().length === 0) {\n <span uni-text=\"label\" role=\"status\">No options match.</span>\n }\n </div>\n\n <uni-divider [border]=\"componentOptions().dividerBorder\" />\n\n <div row-layout gap=\"xs\" justifyContent=\"space-around\" padding=\"xs\">\n <button text-button (click)=\"dropdown.hideDropdown()\" size=\"sm\" variant=\"ghost\">Done</button>\n </div>\n </div>\n</uni-dropdown>\n" }]
|
|
4879
|
+
}], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }, { type: i0.Output, args: ["touchedChange"] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], dirty: [{ type: i0.Input, args: [{ isSignal: true, alias: "dirty", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], ariaDescribedBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaDescribedBy", required: false }] }], options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: true }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], debounceTime: [{ type: i0.Input, args: [{ isSignal: true, alias: "debounceTime", required: false }] }], optionRefs: [{ type: i0.ViewChildren, args: ['optionRow', { isSignal: true }] }] } });
|
|
4633
4880
|
|
|
4634
4881
|
class UniNotificationBadgeComponent extends BaseComponent {
|
|
4635
4882
|
count = input(undefined, ...(ngDevMode ? [{ debugName: "count" }] : /* istanbul ignore next */ []));
|
|
@@ -5394,71 +5641,59 @@ class UniSearchInputComponent extends BaseComponent {
|
|
|
5394
5641
|
search = output();
|
|
5395
5642
|
suggestionSelected = output();
|
|
5396
5643
|
field = viewChild.required(UniDebounceInputComponent);
|
|
5397
|
-
listboxId = uniqueId('uni-search-listbox');
|
|
5398
|
-
listOpen = signal(false, ...(ngDevMode ? [{ debugName: "listOpen" }] : /* istanbul ignore next */ []));
|
|
5399
|
-
activeIndex = signal(-1, ...(ngDevMode ? [{ debugName: "activeIndex" }] : /* istanbul ignore next */ []));
|
|
5400
5644
|
visibleSuggestions = computed(() => this.suggestions().slice(0, this.componentOptions().maxSuggestions ?? 8), ...(ngDevMode ? [{ debugName: "visibleSuggestions" }] : /* istanbul ignore next */ []));
|
|
5401
|
-
|
|
5402
|
-
|
|
5645
|
+
/** Shared combobox bookkeeping: open state, active option, ARIA ids. */
|
|
5646
|
+
list = createListboxNavigation({
|
|
5647
|
+
count: () => this.visibleSuggestions().length,
|
|
5648
|
+
idPrefix: 'uni-search-listbox',
|
|
5649
|
+
});
|
|
5650
|
+
listboxId = this.list.listboxId;
|
|
5403
5651
|
hasQuery = computed(() => !!this.field()?.value(), ...(ngDevMode ? [{ debugName: "hasQuery" }] : /* istanbul ignore next */ []));
|
|
5404
5652
|
handleChange(value) {
|
|
5405
|
-
this.
|
|
5406
|
-
this.
|
|
5653
|
+
this.list.show();
|
|
5654
|
+
this.list.setActive(-1);
|
|
5407
5655
|
this.change.emit(value);
|
|
5408
5656
|
}
|
|
5409
5657
|
submit() {
|
|
5410
|
-
const active = this.activeIndex();
|
|
5411
|
-
if (this.
|
|
5658
|
+
const active = this.list.activeIndex();
|
|
5659
|
+
if (this.list.open() && active >= 0) {
|
|
5412
5660
|
this.select(this.visibleSuggestions()[active]);
|
|
5413
5661
|
return;
|
|
5414
5662
|
}
|
|
5415
|
-
this.
|
|
5663
|
+
this.list.hide();
|
|
5416
5664
|
this.search.emit(this.field().value() ?? '');
|
|
5417
5665
|
}
|
|
5418
5666
|
select(suggestion) {
|
|
5419
5667
|
this.field().value.set(suggestion);
|
|
5420
|
-
this.
|
|
5421
|
-
this.activeIndex.set(-1);
|
|
5668
|
+
this.list.hide();
|
|
5422
5669
|
this.suggestionSelected.emit(suggestion);
|
|
5423
5670
|
this.search.emit(suggestion);
|
|
5424
5671
|
}
|
|
5425
5672
|
clear() {
|
|
5426
5673
|
this.field().clear();
|
|
5427
|
-
this.
|
|
5428
|
-
this.activeIndex.set(-1);
|
|
5674
|
+
this.list.hide();
|
|
5429
5675
|
this.field().focus();
|
|
5430
5676
|
}
|
|
5431
5677
|
onKeydown(event) {
|
|
5678
|
+
// Arrows and Home/End belong to the shared listbox contract.
|
|
5679
|
+
if (this.list.navigate(event))
|
|
5680
|
+
return;
|
|
5432
5681
|
switch (event.key) {
|
|
5433
5682
|
case 'Enter':
|
|
5434
5683
|
event.preventDefault();
|
|
5435
5684
|
this.submit();
|
|
5436
5685
|
break;
|
|
5437
|
-
case 'ArrowDown':
|
|
5438
|
-
case 'ArrowUp': {
|
|
5439
|
-
const count = this.visibleSuggestions().length;
|
|
5440
|
-
if (count === 0)
|
|
5441
|
-
return;
|
|
5442
|
-
event.preventDefault();
|
|
5443
|
-
this.listOpen.set(true);
|
|
5444
|
-
const delta = event.key === 'ArrowDown' ? 1 : -1;
|
|
5445
|
-
this.activeIndex.set((this.activeIndex() + delta + count) % count);
|
|
5446
|
-
break;
|
|
5447
|
-
}
|
|
5448
5686
|
case 'Escape':
|
|
5449
|
-
|
|
5450
|
-
|
|
5687
|
+
// Escape backs out one layer at a time: close the list, then clear.
|
|
5688
|
+
if (this.list.open())
|
|
5689
|
+
this.list.hide();
|
|
5451
5690
|
else if (this.hasQuery())
|
|
5452
5691
|
this.clear();
|
|
5453
5692
|
break;
|
|
5454
5693
|
}
|
|
5455
5694
|
}
|
|
5456
5695
|
onFocusOut(event) {
|
|
5457
|
-
|
|
5458
|
-
const next = event.relatedTarget;
|
|
5459
|
-
if (!next || !event.currentTarget.contains(next)) {
|
|
5460
|
-
this.listOpen.set(false);
|
|
5461
|
-
}
|
|
5696
|
+
this.list.closeOnFocusOut(event);
|
|
5462
5697
|
}
|
|
5463
5698
|
className = computed(() => css({
|
|
5464
5699
|
display: 'block',
|
|
@@ -5500,11 +5735,11 @@ class UniSearchInputComponent extends BaseComponent {
|
|
|
5500
5735
|
});
|
|
5501
5736
|
}, ...(ngDevMode ? [{ debugName: "listClass" }] : /* istanbul ignore next */ []));
|
|
5502
5737
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniSearchInputComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
|
|
5503
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniSearchInputComponent, isStandalone: true, selector: "uni-search-input", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null }, debounceTime: { classPropertyName: "debounceTime", publicName: "debounceTime", isSignal: true, isRequired: false, transformFunction: null }, suggestions: { classPropertyName: "suggestions", publicName: "suggestions", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { change: "change", search: "search", suggestionSelected: "suggestionSelected" }, host: { properties: { "class": "className()" } }, providers: [{ provide: COMPONENT_NAME, useValue: 'searchInput' }], viewQueries: [{ propertyName: "field", first: true, predicate: UniDebounceInputComponent, descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<!-- Keydown/focusout ride the bubbling events from the inner input; the\n wrapper itself stays out of the tab order. -->\n<!-- eslint-disable-next-line @angular-eslint/template/interactive-supports-focus -->\n<div (keydown)=\"onKeydown($event)\" (focusout)=\"onFocusOut($event)\">\n <uni-debounce-input\n #searchField\n [label]=\"label()\"\n [placeholder]=\"placeholder() ?? label()\"\n [debounceTime]=\"debounceTime()\"\n role=\"combobox\"\n [ariaExpanded]=\"
|
|
5738
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniSearchInputComponent, isStandalone: true, selector: "uni-search-input", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null }, debounceTime: { classPropertyName: "debounceTime", publicName: "debounceTime", isSignal: true, isRequired: false, transformFunction: null }, suggestions: { classPropertyName: "suggestions", publicName: "suggestions", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { change: "change", search: "search", suggestionSelected: "suggestionSelected" }, host: { properties: { "class": "className()" } }, providers: [{ provide: COMPONENT_NAME, useValue: 'searchInput' }], viewQueries: [{ propertyName: "field", first: true, predicate: UniDebounceInputComponent, descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<!-- Keydown/focusout ride the bubbling events from the inner input; the\n wrapper itself stays out of the tab order. -->\n<!-- eslint-disable-next-line @angular-eslint/template/interactive-supports-focus -->\n<div (keydown)=\"onKeydown($event)\" (focusout)=\"onFocusOut($event)\">\n <uni-debounce-input\n #searchField\n [label]=\"label()\"\n [placeholder]=\"placeholder() ?? label()\"\n [debounceTime]=\"debounceTime()\"\n role=\"combobox\"\n [ariaExpanded]=\"list.open()\"\n [ariaControls]=\"listboxId\"\n [ariaActivedescendant]=\"list.activeDescendantId() ?? undefined\"\n (change)=\"handleChange($event)\"\n >\n <uni-symbol pre-input class=\"uni-search-lead\" name=\"{{ componentOptions().searchSymbol ?? 'search' }}\" aria-hidden=\"true\" />\n @if (hasQuery()) {\n <button\n post-input\n icon-button\n [iconName]=\"componentOptions().clearSymbol ?? 'close'\"\n variant=\"ghost\"\n size=\"sm\"\n (click)=\"clear()\"\n >\n Clear search\n </button>\n }\n </uni-debounce-input>\n\n @if (list.open()) {\n <ul [id]=\"listboxId\" role=\"listbox\" [attr.aria-label]=\"label()\" [class]=\"listClass()\">\n @for (suggestion of visibleSuggestions(); track suggestion; let i = $index) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n <li\n role=\"option\"\n [id]=\"list.optionId(i)\"\n [class.active]=\"i === list.activeIndex()\"\n [attr.aria-selected]=\"i === list.activeIndex()\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"select(suggestion)\"\n >\n {{ suggestion }}\n </li>\n }\n </ul>\n }\n</div>\n", dependencies: [{ kind: "component", type: UniDebounceInputComponent, selector: "uni-debounce-input", inputs: ["inputName", "inputId", "debounceTime", "label", "placeholder", "disabled", "role", "ariaExpanded", "ariaControls", "ariaActivedescendant"], outputs: ["change"] }, { kind: "component", type: UniIconButtonComponent, selector: "button[uni-icon-button], button[icon-button]", inputs: ["ariaLabel", "iconName", "symbolName", "variant", "size", "disable", "loading", "opticalSize"] }, { kind: "component", type: UniSymbolComponent, selector: "uni-symbol", inputs: ["name", "fill", "weight", "grade", "opticalSize"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
5504
5739
|
}
|
|
5505
5740
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniSearchInputComponent, decorators: [{
|
|
5506
5741
|
type: Component,
|
|
5507
|
-
args: [{ selector: 'uni-search-input', imports: [UniDebounceInputComponent, UniIconButtonComponent, UniSymbolComponent], providers: [{ provide: COMPONENT_NAME, useValue: 'searchInput' }], host: { '[class]': 'className()' }, changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Keydown/focusout ride the bubbling events from the inner input; the\n wrapper itself stays out of the tab order. -->\n<!-- eslint-disable-next-line @angular-eslint/template/interactive-supports-focus -->\n<div (keydown)=\"onKeydown($event)\" (focusout)=\"onFocusOut($event)\">\n <uni-debounce-input\n #searchField\n [label]=\"label()\"\n [placeholder]=\"placeholder() ?? label()\"\n [debounceTime]=\"debounceTime()\"\n role=\"combobox\"\n [ariaExpanded]=\"
|
|
5742
|
+
args: [{ selector: 'uni-search-input', imports: [UniDebounceInputComponent, UniIconButtonComponent, UniSymbolComponent], providers: [{ provide: COMPONENT_NAME, useValue: 'searchInput' }], host: { '[class]': 'className()' }, changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Keydown/focusout ride the bubbling events from the inner input; the\n wrapper itself stays out of the tab order. -->\n<!-- eslint-disable-next-line @angular-eslint/template/interactive-supports-focus -->\n<div (keydown)=\"onKeydown($event)\" (focusout)=\"onFocusOut($event)\">\n <uni-debounce-input\n #searchField\n [label]=\"label()\"\n [placeholder]=\"placeholder() ?? label()\"\n [debounceTime]=\"debounceTime()\"\n role=\"combobox\"\n [ariaExpanded]=\"list.open()\"\n [ariaControls]=\"listboxId\"\n [ariaActivedescendant]=\"list.activeDescendantId() ?? undefined\"\n (change)=\"handleChange($event)\"\n >\n <uni-symbol pre-input class=\"uni-search-lead\" name=\"{{ componentOptions().searchSymbol ?? 'search' }}\" aria-hidden=\"true\" />\n @if (hasQuery()) {\n <button\n post-input\n icon-button\n [iconName]=\"componentOptions().clearSymbol ?? 'close'\"\n variant=\"ghost\"\n size=\"sm\"\n (click)=\"clear()\"\n >\n Clear search\n </button>\n }\n </uni-debounce-input>\n\n @if (list.open()) {\n <ul [id]=\"listboxId\" role=\"listbox\" [attr.aria-label]=\"label()\" [class]=\"listClass()\">\n @for (suggestion of visibleSuggestions(); track suggestion; let i = $index) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n <li\n role=\"option\"\n [id]=\"list.optionId(i)\"\n [class.active]=\"i === list.activeIndex()\"\n [attr.aria-selected]=\"i === list.activeIndex()\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"select(suggestion)\"\n >\n {{ suggestion }}\n </li>\n }\n </ul>\n }\n</div>\n" }]
|
|
5508
5743
|
}], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], width: [{ type: i0.Input, args: [{ isSignal: true, alias: "width", required: false }] }], debounceTime: [{ type: i0.Input, args: [{ isSignal: true, alias: "debounceTime", required: false }] }], suggestions: [{ type: i0.Input, args: [{ isSignal: true, alias: "suggestions", required: false }] }], change: [{ type: i0.Output, args: ["change"] }], search: [{ type: i0.Output, args: ["search"] }], suggestionSelected: [{ type: i0.Output, args: ["suggestionSelected"] }], field: [{ type: i0.ViewChild, args: [i0.forwardRef(() => UniDebounceInputComponent), { isSignal: true }] }] } });
|
|
5509
5744
|
|
|
5510
5745
|
/**
|
|
@@ -5574,11 +5809,11 @@ class UniSelectComponent {
|
|
|
5574
5809
|
pointerEvents: 'none' /* Crucial for clicking through */,
|
|
5575
5810
|
});
|
|
5576
5811
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniSelectComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
5577
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniSelectComponent, isStandalone: true, selector: "uni-select", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, dirty: { classPropertyName: "dirty", publicName: "dirty", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, ariaDescribedBy: { classPropertyName: "ariaDescribedBy", publicName: "ariaDescribedBy", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touched: "touchedChange" }, ngImport: i0, template: "<uni-input-box [error]=\"showError()\" [class]=\"selectClass\">\n <select\n [disabled]=\"disabled()\"\n [value]=\"currentSelectedIndex()\"\n (change)=\"handleSelectChange($event)\"\n (blur)=\"touched.set(true)\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.aria-invalid]=\"showError() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n [attr.aria-describedby]=\"ariaDescribedBy() || null\"\n >\n @if (placeholder()) {\n <option [value]=\"UNSELECTED\" disabled selected>\n {{ placeholder() }}\n </option>\n }\n\n @for (opt of options(); track opt.label; let i = $index) {\n <option [value]=\"i\">\n {{ opt.label }}\n </option>\n }\n </select>\n <uni-symbol name=\"keyboard_arrow_down\" [class]=\"arrowClass\" />\n</uni-input-box>\n", dependencies: [{ kind: "component", type: UniInputBoxComponent, selector: "uni-input-box", inputs: ["disabled", "error", "minWidth", "height"] }, { kind: "component", type: UniSymbolComponent, selector: "uni-symbol", inputs: ["name", "fill", "weight", "grade", "opticalSize"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
5812
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniSelectComponent, isStandalone: true, selector: "uni-select", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, dirty: { classPropertyName: "dirty", publicName: "dirty", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, ariaDescribedBy: { classPropertyName: "ariaDescribedBy", publicName: "ariaDescribedBy", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touched: "touchedChange" }, ngImport: i0, template: "<uni-input-box [error]=\"showError()\" [class]=\"selectClass\">\n <select\n [disabled]=\"disabled()\"\n [value]=\"currentSelectedIndex()\"\n (change)=\"handleSelectChange($event)\"\n (blur)=\"touched.set(true)\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.aria-invalid]=\"showError() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n [attr.aria-describedby]=\"ariaDescribedBy() || null\"\n >\n @if (placeholder()) {\n <option [value]=\"UNSELECTED\" disabled selected>\n {{ placeholder() }}\n </option>\n }\n\n @for (opt of options(); track opt.label; let i = $index) {\n <!-- [selected] evaluates during the option's own render, so a selection\n pointing at an option added in the same change-detection pass still\n applies (the select-level [value] write lands before new options\n exist and is ignored by the browser). -->\n <option [value]=\"i\" [selected]=\"currentSelectedIndex() === i.toString()\">\n {{ opt.label }}\n </option>\n }\n </select>\n <uni-symbol name=\"keyboard_arrow_down\" [class]=\"arrowClass\" />\n</uni-input-box>\n", dependencies: [{ kind: "component", type: UniInputBoxComponent, selector: "uni-input-box", inputs: ["disabled", "error", "minWidth", "height"] }, { kind: "component", type: UniSymbolComponent, selector: "uni-symbol", inputs: ["name", "fill", "weight", "grade", "opticalSize"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
5578
5813
|
}
|
|
5579
5814
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniSelectComponent, decorators: [{
|
|
5580
5815
|
type: Component,
|
|
5581
|
-
args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'uni-select', imports: [UniInputBoxComponent, UniSymbolComponent], template: "<uni-input-box [error]=\"showError()\" [class]=\"selectClass\">\n <select\n [disabled]=\"disabled()\"\n [value]=\"currentSelectedIndex()\"\n (change)=\"handleSelectChange($event)\"\n (blur)=\"touched.set(true)\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.aria-invalid]=\"showError() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n [attr.aria-describedby]=\"ariaDescribedBy() || null\"\n >\n @if (placeholder()) {\n <option [value]=\"UNSELECTED\" disabled selected>\n {{ placeholder() }}\n </option>\n }\n\n @for (opt of options(); track opt.label; let i = $index) {\n <option [value]=\"i\">\n {{ opt.label }}\n </option>\n }\n </select>\n <uni-symbol name=\"keyboard_arrow_down\" [class]=\"arrowClass\" />\n</uni-input-box>\n" }]
|
|
5816
|
+
args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'uni-select', imports: [UniInputBoxComponent, UniSymbolComponent], template: "<uni-input-box [error]=\"showError()\" [class]=\"selectClass\">\n <select\n [disabled]=\"disabled()\"\n [value]=\"currentSelectedIndex()\"\n (change)=\"handleSelectChange($event)\"\n (blur)=\"touched.set(true)\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.aria-invalid]=\"showError() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n [attr.aria-describedby]=\"ariaDescribedBy() || null\"\n >\n @if (placeholder()) {\n <option [value]=\"UNSELECTED\" disabled selected>\n {{ placeholder() }}\n </option>\n }\n\n @for (opt of options(); track opt.label; let i = $index) {\n <!-- [selected] evaluates during the option's own render, so a selection\n pointing at an option added in the same change-detection pass still\n applies (the select-level [value] write lands before new options\n exist and is ignored by the browser). -->\n <option [value]=\"i\" [selected]=\"currentSelectedIndex() === i.toString()\">\n {{ opt.label }}\n </option>\n }\n </select>\n <uni-symbol name=\"keyboard_arrow_down\" [class]=\"arrowClass\" />\n</uni-input-box>\n" }]
|
|
5582
5817
|
}], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }, { type: i0.Output, args: ["touchedChange"] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], dirty: [{ type: i0.Input, args: [{ isSignal: true, alias: "dirty", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], ariaDescribedBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaDescribedBy", required: false }] }], options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }] } });
|
|
5583
5818
|
|
|
5584
5819
|
/**
|
|
@@ -6235,24 +6470,180 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
|
|
|
6235
6470
|
* This file exports all public-facing elements of the sort-header component.
|
|
6236
6471
|
*/
|
|
6237
6472
|
|
|
6238
|
-
|
|
6473
|
+
/**
|
|
6474
|
+
* Compact chip for categories, states, filters and tokens.
|
|
6475
|
+
*
|
|
6476
|
+
* Two orthogonal style axes: `variant` picks the colour role and `tone` picks
|
|
6477
|
+
* the archetype (soft / solid / outline). Both live in the theme's `tag` entry,
|
|
6478
|
+
* so a theme restyles every chip in the app without touching markup.
|
|
6479
|
+
*
|
|
6480
|
+
* Structurally a chip is **body + trailing action as siblings**, never nested
|
|
6481
|
+
* buttons: an interactive chip whose body is a `<button>` cannot contain the
|
|
6482
|
+
* remove `<button>` (invalid HTML, and the inner control becomes unreachable
|
|
6483
|
+
* for keyboard users). Both stay independently operable.
|
|
6484
|
+
*/
|
|
6485
|
+
class UniTagComponent extends BaseComponent {
|
|
6486
|
+
// Presentation. `variant` comes from BaseComponent; chips default to `md`
|
|
6487
|
+
// rather than the library-wide `lg`, since they sit inside dense content.
|
|
6488
|
+
size = input('md', ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
|
|
6489
|
+
tone = input('soft', ...(ngDevMode ? [{ debugName: "tone" }] : /* istanbul ignore next */ []));
|
|
6239
6490
|
label = input(...(ngDevMode ? [undefined, { debugName: "label" }] : /* istanbul ignore next */ []));
|
|
6240
6491
|
value = input(...(ngDevMode ? [undefined, { debugName: "value" }] : /* istanbul ignore next */ []));
|
|
6241
|
-
|
|
6242
|
-
|
|
6243
|
-
|
|
6244
|
-
|
|
6245
|
-
|
|
6246
|
-
|
|
6247
|
-
|
|
6248
|
-
}
|
|
6249
|
-
|
|
6250
|
-
|
|
6492
|
+
/** Truncation budget, e.g. `'14ch'`. A number is treated as px. */
|
|
6493
|
+
maxWidth = input(...(ngDevMode ? [undefined, { debugName: "maxWidth" }] : /* istanbul ignore next */ []));
|
|
6494
|
+
// Lead convenience inputs. Anything richer goes in the `[tag-lead]` slot.
|
|
6495
|
+
avatarSrc = input(...(ngDevMode ? [undefined, { debugName: "avatarSrc" }] : /* istanbul ignore next */ []));
|
|
6496
|
+
/** Initials fallback when `avatarSrc` is absent or fails to load. */
|
|
6497
|
+
avatarName = input(...(ngDevMode ? [undefined, { debugName: "avatarName" }] : /* istanbul ignore next */ []));
|
|
6498
|
+
/** Theme icon primitive — the preferred glyph path. */
|
|
6499
|
+
iconName = input(...(ngDevMode ? [undefined, { debugName: "iconName" }] : /* istanbul ignore next */ []));
|
|
6500
|
+
/** Material Symbols ligature, for glyphs the theme's icon set doesn't carry. */
|
|
6501
|
+
symbolName = input(...(ngDevMode ? [undefined, { debugName: "symbolName" }] : /* istanbul ignore next */ []));
|
|
6502
|
+
/** Status dot in the current colour. */
|
|
6503
|
+
dot = input(false, ...(ngDevMode ? [{ debugName: "dot" }] : /* istanbul ignore next */ []));
|
|
6504
|
+
// Behaviour
|
|
6505
|
+
removable = input(false, ...(ngDevMode ? [{ debugName: "removable" }] : /* istanbul ignore next */ []));
|
|
6506
|
+
interactive = input(false, ...(ngDevMode ? [{ debugName: "interactive" }] : /* istanbul ignore next */ []));
|
|
6507
|
+
/**
|
|
6508
|
+
* Toggle state. Left undefined the chip carries no `aria-pressed` at all —
|
|
6509
|
+
* an interactive chip is not always a toggle (inside a tag input it is
|
|
6510
|
+
* focusable so it can be removed), and announcing "not pressed" on a
|
|
6511
|
+
* recipient chip is worse than announcing nothing.
|
|
6512
|
+
*/
|
|
6513
|
+
selected = input(undefined, ...(ngDevMode ? [{ debugName: "selected" }] : /* istanbul ignore next */ []));
|
|
6514
|
+
invalid = input(false, ...(ngDevMode ? [{ debugName: "invalid" }] : /* istanbul ignore next */ []));
|
|
6515
|
+
disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
|
|
6516
|
+
/** Accessible-name override for the remove button. */
|
|
6517
|
+
removeLabel = input(...(ngDevMode ? [undefined, { debugName: "removeLabel" }] : /* istanbul ignore next */ []));
|
|
6518
|
+
/**
|
|
6519
|
+
* Tab position of the chip's controls. A composite that owns its own
|
|
6520
|
+
* roving focus — `uni-tag-input`, where the whole field is one tab stop —
|
|
6521
|
+
* passes `-1` so Tab does not walk through every chip to reach the next
|
|
6522
|
+
* control.
|
|
6523
|
+
*/
|
|
6524
|
+
controlTabIndex = input(0, ...(ngDevMode ? [{ debugName: "controlTabIndex" }] : /* istanbul ignore next */ []));
|
|
6525
|
+
removed = output();
|
|
6526
|
+
activated = output();
|
|
6527
|
+
/** A disabled chip wears the theme's `disabled` role, whatever its variant. */
|
|
6528
|
+
resolvedVariant = computed(() => this.disabled() ? 'disabled' : this.variant(), ...(ngDevMode ? [{ debugName: "resolvedVariant" }] : /* istanbul ignore next */ []));
|
|
6529
|
+
themeStyle = computed(() => this.theme.componentStyle('tag', this.resolvedVariant(), this.size())(), ...(ngDevMode ? [{ debugName: "themeStyle" }] : /* istanbul ignore next */ []));
|
|
6530
|
+
/** Lead elements derive from the chip height, so no second size token. */
|
|
6531
|
+
leadSize = computed(() => {
|
|
6532
|
+
const height = Number(this.themeStyle()['height'] ?? 24);
|
|
6533
|
+
return Number.isFinite(height) ? Math.max(height - 6, 0) : 18;
|
|
6534
|
+
}, ...(ngDevMode ? [{ debugName: "leadSize" }] : /* istanbul ignore next */ []));
|
|
6535
|
+
/** Remove glyph, proportional to the chip rather than to the icon-button. */
|
|
6536
|
+
removeGlyphSize = computed(() => Math.max(Math.round(this.leadSize() * 0.8), 10), ...(ngDevMode ? [{ debugName: "removeGlyphSize" }] : /* istanbul ignore next */ []));
|
|
6537
|
+
initials = computed(() => (this.avatarName() ?? '')
|
|
6538
|
+
.split(/\s+/)
|
|
6539
|
+
.filter(Boolean)
|
|
6540
|
+
.slice(0, 2)
|
|
6541
|
+
.map((part) => part[0]?.toUpperCase() ?? '')
|
|
6542
|
+
.join(''), ...(ngDevMode ? [{ debugName: "initials" }] : /* istanbul ignore next */ []));
|
|
6543
|
+
hostClass = computed(() => {
|
|
6544
|
+
const options = this.componentOptions();
|
|
6545
|
+
return `${css([
|
|
6546
|
+
this.theme.radius(options.borderRadius),
|
|
6547
|
+
{ ...this.theme.typeface(options.typeface) },
|
|
6548
|
+
// Theme `fixed` + variant (incl. its nested `&.tone-*` rules) + size.
|
|
6549
|
+
{ ...this.themeStyle() },
|
|
6550
|
+
{ ...this.theme.gap(options.gap) },
|
|
6551
|
+
{
|
|
6552
|
+
boxSizing: 'border-box',
|
|
6553
|
+
maxWidth: this.maxWidth(),
|
|
6554
|
+
// The chip is not a widget; only its sub-controls take focus.
|
|
6555
|
+
'& > button': this.theme.focusRing(),
|
|
6556
|
+
// The remove control is the trailing counterpart of the lead, so it
|
|
6557
|
+
// sizes from the chip too. Left at the icon-button's own `sm` size it
|
|
6558
|
+
// is 22px inside a 24px chip — and taller than an `sm` chip entirely.
|
|
6559
|
+
'& > button[uni-icon-button]': {
|
|
6560
|
+
flex: 'none',
|
|
6561
|
+
width: this.leadSize(),
|
|
6562
|
+
minWidth: this.leadSize(),
|
|
6563
|
+
height: this.leadSize(),
|
|
6564
|
+
minHeight: this.leadSize(),
|
|
6565
|
+
padding: 0,
|
|
6566
|
+
fontSize: this.removeGlyphSize(),
|
|
6567
|
+
// uni-icon writes width/height as *inline* styles, sized from the
|
|
6568
|
+
// icon-button's own `sm` token (18px — wider than an `sm` chip).
|
|
6569
|
+
// Only !important can reach past an inline style to keep the glyph
|
|
6570
|
+
// proportional to the chip.
|
|
6571
|
+
'& uni-icon': {
|
|
6572
|
+
width: `${this.removeGlyphSize()}px !important`,
|
|
6573
|
+
height: `${this.removeGlyphSize()}px !important`,
|
|
6574
|
+
},
|
|
6575
|
+
},
|
|
6576
|
+
},
|
|
6577
|
+
this.invalid() && {
|
|
6578
|
+
// Colour alone cannot carry "this entry is malformed" (WCAG 1.4.1).
|
|
6579
|
+
textDecoration: 'underline dashed',
|
|
6580
|
+
textUnderlineOffset: 3,
|
|
6581
|
+
},
|
|
6582
|
+
this.disabled() && { pointerEvents: 'none' },
|
|
6583
|
+
])} tone-${this.tone()}`;
|
|
6584
|
+
}, ...(ngDevMode ? [{ debugName: "hostClass" }] : /* istanbul ignore next */ []));
|
|
6585
|
+
/** Truncating label; `title` exposes the full text when a budget is set. */
|
|
6586
|
+
labelClass = css({
|
|
6587
|
+
overflow: 'hidden',
|
|
6588
|
+
textOverflow: 'ellipsis',
|
|
6589
|
+
whiteSpace: 'nowrap',
|
|
6590
|
+
});
|
|
6591
|
+
bodyClass = computed(() => css([
|
|
6592
|
+
{
|
|
6593
|
+
display: 'inline-flex',
|
|
6594
|
+
alignItems: 'center',
|
|
6595
|
+
minWidth: 0,
|
|
6596
|
+
// The body inherits the chip's own colours in every case.
|
|
6597
|
+
font: 'inherit',
|
|
6598
|
+
color: 'inherit',
|
|
6599
|
+
background: 'none',
|
|
6600
|
+
border: 0,
|
|
6601
|
+
padding: 0,
|
|
6602
|
+
...this.theme.gap(this.componentOptions().gap),
|
|
6603
|
+
},
|
|
6604
|
+
this.interactive() && { cursor: 'pointer' },
|
|
6605
|
+
]), ...(ngDevMode ? [{ debugName: "bodyClass" }] : /* istanbul ignore next */ []));
|
|
6606
|
+
leadClass = computed(() => css({
|
|
6607
|
+
flex: 'none',
|
|
6608
|
+
display: 'inline-flex',
|
|
6609
|
+
alignItems: 'center',
|
|
6610
|
+
justifyContent: 'center',
|
|
6611
|
+
overflow: 'hidden',
|
|
6612
|
+
width: this.leadSize(),
|
|
6613
|
+
height: this.leadSize(),
|
|
6614
|
+
borderRadius: '50%',
|
|
6615
|
+
fontSize: Math.max(Math.round(this.leadSize() * 0.5), 8),
|
|
6616
|
+
// Initials sit on a wash of the current ink so they read on any tone.
|
|
6617
|
+
backgroundColor: this.avatarSrc() ? undefined : 'rgba(0, 0, 0, 0.12)',
|
|
6618
|
+
'& img': { width: '100%', height: '100%', objectFit: 'cover' },
|
|
6619
|
+
}), ...(ngDevMode ? [{ debugName: "leadClass" }] : /* istanbul ignore next */ []));
|
|
6620
|
+
dotClass = computed(() => css({
|
|
6621
|
+
flex: 'none',
|
|
6622
|
+
width: Math.max(Math.round(this.leadSize() / 3), 6),
|
|
6623
|
+
height: Math.max(Math.round(this.leadSize() / 3), 6),
|
|
6624
|
+
borderRadius: '50%',
|
|
6625
|
+
backgroundColor: 'currentColor',
|
|
6626
|
+
}), ...(ngDevMode ? [{ debugName: "dotClass" }] : /* istanbul ignore next */ []));
|
|
6627
|
+
remove() {
|
|
6628
|
+
if (this.disabled())
|
|
6629
|
+
return;
|
|
6630
|
+
this.removed.emit(this.value());
|
|
6631
|
+
}
|
|
6632
|
+
activate() {
|
|
6633
|
+
if (this.disabled())
|
|
6634
|
+
return;
|
|
6635
|
+
this.activated.emit(this.value());
|
|
6636
|
+
}
|
|
6637
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniTagComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
|
|
6638
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniTagComponent, isStandalone: true, selector: "uni-tag", inputs: { size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, tone: { classPropertyName: "tone", publicName: "tone", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, maxWidth: { classPropertyName: "maxWidth", publicName: "maxWidth", isSignal: true, isRequired: false, transformFunction: null }, avatarSrc: { classPropertyName: "avatarSrc", publicName: "avatarSrc", isSignal: true, isRequired: false, transformFunction: null }, avatarName: { classPropertyName: "avatarName", publicName: "avatarName", isSignal: true, isRequired: false, transformFunction: null }, iconName: { classPropertyName: "iconName", publicName: "iconName", isSignal: true, isRequired: false, transformFunction: null }, symbolName: { classPropertyName: "symbolName", publicName: "symbolName", isSignal: true, isRequired: false, transformFunction: null }, dot: { classPropertyName: "dot", publicName: "dot", isSignal: true, isRequired: false, transformFunction: null }, removable: { classPropertyName: "removable", publicName: "removable", isSignal: true, isRequired: false, transformFunction: null }, interactive: { classPropertyName: "interactive", publicName: "interactive", isSignal: true, isRequired: false, transformFunction: null }, selected: { classPropertyName: "selected", publicName: "selected", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, removeLabel: { classPropertyName: "removeLabel", publicName: "removeLabel", isSignal: true, isRequired: false, transformFunction: null }, controlTabIndex: { classPropertyName: "controlTabIndex", publicName: "controlTabIndex", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { removed: "removed", activated: "activated" }, host: { properties: { "class": "hostClass()", "attr.aria-invalid": "invalid() ? 'true' : null" } }, providers: [{ provide: COMPONENT_NAME, useValue: 'tag' }], usesInheritance: true, ngImport: i0, template: "<!-- Lead + label. Shared so the static and interactive bodies stay identical. -->\n<ng-template #body>\n <ng-content select=\"[tag-lead]\"></ng-content>\n\n @if (selected() && componentOptions().selectedIcon; as selectedIcon) {\n <uni-icon [name]=\"selectedIcon\" [size]=\"leadSize()\" />\n } @else if (avatarSrc(); as src) {\n <span [class]=\"leadClass()\" aria-hidden=\"true\">\n <img [src]=\"src\" alt=\"\" />\n </span>\n } @else if (initials(); as text) {\n <span [class]=\"leadClass()\" aria-hidden=\"true\">{{ text }}</span>\n } @else if (iconName(); as icon) {\n <uni-icon [name]=\"icon\" [size]=\"leadSize()\" />\n } @else if (symbolName(); as symbol) {\n <uni-symbol [name]=\"symbol\" [style.font-size.px]=\"leadSize()\" />\n } @else if (dot()) {\n <span [class]=\"dotClass()\" aria-hidden=\"true\"></span>\n }\n\n @if (label(); as text) {\n <span [class]=\"labelClass\" [attr.title]=\"maxWidth() ? text : null\">{{ text }}</span>\n } @else {\n <span [class]=\"labelClass\"><ng-content></ng-content></span>\n }\n</ng-template>\n\n<!-- An interactive chip's body is the button; a static chip is plain text with\n no role and no tab stop, so screen readers read it as content. -->\n@if (interactive()) {\n <button\n type=\"button\"\n [class]=\"bodyClass()\"\n [disabled]=\"disabled()\"\n [attr.aria-pressed]=\"selected() ?? null\"\n [attr.tabindex]=\"controlTabIndex()\"\n (click)=\"activate()\"\n >\n <ng-container [ngTemplateOutlet]=\"body\"></ng-container>\n </button>\n} @else {\n <span [class]=\"bodyClass()\">\n <ng-container [ngTemplateOutlet]=\"body\"></ng-container>\n </span>\n}\n\n@if (removable()) {\n <button\n uni-icon-button\n variant=\"ghost\"\n size=\"sm\"\n [iconName]=\"componentOptions().removeIcon ?? 'close'\"\n [disable]=\"disabled()\"\n [attr.tabindex]=\"controlTabIndex()\"\n (click)=\"remove()\"\n >\n {{ removeLabel() ?? 'Remove ' + (label() ?? '') }}\n </button>\n}\n", dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: UniIconComponent, selector: "uni-icon", inputs: ["color", "name", "size"] }, { kind: "component", type: UniIconButtonComponent, selector: "button[uni-icon-button], button[icon-button]", inputs: ["ariaLabel", "iconName", "symbolName", "variant", "size", "disable", "loading", "opticalSize"] }, { kind: "component", type: UniSymbolComponent, selector: "uni-symbol", inputs: ["name", "fill", "weight", "grade", "opticalSize"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
6251
6639
|
}
|
|
6252
6640
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniTagComponent, decorators: [{
|
|
6253
6641
|
type: Component,
|
|
6254
|
-
args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'uni-tag', imports: [
|
|
6255
|
-
|
|
6642
|
+
args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'uni-tag', imports: [NgTemplateOutlet, UniIconComponent, UniIconButtonComponent, UniSymbolComponent], providers: [{ provide: COMPONENT_NAME, useValue: 'tag' }], host: {
|
|
6643
|
+
'[class]': 'hostClass()',
|
|
6644
|
+
'[attr.aria-invalid]': "invalid() ? 'true' : null",
|
|
6645
|
+
}, template: "<!-- Lead + label. Shared so the static and interactive bodies stay identical. -->\n<ng-template #body>\n <ng-content select=\"[tag-lead]\"></ng-content>\n\n @if (selected() && componentOptions().selectedIcon; as selectedIcon) {\n <uni-icon [name]=\"selectedIcon\" [size]=\"leadSize()\" />\n } @else if (avatarSrc(); as src) {\n <span [class]=\"leadClass()\" aria-hidden=\"true\">\n <img [src]=\"src\" alt=\"\" />\n </span>\n } @else if (initials(); as text) {\n <span [class]=\"leadClass()\" aria-hidden=\"true\">{{ text }}</span>\n } @else if (iconName(); as icon) {\n <uni-icon [name]=\"icon\" [size]=\"leadSize()\" />\n } @else if (symbolName(); as symbol) {\n <uni-symbol [name]=\"symbol\" [style.font-size.px]=\"leadSize()\" />\n } @else if (dot()) {\n <span [class]=\"dotClass()\" aria-hidden=\"true\"></span>\n }\n\n @if (label(); as text) {\n <span [class]=\"labelClass\" [attr.title]=\"maxWidth() ? text : null\">{{ text }}</span>\n } @else {\n <span [class]=\"labelClass\"><ng-content></ng-content></span>\n }\n</ng-template>\n\n<!-- An interactive chip's body is the button; a static chip is plain text with\n no role and no tab stop, so screen readers read it as content. -->\n@if (interactive()) {\n <button\n type=\"button\"\n [class]=\"bodyClass()\"\n [disabled]=\"disabled()\"\n [attr.aria-pressed]=\"selected() ?? null\"\n [attr.tabindex]=\"controlTabIndex()\"\n (click)=\"activate()\"\n >\n <ng-container [ngTemplateOutlet]=\"body\"></ng-container>\n </button>\n} @else {\n <span [class]=\"bodyClass()\">\n <ng-container [ngTemplateOutlet]=\"body\"></ng-container>\n </span>\n}\n\n@if (removable()) {\n <button\n uni-icon-button\n variant=\"ghost\"\n size=\"sm\"\n [iconName]=\"componentOptions().removeIcon ?? 'close'\"\n [disable]=\"disabled()\"\n [attr.tabindex]=\"controlTabIndex()\"\n (click)=\"remove()\"\n >\n {{ removeLabel() ?? 'Remove ' + (label() ?? '') }}\n </button>\n}\n" }]
|
|
6646
|
+
}], propDecorators: { size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], tone: [{ type: i0.Input, args: [{ isSignal: true, alias: "tone", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], maxWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxWidth", required: false }] }], avatarSrc: [{ type: i0.Input, args: [{ isSignal: true, alias: "avatarSrc", required: false }] }], avatarName: [{ type: i0.Input, args: [{ isSignal: true, alias: "avatarName", required: false }] }], iconName: [{ type: i0.Input, args: [{ isSignal: true, alias: "iconName", required: false }] }], symbolName: [{ type: i0.Input, args: [{ isSignal: true, alias: "symbolName", required: false }] }], dot: [{ type: i0.Input, args: [{ isSignal: true, alias: "dot", required: false }] }], removable: [{ type: i0.Input, args: [{ isSignal: true, alias: "removable", required: false }] }], interactive: [{ type: i0.Input, args: [{ isSignal: true, alias: "interactive", required: false }] }], selected: [{ type: i0.Input, args: [{ isSignal: true, alias: "selected", required: false }] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], removeLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "removeLabel", required: false }] }], controlTabIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "controlTabIndex", required: false }] }], removed: [{ type: i0.Output, args: ["removed"] }], activated: [{ type: i0.Output, args: ["activated"] }] } });
|
|
6256
6647
|
|
|
6257
6648
|
/**
|
|
6258
6649
|
* UniTagComponent Barrel File
|
|
@@ -6260,6 +6651,392 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
|
|
|
6260
6651
|
* This file exports all public-facing elements of the tag component.
|
|
6261
6652
|
*/
|
|
6262
6653
|
|
|
6654
|
+
/** Loose address check — deliberately permissive, matching what mail clients accept. */
|
|
6655
|
+
const EMAIL = /^[^\s@,;]+@[^\s@,;.]+\.[^\s@,;]+$/;
|
|
6656
|
+
/** `Name <a@b.com>` / `"Name" <a@b.com>` → the address, plus the display name. */
|
|
6657
|
+
const unwrapAddress = (raw) => {
|
|
6658
|
+
const match = raw.match(/^\s*"?([^"<]*?)"?\s*<([^>]+)>\s*$/);
|
|
6659
|
+
if (!match)
|
|
6660
|
+
return { value: raw.trim() };
|
|
6661
|
+
const label = match[1].trim();
|
|
6662
|
+
return { value: match[2].trim(), ...(label ? { label } : {}) };
|
|
6663
|
+
};
|
|
6664
|
+
class UniTagInputComponent extends BaseComponent {
|
|
6665
|
+
// --- Signal Forms block (explicit per AGENTS.md, not a base class) --------
|
|
6666
|
+
value = model([], ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
|
|
6667
|
+
disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
|
|
6668
|
+
touched = model(false, ...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
|
|
6669
|
+
invalid = input(false, ...(ngDevMode ? [{ debugName: "invalid" }] : /* istanbul ignore next */ []));
|
|
6670
|
+
dirty = input(false, ...(ngDevMode ? [{ debugName: "dirty" }] : /* istanbul ignore next */ []));
|
|
6671
|
+
required = input(false, ...(ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []));
|
|
6672
|
+
ariaDescribedBy = input(...(ngDevMode ? [undefined, { debugName: "ariaDescribedBy" }] : /* istanbul ignore next */ []));
|
|
6673
|
+
// --- Configuration -------------------------------------------------------
|
|
6674
|
+
/** Accessible name for the field, e.g. "To". */
|
|
6675
|
+
label = input.required(...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
|
|
6676
|
+
placeholder = input(...(ngDevMode ? [undefined, { debugName: "placeholder" }] : /* istanbul ignore next */ []));
|
|
6677
|
+
/** `email` wires an address validator, paste parser and space separator. */
|
|
6678
|
+
preset = input('text', ...(ngDevMode ? [{ debugName: "preset" }] : /* istanbul ignore next */ []));
|
|
6679
|
+
separators = input([',', ';'], ...(ngDevMode ? [{ debugName: "separators" }] : /* istanbul ignore next */ []));
|
|
6680
|
+
commitOnBlur = input(true, ...(ngDevMode ? [{ debugName: "commitOnBlur" }] : /* istanbul ignore next */ []));
|
|
6681
|
+
allowDuplicates = input(false, ...(ngDevMode ? [{ debugName: "allowDuplicates" }] : /* istanbul ignore next */ []));
|
|
6682
|
+
max = input(...(ngDevMode ? [undefined, { debugName: "max" }] : /* istanbul ignore next */ []));
|
|
6683
|
+
validate = input(...(ngDevMode ? [undefined, { debugName: "validate" }] : /* istanbul ignore next */ []));
|
|
6684
|
+
parse = input(...(ngDevMode ? [undefined, { debugName: "parse" }] : /* istanbul ignore next */ []));
|
|
6685
|
+
// Chip presentation, forwarded to uni-tag.
|
|
6686
|
+
tagVariant = input('primary', ...(ngDevMode ? [{ debugName: "tagVariant" }] : /* istanbul ignore next */ []));
|
|
6687
|
+
tagTone = input('soft', ...(ngDevMode ? [{ debugName: "tagTone" }] : /* istanbul ignore next */ []));
|
|
6688
|
+
tagSize = input(...(ngDevMode ? [undefined, { debugName: "tagSize" }] : /* istanbul ignore next */ []));
|
|
6689
|
+
// --- Autocomplete (same contract as uni-search-input: the app filters) ----
|
|
6690
|
+
suggestions = input([], ...(ngDevMode ? [{ debugName: "suggestions" }] : /* istanbul ignore next */ []));
|
|
6691
|
+
/** Debounced text the app should filter `suggestions` from. */
|
|
6692
|
+
query = output();
|
|
6693
|
+
debounceTime = input(250, ...(ngDevMode ? [{ debugName: "debounceTime" }] : /* istanbul ignore next */ []));
|
|
6694
|
+
// --- Events --------------------------------------------------------------
|
|
6695
|
+
added = output();
|
|
6696
|
+
removed = output();
|
|
6697
|
+
rejected = output();
|
|
6698
|
+
inputRef = viewChild.required('field');
|
|
6699
|
+
chipRefs = viewChildren('chip', ...(ngDevMode ? [{ debugName: "chipRefs" }] : /* istanbul ignore next */ []));
|
|
6700
|
+
/** Uncommitted text in the field. */
|
|
6701
|
+
draft = signal('', ...(ngDevMode ? [{ debugName: "draft" }] : /* istanbul ignore next */ []));
|
|
6702
|
+
/** Index of the focused chip, or -1 when focus is in the text input. */
|
|
6703
|
+
focusedChip = signal(-1, ...(ngDevMode ? [{ debugName: "focusedChip" }] : /* istanbul ignore next */ []));
|
|
6704
|
+
/** Announcement for the status region; add/remove are otherwise silent. */
|
|
6705
|
+
announcement = signal('', ...(ngDevMode ? [{ debugName: "announcement" }] : /* istanbul ignore next */ []));
|
|
6706
|
+
hintId = uniqueId('uni-tag-input-hint');
|
|
6707
|
+
srOnly = css(visuallyHidden);
|
|
6708
|
+
queryTimer;
|
|
6709
|
+
visibleSuggestions = computed(() => {
|
|
6710
|
+
const taken = new Set(this.value().map((item) => item.value));
|
|
6711
|
+
return this.suggestions()
|
|
6712
|
+
.filter((suggestion) => this.allowDuplicates() || !taken.has(suggestion.value))
|
|
6713
|
+
.slice(0, this.componentOptions().maxSuggestions ?? 8);
|
|
6714
|
+
}, ...(ngDevMode ? [{ debugName: "visibleSuggestions" }] : /* istanbul ignore next */ []));
|
|
6715
|
+
/** Shared combobox bookkeeping — identical contract to uni-search-input. */
|
|
6716
|
+
list = createListboxNavigation({
|
|
6717
|
+
count: () => this.visibleSuggestions().length,
|
|
6718
|
+
idPrefix: 'uni-tag-listbox',
|
|
6719
|
+
});
|
|
6720
|
+
showError = computed(() => this.invalid() && (this.touched() || this.dirty()), ...(ngDevMode ? [{ debugName: "showError" }] : /* istanbul ignore next */ []));
|
|
6721
|
+
separatorKeys = computed(() => this.preset() === 'email' ? [...this.separators(), ' '] : this.separators(), ...(ngDevMode ? [{ debugName: "separatorKeys" }] : /* istanbul ignore next */ []));
|
|
6722
|
+
describedBy = computed(() => [this.ariaDescribedBy(), this.hintId].filter(Boolean).join(' '), ...(ngDevMode ? [{ debugName: "describedBy" }] : /* istanbul ignore next */ []));
|
|
6723
|
+
labelOf(item) {
|
|
6724
|
+
return item.label ?? item.value;
|
|
6725
|
+
}
|
|
6726
|
+
// --- Committing ----------------------------------------------------------
|
|
6727
|
+
/** Split pasted text into candidate tokens. */
|
|
6728
|
+
parseRaw(raw) {
|
|
6729
|
+
const custom = this.parse();
|
|
6730
|
+
if (custom)
|
|
6731
|
+
return custom(raw);
|
|
6732
|
+
const pattern = this.preset() === 'email' ? /[,;\n\t]+/ : new RegExp(`[${this.separators().map((s) => `\\${s}`).join('')}\n\t]+`);
|
|
6733
|
+
return raw.split(pattern);
|
|
6734
|
+
}
|
|
6735
|
+
isValid(candidate) {
|
|
6736
|
+
const custom = this.validate();
|
|
6737
|
+
if (custom)
|
|
6738
|
+
return custom(candidate);
|
|
6739
|
+
return this.preset() === 'email' ? EMAIL.test(candidate) : true;
|
|
6740
|
+
}
|
|
6741
|
+
/**
|
|
6742
|
+
* Turn typed text into a chip. Invalid entries are kept and flagged rather
|
|
6743
|
+
* than dropped; duplicates and over-max are refused with a reason.
|
|
6744
|
+
*/
|
|
6745
|
+
commit(raw) {
|
|
6746
|
+
const trimmed = raw.trim();
|
|
6747
|
+
if (!trimmed)
|
|
6748
|
+
return false;
|
|
6749
|
+
const { value: candidate, label } = this.preset() === 'email' ? unwrapAddress(trimmed) : { value: trimmed, label: undefined };
|
|
6750
|
+
if (!candidate)
|
|
6751
|
+
return false;
|
|
6752
|
+
const current = this.value();
|
|
6753
|
+
if (!this.allowDuplicates() && current.some((item) => item.value === candidate)) {
|
|
6754
|
+
this.reject(candidate, 'duplicate');
|
|
6755
|
+
return false;
|
|
6756
|
+
}
|
|
6757
|
+
const max = this.max();
|
|
6758
|
+
if (max !== undefined && current.length >= max) {
|
|
6759
|
+
this.reject(candidate, 'max');
|
|
6760
|
+
return false;
|
|
6761
|
+
}
|
|
6762
|
+
const match = this.suggestions().find((suggestion) => suggestion.value === candidate);
|
|
6763
|
+
const item = {
|
|
6764
|
+
value: candidate,
|
|
6765
|
+
...(label || match?.label ? { label: label ?? match?.label } : {}),
|
|
6766
|
+
...(match?.avatarSrc ? { avatarSrc: match.avatarSrc } : {}),
|
|
6767
|
+
...(this.isValid(candidate) ? {} : { invalid: true }),
|
|
6768
|
+
};
|
|
6769
|
+
this.value.update((items) => [...items, item]);
|
|
6770
|
+
this.added.emit(item);
|
|
6771
|
+
this.announce(`${this.labelOf(item)} added. ${this.value().length} ${this.countNoun()}.`);
|
|
6772
|
+
return true;
|
|
6773
|
+
}
|
|
6774
|
+
reject(raw, reason) {
|
|
6775
|
+
this.rejected.emit({ raw, reason });
|
|
6776
|
+
// The visual cue is a brief pulse a screen reader cannot see.
|
|
6777
|
+
this.announce(reason === 'duplicate' ? `${raw} is already added.` : `${raw} was not added: limit reached.`);
|
|
6778
|
+
}
|
|
6779
|
+
removeAt(index, focus = 'input') {
|
|
6780
|
+
const item = this.value()[index];
|
|
6781
|
+
if (!item || item.disabled)
|
|
6782
|
+
return;
|
|
6783
|
+
this.value.update((items) => items.filter((_, i) => i !== index));
|
|
6784
|
+
this.removed.emit(item);
|
|
6785
|
+
this.announce(`${this.labelOf(item)} removed. ${this.value().length} ${this.countNoun()}.`);
|
|
6786
|
+
const remaining = this.value().length;
|
|
6787
|
+
if (focus === 'left' && index > 0)
|
|
6788
|
+
this.focusChip(index - 1);
|
|
6789
|
+
else if (focus === 'right' && index < remaining)
|
|
6790
|
+
this.focusChip(index);
|
|
6791
|
+
else
|
|
6792
|
+
this.focusInput();
|
|
6793
|
+
}
|
|
6794
|
+
countNoun() {
|
|
6795
|
+
return this.value().length === 1 ? 'item' : 'items';
|
|
6796
|
+
}
|
|
6797
|
+
announce(message) {
|
|
6798
|
+
// Re-announce identical text by breaking the string equality.
|
|
6799
|
+
this.announcement.set(this.announcement() === message ? `${message} ` : message);
|
|
6800
|
+
}
|
|
6801
|
+
// --- Focus ---------------------------------------------------------------
|
|
6802
|
+
focusInput() {
|
|
6803
|
+
this.focusedChip.set(-1);
|
|
6804
|
+
queueMicrotask(() => this.inputRef().nativeElement.focus());
|
|
6805
|
+
}
|
|
6806
|
+
focusChip(index) {
|
|
6807
|
+
const clamped = Math.max(0, Math.min(index, this.value().length - 1));
|
|
6808
|
+
this.focusedChip.set(clamped);
|
|
6809
|
+
queueMicrotask(() => {
|
|
6810
|
+
const chip = this.chipRefs()[clamped]?.nativeElement;
|
|
6811
|
+
chip?.querySelector('button')?.focus();
|
|
6812
|
+
});
|
|
6813
|
+
}
|
|
6814
|
+
// --- Keyboard: focus in the text input -----------------------------------
|
|
6815
|
+
onInputKeydown(event) {
|
|
6816
|
+
if (this.list.navigate(event))
|
|
6817
|
+
return;
|
|
6818
|
+
const input = this.inputRef().nativeElement;
|
|
6819
|
+
const empty = input.value === '';
|
|
6820
|
+
if (this.separatorKeys().includes(event.key) && !empty) {
|
|
6821
|
+
event.preventDefault();
|
|
6822
|
+
this.commitDraft();
|
|
6823
|
+
return;
|
|
6824
|
+
}
|
|
6825
|
+
switch (event.key) {
|
|
6826
|
+
case 'Enter': {
|
|
6827
|
+
event.preventDefault();
|
|
6828
|
+
const active = this.list.activeIndex();
|
|
6829
|
+
if (this.list.open() && active >= 0)
|
|
6830
|
+
this.selectSuggestion(this.visibleSuggestions()[active]);
|
|
6831
|
+
else
|
|
6832
|
+
this.commitDraft();
|
|
6833
|
+
break;
|
|
6834
|
+
}
|
|
6835
|
+
case 'Tab':
|
|
6836
|
+
// Never trap: commit what is typed, then let focus move on.
|
|
6837
|
+
if (!empty)
|
|
6838
|
+
this.commitDraft();
|
|
6839
|
+
break;
|
|
6840
|
+
case 'Backspace':
|
|
6841
|
+
if (empty && this.value().length) {
|
|
6842
|
+
// Focus the last chip rather than deleting blind — a second
|
|
6843
|
+
// Backspace, now on the chip, removes it.
|
|
6844
|
+
event.preventDefault();
|
|
6845
|
+
this.focusChip(this.value().length - 1);
|
|
6846
|
+
}
|
|
6847
|
+
break;
|
|
6848
|
+
case 'ArrowLeft':
|
|
6849
|
+
if (input.selectionStart === 0 && this.value().length) {
|
|
6850
|
+
event.preventDefault();
|
|
6851
|
+
this.focusChip(this.value().length - 1);
|
|
6852
|
+
}
|
|
6853
|
+
break;
|
|
6854
|
+
case 'Escape':
|
|
6855
|
+
if (this.list.open())
|
|
6856
|
+
this.list.hide();
|
|
6857
|
+
else
|
|
6858
|
+
this.setDraft('');
|
|
6859
|
+
break;
|
|
6860
|
+
}
|
|
6861
|
+
}
|
|
6862
|
+
onInput(value) {
|
|
6863
|
+
this.setDraft(value);
|
|
6864
|
+
this.list.show();
|
|
6865
|
+
this.list.setActive(-1);
|
|
6866
|
+
clearTimeout(this.queryTimer);
|
|
6867
|
+
this.queryTimer = setTimeout(() => this.query.emit(value), this.debounceTime());
|
|
6868
|
+
}
|
|
6869
|
+
onPaste(event) {
|
|
6870
|
+
const text = event.clipboardData?.getData('text') ?? '';
|
|
6871
|
+
if (!text)
|
|
6872
|
+
return;
|
|
6873
|
+
event.preventDefault();
|
|
6874
|
+
const tokens = this.parseRaw(text);
|
|
6875
|
+
// A trailing fragment with no separator after it is still being typed, so
|
|
6876
|
+
// it stays in the field rather than committing as a half-address. With a
|
|
6877
|
+
// custom `parse` the app owns tokenization and every token is complete.
|
|
6878
|
+
const separators = [...this.separatorKeys(), '\n', '\t'].map((key) => key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
|
|
6879
|
+
const endsWithSeparator = new RegExp(`[${separators.join('')}]\\s*$`).test(text);
|
|
6880
|
+
const tail = this.parse() || endsWithSeparator ? '' : (tokens.pop() ?? '');
|
|
6881
|
+
tokens.forEach((token) => this.commit(token));
|
|
6882
|
+
this.setDraft(tail.trim());
|
|
6883
|
+
}
|
|
6884
|
+
onBlur() {
|
|
6885
|
+
this.touched.set(true);
|
|
6886
|
+
if (this.commitOnBlur())
|
|
6887
|
+
this.commitDraft();
|
|
6888
|
+
}
|
|
6889
|
+
onFocusOut(event) {
|
|
6890
|
+
this.list.closeOnFocusOut(event);
|
|
6891
|
+
}
|
|
6892
|
+
// --- Keyboard: focus on a chip -------------------------------------------
|
|
6893
|
+
onChipKeydown(event, index) {
|
|
6894
|
+
switch (event.key) {
|
|
6895
|
+
case 'ArrowLeft':
|
|
6896
|
+
event.preventDefault();
|
|
6897
|
+
if (index > 0)
|
|
6898
|
+
this.focusChip(index - 1);
|
|
6899
|
+
break;
|
|
6900
|
+
case 'ArrowRight':
|
|
6901
|
+
event.preventDefault();
|
|
6902
|
+
if (index < this.value().length - 1)
|
|
6903
|
+
this.focusChip(index + 1);
|
|
6904
|
+
else
|
|
6905
|
+
this.focusInput();
|
|
6906
|
+
break;
|
|
6907
|
+
case 'Home':
|
|
6908
|
+
event.preventDefault();
|
|
6909
|
+
this.focusChip(0);
|
|
6910
|
+
break;
|
|
6911
|
+
case 'End':
|
|
6912
|
+
event.preventDefault();
|
|
6913
|
+
this.focusChip(this.value().length - 1);
|
|
6914
|
+
break;
|
|
6915
|
+
case 'Backspace':
|
|
6916
|
+
// Two deletion keys with different focus outcomes: hold Backspace to
|
|
6917
|
+
// eat backwards, Delete to eat forwards, without the cursor jumping.
|
|
6918
|
+
event.preventDefault();
|
|
6919
|
+
this.removeAt(index, 'left');
|
|
6920
|
+
break;
|
|
6921
|
+
case 'Delete':
|
|
6922
|
+
event.preventDefault();
|
|
6923
|
+
this.removeAt(index, 'right');
|
|
6924
|
+
break;
|
|
6925
|
+
case 'Enter':
|
|
6926
|
+
case 'F2':
|
|
6927
|
+
event.preventDefault();
|
|
6928
|
+
this.editChip(index);
|
|
6929
|
+
break;
|
|
6930
|
+
case 'Escape':
|
|
6931
|
+
event.preventDefault();
|
|
6932
|
+
this.focusInput();
|
|
6933
|
+
break;
|
|
6934
|
+
default:
|
|
6935
|
+
// A printable key means the user wants to type, not navigate chips.
|
|
6936
|
+
if (event.key.length === 1 && !event.metaKey && !event.ctrlKey && !event.altKey) {
|
|
6937
|
+
this.setDraft(this.draft() + event.key);
|
|
6938
|
+
this.focusInput();
|
|
6939
|
+
}
|
|
6940
|
+
}
|
|
6941
|
+
}
|
|
6942
|
+
/** Lift a chip back into the input for correction. */
|
|
6943
|
+
editChip(index) {
|
|
6944
|
+
const item = this.value()[index];
|
|
6945
|
+
if (!item || item.disabled)
|
|
6946
|
+
return;
|
|
6947
|
+
this.value.update((items) => items.filter((_, i) => i !== index));
|
|
6948
|
+
this.removed.emit(item);
|
|
6949
|
+
const text = item.label && item.label !== item.value ? `${item.label} <${item.value}>` : item.value;
|
|
6950
|
+
this.setDraft(text);
|
|
6951
|
+
this.focusInput();
|
|
6952
|
+
}
|
|
6953
|
+
selectSuggestion(suggestion) {
|
|
6954
|
+
this.commit(suggestion.value);
|
|
6955
|
+
this.setDraft('');
|
|
6956
|
+
this.list.hide();
|
|
6957
|
+
this.focusInput();
|
|
6958
|
+
}
|
|
6959
|
+
commitDraft() {
|
|
6960
|
+
if (this.commit(this.draft()))
|
|
6961
|
+
this.setDraft('');
|
|
6962
|
+
else
|
|
6963
|
+
this.setDraft('');
|
|
6964
|
+
this.list.hide();
|
|
6965
|
+
}
|
|
6966
|
+
setDraft(text) {
|
|
6967
|
+
this.draft.set(text);
|
|
6968
|
+
const input = this.inputRef?.().nativeElement;
|
|
6969
|
+
if (input)
|
|
6970
|
+
input.value = text;
|
|
6971
|
+
}
|
|
6972
|
+
// --- Styling -------------------------------------------------------------
|
|
6973
|
+
className = computed(() => css({ display: 'block' }), ...(ngDevMode ? [{ debugName: "className" }] : /* istanbul ignore next */ []));
|
|
6974
|
+
fieldClass = computed(() => {
|
|
6975
|
+
const options = this.componentOptions();
|
|
6976
|
+
return css({
|
|
6977
|
+
display: 'flex',
|
|
6978
|
+
flexWrap: 'wrap',
|
|
6979
|
+
alignItems: 'center',
|
|
6980
|
+
width: '100%',
|
|
6981
|
+
listStyle: 'none',
|
|
6982
|
+
margin: 0,
|
|
6983
|
+
padding: 0,
|
|
6984
|
+
...this.theme.gap(options.chipGap),
|
|
6985
|
+
});
|
|
6986
|
+
}, ...(ngDevMode ? [{ debugName: "fieldClass" }] : /* istanbul ignore next */ []));
|
|
6987
|
+
inputClass = computed(() => css({
|
|
6988
|
+
flex: 1,
|
|
6989
|
+
minWidth: this.componentOptions().minInputWidth ?? '12ch',
|
|
6990
|
+
border: 0,
|
|
6991
|
+
outline: 'none',
|
|
6992
|
+
background: 'transparent',
|
|
6993
|
+
color: 'inherit',
|
|
6994
|
+
font: 'inherit',
|
|
6995
|
+
padding: 0,
|
|
6996
|
+
}), ...(ngDevMode ? [{ debugName: "inputClass" }] : /* istanbul ignore next */ []));
|
|
6997
|
+
listClass = computed(() => {
|
|
6998
|
+
const options = this.componentOptions();
|
|
6999
|
+
return css({
|
|
7000
|
+
position: 'absolute',
|
|
7001
|
+
top: '100%',
|
|
7002
|
+
left: 0,
|
|
7003
|
+
right: 0,
|
|
7004
|
+
zIndex: 20,
|
|
7005
|
+
margin: '4px 0 0',
|
|
7006
|
+
padding: 4,
|
|
7007
|
+
listStyle: 'none',
|
|
7008
|
+
maxHeight: 280,
|
|
7009
|
+
overflowY: 'auto',
|
|
7010
|
+
...this.theme.backgroundColor(options.listColor ?? 'primary-surface'),
|
|
7011
|
+
...this.theme.boxShadow(options.listShadow ?? 'menu'),
|
|
7012
|
+
...this.theme.radius(options.listBorderRadius ?? 'xs'),
|
|
7013
|
+
'& [role="option"]': {
|
|
7014
|
+
padding: '8px 12px',
|
|
7015
|
+
cursor: 'pointer',
|
|
7016
|
+
...this.theme.typeface('label'),
|
|
7017
|
+
...this.theme.color('on-primary-surface'),
|
|
7018
|
+
...this.theme.radius('xxs'),
|
|
7019
|
+
'&.active, &:hover': {
|
|
7020
|
+
...this.theme.backgroundColor('primary-container'),
|
|
7021
|
+
...this.theme.color('on-primary-container'),
|
|
7022
|
+
},
|
|
7023
|
+
},
|
|
7024
|
+
});
|
|
7025
|
+
}, ...(ngDevMode ? [{ debugName: "listClass" }] : /* istanbul ignore next */ []));
|
|
7026
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniTagInputComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
|
|
7027
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniTagInputComponent, isStandalone: true, selector: "uni-tag-input", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, dirty: { classPropertyName: "dirty", publicName: "dirty", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, ariaDescribedBy: { classPropertyName: "ariaDescribedBy", publicName: "ariaDescribedBy", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, preset: { classPropertyName: "preset", publicName: "preset", isSignal: true, isRequired: false, transformFunction: null }, separators: { classPropertyName: "separators", publicName: "separators", isSignal: true, isRequired: false, transformFunction: null }, commitOnBlur: { classPropertyName: "commitOnBlur", publicName: "commitOnBlur", isSignal: true, isRequired: false, transformFunction: null }, allowDuplicates: { classPropertyName: "allowDuplicates", publicName: "allowDuplicates", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, validate: { classPropertyName: "validate", publicName: "validate", isSignal: true, isRequired: false, transformFunction: null }, parse: { classPropertyName: "parse", publicName: "parse", isSignal: true, isRequired: false, transformFunction: null }, tagVariant: { classPropertyName: "tagVariant", publicName: "tagVariant", isSignal: true, isRequired: false, transformFunction: null }, tagTone: { classPropertyName: "tagTone", publicName: "tagTone", isSignal: true, isRequired: false, transformFunction: null }, tagSize: { classPropertyName: "tagSize", publicName: "tagSize", isSignal: true, isRequired: false, transformFunction: null }, suggestions: { classPropertyName: "suggestions", publicName: "suggestions", isSignal: true, isRequired: false, transformFunction: null }, debounceTime: { classPropertyName: "debounceTime", publicName: "debounceTime", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touched: "touchedChange", query: "query", added: "added", removed: "removed", rejected: "rejected" }, host: { properties: { "class": "className()" } }, providers: [{ provide: COMPONENT_NAME, useValue: 'tagInput' }], viewQueries: [{ propertyName: "inputRef", first: true, predicate: ["field"], descendants: true, isSignal: true }, { propertyName: "chipRefs", predicate: ["chip"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<!-- Focusout rides the bubbling event so the list closes only when focus\n leaves the whole field, not while moving between chips and the input. -->\n<div style=\"position: relative\" (focusout)=\"onFocusOut($event)\">\n <uni-input-box [error]=\"showError()\" height=\"auto\">\n <ul [class]=\"fieldClass()\">\n @for (item of value(); track item.value; let i = $index) {\n <!-- The focusable element is the chip's own button; this <li> only\n receives keys bubbling up from it, so it must stay out of the tab\n order (same delegation the menu uses for its roving focus). -->\n <!-- eslint-disable-next-line @angular-eslint/template/interactive-supports-focus -->\n <li #chip (keydown)=\"onChipKeydown($event, i)\" (dblclick)=\"editChip(i)\">\n <uni-tag\n [label]=\"labelOf(item)\"\n [value]=\"item.value\"\n [avatarSrc]=\"item.avatarSrc\"\n [variant]=\"item.invalid ? 'warn' : tagVariant()\"\n [tone]=\"tagTone()\"\n [size]=\"tagSize() ?? componentOptions().chipSize ?? 'md'\"\n [invalid]=\"!!item.invalid\"\n [disabled]=\"!!item.disabled || disabled()\"\n [interactive]=\"true\"\n [removable]=\"!item.disabled && !disabled()\"\n [controlTabIndex]=\"-1\"\n (removed)=\"removeAt(i)\"\n />\n </li>\n }\n\n <li [class]=\"inputClass()\">\n <!-- The field is one tab stop: chips carry tabindex=\"-1\" and are\n entered with Backspace/ArrowLeft, so Tab never walks through\n eight recipients to reach the next control. -->\n <input\n #field\n type=\"text\"\n role=\"combobox\"\n autocomplete=\"off\"\n [class]=\"inputClass()\"\n [disabled]=\"disabled()\"\n [placeholder]=\"placeholder() ?? ''\"\n [attr.aria-label]=\"label()\"\n [attr.aria-expanded]=\"list.open()\"\n [attr.aria-controls]=\"list.listboxId\"\n [attr.aria-activedescendant]=\"list.activeDescendantId()\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.aria-invalid]=\"showError() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n (keydown)=\"onInputKeydown($event)\"\n (input)=\"onInput($any($event.target).value)\"\n (paste)=\"onPaste($event)\"\n (blur)=\"onBlur()\"\n />\n </li>\n </ul>\n </uni-input-box>\n\n @if (list.open()) {\n <ul [id]=\"list.listboxId\" role=\"listbox\" [attr.aria-label]=\"label()\" [class]=\"listClass()\">\n @for (suggestion of visibleSuggestions(); track suggestion.value; let i = $index) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n <li\n role=\"option\"\n [id]=\"list.optionId(i)\"\n [class.active]=\"i === list.activeIndex()\"\n [attr.aria-selected]=\"i === list.activeIndex()\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"selectSuggestion(suggestion)\"\n >\n {{ labelOf(suggestion) }}\n @if (suggestion.description) {\n <span [class]=\"srOnly\">{{ suggestion.description }}</span>\n }\n </li>\n }\n </ul>\n }\n\n <!-- Said once for the whole field, rather than every chip advertising it. -->\n <span [id]=\"hintId\" [class]=\"srOnly\">\n Press Backspace to reach the last entry, then Backspace again to remove it.\n </span>\n\n <!-- Adds and removes are otherwise silent for a screen reader. -->\n <span role=\"status\" aria-live=\"polite\" [class]=\"srOnly\">{{ announcement() }}</span>\n</div>\n", dependencies: [{ kind: "component", type: UniInputBoxComponent, selector: "uni-input-box", inputs: ["disabled", "error", "minWidth", "height"] }, { kind: "component", type: UniTagComponent, selector: "uni-tag", inputs: ["size", "tone", "label", "value", "maxWidth", "avatarSrc", "avatarName", "iconName", "symbolName", "dot", "removable", "interactive", "selected", "invalid", "disabled", "removeLabel", "controlTabIndex"], outputs: ["removed", "activated"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
7028
|
+
}
|
|
7029
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniTagInputComponent, decorators: [{
|
|
7030
|
+
type: Component,
|
|
7031
|
+
args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'uni-tag-input', imports: [UniInputBoxComponent, UniTagComponent], providers: [{ provide: COMPONENT_NAME, useValue: 'tagInput' }], host: { '[class]': 'className()' }, template: "<!-- Focusout rides the bubbling event so the list closes only when focus\n leaves the whole field, not while moving between chips and the input. -->\n<div style=\"position: relative\" (focusout)=\"onFocusOut($event)\">\n <uni-input-box [error]=\"showError()\" height=\"auto\">\n <ul [class]=\"fieldClass()\">\n @for (item of value(); track item.value; let i = $index) {\n <!-- The focusable element is the chip's own button; this <li> only\n receives keys bubbling up from it, so it must stay out of the tab\n order (same delegation the menu uses for its roving focus). -->\n <!-- eslint-disable-next-line @angular-eslint/template/interactive-supports-focus -->\n <li #chip (keydown)=\"onChipKeydown($event, i)\" (dblclick)=\"editChip(i)\">\n <uni-tag\n [label]=\"labelOf(item)\"\n [value]=\"item.value\"\n [avatarSrc]=\"item.avatarSrc\"\n [variant]=\"item.invalid ? 'warn' : tagVariant()\"\n [tone]=\"tagTone()\"\n [size]=\"tagSize() ?? componentOptions().chipSize ?? 'md'\"\n [invalid]=\"!!item.invalid\"\n [disabled]=\"!!item.disabled || disabled()\"\n [interactive]=\"true\"\n [removable]=\"!item.disabled && !disabled()\"\n [controlTabIndex]=\"-1\"\n (removed)=\"removeAt(i)\"\n />\n </li>\n }\n\n <li [class]=\"inputClass()\">\n <!-- The field is one tab stop: chips carry tabindex=\"-1\" and are\n entered with Backspace/ArrowLeft, so Tab never walks through\n eight recipients to reach the next control. -->\n <input\n #field\n type=\"text\"\n role=\"combobox\"\n autocomplete=\"off\"\n [class]=\"inputClass()\"\n [disabled]=\"disabled()\"\n [placeholder]=\"placeholder() ?? ''\"\n [attr.aria-label]=\"label()\"\n [attr.aria-expanded]=\"list.open()\"\n [attr.aria-controls]=\"list.listboxId\"\n [attr.aria-activedescendant]=\"list.activeDescendantId()\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.aria-invalid]=\"showError() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n (keydown)=\"onInputKeydown($event)\"\n (input)=\"onInput($any($event.target).value)\"\n (paste)=\"onPaste($event)\"\n (blur)=\"onBlur()\"\n />\n </li>\n </ul>\n </uni-input-box>\n\n @if (list.open()) {\n <ul [id]=\"list.listboxId\" role=\"listbox\" [attr.aria-label]=\"label()\" [class]=\"listClass()\">\n @for (suggestion of visibleSuggestions(); track suggestion.value; let i = $index) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n <li\n role=\"option\"\n [id]=\"list.optionId(i)\"\n [class.active]=\"i === list.activeIndex()\"\n [attr.aria-selected]=\"i === list.activeIndex()\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"selectSuggestion(suggestion)\"\n >\n {{ labelOf(suggestion) }}\n @if (suggestion.description) {\n <span [class]=\"srOnly\">{{ suggestion.description }}</span>\n }\n </li>\n }\n </ul>\n }\n\n <!-- Said once for the whole field, rather than every chip advertising it. -->\n <span [id]=\"hintId\" [class]=\"srOnly\">\n Press Backspace to reach the last entry, then Backspace again to remove it.\n </span>\n\n <!-- Adds and removes are otherwise silent for a screen reader. -->\n <span role=\"status\" aria-live=\"polite\" [class]=\"srOnly\">{{ announcement() }}</span>\n</div>\n" }]
|
|
7032
|
+
}], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }, { type: i0.Output, args: ["touchedChange"] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], dirty: [{ type: i0.Input, args: [{ isSignal: true, alias: "dirty", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], ariaDescribedBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaDescribedBy", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], preset: [{ type: i0.Input, args: [{ isSignal: true, alias: "preset", required: false }] }], separators: [{ type: i0.Input, args: [{ isSignal: true, alias: "separators", required: false }] }], commitOnBlur: [{ type: i0.Input, args: [{ isSignal: true, alias: "commitOnBlur", required: false }] }], allowDuplicates: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowDuplicates", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], validate: [{ type: i0.Input, args: [{ isSignal: true, alias: "validate", required: false }] }], parse: [{ type: i0.Input, args: [{ isSignal: true, alias: "parse", required: false }] }], tagVariant: [{ type: i0.Input, args: [{ isSignal: true, alias: "tagVariant", required: false }] }], tagTone: [{ type: i0.Input, args: [{ isSignal: true, alias: "tagTone", required: false }] }], tagSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "tagSize", required: false }] }], suggestions: [{ type: i0.Input, args: [{ isSignal: true, alias: "suggestions", required: false }] }], query: [{ type: i0.Output, args: ["query"] }], debounceTime: [{ type: i0.Input, args: [{ isSignal: true, alias: "debounceTime", required: false }] }], added: [{ type: i0.Output, args: ["added"] }], removed: [{ type: i0.Output, args: ["removed"] }], rejected: [{ type: i0.Output, args: ["rejected"] }], inputRef: [{ type: i0.ViewChild, args: ['field', { isSignal: true }] }], chipRefs: [{ type: i0.ViewChildren, args: ['chip', { isSignal: true }] }] } });
|
|
7033
|
+
|
|
7034
|
+
/**
|
|
7035
|
+
* UniTagInputComponent Barrel File
|
|
7036
|
+
*
|
|
7037
|
+
* This file exports all public-facing elements of the tag-input component.
|
|
7038
|
+
*/
|
|
7039
|
+
|
|
6263
7040
|
const SCHEMES = [
|
|
6264
7041
|
'monochromatic',
|
|
6265
7042
|
'analogous',
|
|
@@ -7521,5 +8298,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
|
|
|
7521
8298
|
* Generated bundle index. Do not edit.
|
|
7522
8299
|
*/
|
|
7523
8300
|
|
|
7524
|
-
export { BodyRenderDirective, ConfirmationDialogComponent, DragAndDropDirective, FOCUSABLE_SELECTOR, LocalStorageService, NotificationService, NotificationsComponent, RippleDirective, ThemeService, UNI_THEMES, UniAlertComponent, UniAppBarComponent, UniAvatarComponent, UniAvatarGroupComponent, UniBackgroundComponent, UniBadgeComponent, UniBaseDatasource, UniBoxComponent, UniBreadcrumbComponent, UniButtonComponent, UniButtonGroupComponent, UniCardComponent, UniCardContentComponent, UniCardHeaderComponent, UniCenterComponent, UniCheckboxComponent, UniDataSearchComponent, UniDataTableComponent, UniDebounceInputComponent, UniDialogButtonsComponent, UniDialogComponent, UniDialogHeaderComponent, UniDividerComponent, UniDrawerComponent, UniDropdownComponent, UniExpandAreaComponent, UniExpandComponent, UniExpandToggleComponent, UniFileDropZoneComponent, UniGridAreaComponent, UniGridComponent, UniIconButtonComponent, UniIconComponent, UniInputBoxComponent, UniInputComponent, UniJsonViewComponent, UniMenuComponent, UniMultiSelectComponent, UniMultiSelectDropdownComponent, UniNotificationBadgeComponent, UniPaginatorComponent, UniPopoverComponent, UniProgressBarComponent, UniProgressGaugeComponent, UniRadioComponent, UniRecordDatasource, UniRowComponent, UniScrollAreaComponent, UniSearchInputComponent, UniSelectComponent, UniServerSideDatasource, UniSkeletonComponent, UniSliderComponent, UniSnackbarComponent, UniSortHeaderComponent, UniStackComponent, UniStatComponent, UniSymbolComponent, UniTabComponent, UniTabsComponent, UniTagComponent, UniTextComponent, UniTextareaComponent, UniThemeBuilderComponent, UniThemeSwitchComponent, UniToggleComponent, UniTooltipComponent, UniWrapComponent, acceptableFile, anchorArrowStyles, anchorStyles, getFileExtension, isDivider, motionSafe, newAnchorName, resolveFocusTarget, uniqueId, useTimer, visuallyHidden };
|
|
8301
|
+
export { BodyRenderDirective, ConfirmationDialogComponent, DragAndDropDirective, FOCUSABLE_SELECTOR, ListboxNavigation, LocalStorageService, NotificationService, NotificationsComponent, RippleDirective, ThemeService, UNI_THEMES, UniAlertComponent, UniAppBarComponent, UniAvatarComponent, UniAvatarGroupComponent, UniBackgroundComponent, UniBadgeComponent, UniBaseDatasource, UniBoxComponent, UniBreadcrumbComponent, UniButtonComponent, UniButtonGroupComponent, UniCardComponent, UniCardContentComponent, UniCardHeaderComponent, UniCenterComponent, UniCheckboxComponent, UniDataSearchComponent, UniDataTableComponent, UniDebounceInputComponent, UniDialogButtonsComponent, UniDialogComponent, UniDialogHeaderComponent, UniDividerComponent, UniDrawerComponent, UniDropdownComponent, UniExpandAreaComponent, UniExpandComponent, UniExpandToggleComponent, UniFileDropZoneComponent, UniGridAreaComponent, UniGridComponent, UniIconButtonComponent, UniIconComponent, UniInputBoxComponent, UniInputComponent, UniJsonViewComponent, UniMenuComponent, UniMultiSelectComponent, UniMultiSelectDropdownComponent, UniNotificationBadgeComponent, UniPaginatorComponent, UniPopoverComponent, UniProgressBarComponent, UniProgressGaugeComponent, UniRadioComponent, UniRecordDatasource, UniRowComponent, UniScrollAreaComponent, UniSearchInputComponent, UniSelectComponent, UniServerSideDatasource, UniSkeletonComponent, UniSliderComponent, UniSnackbarComponent, UniSortHeaderComponent, UniStackComponent, UniStatComponent, UniSymbolComponent, UniTabComponent, UniTabsComponent, UniTagComponent, UniTagInputComponent, UniTextComponent, UniTextareaComponent, UniThemeBuilderComponent, UniThemeSwitchComponent, UniToggleComponent, UniTooltipComponent, UniWrapComponent, acceptableFile, anchorArrowStyles, anchorStyles, createListboxNavigation, getFileExtension, isDivider, motionSafe, newAnchorName, resolveFocusTarget, uniqueId, useTimer, visuallyHidden };
|
|
7525
8302
|
//# sourceMappingURL=uni-design-system-uni-angular.mjs.map
|