ngwr 9.0.1 → 9.1.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.
@@ -3,9 +3,9 @@ import { ScrollStrategyOptions } from '@angular/cdk/overlay';
3
3
  import { TemplatePortal } from '@angular/cdk/portal';
4
4
  import { isPlatformBrowser } from '@angular/common';
5
5
  import * as i0 from '@angular/core';
6
- import { InjectionToken, input, inject, ElementRef, computed, effect, DestroyRef, ViewEncapsulation, Component, signal, model, output, contentChildren, viewChild, TemplateRef, ViewContainerRef, PLATFORM_ID, forwardRef } from '@angular/core';
6
+ import { InjectionToken, input, inject, ElementRef, computed, effect, DestroyRef, ViewEncapsulation, Component, model, output, signal, contentChildren, viewChild, TemplateRef, ViewContainerRef, PLATFORM_ID, forwardRef } from '@angular/core';
7
7
  import { toObservable, takeUntilDestroyed } from '@angular/core/rxjs-interop';
8
- import { debounceTime, switchMap, of, isObservable, from, finalize } from 'rxjs';
8
+ import { skip, debounce, timer, switchMap, of, isObservable, from, tap, finalize } from 'rxjs';
9
9
  import { useI18nText, useI18nFormatter } from 'ngwr/i18n';
10
10
  import { WR_OVERLAY, WR_RESPONSIVE_OVERLAYS, wrPresentAsSheet } from 'ngwr/overlay';
11
11
 
