@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
- theme = signal(LightTheme, ...(ngDevMode ? [{ debugName: "theme" }] : /* istanbul ignore next */ []));
654
- themeOptions = signal([], ...(ngDevMode ? [{ debugName: "themeOptions" }] : /* istanbul ignore next */ []));
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
- this.themeOptions.set(Object.keys(this.themes).map((key) => {
681
- return { label: this.themes[key].name, value: key };
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(this.themes)[0] || 'base');
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
- this.theme.set(createThemeFromPalette({ ...config, id: ThemeService.CUSTOM_KEY, name: 'Your Brand' }));
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 built-in theme. */
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.selectTheme(Object.keys(this.themes)[0] || 'base');
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
- '&:hover, &:focus': {
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; a same-key '&:hover, &:focus'
4217
- // in the variant replaces the default hover pair outright.
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
- this.query.set(event.target.value);
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 stack-layout gap=\"xs\" padding=\"xs\">\n @for (option of filteredOptions(); track option.value) {\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 }\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</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 });
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 stack-layout gap=\"xs\" padding=\"xs\">\n @for (option of filteredOptions(); track option.value) {\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 }\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</uni-dropdown>\n" }]
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
- showList = computed(() => this.listOpen() && this.visibleSuggestions().length > 0, ...(ngDevMode ? [{ debugName: "showList" }] : /* istanbul ignore next */ []));
5402
- activeOptionId = computed(() => this.activeIndex() >= 0 ? `${this.listboxId}-${this.activeIndex()}` : undefined, ...(ngDevMode ? [{ debugName: "activeOptionId" }] : /* istanbul ignore next */ []));
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.listOpen.set(true);
5406
- this.activeIndex.set(-1);
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.showList() && active >= 0) {
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.listOpen.set(false);
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.listOpen.set(false);
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.listOpen.set(false);
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
- if (this.showList())
5450
- this.listOpen.set(false);
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
- // Close unless focus moved somewhere inside the component.
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]=\"showList()\"\n [ariaControls]=\"listboxId\"\n [ariaActivedescendant]=\"activeOptionId()\"\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 (showList()) {\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]=\"listboxId + '-' + i\"\n [class.active]=\"i === activeIndex()\"\n [attr.aria-selected]=\"i === 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 });
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]=\"showList()\"\n [ariaControls]=\"listboxId\"\n [ariaActivedescendant]=\"activeOptionId()\"\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 (showList()) {\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]=\"listboxId + '-' + i\"\n [class.active]=\"i === activeIndex()\"\n [attr.aria-selected]=\"i === activeIndex()\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"select(suggestion)\"\n >\n {{ suggestion }}\n </li>\n }\n </ul>\n }\n</div>\n" }]
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
- class UniTagComponent {
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
- // TODO(v4): rename to closed renaming is breaking
6242
- // eslint-disable-next-line @angular-eslint/no-output-native
6243
- close = output();
6244
- handleClose() {
6245
- const v = this.value();
6246
- if (v)
6247
- this.close.emit(v);
6248
- }
6249
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniTagComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
6250
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.12", type: UniTagComponent, isStandalone: true, selector: "uni-tag", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { close: "close" }, ngImport: i0, template: "<div box-layout color=\"primary\" borderRadius=\"max\" display=\"inline-block\">\n <div row-layout paddingLeft=\"sm\" gap=\"xs\">\n <span uni-text display=\"block\" typeface=\"tag\">{{ label() }}</span>\n <button icon-button symbolName=\"close\" size=\"sm\" (click)=\"handleClose()\">\n Remove {{ label() }}\n </button>\n </div>\n</div>\n", dependencies: [{ 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: UniIconButtonComponent, selector: "button[uni-icon-button], button[icon-button]", inputs: ["ariaLabel", "iconName", "symbolName", "variant", "size", "disable", "loading", "opticalSize"] }, { kind: "component", type: UniTextComponent, selector: "[uni-text]", inputs: ["uni-text", "typeface", "color", "display", "align", "nowrap", "maxWidth", "ellipsis"] }, { kind: "component", type: UniRowComponent, selector: "[uni-row-layout], [row-layout]", inputs: ["display", "flexDirection", "minWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
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: [UniBoxComponent, UniIconButtonComponent, UniTextComponent, UniRowComponent], template: "<div box-layout color=\"primary\" borderRadius=\"max\" display=\"inline-block\">\n <div row-layout paddingLeft=\"sm\" gap=\"xs\">\n <span uni-text display=\"block\" typeface=\"tag\">{{ label() }}</span>\n <button icon-button symbolName=\"close\" size=\"sm\" (click)=\"handleClose()\">\n Remove {{ label() }}\n </button>\n </div>\n</div>\n" }]
6255
- }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], close: [{ type: i0.Output, args: ["close"] }] } });
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