@@ -69,14 +69,14 @@ class WrOption {
69
69
  active = computed(() => this.parent?.activeOptionId() === this.id, /* @ts-ignore */
70
70
  ...(ngDevMode ? [{ debugName: "active" }] : /* istanbul ignore next */ []));
71
71
  /**
72
- * @internal — search mode only. True when the parent has a query that
72
+ * @internal — searchable selects only. True when the parent has a query that
73
73
  * the option's text content does not match (case-insensitive substring).
74
74
  * Hidden options stay in the DOM so registration order survives but
75
75
  * collapse via CSS.
76
76
  */
77
77
  hidden = computed(() => {
78
78
  const parent = this.parent;
79
- if (!parent?.isSearch())
79
+ if (!parent?.isSearchable() || !parent.clientFilter())
80
80
  return false;
81
81
  const q = parent.searchQuery().trim().toLowerCase();
82
82
  if (!q)
@@ -160,6 +160,11 @@ let listboxUid = 0;
160
160
  * - `'tag'` — free-text + chips with `[separators]` / `[validate]` /
161
161
  * `[allowDuplicates]`. Replaces `<wr-chips-input>`.
162
162
  *
163
+ * `[searchable]` is orthogonal to the mode — it adds the type-ahead filter to
164
+ * `single` / `multi` without changing the value shape, which is how you get a
165
+ * searchable multi-select. Server-side search hangs off the `(searchChange)` output
166
+ * plus `[options]` / `[loading]`.
167
+ *
163
168
  * A signal-forms native control: it implements `FormValueControl<unknown>`, so
164
169
  * `[formField]` binds straight to its `value` model — no
165
170
  * `ControlValueAccessor` in between. `[(value)]` works standalone, and classic
@@ -179,6 +184,16 @@ let listboxUid = 0;
179
184
  * <wr-option value="ts">TypeScript</wr-option>
180
185
  * <wr-option value="ng">Angular</wr-option>
181
186
  * </wr-select>
187
+ *
188
+ * <!-- Multi + type-ahead, options from a store -->
189
+ * <wr-select
190
+ * mode="multi"
191
+ * searchable
192
+ * [options]="results()"
193
+ * [loading]="pending()"
194
+ * [(value)]="categories"
195
+ * (searchChange)="store.search($event)"
196
+ * />
182
197
  * ```
183
198
  *
184
199
  * The legacy `[multi]="true"` boolean is still accepted but is now an
@@ -240,6 +255,34 @@ class WrSelect {
240
255
  /** Convenience — `effectiveMode() === 'search'`. Exposed via context for options. */
241
256
  isSearch = computed(() => this.effectiveMode() === 'search', /* @ts-ignore */
242
257
  ...(ngDevMode ? [{ debugName: "isSearch" }] : /* istanbul ignore next */ []));
258
+ /**
259
+ * Add a type-ahead filter without changing the value shape — the missing
260
+ * multi-with-search combination, since `[mode]` picks exactly one shape:
261
+ *
262
+ * ```html
263
+ * <wr-select mode="multi" searchable [(value)]="categories">…</wr-select>
264
+ * ```
265
+ *
266
+ * On `multi` the trigger keeps its chips and grows an inline text field (the
267
+ * shape `tag` mode already uses); on `single` it is equivalent to
268
+ * `mode="search"`. Every search input (`[options]`, `[loader]`,
269
+ * `[debounceMs]`, `[minChars]`, `[virtualScroll]`, the `(searchChange)` output, …)
270
+ * applies either way. Ignored in `tag` mode, which owns its own input.
271
+ * @default false
272
+ */
273
+ searchable = input(false, { ...(ngDevMode ? { debugName: "searchable" } : /* istanbul ignore next */ {}), transform: coerceBooleanProperty });
274
+ /**
275
+ * Filtering is active — either `mode="search"` or `[searchable]`. Exposed via
276
+ * context so each `<wr-option>` can self-hide non-matching rows.
277
+ */
278
+ isSearchable = computed(() => this.isSearch() || (this.searchable() && !this.isTag()), /* @ts-ignore */
279
+ ...(ngDevMode ? [{ debugName: "isSearchable" }] : /* istanbul ignore next */ []));
280
+ /** The trigger *is* the search input: `mode="search"`, or a searchable single. */
281
+ isSearchTrigger = computed(() => this.isSearch() || (this.searchable() && !this.hasChips()), /* @ts-ignore */
282
+ ...(ngDevMode ? [{ debugName: "isSearchTrigger" }] : /* istanbul ignore next */ []));
283
+ /** Chips plus an inline search field — a searchable multi-select. */
284
+ hasChipSearch = computed(() => this.searchable() && this.isMulti(), /* @ts-ignore */
285
+ ...(ngDevMode ? [{ debugName: "hasChipSearch" }] : /* istanbul ignore next */ []));
243
286
  /** WrSelectContext — resolved multi-selection flag for child options. */
244
287
  multi = computed(() => this.effectiveMode() === 'multi' || this.effectiveMode() === 'tag', /* @ts-ignore */
245
288
  ...(ngDevMode ? [{ debugName: "multi" }] : /* istanbul ignore next */ []));
@@ -250,10 +293,14 @@ class WrSelect {
250
293
  }, /* @ts-ignore */
251
294
  ...(ngDevMode ? [{ debugName: "hasChips" }] : /* istanbul ignore next */ []));
252
295
  /**
253
- * Search-mode query. Empty string = no filter. Exposed via context so
296
+ * The live search query. Empty string = no filter. Exposed via context so
254
297
  * each `<wr-option>` can self-hide non-matching rows.
298
+ *
299
+ * Two-way bindable, so the query can be owned or reset from outside:
300
+ * `[(searchQuery)]="query"`. For server-side search prefer the debounced
301
+ * {@link searchChange} output — `searchQueryChange` fires on every keystroke.
255
302
  */
256
- searchQuery = signal('', /* @ts-ignore */
303
+ searchQuery = model('', /* @ts-ignore */
257
304
  ...(ngDevMode ? [{ debugName: "searchQuery" }] : /* istanbul ignore next */ []));
258
305
  // Search-mode-only inputs (ignored in other modes)
259
306
  /**
@@ -274,6 +321,22 @@ class WrSelect {
274
321
  */
275
322
  loader = input(null, /* @ts-ignore */
276
323
  ...(ngDevMode ? [{ debugName: "loader" }] : /* istanbul ignore next */ []));
324
+ /**
325
+ * The option list is already scoped to the query upstream — skip the built-in
326
+ * client-side label filter. Set it when `[options]` is fed from a server via
327
+ * the {@link searchChange} output; the async `[loader]` path implies it.
328
+ *
329
+ * Without this, a server that ranks or fuzzy-matches (returning rows whose
330
+ * labels don't literally contain the query) would have those rows hidden
331
+ * again on the client. @default false
332
+ */
333
+ serverSearch = input(false, { ...(ngDevMode ? { debugName: "serverSearch" } : /* istanbul ignore next */ {}), transform: coerceBooleanProperty });
334
+ /**
335
+ * Whether the built-in label filter runs. Exposed via context so each
336
+ * `<wr-option>` knows whether to self-hide. @internal
337
+ */
338
+ clientFilter = computed(() => !this.loader() && !this.serverSearch(), /* @ts-ignore */
339
+ ...(ngDevMode ? [{ debugName: "clientFilter" }] : /* istanbul ignore next */ []));
277
340
  /** Search mode: debounce (ms) applied to the loader. @default 250 */
278
341
  debounceMs = input(250, { ...(ngDevMode ? { debugName: "debounceMs" } : /* istanbul ignore next */ {}), transform: (v) => Math.max(0, Number(v) || 0) });
279
342
  /** Search mode: minimum query length before the panel opens / loader fires. @default 0 */
@@ -314,12 +377,45 @@ class WrSelect {
314
377
  ...(ngDevMode ? [{ debugName: "loadingText" }] : /* istanbul ignore next */ []));
315
378
  resolvedNoResults = useI18nText(this.noResultsText, 'select.noResults', 'No results');
316
379
  resolvedLoading = useI18nText(this.loadingText, 'select.loading', 'Loading…');
380
+ /**
381
+ * Debounced search query, gated to searchable selects. Emits on every settled
382
+ * change — including the empty string when the field is cleared — so a
383
+ * store-backed option list can stay in sync and reset itself. `[debounceMs]`
384
+ * controls the delay; `[minChars]` does NOT gate it, for the same reason the
385
+ * `[loader]` path clears its results below that threshold.
386
+ *
387
+ * This is the hook for server-side search: dispatch on `(searchChange)`, feed
388
+ * the results back through `[options]` (plus `[serverSearch]`), and drive the
389
+ * panel's progress row with `[loading]`. Prefer it over the model's raw
390
+ * `(searchQueryChange)`, which fires on every keystroke.
391
+ *
392
+ * @example
393
+ * ```html
394
+ * <wr-select
395
+ * mode="search"
396
+ * serverSearch
397
+ * [options]="results()"
398
+ * [loading]="pending()"
399
+ * (searchChange)="store.search($event)"
400
+ * />
401
+ * ```
402
+ */
403
+ searchChange = output();
404
+ /**
405
+ * Show the panel's progress row. Independent of `[loader]`, which raises and
406
+ * lowers its own flag — use this when the options come from a store fed by
407
+ * the {@link searchChange} output. @default false
408
+ */
409
+ loading = input(false, { ...(ngDevMode ? { debugName: "loading" } : /* istanbul ignore next */ {}), transform: coerceBooleanProperty });
317
410
  /** Search mode: results returned by the async loader. @internal */
318
411
  loadedOptions = signal([], /* @ts-ignore */
319
412
  ...(ngDevMode ? [{ debugName: "loadedOptions" }] : /* istanbul ignore next */ []));
320
- /** Search mode: true while the loader is in flight. @internal */
321
- loading = signal(false, /* @ts-ignore */
322
- ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
413
+ /** True while the async `[loader]` is in flight. @internal */
414
+ loaderLoading = signal(false, /* @ts-ignore */
415
+ ...(ngDevMode ? [{ debugName: "loaderLoading" }] : /* istanbul ignore next */ []));
416
+ /** Either flag shows the panel's progress row. @internal */
417
+ isLoading = computed(() => this.loading() || this.loaderLoading(), /* @ts-ignore */
418
+ ...(ngDevMode ? [{ debugName: "isLoading" }] : /* istanbul ignore next */ []));
323
419
  // Tag-mode-only inputs (ignored in other modes)
324
420
  /**
325
421
  * Tag mode: keys / characters that commit the current draft into a chip.
@@ -404,7 +500,11 @@ class WrSelect {
404
500
  const list = this.registry();
405
501
  return arr.map(v => {
406
502
  const found = list.find(o => o.value === v);
407
- return { value: v, label: found?.getLabel() ?? String(v) };
503
+ // Fall back to `displayWith` (defaults to `String`) rather than `String`
504
+ // directly: a searchable multi fed from `[options]` has no registered
505
+ // `<wr-option>` for a virtualized or unmounted row, and object items
506
+ // would otherwise render as "[object Object]".
507
+ return { value: v, label: found?.getLabel() ?? this.displayWith()(v) };
408
508
  });
409
509
  }, /* @ts-ignore */
410
510
  ...(ngDevMode ? [{ debugName: "selectedChips" }] : /* istanbul ignore next */ []));
@@ -442,8 +542,10 @@ class WrSelect {
442
542
  parts.push('wr-select--multi');
443
543
  if (this.isTag())
444
544
  parts.push('wr-select--tag');
445
- if (this.isSearch())
545
+ if (this.isSearchTrigger())
446
546
  parts.push('wr-select--search');
547
+ if (this.hasChipSearch())
548
+ parts.push('wr-select--searchable');
447
549
  if (this.isDisabled())
448
550
  parts.push('wr-select--disabled');
449
551
  return parts.join(' ');
@@ -466,11 +568,11 @@ class WrSelect {
466
568
  * `dynamicOptions()` narrowed to the current search query. `dynamicOptions()`
467
569
  * is intentionally UNFILTERED (projected `<wr-option>` self-hide via CSS), so
468
570
  * the virtual path — which renders plain rows that can't self-hide — filters
469
- * here. Loader results are already query-scoped upstream.
571
+ * here. Loader / `serverSearch` results are already query-scoped upstream.
470
572
  */
471
573
  filteredDynamicOptions = computed(() => {
472
574
  const items = this.dynamicOptions();
473
- if (this.loader())
575
+ if (!this.clientFilter())
474
576
  return items;
475
577
  const q = this.searchQuery().trim().toLowerCase();
476
578
  if (!q)
@@ -485,7 +587,7 @@ class WrSelect {
485
587
  * so their DOM-derived labels / selection still work).
486
588
  */
487
589
  virtualActive = computed(() => this.virtualScroll() &&
488
- this.isSearch() &&
590
+ this.isSearchable() &&
489
591
  this.projectedOptions().length === 0 &&
490
592
  this.filteredDynamicOptions().length > 0, /* @ts-ignore */
491
593
  ...(ngDevMode ? [{ debugName: "virtualActive" }] : /* istanbul ignore next */ []));
@@ -568,7 +670,7 @@ class WrSelect {
568
670
  if (this.virtualActive())
569
671
  return this.filteredDynamicOptions().length;
570
672
  const list = this.registry();
571
- if (!this.isSearch())
673
+ if (!this.isSearchable() || !this.clientFilter())
572
674
  return list.length;
573
675
  const q = this.searchQuery().trim().toLowerCase();
574
676
  if (!q)
@@ -583,7 +685,7 @@ class WrSelect {
583
685
  ...(ngDevMode ? [{ debugName: "visibleCount" }] : /* istanbul ignore next */ []));
584
686
  /** Search mode: show "no results" when the panel has nothing to offer. */
585
687
  hasNoResults = computed(() => {
586
- if (!this.isSearch() || !this.open() || this.loading())
688
+ if (!this.isSearchable() || !this.open() || this.isLoading())
587
689
  return false;
588
690
  if (this.searchQuery().trim().length < this.minChars())
589
691
  return false;
@@ -592,7 +694,7 @@ class WrSelect {
592
694
  ...(ngDevMode ? [{ debugName: "hasNoResults" }] : /* istanbul ignore next */ []));
593
695
  /** Search mode: is this option hidden by the current query? @internal */
594
696
  isOptionHidden(label) {
595
- if (!this.isSearch())
697
+ if (!this.isSearchable() || !this.clientFilter())
596
698
  return false;
597
699
  const q = this.searchQuery().trim().toLowerCase();
598
700
  if (!q)
@@ -600,7 +702,7 @@ class WrSelect {
600
702
  return !label.toLowerCase().includes(q);
601
703
  }
602
704
  panelTpl = viewChild.required('panelTpl', { ...(ngDevMode ? { debugName: "panelTpl" } : /* istanbul ignore next */ {}), read: TemplateRef });
603
- /** Tag-mode inline input. Present only when `mode="tag"`. @internal */
705
+ /** The inline input inside a chip trigger — tag mode or a searchable multi. @internal */
604
706
  tagInputEl = viewChild('tagInput', /* @ts-ignore */
605
707
  ...(ngDevMode ? [{ debugName: "tagInputEl" }] : /* istanbul ignore next */ []));
606
708
  /** Search-mode inline input. Present only when `mode="search"`. @internal */
@@ -612,6 +714,37 @@ class WrSelect {
612
714
  focusSearchInput() {
613
715
  this.searchInputEl()?.nativeElement.focus();
614
716
  }
717
+ // Chip-trigger input dispatch. One inline input, two owners: tag mode
718
+ // commits a draft into chips, a searchable multi filters the panel.
719
+ onChipsInput(event) {
720
+ if (this.hasChipSearch())
721
+ this.onSearchInput(event);
722
+ else
723
+ this.onTagInput(event);
724
+ }
725
+ onChipsKeydown(event) {
726
+ if (this.hasChipSearch())
727
+ this.onSearchKey(event);
728
+ else
729
+ this.onTagKeydown(event);
730
+ }
731
+ onChipsFocus() {
732
+ // Tag mode has no panel to open.
733
+ if (this.hasChipSearch())
734
+ this.onSearchFocus();
735
+ }
736
+ onChipsPaste(event) {
737
+ // Splitting a paste into chips is tag-mode behaviour; a search query is
738
+ // pasted as-is.
739
+ if (!this.hasChipSearch())
740
+ this.onTagPaste(event);
741
+ }
742
+ onChipsBlur() {
743
+ if (this.hasChipSearch())
744
+ this.touch.emit();
745
+ else
746
+ this.onTagBlur();
747
+ }
615
748
  // Search-mode handlers
616
749
  onSearchInput(event) {
617
750
  if (this.isDisabled())
@@ -620,11 +753,14 @@ class WrSelect {
620
753
  this.searchQuery.set(value);
621
754
  if (!this.open())
622
755
  this.open.set(true);
623
- // A new filter re-tops the virtual window and cursor (the filtered array
624
- // — hence every index — just changed under `activeIndex`).
756
+ // A new filter invalidates every index, so re-seed the cursor onto the first
757
+ // option that still matches. This must happen on BOTH paths: on the
758
+ // projected-`<wr-option>` path the registry keeps filtered-out options (they
759
+ // only self-hide via CSS), so a stale `activeIndex` would leave the cursor on
760
+ // a row the user can no longer see — and Enter would select it.
761
+ this.activeIndex.set(this.firstEnabled());
625
762
  if (this.virtualActive()) {
626
763
  this.scrollTop.set(0);
627
- this.activeIndex.set(this.firstEnabled());
628
764
  this.vlistElement?.scrollTo({ top: 0 });
629
765
  }
630
766
  }
@@ -655,6 +791,23 @@ class WrSelect {
655
791
  return;
656
792
  }
657
793
  }
794
+ // Space belongs to the text field, not to the listbox. The button trigger
795
+ // uses it to pick the active option, but here swallowing it would make a
796
+ // multi-word query ("dump trucks") impossible to type.
797
+ if (event.key === ' ')
798
+ return;
799
+ // Backspace on an empty query drops the last chip, matching tag mode. The
800
+ // button-trigger path only does this while closed, and a chip-search input
801
+ // keeps the panel open, so it never reaches that branch.
802
+ if (event.key === 'Backspace' && this.hasChipSearch() && this.searchQuery() === '' && this.hasSelection()) {
803
+ const chips = this.selectedChips();
804
+ const last = chips[chips.length - 1];
805
+ if (last) {
806
+ event.preventDefault();
807
+ this.removeChip(last.value, event);
808
+ return;
809
+ }
810
+ }
658
811
  // Otherwise route through the same keyboard plumbing as the button trigger.
659
812
  this.onTriggerKey(event);
660
813
  }
@@ -686,23 +839,43 @@ class WrSelect {
686
839
  this.closeOverlay();
687
840
  // Reset the search query when the panel closes so a re-open
688
841
  // doesn't carry a stale filter.
689
- if (this.isSearch())
842
+ if (this.isSearchable())
690
843
  this.searchQuery.set('');
691
844
  }
692
845
  });
693
- // Async loader pipeline (search mode only). `switchMap` cancels
846
+ // One debounced query stream feeds both the async loader and the public
847
+ // `searchChange` output, so they fire on exactly the same cadence.
848
+ // `debounce(() => timer(...))` rather than `debounceTime(this.debounceMs())`:
849
+ // this runs in the constructor, where inputs aren't bound yet, so reading
850
+ // the input eagerly would pin the delay to the default and ignore
851
+ // `[debounceMs]` forever.
852
+ // `skip(1)` sits BEFORE the debounce so it drops the mount-time replay of the
853
+ // empty query and nothing else. Downstream of `debounce` it would instead
854
+ // drop the first *settled* emission — and when a fast typist's first
855
+ // keystrokes coalesce with that initial `''`, the dropped emission is their
856
+ // query, not the replay.
857
+ const debouncedQuery = toObservable(this.searchQuery).pipe(skip(1), debounce(() => timer(this.debounceMs())));
858
+ debouncedQuery.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(query => {
859
+ if (this.isSearchable())
860
+ this.searchChange.emit(query);
861
+ });
862
+ // Async loader pipeline (searchable selects only). `switchMap` cancels
694
863
  // in-flight requests when a new keystroke arrives.
695
- toObservable(this.searchQuery)
696
- .pipe(debounceTime(this.debounceMs()), switchMap(query => {
864
+ debouncedQuery
865
+ .pipe(switchMap(query => {
697
866
  const loader = this.loader();
698
- if (!loader || !this.isSearch() || query.length < this.minChars()) {
699
- this.loading.set(false);
867
+ if (!loader || !this.isSearchable() || query.length < this.minChars()) {
868
+ this.loaderLoading.set(false);
700
869
  return of([]);
701
870
  }
702
- this.loading.set(true);
871
+ this.loaderLoading.set(true);
703
872
  const result = loader(query);
704
873
  const source = isObservable(result) ? result : from(Promise.resolve(result));
705
- return source.pipe(finalize(() => this.loading.set(false)));
874
+ // A loader can hand back a long-lived observable (a store selector,
875
+ // say) that emits and never completes — so the first value lowers the
876
+ // flag. `finalize` still covers completion, errors and cancellation,
877
+ // including a loader that completes without emitting.
878
+ return source.pipe(tap(() => this.loaderLoading.set(false)), finalize(() => this.loaderLoading.set(false)));
706
879
  }), takeUntilDestroyed(this.destroyRef))
707
880
  .subscribe(items => this.loadedOptions.set(items));
708
881
  // Resolve the single-mode trigger label whenever the value or
@@ -723,7 +896,7 @@ class WrSelect {
723
896
  // Search mode: a value picked from a virtualized (or since-unmounted) row
724
897
  // has no registered `<wr-option>`, so derive its label from `displayWith`
725
898
  // — the same function that rendered the row.
726
- this.selectedLabel.set(v != null && this.isSearch() ? this.displayWith()(v) : null);
899
+ this.selectedLabel.set(v != null && this.isSearchable() ? this.displayWith()(v) : null);
727
900
  });
728
901
  // Wire the virtual scroll listener + viewport/row measurement while the panel
729
902
  // is open and virtualized. Re-runs when the panel opens/closes or the virtual
@@ -832,8 +1005,10 @@ class WrSelect {
832
1005
  return;
833
1006
  if (this.hasChips()) {
834
1007
  this.value.set([]);
1008
+ // `isSearchTrigger()`, not `isSearch()` — a searchable single renders the
1009
+ // same × and would otherwise fall through to the no-op below.
835
1010
  }
836
- else if (this.isSearch()) {
1011
+ else if (this.isSearchTrigger()) {
837
1012
  this.value.set(null);
838
1013
  this.searchQuery.set('');
839
1014
  }
@@ -934,8 +1109,13 @@ class WrSelect {
934
1109
  return;
935
1110
  }
936
1111
  // Multi mode: Backspace on closed trigger removes last chip. Tag
937
- // mode has its own input handler — never reaches this branch.
938
- if (event.key === 'Backspace' && this.isMulti() && this.hasSelection()) {
1112
+ // mode has its own input handler — never reaches this branch. A
1113
+ // searchable multi does, so only claim Backspace once its query is empty
1114
+ // — otherwise it would eat a chip and a character at once.
1115
+ if (event.key === 'Backspace' &&
1116
+ this.isMulti() &&
1117
+ this.hasSelection() &&
1118
+ (!this.hasChipSearch() || this.searchQuery() === '')) {
939
1119
  event.preventDefault();
940
1120
  const chips = this.selectedChips();
941
1121
  const last = chips[chips.length - 1];
@@ -976,7 +1156,10 @@ class WrSelect {
976
1156
  break;
977
1157
  }
978
1158
  const list = this.registry();
979
- if (idx >= 0 && idx < list.length && !list[idx].disabled) {
1159
+ // Filtered-out options stay registered (they only self-hide via CSS), so
1160
+ // the hidden check is load-bearing: without it Enter can commit a row
1161
+ // that is not on screen.
1162
+ if (idx >= 0 && idx < list.length && !list[idx].disabled && !this.isOptionHidden(list[idx].getLabel())) {
980
1163
  const id = list[idx].id;
981
1164
  const el = this.overlayRef?.overlayElement.querySelector(`#${CSS.escape(id)}`);
982
1165
  el?.click();
@@ -1122,13 +1305,13 @@ class WrSelect {
1122
1305
  this.overlayRef = null;
1123
1306
  }
1124
1307
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: WrSelect, deps: [], target: i0.ɵɵFactoryTarget.Component });
1125
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.0", type: WrSelect, isStandalone: true, selector: "wr-select", inputs: { placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, clearLabel: { classPropertyName: "clearLabel", publicName: "clearLabel", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, rounded: { classPropertyName: "rounded", publicName: "rounded", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, responsive: { classPropertyName: "responsive", publicName: "responsive", isSignal: true, isRequired: false, transformFunction: null }, mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, displayWith: { classPropertyName: "displayWith", publicName: "displayWith", isSignal: true, isRequired: false, transformFunction: null }, loader: { classPropertyName: "loader", publicName: "loader", isSignal: true, isRequired: false, transformFunction: null }, debounceMs: { classPropertyName: "debounceMs", publicName: "debounceMs", isSignal: true, isRequired: false, transformFunction: null }, minChars: { classPropertyName: "minChars", publicName: "minChars", isSignal: true, isRequired: false, transformFunction: null }, freeText: { classPropertyName: "freeText", publicName: "freeText", isSignal: true, isRequired: false, transformFunction: null }, virtualScroll: { classPropertyName: "virtualScroll", publicName: "virtualScroll", isSignal: true, isRequired: false, transformFunction: null }, rowHeight: { classPropertyName: "rowHeight", publicName: "rowHeight", isSignal: true, isRequired: false, transformFunction: null }, viewportHeight: { classPropertyName: "viewportHeight", publicName: "viewportHeight", isSignal: true, isRequired: false, transformFunction: null }, overscan: { classPropertyName: "overscan", publicName: "overscan", isSignal: true, isRequired: false, transformFunction: null }, noResultsText: { classPropertyName: "noResultsText", publicName: "noResultsText", isSignal: true, isRequired: false, transformFunction: null }, loadingText: { classPropertyName: "loadingText", publicName: "loadingText", isSignal: true, isRequired: false, transformFunction: null }, separators: { classPropertyName: "separators", publicName: "separators", isSignal: true, isRequired: false, transformFunction: null }, allowDuplicates: { classPropertyName: "allowDuplicates", publicName: "allowDuplicates", isSignal: true, isRequired: false, transformFunction: null }, validate: { classPropertyName: "validate", publicName: "validate", isSignal: true, isRequired: false, transformFunction: null }, clearable: { classPropertyName: "clearable", publicName: "clearable", isSignal: true, isRequired: false, transformFunction: null }, maxItems: { classPropertyName: "maxItems", publicName: "maxItems", isSignal: true, isRequired: false, transformFunction: null }, maxTagCount: { classPropertyName: "maxTagCount", publicName: "maxTagCount", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touch: "touch" }, host: { properties: { "class": "classes()" } }, providers: [
1308
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.0", type: WrSelect, isStandalone: true, selector: "wr-select", inputs: { placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, clearLabel: { classPropertyName: "clearLabel", publicName: "clearLabel", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, rounded: { classPropertyName: "rounded", publicName: "rounded", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, responsive: { classPropertyName: "responsive", publicName: "responsive", isSignal: true, isRequired: false, transformFunction: null }, mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, searchable: { classPropertyName: "searchable", publicName: "searchable", isSignal: true, isRequired: false, transformFunction: null }, searchQuery: { classPropertyName: "searchQuery", publicName: "searchQuery", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, displayWith: { classPropertyName: "displayWith", publicName: "displayWith", isSignal: true, isRequired: false, transformFunction: null }, loader: { classPropertyName: "loader", publicName: "loader", isSignal: true, isRequired: false, transformFunction: null }, serverSearch: { classPropertyName: "serverSearch", publicName: "serverSearch", isSignal: true, isRequired: false, transformFunction: null }, debounceMs: { classPropertyName: "debounceMs", publicName: "debounceMs", isSignal: true, isRequired: false, transformFunction: null }, minChars: { classPropertyName: "minChars", publicName: "minChars", isSignal: true, isRequired: false, transformFunction: null }, freeText: { classPropertyName: "freeText", publicName: "freeText", isSignal: true, isRequired: false, transformFunction: null }, virtualScroll: { classPropertyName: "virtualScroll", publicName: "virtualScroll", isSignal: true, isRequired: false, transformFunction: null }, rowHeight: { classPropertyName: "rowHeight", publicName: "rowHeight", isSignal: true, isRequired: false, transformFunction: null }, viewportHeight: { classPropertyName: "viewportHeight", publicName: "viewportHeight", isSignal: true, isRequired: false, transformFunction: null }, overscan: { classPropertyName: "overscan", publicName: "overscan", isSignal: true, isRequired: false, transformFunction: null }, noResultsText: { classPropertyName: "noResultsText", publicName: "noResultsText", isSignal: true, isRequired: false, transformFunction: null }, loadingText: { classPropertyName: "loadingText", publicName: "loadingText", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, separators: { classPropertyName: "separators", publicName: "separators", isSignal: true, isRequired: false, transformFunction: null }, allowDuplicates: { classPropertyName: "allowDuplicates", publicName: "allowDuplicates", isSignal: true, isRequired: false, transformFunction: null }, validate: { classPropertyName: "validate", publicName: "validate", isSignal: true, isRequired: false, transformFunction: null }, clearable: { classPropertyName: "clearable", publicName: "clearable", isSignal: true, isRequired: false, transformFunction: null }, maxItems: { classPropertyName: "maxItems", publicName: "maxItems", isSignal: true, isRequired: false, transformFunction: null }, maxTagCount: { classPropertyName: "maxTagCount", publicName: "maxTagCount", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { searchQuery: "searchQueryChange", searchChange: "searchChange", value: "valueChange", touch: "touch" }, host: { properties: { "class": "classes()" } }, providers: [
1126
1309
  {
1127
1310
  provide: WR_SELECT,
1128
1311
  // eslint-disable-next-line @angular-eslint/no-forward-ref
1129
1312
  useExisting: forwardRef(() => WrSelect),
1130
1313
  },
1131
- ], queries: [{ propertyName: "projectedOptions", predicate: WrOption, isSignal: true }], viewQueries: [{ propertyName: "panelTpl", first: true, predicate: ["panelTpl"], descendants: true, read: TemplateRef, isSignal: true }, { propertyName: "tagInputEl", first: true, predicate: ["tagInput"], descendants: true, isSignal: true }, { propertyName: "searchInputEl", first: true, predicate: ["searchInput"], descendants: true, isSignal: true }], ngImport: i0, template: "@if (isSearch()) {\n <!-- Search mode: a real text input drives a filtered overlay panel.\n The wrapper isn't itself focusable \u2014 the input is the tabstop. -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div class=\"wr-select__trigger wr-select__trigger--search\" (click)=\"focusSearchInput()\">\n <input\n #searchInput\n type=\"text\"\n role=\"combobox\"\n class=\"wr-select__search-input\"\n autocomplete=\"off\"\n spellcheck=\"false\"\n [disabled]=\"isDisabled()\"\n [attr.aria-haspopup]=\"'listbox'\"\n [attr.aria-expanded]=\"open()\"\n [attr.aria-controls]=\"listboxId\"\n [attr.aria-activedescendant]=\"open() ? activeOptionId() || null : null\"\n [placeholder]=\"resolvedPlaceholder()\"\n [value]=\"searchDisplay()\"\n (input)=\"onSearchInput($event)\"\n (focus)=\"onSearchFocus()\"\n (keydown)=\"onSearchKey($event)\"\n (blur)=\"touch.emit()\"\n />\n @if (clearable() && hasSelection() && !isDisabled()) {\n <span\n class=\"wr-select__clear\"\n role=\"button\"\n [attr.aria-label]=\"resolvedClear()\"\n tabindex=\"-1\"\n (click)=\"clearAll($event)\"\n (keydown.enter)=\"clearAll($event)\"\n (keydown.space)=\"clearAll($event)\"\n >\n <svg\n class=\"wr-icon__svg\"\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"M18 6 6 18\" />\n <path d=\"m6 6 12 12\" />\n </svg>\n </span>\n }\n <svg\n class=\"wr-icon__svg wr-select__chevron\"\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"m6 9 6 6 6-6\" />\n </svg>\n </div>\n} @else if (isTag()) {\n <!-- Tag mode: a focusable input lives inside, so the wrapper itself\n only exists to host chips and forward whitespace clicks to the\n input. Keyboard users tab straight into the input \u2014 no need for a\n separate role. The click handler is a mouse-only convenience. -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div\n class=\"wr-select__trigger wr-select__trigger--tag\"\n [attr.aria-disabled]=\"isDisabled() || null\"\n (click)=\"focusTagInput()\"\n >\n <span class=\"wr-select__chips\">\n @for (chip of visibleChips(); track $index) {\n <span class=\"wr-select__chip\">\n <span class=\"wr-select__chip-label\">{{ chip.label }}</span>\n <span\n class=\"wr-select__chip-remove\"\n role=\"button\"\n [attr.aria-label]=\"chipRemoveLabel({ label: chip.label })\"\n tabindex=\"-1\"\n (click)=\"removeChip(chip.value, $event)\"\n (keydown.enter)=\"removeChip(chip.value, $event)\"\n (keydown.space)=\"removeChip(chip.value, $event)\"\n >\n <svg\n class=\"wr-icon__svg\"\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"M18 6 6 18\" />\n <path d=\"m6 6 12 12\" />\n </svg>\n </span>\n </span>\n }\n @if (hiddenChipCount(); as extra) {\n <span class=\"wr-select__chip wr-select__chip--more\">+{{ extra }} more</span>\n }\n <input\n #tagInput\n type=\"text\"\n class=\"wr-select__tag-input\"\n [value]=\"draft()\"\n [disabled]=\"isDisabled() || atCapacity()\"\n [placeholder]=\"hasSelection() ? '' : resolvedPlaceholder()\"\n (input)=\"onTagInput($event)\"\n (keydown)=\"onTagKeydown($event)\"\n (paste)=\"onTagPaste($event)\"\n (blur)=\"onTagBlur()\"\n />\n </span>\n\n @if (clearable() && hasSelection() && !isDisabled()) {\n <span\n class=\"wr-select__clear\"\n role=\"button\"\n [attr.aria-label]=\"resolvedClear()\"\n tabindex=\"-1\"\n (click)=\"clearAll($event)\"\n (keydown.enter)=\"clearAll($event)\"\n (keydown.space)=\"clearAll($event)\"\n >\n <svg\n class=\"wr-icon__svg\"\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"M18 6 6 18\" />\n <path d=\"m6 6 12 12\" />\n </svg>\n </span>\n }\n </div>\n} @else {\n <button\n type=\"button\"\n class=\"wr-select__trigger\"\n role=\"combobox\"\n [disabled]=\"isDisabled()\"\n [attr.aria-haspopup]=\"'listbox'\"\n [attr.aria-expanded]=\"open()\"\n [attr.aria-controls]=\"listboxId\"\n [attr.aria-multiselectable]=\"isMulti() ? 'true' : null\"\n [attr.aria-activedescendant]=\"open() ? activeOptionId() || null : null\"\n (click)=\"onTriggerClick()\"\n (keydown)=\"onTriggerKey($event)\"\n (blur)=\"touch.emit()\"\n >\n @if (isMulti()) {\n @if (hasSelection()) {\n <span class=\"wr-select__chips\">\n @for (chip of visibleChips(); track $index) {\n <span class=\"wr-select__chip\">\n <span class=\"wr-select__chip-label\">{{ chip.label }}</span>\n <span\n class=\"wr-select__chip-remove\"\n role=\"button\"\n [attr.aria-label]=\"chipRemoveLabel({ label: chip.label })\"\n tabindex=\"-1\"\n (click)=\"removeChip(chip.value, $event)\"\n (keydown.enter)=\"removeChip(chip.value, $event)\"\n (keydown.space)=\"removeChip(chip.value, $event)\"\n >\n <svg\n class=\"wr-icon__svg\"\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"M18 6 6 18\" />\n <path d=\"m6 6 12 12\" />\n </svg>\n </span>\n </span>\n }\n @if (hiddenChipCount(); as extra) {\n <span class=\"wr-select__chip wr-select__chip--more\">+{{ extra }} more</span>\n }\n </span>\n } @else {\n <span class=\"wr-select__placeholder\">{{ resolvedPlaceholder() }}</span>\n }\n } @else {\n @if (selectedLabel(); as label) {\n <span class=\"wr-select__value\">{{ label }}</span>\n } @else {\n <span class=\"wr-select__placeholder\">{{ resolvedPlaceholder() }}</span>\n }\n }\n\n @if (isMulti() && clearable() && hasSelection() && !isDisabled()) {\n <span\n class=\"wr-select__clear\"\n role=\"button\"\n [attr.aria-label]=\"resolvedClear()\"\n tabindex=\"-1\"\n (click)=\"clearAll($event)\"\n (keydown.enter)=\"clearAll($event)\"\n (keydown.space)=\"clearAll($event)\"\n >\n <svg\n class=\"wr-icon__svg\"\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"M18 6 6 18\" />\n <path d=\"m6 6 12 12\" />\n </svg>\n </span>\n }\n\n <svg\n class=\"wr-icon__svg wr-select__chevron\"\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"m6 9 6 6 6-6\" />\n </svg>\n </button>\n}\n\n<ng-template #panelTpl>\n <div\n class=\"wr-select-panel\"\n [class.wr-select-panel--sm]=\"size() === 'sm'\"\n [class.wr-select-panel--lg]=\"size() === 'lg'\"\n [class.wr-select-panel--virtual]=\"virtualActive()\"\n role=\"listbox\"\n [attr.id]=\"listboxId\"\n [attr.aria-multiselectable]=\"isMulti() ? 'true' : null\"\n >\n @if (loading()) {\n <div class=\"wr-select-panel__loading\" aria-busy=\"true\">{{ resolvedLoading() }}</div>\n }\n\n @if (virtualActive()) {\n <!-- Windowed data-array rows: plain role=\"option\" elements (not <wr-option>),\n navigated via the input's aria-activedescendant, selected by value. -->\n <div class=\"wr-select-panel__vlist\" [style.max-height]=\"resolvedViewportHeight()\">\n @if (virtualWindow().padTop > 0) {\n <div class=\"wr-select-panel__vspacer\" aria-hidden=\"true\" [style.height.px]=\"virtualWindow().padTop\"></div>\n }\n @for (entry of windowOptions(); track entry.index) {\n <!-- Keyboard handled by the search input via aria-activedescendant; the\n row is a pointer target only. -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div\n role=\"option\"\n class=\"wr-option\"\n [id]=\"virtualRowId(entry.index)\"\n [class.wr-option--active]=\"entry.index === activeIndex()\"\n [class.wr-option--selected]=\"isSelected(entry.item)\"\n [attr.aria-selected]=\"isSelected(entry.item)\"\n [attr.aria-setsize]=\"filteredDynamicOptions().length\"\n [attr.aria-posinset]=\"entry.index + 1\"\n (click)=\"selectOption(entry.item)\"\n >\n {{ displayWith()(entry.item) }}\n </div>\n }\n @if (virtualWindow().padBottom > 0) {\n <div class=\"wr-select-panel__vspacer\" aria-hidden=\"true\" [style.height.px]=\"virtualWindow().padBottom\"></div>\n }\n </div>\n } @else {\n <!-- Dynamic options from [options] or async [loader] (search mode).\n Rendered as wr-option so the standard registration / filter\n pipeline applies uniformly. -->\n @for (item of dynamicOptions(); track $index) {\n <wr-option [value]=\"item\">{{ displayWith()(item) }}</wr-option>\n }\n }\n\n <!-- Projected static options \u2014 always present (a conditional `<ng-content>`\n would drop projected content). Empty whenever `virtualActive`. -->\n <ng-content />\n\n @if (hasNoResults()) {\n <div class=\"wr-select-panel__empty\">{{ resolvedNoResults() }}</div>\n }\n </div>\n</ng-template>\n", dependencies: [{ kind: "component", type: WrOption, selector: "wr-option", inputs: ["value", "disabled"] }], encapsulation: i0.ViewEncapsulation.None });
1314
+ ], queries: [{ propertyName: "projectedOptions", predicate: WrOption, isSignal: true }], viewQueries: [{ propertyName: "panelTpl", first: true, predicate: ["panelTpl"], descendants: true, read: TemplateRef, isSignal: true }, { propertyName: "tagInputEl", first: true, predicate: ["tagInput"], descendants: true, isSignal: true }, { propertyName: "searchInputEl", first: true, predicate: ["searchInput"], descendants: true, isSignal: true }], ngImport: i0, template: "@if (isSearchTrigger()) {\n <!-- Search mode (or a searchable single): a real text input drives a filtered\n overlay panel. The wrapper isn't itself focusable \u2014 the input is the\n tabstop. -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div class=\"wr-select__trigger wr-select__trigger--search\" (click)=\"focusSearchInput()\">\n <input\n #searchInput\n type=\"text\"\n role=\"combobox\"\n class=\"wr-select__search-input\"\n autocomplete=\"off\"\n spellcheck=\"false\"\n [disabled]=\"isDisabled()\"\n [attr.aria-haspopup]=\"'listbox'\"\n [attr.aria-expanded]=\"open()\"\n [attr.aria-controls]=\"listboxId\"\n [attr.aria-activedescendant]=\"open() ? activeOptionId() || null : null\"\n [placeholder]=\"resolvedPlaceholder()\"\n [value]=\"searchDisplay()\"\n (input)=\"onSearchInput($event)\"\n (focus)=\"onSearchFocus()\"\n (keydown)=\"onSearchKey($event)\"\n (blur)=\"touch.emit()\"\n />\n @if (clearable() && hasSelection() && !isDisabled()) {\n <span\n class=\"wr-select__clear\"\n role=\"button\"\n [attr.aria-label]=\"resolvedClear()\"\n tabindex=\"-1\"\n (click)=\"clearAll($event)\"\n (keydown.enter)=\"clearAll($event)\"\n (keydown.space)=\"clearAll($event)\"\n >\n <svg\n class=\"wr-icon__svg\"\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"M18 6 6 18\" />\n <path d=\"m6 6 12 12\" />\n </svg>\n </span>\n }\n <svg\n class=\"wr-icon__svg wr-select__chevron\"\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"m6 9 6 6 6-6\" />\n </svg>\n </div>\n} @else if (isTag() || hasChipSearch()) {\n <!-- Chip trigger \u2014 tag mode, or a searchable multi. A focusable input lives\n inside, so the wrapper itself only exists to host chips and forward\n whitespace clicks to the input. Tag mode needs no role (users tab\n straight into the input); the searchable multi is a real combobox, so it\n carries the listbox wiring. The click handler is a mouse-only\n convenience. -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div\n class=\"wr-select__trigger wr-select__trigger--tag\"\n [attr.aria-disabled]=\"isDisabled() || null\"\n (click)=\"focusTagInput()\"\n >\n <span class=\"wr-select__chips\">\n @for (chip of visibleChips(); track $index) {\n <span class=\"wr-select__chip\">\n <span class=\"wr-select__chip-label\">{{ chip.label }}</span>\n <span\n class=\"wr-select__chip-remove\"\n role=\"button\"\n [attr.aria-label]=\"chipRemoveLabel({ label: chip.label })\"\n tabindex=\"-1\"\n (click)=\"removeChip(chip.value, $event)\"\n (keydown.enter)=\"removeChip(chip.value, $event)\"\n (keydown.space)=\"removeChip(chip.value, $event)\"\n >\n <svg\n class=\"wr-icon__svg\"\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"M18 6 6 18\" />\n <path d=\"m6 6 12 12\" />\n </svg>\n </span>\n </span>\n }\n @if (hiddenChipCount(); as extra) {\n <span class=\"wr-select__chip wr-select__chip--more\">+{{ extra }} more</span>\n }\n <input\n #tagInput\n type=\"text\"\n class=\"wr-select__tag-input\"\n autocomplete=\"off\"\n spellcheck=\"false\"\n [value]=\"hasChipSearch() ? searchQuery() : draft()\"\n [disabled]=\"isDisabled() || (isTag() && atCapacity())\"\n [placeholder]=\"hasSelection() ? '' : resolvedPlaceholder()\"\n [attr.role]=\"hasChipSearch() ? 'combobox' : null\"\n [attr.aria-haspopup]=\"hasChipSearch() ? 'listbox' : null\"\n [attr.aria-expanded]=\"hasChipSearch() ? open() : null\"\n [attr.aria-controls]=\"hasChipSearch() ? listboxId : null\"\n [attr.aria-activedescendant]=\"hasChipSearch() && open() ? activeOptionId() || null : null\"\n (input)=\"onChipsInput($event)\"\n (keydown)=\"onChipsKeydown($event)\"\n (paste)=\"onChipsPaste($event)\"\n (focus)=\"onChipsFocus()\"\n (blur)=\"onChipsBlur()\"\n />\n </span>\n\n @if (clearable() && hasSelection() && !isDisabled()) {\n <span\n class=\"wr-select__clear\"\n role=\"button\"\n [attr.aria-label]=\"resolvedClear()\"\n tabindex=\"-1\"\n (click)=\"clearAll($event)\"\n (keydown.enter)=\"clearAll($event)\"\n (keydown.space)=\"clearAll($event)\"\n >\n <svg\n class=\"wr-icon__svg\"\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"M18 6 6 18\" />\n <path d=\"m6 6 12 12\" />\n </svg>\n </span>\n }\n </div>\n} @else {\n <button\n type=\"button\"\n class=\"wr-select__trigger\"\n role=\"combobox\"\n [disabled]=\"isDisabled()\"\n [attr.aria-haspopup]=\"'listbox'\"\n [attr.aria-expanded]=\"open()\"\n [attr.aria-controls]=\"listboxId\"\n [attr.aria-multiselectable]=\"isMulti() ? 'true' : null\"\n [attr.aria-activedescendant]=\"open() ? activeOptionId() || null : null\"\n (click)=\"onTriggerClick()\"\n (keydown)=\"onTriggerKey($event)\"\n (blur)=\"touch.emit()\"\n >\n @if (isMulti()) {\n @if (hasSelection()) {\n <span class=\"wr-select__chips\">\n @for (chip of visibleChips(); track $index) {\n <span class=\"wr-select__chip\">\n <span class=\"wr-select__chip-label\">{{ chip.label }}</span>\n <span\n class=\"wr-select__chip-remove\"\n role=\"button\"\n [attr.aria-label]=\"chipRemoveLabel({ label: chip.label })\"\n tabindex=\"-1\"\n (click)=\"removeChip(chip.value, $event)\"\n (keydown.enter)=\"removeChip(chip.value, $event)\"\n (keydown.space)=\"removeChip(chip.value, $event)\"\n >\n <svg\n class=\"wr-icon__svg\"\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"M18 6 6 18\" />\n <path d=\"m6 6 12 12\" />\n </svg>\n </span>\n </span>\n }\n @if (hiddenChipCount(); as extra) {\n <span class=\"wr-select__chip wr-select__chip--more\">+{{ extra }} more</span>\n }\n </span>\n } @else {\n <span class=\"wr-select__placeholder\">{{ resolvedPlaceholder() }}</span>\n }\n } @else {\n @if (selectedLabel(); as label) {\n <span class=\"wr-select__value\">{{ label }}</span>\n } @else {\n <span class=\"wr-select__placeholder\">{{ resolvedPlaceholder() }}</span>\n }\n }\n\n @if (isMulti() && clearable() && hasSelection() && !isDisabled()) {\n <span\n class=\"wr-select__clear\"\n role=\"button\"\n [attr.aria-label]=\"resolvedClear()\"\n tabindex=\"-1\"\n (click)=\"clearAll($event)\"\n (keydown.enter)=\"clearAll($event)\"\n (keydown.space)=\"clearAll($event)\"\n >\n <svg\n class=\"wr-icon__svg\"\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"M18 6 6 18\" />\n <path d=\"m6 6 12 12\" />\n </svg>\n </span>\n }\n\n <svg\n class=\"wr-icon__svg wr-select__chevron\"\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"m6 9 6 6 6-6\" />\n </svg>\n </button>\n}\n\n<ng-template #panelTpl>\n <div\n class=\"wr-select-panel\"\n [class.wr-select-panel--sm]=\"size() === 'sm'\"\n [class.wr-select-panel--lg]=\"size() === 'lg'\"\n [class.wr-select-panel--virtual]=\"virtualActive()\"\n role=\"listbox\"\n [attr.id]=\"listboxId\"\n [attr.aria-multiselectable]=\"isMulti() ? 'true' : null\"\n >\n @if (isLoading()) {\n <div class=\"wr-select-panel__loading\" aria-busy=\"true\">{{ resolvedLoading() }}</div>\n }\n\n @if (virtualActive()) {\n <!-- Windowed data-array rows: plain role=\"option\" elements (not <wr-option>),\n navigated via the input's aria-activedescendant, selected by value. -->\n <div class=\"wr-select-panel__vlist\" [style.max-height]=\"resolvedViewportHeight()\">\n @if (virtualWindow().padTop > 0) {\n <div class=\"wr-select-panel__vspacer\" aria-hidden=\"true\" [style.height.px]=\"virtualWindow().padTop\"></div>\n }\n @for (entry of windowOptions(); track entry.index) {\n <!-- Keyboard handled by the search input via aria-activedescendant; the\n row is a pointer target only. -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div\n role=\"option\"\n class=\"wr-option\"\n [id]=\"virtualRowId(entry.index)\"\n [class.wr-option--active]=\"entry.index === activeIndex()\"\n [class.wr-option--selected]=\"isSelected(entry.item)\"\n [attr.aria-selected]=\"isSelected(entry.item)\"\n [attr.aria-setsize]=\"filteredDynamicOptions().length\"\n [attr.aria-posinset]=\"entry.index + 1\"\n (click)=\"selectOption(entry.item)\"\n >\n {{ displayWith()(entry.item) }}\n </div>\n }\n @if (virtualWindow().padBottom > 0) {\n <div class=\"wr-select-panel__vspacer\" aria-hidden=\"true\" [style.height.px]=\"virtualWindow().padBottom\"></div>\n }\n </div>\n } @else {\n <!-- Dynamic options from [options] or async [loader] (search mode).\n Rendered as wr-option so the standard registration / filter\n pipeline applies uniformly. -->\n @for (item of dynamicOptions(); track $index) {\n <wr-option [value]=\"item\">{{ displayWith()(item) }}</wr-option>\n }\n }\n\n <!-- Projected static options \u2014 always present (a conditional `<ng-content>`\n would drop projected content). Empty whenever `virtualActive`. -->\n <ng-content />\n\n @if (hasNoResults()) {\n <div class=\"wr-select-panel__empty\">{{ resolvedNoResults() }}</div>\n }\n </div>\n</ng-template>\n", dependencies: [{ kind: "component", type: WrOption, selector: "wr-option", inputs: ["value", "disabled"] }], encapsulation: i0.ViewEncapsulation.None });
1132
1315
  }
1133
1316
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: WrSelect, decorators: [{
1134
1317
  type: Component,
@@ -1138,8 +1321,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImpor
1138
1321
  // eslint-disable-next-line @angular-eslint/no-forward-ref
1139
1322
  useExisting: forwardRef(() => WrSelect),
1140
1323
  },
1141
- ], template: "@if (isSearch()) {\n <!-- Search mode: a real text input drives a filtered overlay panel.\n The wrapper isn't itself focusable \u2014 the input is the tabstop. -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div class=\"wr-select__trigger wr-select__trigger--search\" (click)=\"focusSearchInput()\">\n <input\n #searchInput\n type=\"text\"\n role=\"combobox\"\n class=\"wr-select__search-input\"\n autocomplete=\"off\"\n spellcheck=\"false\"\n [disabled]=\"isDisabled()\"\n [attr.aria-haspopup]=\"'listbox'\"\n [attr.aria-expanded]=\"open()\"\n [attr.aria-controls]=\"listboxId\"\n [attr.aria-activedescendant]=\"open() ? activeOptionId() || null : null\"\n [placeholder]=\"resolvedPlaceholder()\"\n [value]=\"searchDisplay()\"\n (input)=\"onSearchInput($event)\"\n (focus)=\"onSearchFocus()\"\n (keydown)=\"onSearchKey($event)\"\n (blur)=\"touch.emit()\"\n />\n @if (clearable() && hasSelection() && !isDisabled()) {\n <span\n class=\"wr-select__clear\"\n role=\"button\"\n [attr.aria-label]=\"resolvedClear()\"\n tabindex=\"-1\"\n (click)=\"clearAll($event)\"\n (keydown.enter)=\"clearAll($event)\"\n (keydown.space)=\"clearAll($event)\"\n >\n <svg\n class=\"wr-icon__svg\"\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"M18 6 6 18\" />\n <path d=\"m6 6 12 12\" />\n </svg>\n </span>\n }\n <svg\n class=\"wr-icon__svg wr-select__chevron\"\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"m6 9 6 6 6-6\" />\n </svg>\n </div>\n} @else if (isTag()) {\n <!-- Tag mode: a focusable input lives inside, so the wrapper itself\n only exists to host chips and forward whitespace clicks to the\n input. Keyboard users tab straight into the input \u2014 no need for a\n separate role. The click handler is a mouse-only convenience. -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div\n class=\"wr-select__trigger wr-select__trigger--tag\"\n [attr.aria-disabled]=\"isDisabled() || null\"\n (click)=\"focusTagInput()\"\n >\n <span class=\"wr-select__chips\">\n @for (chip of visibleChips(); track $index) {\n <span class=\"wr-select__chip\">\n <span class=\"wr-select__chip-label\">{{ chip.label }}</span>\n <span\n class=\"wr-select__chip-remove\"\n role=\"button\"\n [attr.aria-label]=\"chipRemoveLabel({ label: chip.label })\"\n tabindex=\"-1\"\n (click)=\"removeChip(chip.value, $event)\"\n (keydown.enter)=\"removeChip(chip.value, $event)\"\n (keydown.space)=\"removeChip(chip.value, $event)\"\n >\n <svg\n class=\"wr-icon__svg\"\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"M18 6 6 18\" />\n <path d=\"m6 6 12 12\" />\n </svg>\n </span>\n </span>\n }\n @if (hiddenChipCount(); as extra) {\n <span class=\"wr-select__chip wr-select__chip--more\">+{{ extra }} more</span>\n }\n <input\n #tagInput\n type=\"text\"\n class=\"wr-select__tag-input\"\n [value]=\"draft()\"\n [disabled]=\"isDisabled() || atCapacity()\"\n [placeholder]=\"hasSelection() ? '' : resolvedPlaceholder()\"\n (input)=\"onTagInput($event)\"\n (keydown)=\"onTagKeydown($event)\"\n (paste)=\"onTagPaste($event)\"\n (blur)=\"onTagBlur()\"\n />\n </span>\n\n @if (clearable() && hasSelection() && !isDisabled()) {\n <span\n class=\"wr-select__clear\"\n role=\"button\"\n [attr.aria-label]=\"resolvedClear()\"\n tabindex=\"-1\"\n (click)=\"clearAll($event)\"\n (keydown.enter)=\"clearAll($event)\"\n (keydown.space)=\"clearAll($event)\"\n >\n <svg\n class=\"wr-icon__svg\"\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"M18 6 6 18\" />\n <path d=\"m6 6 12 12\" />\n </svg>\n </span>\n }\n </div>\n} @else {\n <button\n type=\"button\"\n class=\"wr-select__trigger\"\n role=\"combobox\"\n [disabled]=\"isDisabled()\"\n [attr.aria-haspopup]=\"'listbox'\"\n [attr.aria-expanded]=\"open()\"\n [attr.aria-controls]=\"listboxId\"\n [attr.aria-multiselectable]=\"isMulti() ? 'true' : null\"\n [attr.aria-activedescendant]=\"open() ? activeOptionId() || null : null\"\n (click)=\"onTriggerClick()\"\n (keydown)=\"onTriggerKey($event)\"\n (blur)=\"touch.emit()\"\n >\n @if (isMulti()) {\n @if (hasSelection()) {\n <span class=\"wr-select__chips\">\n @for (chip of visibleChips(); track $index) {\n <span class=\"wr-select__chip\">\n <span class=\"wr-select__chip-label\">{{ chip.label }}</span>\n <span\n class=\"wr-select__chip-remove\"\n role=\"button\"\n [attr.aria-label]=\"chipRemoveLabel({ label: chip.label })\"\n tabindex=\"-1\"\n (click)=\"removeChip(chip.value, $event)\"\n (keydown.enter)=\"removeChip(chip.value, $event)\"\n (keydown.space)=\"removeChip(chip.value, $event)\"\n >\n <svg\n class=\"wr-icon__svg\"\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"M18 6 6 18\" />\n <path d=\"m6 6 12 12\" />\n </svg>\n </span>\n </span>\n }\n @if (hiddenChipCount(); as extra) {\n <span class=\"wr-select__chip wr-select__chip--more\">+{{ extra }} more</span>\n }\n </span>\n } @else {\n <span class=\"wr-select__placeholder\">{{ resolvedPlaceholder() }}</span>\n }\n } @else {\n @if (selectedLabel(); as label) {\n <span class=\"wr-select__value\">{{ label }}</span>\n } @else {\n <span class=\"wr-select__placeholder\">{{ resolvedPlaceholder() }}</span>\n }\n }\n\n @if (isMulti() && clearable() && hasSelection() && !isDisabled()) {\n <span\n class=\"wr-select__clear\"\n role=\"button\"\n [attr.aria-label]=\"resolvedClear()\"\n tabindex=\"-1\"\n (click)=\"clearAll($event)\"\n (keydown.enter)=\"clearAll($event)\"\n (keydown.space)=\"clearAll($event)\"\n >\n <svg\n class=\"wr-icon__svg\"\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"M18 6 6 18\" />\n <path d=\"m6 6 12 12\" />\n </svg>\n </span>\n }\n\n <svg\n class=\"wr-icon__svg wr-select__chevron\"\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"m6 9 6 6 6-6\" />\n </svg>\n </button>\n}\n\n<ng-template #panelTpl>\n <div\n class=\"wr-select-panel\"\n [class.wr-select-panel--sm]=\"size() === 'sm'\"\n [class.wr-select-panel--lg]=\"size() === 'lg'\"\n [class.wr-select-panel--virtual]=\"virtualActive()\"\n role=\"listbox\"\n [attr.id]=\"listboxId\"\n [attr.aria-multiselectable]=\"isMulti() ? 'true' : null\"\n >\n @if (loading()) {\n <div class=\"wr-select-panel__loading\" aria-busy=\"true\">{{ resolvedLoading() }}</div>\n }\n\n @if (virtualActive()) {\n <!-- Windowed data-array rows: plain role=\"option\" elements (not <wr-option>),\n navigated via the input's aria-activedescendant, selected by value. -->\n <div class=\"wr-select-panel__vlist\" [style.max-height]=\"resolvedViewportHeight()\">\n @if (virtualWindow().padTop > 0) {\n <div class=\"wr-select-panel__vspacer\" aria-hidden=\"true\" [style.height.px]=\"virtualWindow().padTop\"></div>\n }\n @for (entry of windowOptions(); track entry.index) {\n <!-- Keyboard handled by the search input via aria-activedescendant; the\n row is a pointer target only. -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div\n role=\"option\"\n class=\"wr-option\"\n [id]=\"virtualRowId(entry.index)\"\n [class.wr-option--active]=\"entry.index === activeIndex()\"\n [class.wr-option--selected]=\"isSelected(entry.item)\"\n [attr.aria-selected]=\"isSelected(entry.item)\"\n [attr.aria-setsize]=\"filteredDynamicOptions().length\"\n [attr.aria-posinset]=\"entry.index + 1\"\n (click)=\"selectOption(entry.item)\"\n >\n {{ displayWith()(entry.item) }}\n </div>\n }\n @if (virtualWindow().padBottom > 0) {\n <div class=\"wr-select-panel__vspacer\" aria-hidden=\"true\" [style.height.px]=\"virtualWindow().padBottom\"></div>\n }\n </div>\n } @else {\n <!-- Dynamic options from [options] or async [loader] (search mode).\n Rendered as wr-option so the standard registration / filter\n pipeline applies uniformly. -->\n @for (item of dynamicOptions(); track $index) {\n <wr-option [value]=\"item\">{{ displayWith()(item) }}</wr-option>\n }\n }\n\n <!-- Projected static options \u2014 always present (a conditional `<ng-content>`\n would drop projected content). Empty whenever `virtualActive`. -->\n <ng-content />\n\n @if (hasNoResults()) {\n <div class=\"wr-select-panel__empty\">{{ resolvedNoResults() }}</div>\n }\n </div>\n</ng-template>\n" }]
1142
- }], ctorParameters: () => [], propDecorators: { placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], clearLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearLabel", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], rounded: [{ type: i0.Input, args: [{ isSignal: true, alias: "rounded", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], responsive: [{ type: i0.Input, args: [{ isSignal: true, alias: "responsive", required: false }] }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], displayWith: [{ type: i0.Input, args: [{ isSignal: true, alias: "displayWith", required: false }] }], loader: [{ type: i0.Input, args: [{ isSignal: true, alias: "loader", required: false }] }], debounceMs: [{ type: i0.Input, args: [{ isSignal: true, alias: "debounceMs", required: false }] }], minChars: [{ type: i0.Input, args: [{ isSignal: true, alias: "minChars", required: false }] }], freeText: [{ type: i0.Input, args: [{ isSignal: true, alias: "freeText", required: false }] }], virtualScroll: [{ type: i0.Input, args: [{ isSignal: true, alias: "virtualScroll", required: false }] }], rowHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowHeight", required: false }] }], viewportHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "viewportHeight", required: false }] }], overscan: [{ type: i0.Input, args: [{ isSignal: true, alias: "overscan", required: false }] }], noResultsText: [{ type: i0.Input, args: [{ isSignal: true, alias: "noResultsText", required: false }] }], loadingText: [{ type: i0.Input, args: [{ isSignal: true, alias: "loadingText", required: false }] }], separators: [{ type: i0.Input, args: [{ isSignal: true, alias: "separators", required: false }] }], allowDuplicates: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowDuplicates", required: false }] }], validate: [{ type: i0.Input, args: [{ isSignal: true, alias: "validate", required: false }] }], clearable: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearable", required: false }] }], maxItems: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxItems", required: false }] }], maxTagCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxTagCount", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], touch: [{ type: i0.Output, args: ["touch"] }], projectedOptions: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => WrOption), { isSignal: true }] }], panelTpl: [{ type: i0.ViewChild, args: ['panelTpl', { ...{ read: TemplateRef }, isSignal: true }] }], tagInputEl: [{ type: i0.ViewChild, args: ['tagInput', { isSignal: true }] }], searchInputEl: [{ type: i0.ViewChild, args: ['searchInput', { isSignal: true }] }] } });
1324
+ ], template: "@if (isSearchTrigger()) {\n <!-- Search mode (or a searchable single): a real text input drives a filtered\n overlay panel. The wrapper isn't itself focusable \u2014 the input is the\n tabstop. -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div class=\"wr-select__trigger wr-select__trigger--search\" (click)=\"focusSearchInput()\">\n <input\n #searchInput\n type=\"text\"\n role=\"combobox\"\n class=\"wr-select__search-input\"\n autocomplete=\"off\"\n spellcheck=\"false\"\n [disabled]=\"isDisabled()\"\n [attr.aria-haspopup]=\"'listbox'\"\n [attr.aria-expanded]=\"open()\"\n [attr.aria-controls]=\"listboxId\"\n [attr.aria-activedescendant]=\"open() ? activeOptionId() || null : null\"\n [placeholder]=\"resolvedPlaceholder()\"\n [value]=\"searchDisplay()\"\n (input)=\"onSearchInput($event)\"\n (focus)=\"onSearchFocus()\"\n (keydown)=\"onSearchKey($event)\"\n (blur)=\"touch.emit()\"\n />\n @if (clearable() && hasSelection() && !isDisabled()) {\n <span\n class=\"wr-select__clear\"\n role=\"button\"\n [attr.aria-label]=\"resolvedClear()\"\n tabindex=\"-1\"\n (click)=\"clearAll($event)\"\n (keydown.enter)=\"clearAll($event)\"\n (keydown.space)=\"clearAll($event)\"\n >\n <svg\n class=\"wr-icon__svg\"\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"M18 6 6 18\" />\n <path d=\"m6 6 12 12\" />\n </svg>\n </span>\n }\n <svg\n class=\"wr-icon__svg wr-select__chevron\"\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"m6 9 6 6 6-6\" />\n </svg>\n </div>\n} @else if (isTag() || hasChipSearch()) {\n <!-- Chip trigger \u2014 tag mode, or a searchable multi. A focusable input lives\n inside, so the wrapper itself only exists to host chips and forward\n whitespace clicks to the input. Tag mode needs no role (users tab\n straight into the input); the searchable multi is a real combobox, so it\n carries the listbox wiring. The click handler is a mouse-only\n convenience. -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div\n class=\"wr-select__trigger wr-select__trigger--tag\"\n [attr.aria-disabled]=\"isDisabled() || null\"\n (click)=\"focusTagInput()\"\n >\n <span class=\"wr-select__chips\">\n @for (chip of visibleChips(); track $index) {\n <span class=\"wr-select__chip\">\n <span class=\"wr-select__chip-label\">{{ chip.label }}</span>\n <span\n class=\"wr-select__chip-remove\"\n role=\"button\"\n [attr.aria-label]=\"chipRemoveLabel({ label: chip.label })\"\n tabindex=\"-1\"\n (click)=\"removeChip(chip.value, $event)\"\n (keydown.enter)=\"removeChip(chip.value, $event)\"\n (keydown.space)=\"removeChip(chip.value, $event)\"\n >\n <svg\n class=\"wr-icon__svg\"\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"M18 6 6 18\" />\n <path d=\"m6 6 12 12\" />\n </svg>\n </span>\n </span>\n }\n @if (hiddenChipCount(); as extra) {\n <span class=\"wr-select__chip wr-select__chip--more\">+{{ extra }} more</span>\n }\n <input\n #tagInput\n type=\"text\"\n class=\"wr-select__tag-input\"\n autocomplete=\"off\"\n spellcheck=\"false\"\n [value]=\"hasChipSearch() ? searchQuery() : draft()\"\n [disabled]=\"isDisabled() || (isTag() && atCapacity())\"\n [placeholder]=\"hasSelection() ? '' : resolvedPlaceholder()\"\n [attr.role]=\"hasChipSearch() ? 'combobox' : null\"\n [attr.aria-haspopup]=\"hasChipSearch() ? 'listbox' : null\"\n [attr.aria-expanded]=\"hasChipSearch() ? open() : null\"\n [attr.aria-controls]=\"hasChipSearch() ? listboxId : null\"\n [attr.aria-activedescendant]=\"hasChipSearch() && open() ? activeOptionId() || null : null\"\n (input)=\"onChipsInput($event)\"\n (keydown)=\"onChipsKeydown($event)\"\n (paste)=\"onChipsPaste($event)\"\n (focus)=\"onChipsFocus()\"\n (blur)=\"onChipsBlur()\"\n />\n </span>\n\n @if (clearable() && hasSelection() && !isDisabled()) {\n <span\n class=\"wr-select__clear\"\n role=\"button\"\n [attr.aria-label]=\"resolvedClear()\"\n tabindex=\"-1\"\n (click)=\"clearAll($event)\"\n (keydown.enter)=\"clearAll($event)\"\n (keydown.space)=\"clearAll($event)\"\n >\n <svg\n class=\"wr-icon__svg\"\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"M18 6 6 18\" />\n <path d=\"m6 6 12 12\" />\n </svg>\n </span>\n }\n </div>\n} @else {\n <button\n type=\"button\"\n class=\"wr-select__trigger\"\n role=\"combobox\"\n [disabled]=\"isDisabled()\"\n [attr.aria-haspopup]=\"'listbox'\"\n [attr.aria-expanded]=\"open()\"\n [attr.aria-controls]=\"listboxId\"\n [attr.aria-multiselectable]=\"isMulti() ? 'true' : null\"\n [attr.aria-activedescendant]=\"open() ? activeOptionId() || null : null\"\n (click)=\"onTriggerClick()\"\n (keydown)=\"onTriggerKey($event)\"\n (blur)=\"touch.emit()\"\n >\n @if (isMulti()) {\n @if (hasSelection()) {\n <span class=\"wr-select__chips\">\n @for (chip of visibleChips(); track $index) {\n <span class=\"wr-select__chip\">\n <span class=\"wr-select__chip-label\">{{ chip.label }}</span>\n <span\n class=\"wr-select__chip-remove\"\n role=\"button\"\n [attr.aria-label]=\"chipRemoveLabel({ label: chip.label })\"\n tabindex=\"-1\"\n (click)=\"removeChip(chip.value, $event)\"\n (keydown.enter)=\"removeChip(chip.value, $event)\"\n (keydown.space)=\"removeChip(chip.value, $event)\"\n >\n <svg\n class=\"wr-icon__svg\"\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"M18 6 6 18\" />\n <path d=\"m6 6 12 12\" />\n </svg>\n </span>\n </span>\n }\n @if (hiddenChipCount(); as extra) {\n <span class=\"wr-select__chip wr-select__chip--more\">+{{ extra }} more</span>\n }\n </span>\n } @else {\n <span class=\"wr-select__placeholder\">{{ resolvedPlaceholder() }}</span>\n }\n } @else {\n @if (selectedLabel(); as label) {\n <span class=\"wr-select__value\">{{ label }}</span>\n } @else {\n <span class=\"wr-select__placeholder\">{{ resolvedPlaceholder() }}</span>\n }\n }\n\n @if (isMulti() && clearable() && hasSelection() && !isDisabled()) {\n <span\n class=\"wr-select__clear\"\n role=\"button\"\n [attr.aria-label]=\"resolvedClear()\"\n tabindex=\"-1\"\n (click)=\"clearAll($event)\"\n (keydown.enter)=\"clearAll($event)\"\n (keydown.space)=\"clearAll($event)\"\n >\n <svg\n class=\"wr-icon__svg\"\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"M18 6 6 18\" />\n <path d=\"m6 6 12 12\" />\n </svg>\n </span>\n }\n\n <svg\n class=\"wr-icon__svg wr-select__chevron\"\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"m6 9 6 6 6-6\" />\n </svg>\n </button>\n}\n\n<ng-template #panelTpl>\n <div\n class=\"wr-select-panel\"\n [class.wr-select-panel--sm]=\"size() === 'sm'\"\n [class.wr-select-panel--lg]=\"size() === 'lg'\"\n [class.wr-select-panel--virtual]=\"virtualActive()\"\n role=\"listbox\"\n [attr.id]=\"listboxId\"\n [attr.aria-multiselectable]=\"isMulti() ? 'true' : null\"\n >\n @if (isLoading()) {\n <div class=\"wr-select-panel__loading\" aria-busy=\"true\">{{ resolvedLoading() }}</div>\n }\n\n @if (virtualActive()) {\n <!-- Windowed data-array rows: plain role=\"option\" elements (not <wr-option>),\n navigated via the input's aria-activedescendant, selected by value. -->\n <div class=\"wr-select-panel__vlist\" [style.max-height]=\"resolvedViewportHeight()\">\n @if (virtualWindow().padTop > 0) {\n <div class=\"wr-select-panel__vspacer\" aria-hidden=\"true\" [style.height.px]=\"virtualWindow().padTop\"></div>\n }\n @for (entry of windowOptions(); track entry.index) {\n <!-- Keyboard handled by the search input via aria-activedescendant; the\n row is a pointer target only. -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div\n role=\"option\"\n class=\"wr-option\"\n [id]=\"virtualRowId(entry.index)\"\n [class.wr-option--active]=\"entry.index === activeIndex()\"\n [class.wr-option--selected]=\"isSelected(entry.item)\"\n [attr.aria-selected]=\"isSelected(entry.item)\"\n [attr.aria-setsize]=\"filteredDynamicOptions().length\"\n [attr.aria-posinset]=\"entry.index + 1\"\n (click)=\"selectOption(entry.item)\"\n >\n {{ displayWith()(entry.item) }}\n </div>\n }\n @if (virtualWindow().padBottom > 0) {\n <div class=\"wr-select-panel__vspacer\" aria-hidden=\"true\" [style.height.px]=\"virtualWindow().padBottom\"></div>\n }\n </div>\n } @else {\n <!-- Dynamic options from [options] or async [loader] (search mode).\n Rendered as wr-option so the standard registration / filter\n pipeline applies uniformly. -->\n @for (item of dynamicOptions(); track $index) {\n <wr-option [value]=\"item\">{{ displayWith()(item) }}</wr-option>\n }\n }\n\n <!-- Projected static options \u2014 always present (a conditional `<ng-content>`\n would drop projected content). Empty whenever `virtualActive`. -->\n <ng-content />\n\n @if (hasNoResults()) {\n <div class=\"wr-select-panel__empty\">{{ resolvedNoResults() }}</div>\n }\n </div>\n</ng-template>\n" }]
1325
+ }], ctorParameters: () => [], propDecorators: { placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], clearLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearLabel", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], rounded: [{ type: i0.Input, args: [{ isSignal: true, alias: "rounded", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], responsive: [{ type: i0.Input, args: [{ isSignal: true, alias: "responsive", required: false }] }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], searchable: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchable", required: false }] }], searchQuery: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchQuery", required: false }] }, { type: i0.Output, args: ["searchQueryChange"] }], options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], displayWith: [{ type: i0.Input, args: [{ isSignal: true, alias: "displayWith", required: false }] }], loader: [{ type: i0.Input, args: [{ isSignal: true, alias: "loader", required: false }] }], serverSearch: [{ type: i0.Input, args: [{ isSignal: true, alias: "serverSearch", required: false }] }], debounceMs: [{ type: i0.Input, args: [{ isSignal: true, alias: "debounceMs", required: false }] }], minChars: [{ type: i0.Input, args: [{ isSignal: true, alias: "minChars", required: false }] }], freeText: [{ type: i0.Input, args: [{ isSignal: true, alias: "freeText", required: false }] }], virtualScroll: [{ type: i0.Input, args: [{ isSignal: true, alias: "virtualScroll", required: false }] }], rowHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowHeight", required: false }] }], viewportHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "viewportHeight", required: false }] }], overscan: [{ type: i0.Input, args: [{ isSignal: true, alias: "overscan", required: false }] }], noResultsText: [{ type: i0.Input, args: [{ isSignal: true, alias: "noResultsText", required: false }] }], loadingText: [{ type: i0.Input, args: [{ isSignal: true, alias: "loadingText", required: false }] }], searchChange: [{ type: i0.Output, args: ["searchChange"] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], separators: [{ type: i0.Input, args: [{ isSignal: true, alias: "separators", required: false }] }], allowDuplicates: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowDuplicates", required: false }] }], validate: [{ type: i0.Input, args: [{ isSignal: true, alias: "validate", required: false }] }], clearable: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearable", required: false }] }], maxItems: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxItems", required: false }] }], maxTagCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxTagCount", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], touch: [{ type: i0.Output, args: ["touch"] }], projectedOptions: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => WrOption), { isSignal: true }] }], panelTpl: [{ type: i0.ViewChild, args: ['panelTpl', { ...{ read: TemplateRef }, isSignal: true }] }], tagInputEl: [{ type: i0.ViewChild, args: ['tagInput', { isSignal: true }] }], searchInputEl: [{ type: i0.ViewChild, args: ['searchInput', { isSignal: true }] }] } });
1143
1326
 
1144
1327
  /**
1145
1328
  * @license