ngwr 9.0.1 → 10.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.
- package/README.md +14 -0
- package/badge/styles/_index.scss +16 -6
- package/badge/styles/_tag.scss +10 -1
- package/button/styles/_index.scss +48 -1
- package/date-picker/styles/_index.scss +62 -0
- package/dialog/styles/_index.scss +65 -0
- package/drawer/styles/_index.scss +83 -0
- package/fesm2022/ngwr-action-sheet.mjs +1 -1
- package/fesm2022/ngwr-action-sheet.mjs.map +1 -1
- package/fesm2022/ngwr-breadcrumbs.mjs +2 -2
- package/fesm2022/ngwr-breadcrumbs.mjs.map +1 -1
- package/fesm2022/ngwr-card.mjs +2 -2
- package/fesm2022/ngwr-card.mjs.map +1 -1
- package/fesm2022/ngwr-cascader.mjs +6 -5
- package/fesm2022/ngwr-cascader.mjs.map +1 -1
- package/fesm2022/ngwr-color-picker.mjs +6 -5
- package/fesm2022/ngwr-color-picker.mjs.map +1 -1
- package/fesm2022/ngwr-context-menu.mjs +7 -7
- package/fesm2022/ngwr-context-menu.mjs.map +1 -1
- package/fesm2022/ngwr-date-picker.mjs +521 -8
- package/fesm2022/ngwr-date-picker.mjs.map +1 -1
- package/fesm2022/ngwr-density.mjs +10 -2
- package/fesm2022/ngwr-density.mjs.map +1 -1
- package/fesm2022/ngwr-dialog.mjs +95 -13
- package/fesm2022/ngwr-dialog.mjs.map +1 -1
- package/fesm2022/ngwr-drag-drop.mjs +2 -2
- package/fesm2022/ngwr-drag-drop.mjs.map +1 -1
- package/fesm2022/ngwr-drawer.mjs +377 -22
- package/fesm2022/ngwr-drawer.mjs.map +1 -1
- package/fesm2022/ngwr-dropdown.mjs +4 -3
- package/fesm2022/ngwr-dropdown.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-en.mjs +21 -0
- package/fesm2022/ngwr-i18n-en.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-ru.mjs +22 -0
- package/fesm2022/ngwr-i18n-ru.mjs.map +1 -1
- package/fesm2022/ngwr-i18n.mjs +74 -1
- package/fesm2022/ngwr-i18n.mjs.map +1 -1
- package/fesm2022/ngwr-icon.mjs +74 -35
- package/fesm2022/ngwr-icon.mjs.map +1 -1
- package/fesm2022/ngwr-lightbox.mjs +2 -2
- package/fesm2022/ngwr-lightbox.mjs.map +1 -1
- package/fesm2022/ngwr-list.mjs +2 -2
- package/fesm2022/ngwr-list.mjs.map +1 -1
- package/fesm2022/ngwr-mention.mjs +166 -10
- package/fesm2022/ngwr-mention.mjs.map +1 -1
- package/fesm2022/ngwr-overlay.mjs +173 -3
- package/fesm2022/ngwr-overlay.mjs.map +1 -1
- package/fesm2022/ngwr-pagination.mjs +17 -4
- package/fesm2022/ngwr-pagination.mjs.map +1 -1
- package/fesm2022/ngwr-popconfirm.mjs +4 -3
- package/fesm2022/ngwr-popconfirm.mjs.map +1 -1
- package/fesm2022/ngwr-popover.mjs +10 -3
- package/fesm2022/ngwr-popover.mjs.map +1 -1
- package/fesm2022/ngwr-select.mjs +233 -43
- package/fesm2022/ngwr-select.mjs.map +1 -1
- package/fesm2022/ngwr-table.mjs +1 -1
- package/fesm2022/ngwr-table.mjs.map +1 -1
- package/fesm2022/ngwr-theme.mjs +13 -3
- package/fesm2022/ngwr-theme.mjs.map +1 -1
- package/fesm2022/ngwr-tree.mjs +4 -3
- package/fesm2022/ngwr-tree.mjs.map +1 -1
- package/fesm2022/ngwr-version.mjs +1 -1
- package/fesm2022/ngwr-version.mjs.map +1 -1
- package/fesm2022/ngwr-waves.mjs +25 -3
- package/fesm2022/ngwr-waves.mjs.map +1 -1
- package/i18n/en.json +165 -0
- package/i18n/ru.json +165 -0
- package/icon/styles/_index.scss +10 -1
- package/input/styles/_index.scss +16 -0
- package/input-number/styles/_index.scss +8 -1
- package/keyboard/styles/_index.scss +10 -3
- package/llms-full.txt +14 -14
- package/llms.txt +12 -6
- package/mention/styles/_index.scss +23 -0
- package/package.json +23 -5
- package/pagination/styles/_index.scss +12 -0
- package/popover/styles/_index.scss +129 -45
- package/schematics/ng-add/index.js +18 -4
- package/schematics/package.json +3 -0
- package/schematics/use/symbol-map.json +7 -0
- package/select/styles/_index.scss +13 -0
- package/splitter/styles/_index.scss +7 -1
- package/table/styles/_index.scss +28 -10
- package/theme/styles/_box-model.scss +29 -0
- package/theme/styles/_colors.scss +74 -15
- package/theme/styles/_dark.scss +52 -6
- package/theme/styles/_index.scss +1 -0
- package/types/ngwr-cascader.d.ts +1 -0
- package/types/ngwr-color-picker.d.ts +1 -0
- package/types/ngwr-context-menu.d.ts +1 -0
- package/types/ngwr-date-picker.d.ts +172 -4
- package/types/ngwr-density.d.ts +8 -1
- package/types/ngwr-dialog.d.ts +80 -8
- package/types/ngwr-drawer.d.ts +245 -10
- package/types/ngwr-dropdown.d.ts +1 -0
- package/types/ngwr-i18n.d.ts +65 -2
- package/types/ngwr-icon.d.ts +11 -19
- package/types/ngwr-mention.d.ts +45 -1
- package/types/ngwr-overlay.d.ts +80 -2
- package/types/ngwr-pagination.d.ts +14 -1
- package/types/ngwr-popconfirm.d.ts +1 -0
- package/types/ngwr-popover.d.ts +1 -0
- package/types/ngwr-select.d.ts +121 -9
- package/types/ngwr-theme.d.ts +8 -1
- package/types/ngwr-tree.d.ts +1 -0
- package/types/ngwr-version.d.ts +1 -1
- package/types/ngwr-waves.d.ts +10 -0
- package/waves/styles/_index.scss +21 -0
package/fesm2022/ngwr-select.mjs
CHANGED
|
@@ -3,11 +3,12 @@ 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,
|
|
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 {
|
|
8
|
+
import { skip, debounce, timer, switchMap, of, isObservable, from, tap, finalize } from 'rxjs';
|
|
9
9
|
import { useI18nText, useI18nFormatter } from 'ngwr/i18n';
|
|
10
|
-
import { WR_OVERLAY, WR_RESPONSIVE_OVERLAYS, wrPresentAsSheet } from 'ngwr/overlay';
|
|
10
|
+
import { WR_OVERLAY, WrOutsideClick, WR_RESPONSIVE_OVERLAYS, wrPresentAsSheet } from 'ngwr/overlay';
|
|
11
|
+
import { randomId } from 'ngwr/utils';
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* @license
|
|
@@ -69,14 +70,14 @@ class WrOption {
|
|
|
69
70
|
active = computed(() => this.parent?.activeOptionId() === this.id, /* @ts-ignore */
|
|
70
71
|
...(ngDevMode ? [{ debugName: "active" }] : /* istanbul ignore next */ []));
|
|
71
72
|
/**
|
|
72
|
-
* @internal —
|
|
73
|
+
* @internal — searchable selects only. True when the parent has a query that
|
|
73
74
|
* the option's text content does not match (case-insensitive substring).
|
|
74
75
|
* Hidden options stay in the DOM so registration order survives but
|
|
75
76
|
* collapse via CSS.
|
|
76
77
|
*/
|
|
77
78
|
hidden = computed(() => {
|
|
78
79
|
const parent = this.parent;
|
|
79
|
-
if (!parent?.
|
|
80
|
+
if (!parent?.isSearchable() || !parent.clientFilter())
|
|
80
81
|
return false;
|
|
81
82
|
const q = parent.searchQuery().trim().toLowerCase();
|
|
82
83
|
if (!q)
|
|
@@ -160,6 +161,11 @@ let listboxUid = 0;
|
|
|
160
161
|
* - `'tag'` — free-text + chips with `[separators]` / `[validate]` /
|
|
161
162
|
* `[allowDuplicates]`. Replaces `<wr-chips-input>`.
|
|
162
163
|
*
|
|
164
|
+
* `[searchable]` is orthogonal to the mode — it adds the type-ahead filter to
|
|
165
|
+
* `single` / `multi` without changing the value shape, which is how you get a
|
|
166
|
+
* searchable multi-select. Server-side search hangs off the `(searchChange)` output
|
|
167
|
+
* plus `[options]` / `[loading]`.
|
|
168
|
+
*
|
|
163
169
|
* A signal-forms native control: it implements `FormValueControl<unknown>`, so
|
|
164
170
|
* `[formField]` binds straight to its `value` model — no
|
|
165
171
|
* `ControlValueAccessor` in between. `[(value)]` works standalone, and classic
|
|
@@ -179,6 +185,16 @@ let listboxUid = 0;
|
|
|
179
185
|
* <wr-option value="ts">TypeScript</wr-option>
|
|
180
186
|
* <wr-option value="ng">Angular</wr-option>
|
|
181
187
|
* </wr-select>
|
|
188
|
+
*
|
|
189
|
+
* <!-- Multi + type-ahead, options from a store -->
|
|
190
|
+
* <wr-select
|
|
191
|
+
* mode="multi"
|
|
192
|
+
* searchable
|
|
193
|
+
* [options]="results()"
|
|
194
|
+
* [loading]="pending()"
|
|
195
|
+
* [(value)]="categories"
|
|
196
|
+
* (searchChange)="store.search($event)"
|
|
197
|
+
* />
|
|
182
198
|
* ```
|
|
183
199
|
*
|
|
184
200
|
* The legacy `[multi]="true"` boolean is still accepted but is now an
|
|
@@ -240,6 +256,34 @@ class WrSelect {
|
|
|
240
256
|
/** Convenience — `effectiveMode() === 'search'`. Exposed via context for options. */
|
|
241
257
|
isSearch = computed(() => this.effectiveMode() === 'search', /* @ts-ignore */
|
|
242
258
|
...(ngDevMode ? [{ debugName: "isSearch" }] : /* istanbul ignore next */ []));
|
|
259
|
+
/**
|
|
260
|
+
* Add a type-ahead filter without changing the value shape — the missing
|
|
261
|
+
* multi-with-search combination, since `[mode]` picks exactly one shape:
|
|
262
|
+
*
|
|
263
|
+
* ```html
|
|
264
|
+
* <wr-select mode="multi" searchable [(value)]="categories">…</wr-select>
|
|
265
|
+
* ```
|
|
266
|
+
*
|
|
267
|
+
* On `multi` the trigger keeps its chips and grows an inline text field (the
|
|
268
|
+
* shape `tag` mode already uses); on `single` it is equivalent to
|
|
269
|
+
* `mode="search"`. Every search input (`[options]`, `[loader]`,
|
|
270
|
+
* `[debounceMs]`, `[minChars]`, `[virtualScroll]`, the `(searchChange)` output, …)
|
|
271
|
+
* applies either way. Ignored in `tag` mode, which owns its own input.
|
|
272
|
+
* @default false
|
|
273
|
+
*/
|
|
274
|
+
searchable = input(false, { ...(ngDevMode ? { debugName: "searchable" } : /* istanbul ignore next */ {}), transform: coerceBooleanProperty });
|
|
275
|
+
/**
|
|
276
|
+
* Filtering is active — either `mode="search"` or `[searchable]`. Exposed via
|
|
277
|
+
* context so each `<wr-option>` can self-hide non-matching rows.
|
|
278
|
+
*/
|
|
279
|
+
isSearchable = computed(() => this.isSearch() || (this.searchable() && !this.isTag()), /* @ts-ignore */
|
|
280
|
+
...(ngDevMode ? [{ debugName: "isSearchable" }] : /* istanbul ignore next */ []));
|
|
281
|
+
/** The trigger *is* the search input: `mode="search"`, or a searchable single. */
|
|
282
|
+
isSearchTrigger = computed(() => this.isSearch() || (this.searchable() && !this.hasChips()), /* @ts-ignore */
|
|
283
|
+
...(ngDevMode ? [{ debugName: "isSearchTrigger" }] : /* istanbul ignore next */ []));
|
|
284
|
+
/** Chips plus an inline search field — a searchable multi-select. */
|
|
285
|
+
hasChipSearch = computed(() => this.searchable() && this.isMulti(), /* @ts-ignore */
|
|
286
|
+
...(ngDevMode ? [{ debugName: "hasChipSearch" }] : /* istanbul ignore next */ []));
|
|
243
287
|
/** WrSelectContext — resolved multi-selection flag for child options. */
|
|
244
288
|
multi = computed(() => this.effectiveMode() === 'multi' || this.effectiveMode() === 'tag', /* @ts-ignore */
|
|
245
289
|
...(ngDevMode ? [{ debugName: "multi" }] : /* istanbul ignore next */ []));
|
|
@@ -250,10 +294,14 @@ class WrSelect {
|
|
|
250
294
|
}, /* @ts-ignore */
|
|
251
295
|
...(ngDevMode ? [{ debugName: "hasChips" }] : /* istanbul ignore next */ []));
|
|
252
296
|
/**
|
|
253
|
-
*
|
|
297
|
+
* The live search query. Empty string = no filter. Exposed via context so
|
|
254
298
|
* each `<wr-option>` can self-hide non-matching rows.
|
|
299
|
+
*
|
|
300
|
+
* Two-way bindable, so the query can be owned or reset from outside:
|
|
301
|
+
* `[(searchQuery)]="query"`. For server-side search prefer the debounced
|
|
302
|
+
* {@link searchChange} output — `searchQueryChange` fires on every keystroke.
|
|
255
303
|
*/
|
|
256
|
-
searchQuery =
|
|
304
|
+
searchQuery = model('', /* @ts-ignore */
|
|
257
305
|
...(ngDevMode ? [{ debugName: "searchQuery" }] : /* istanbul ignore next */ []));
|
|
258
306
|
// Search-mode-only inputs (ignored in other modes)
|
|
259
307
|
/**
|
|
@@ -274,6 +322,22 @@ class WrSelect {
|
|
|
274
322
|
*/
|
|
275
323
|
loader = input(null, /* @ts-ignore */
|
|
276
324
|
...(ngDevMode ? [{ debugName: "loader" }] : /* istanbul ignore next */ []));
|
|
325
|
+
/**
|
|
326
|
+
* The option list is already scoped to the query upstream — skip the built-in
|
|
327
|
+
* client-side label filter. Set it when `[options]` is fed from a server via
|
|
328
|
+
* the {@link searchChange} output; the async `[loader]` path implies it.
|
|
329
|
+
*
|
|
330
|
+
* Without this, a server that ranks or fuzzy-matches (returning rows whose
|
|
331
|
+
* labels don't literally contain the query) would have those rows hidden
|
|
332
|
+
* again on the client. @default false
|
|
333
|
+
*/
|
|
334
|
+
serverSearch = input(false, { ...(ngDevMode ? { debugName: "serverSearch" } : /* istanbul ignore next */ {}), transform: coerceBooleanProperty });
|
|
335
|
+
/**
|
|
336
|
+
* Whether the built-in label filter runs. Exposed via context so each
|
|
337
|
+
* `<wr-option>` knows whether to self-hide. @internal
|
|
338
|
+
*/
|
|
339
|
+
clientFilter = computed(() => !this.loader() && !this.serverSearch(), /* @ts-ignore */
|
|
340
|
+
...(ngDevMode ? [{ debugName: "clientFilter" }] : /* istanbul ignore next */ []));
|
|
277
341
|
/** Search mode: debounce (ms) applied to the loader. @default 250 */
|
|
278
342
|
debounceMs = input(250, { ...(ngDevMode ? { debugName: "debounceMs" } : /* istanbul ignore next */ {}), transform: (v) => Math.max(0, Number(v) || 0) });
|
|
279
343
|
/** Search mode: minimum query length before the panel opens / loader fires. @default 0 */
|
|
@@ -314,12 +378,45 @@ class WrSelect {
|
|
|
314
378
|
...(ngDevMode ? [{ debugName: "loadingText" }] : /* istanbul ignore next */ []));
|
|
315
379
|
resolvedNoResults = useI18nText(this.noResultsText, 'select.noResults', 'No results');
|
|
316
380
|
resolvedLoading = useI18nText(this.loadingText, 'select.loading', 'Loading…');
|
|
381
|
+
/**
|
|
382
|
+
* Debounced search query, gated to searchable selects. Emits on every settled
|
|
383
|
+
* change — including the empty string when the field is cleared — so a
|
|
384
|
+
* store-backed option list can stay in sync and reset itself. `[debounceMs]`
|
|
385
|
+
* controls the delay; `[minChars]` does NOT gate it, for the same reason the
|
|
386
|
+
* `[loader]` path clears its results below that threshold.
|
|
387
|
+
*
|
|
388
|
+
* This is the hook for server-side search: dispatch on `(searchChange)`, feed
|
|
389
|
+
* the results back through `[options]` (plus `[serverSearch]`), and drive the
|
|
390
|
+
* panel's progress row with `[loading]`. Prefer it over the model's raw
|
|
391
|
+
* `(searchQueryChange)`, which fires on every keystroke.
|
|
392
|
+
*
|
|
393
|
+
* @example
|
|
394
|
+
* ```html
|
|
395
|
+
* <wr-select
|
|
396
|
+
* mode="search"
|
|
397
|
+
* serverSearch
|
|
398
|
+
* [options]="results()"
|
|
399
|
+
* [loading]="pending()"
|
|
400
|
+
* (searchChange)="store.search($event)"
|
|
401
|
+
* />
|
|
402
|
+
* ```
|
|
403
|
+
*/
|
|
404
|
+
searchChange = output();
|
|
405
|
+
/**
|
|
406
|
+
* Show the panel's progress row. Independent of `[loader]`, which raises and
|
|
407
|
+
* lowers its own flag — use this when the options come from a store fed by
|
|
408
|
+
* the {@link searchChange} output. @default false
|
|
409
|
+
*/
|
|
410
|
+
loading = input(false, { ...(ngDevMode ? { debugName: "loading" } : /* istanbul ignore next */ {}), transform: coerceBooleanProperty });
|
|
317
411
|
/** Search mode: results returned by the async loader. @internal */
|
|
318
412
|
loadedOptions = signal([], /* @ts-ignore */
|
|
319
413
|
...(ngDevMode ? [{ debugName: "loadedOptions" }] : /* istanbul ignore next */ []));
|
|
320
|
-
/**
|
|
321
|
-
|
|
322
|
-
...(ngDevMode ? [{ debugName: "
|
|
414
|
+
/** True while the async `[loader]` is in flight. @internal */
|
|
415
|
+
loaderLoading = signal(false, /* @ts-ignore */
|
|
416
|
+
...(ngDevMode ? [{ debugName: "loaderLoading" }] : /* istanbul ignore next */ []));
|
|
417
|
+
/** Either flag shows the panel's progress row. @internal */
|
|
418
|
+
isLoading = computed(() => this.loading() || this.loaderLoading(), /* @ts-ignore */
|
|
419
|
+
...(ngDevMode ? [{ debugName: "isLoading" }] : /* istanbul ignore next */ []));
|
|
323
420
|
// Tag-mode-only inputs (ignored in other modes)
|
|
324
421
|
/**
|
|
325
422
|
* Tag mode: keys / characters that commit the current draft into a chip.
|
|
@@ -404,7 +501,11 @@ class WrSelect {
|
|
|
404
501
|
const list = this.registry();
|
|
405
502
|
return arr.map(v => {
|
|
406
503
|
const found = list.find(o => o.value === v);
|
|
407
|
-
|
|
504
|
+
// Fall back to `displayWith` (defaults to `String`) rather than `String`
|
|
505
|
+
// directly: a searchable multi fed from `[options]` has no registered
|
|
506
|
+
// `<wr-option>` for a virtualized or unmounted row, and object items
|
|
507
|
+
// would otherwise render as "[object Object]".
|
|
508
|
+
return { value: v, label: found?.getLabel() ?? this.displayWith()(v) };
|
|
408
509
|
});
|
|
409
510
|
}, /* @ts-ignore */
|
|
410
511
|
...(ngDevMode ? [{ debugName: "selectedChips" }] : /* istanbul ignore next */ []));
|
|
@@ -442,8 +543,10 @@ class WrSelect {
|
|
|
442
543
|
parts.push('wr-select--multi');
|
|
443
544
|
if (this.isTag())
|
|
444
545
|
parts.push('wr-select--tag');
|
|
445
|
-
if (this.
|
|
546
|
+
if (this.isSearchTrigger())
|
|
446
547
|
parts.push('wr-select--search');
|
|
548
|
+
if (this.hasChipSearch())
|
|
549
|
+
parts.push('wr-select--searchable');
|
|
447
550
|
if (this.isDisabled())
|
|
448
551
|
parts.push('wr-select--disabled');
|
|
449
552
|
return parts.join(' ');
|
|
@@ -466,11 +569,11 @@ class WrSelect {
|
|
|
466
569
|
* `dynamicOptions()` narrowed to the current search query. `dynamicOptions()`
|
|
467
570
|
* is intentionally UNFILTERED (projected `<wr-option>` self-hide via CSS), so
|
|
468
571
|
* the virtual path — which renders plain rows that can't self-hide — filters
|
|
469
|
-
* here. Loader results are already query-scoped upstream.
|
|
572
|
+
* here. Loader / `serverSearch` results are already query-scoped upstream.
|
|
470
573
|
*/
|
|
471
574
|
filteredDynamicOptions = computed(() => {
|
|
472
575
|
const items = this.dynamicOptions();
|
|
473
|
-
if (this.
|
|
576
|
+
if (!this.clientFilter())
|
|
474
577
|
return items;
|
|
475
578
|
const q = this.searchQuery().trim().toLowerCase();
|
|
476
579
|
if (!q)
|
|
@@ -485,7 +588,7 @@ class WrSelect {
|
|
|
485
588
|
* so their DOM-derived labels / selection still work).
|
|
486
589
|
*/
|
|
487
590
|
virtualActive = computed(() => this.virtualScroll() &&
|
|
488
|
-
this.
|
|
591
|
+
this.isSearchable() &&
|
|
489
592
|
this.projectedOptions().length === 0 &&
|
|
490
593
|
this.filteredDynamicOptions().length > 0, /* @ts-ignore */
|
|
491
594
|
...(ngDevMode ? [{ debugName: "virtualActive" }] : /* istanbul ignore next */ []));
|
|
@@ -568,7 +671,7 @@ class WrSelect {
|
|
|
568
671
|
if (this.virtualActive())
|
|
569
672
|
return this.filteredDynamicOptions().length;
|
|
570
673
|
const list = this.registry();
|
|
571
|
-
if (!this.
|
|
674
|
+
if (!this.isSearchable() || !this.clientFilter())
|
|
572
675
|
return list.length;
|
|
573
676
|
const q = this.searchQuery().trim().toLowerCase();
|
|
574
677
|
if (!q)
|
|
@@ -583,7 +686,7 @@ class WrSelect {
|
|
|
583
686
|
...(ngDevMode ? [{ debugName: "visibleCount" }] : /* istanbul ignore next */ []));
|
|
584
687
|
/** Search mode: show "no results" when the panel has nothing to offer. */
|
|
585
688
|
hasNoResults = computed(() => {
|
|
586
|
-
if (!this.
|
|
689
|
+
if (!this.isSearchable() || !this.open() || this.isLoading())
|
|
587
690
|
return false;
|
|
588
691
|
if (this.searchQuery().trim().length < this.minChars())
|
|
589
692
|
return false;
|
|
@@ -592,7 +695,7 @@ class WrSelect {
|
|
|
592
695
|
...(ngDevMode ? [{ debugName: "hasNoResults" }] : /* istanbul ignore next */ []));
|
|
593
696
|
/** Search mode: is this option hidden by the current query? @internal */
|
|
594
697
|
isOptionHidden(label) {
|
|
595
|
-
if (!this.
|
|
698
|
+
if (!this.isSearchable() || !this.clientFilter())
|
|
596
699
|
return false;
|
|
597
700
|
const q = this.searchQuery().trim().toLowerCase();
|
|
598
701
|
if (!q)
|
|
@@ -600,7 +703,7 @@ class WrSelect {
|
|
|
600
703
|
return !label.toLowerCase().includes(q);
|
|
601
704
|
}
|
|
602
705
|
panelTpl = viewChild.required('panelTpl', { ...(ngDevMode ? { debugName: "panelTpl" } : /* istanbul ignore next */ {}), read: TemplateRef });
|
|
603
|
-
/**
|
|
706
|
+
/** The inline input inside a chip trigger — tag mode or a searchable multi. @internal */
|
|
604
707
|
tagInputEl = viewChild('tagInput', /* @ts-ignore */
|
|
605
708
|
...(ngDevMode ? [{ debugName: "tagInputEl" }] : /* istanbul ignore next */ []));
|
|
606
709
|
/** Search-mode inline input. Present only when `mode="search"`. @internal */
|
|
@@ -612,6 +715,37 @@ class WrSelect {
|
|
|
612
715
|
focusSearchInput() {
|
|
613
716
|
this.searchInputEl()?.nativeElement.focus();
|
|
614
717
|
}
|
|
718
|
+
// Chip-trigger input dispatch. One inline input, two owners: tag mode
|
|
719
|
+
// commits a draft into chips, a searchable multi filters the panel.
|
|
720
|
+
onChipsInput(event) {
|
|
721
|
+
if (this.hasChipSearch())
|
|
722
|
+
this.onSearchInput(event);
|
|
723
|
+
else
|
|
724
|
+
this.onTagInput(event);
|
|
725
|
+
}
|
|
726
|
+
onChipsKeydown(event) {
|
|
727
|
+
if (this.hasChipSearch())
|
|
728
|
+
this.onSearchKey(event);
|
|
729
|
+
else
|
|
730
|
+
this.onTagKeydown(event);
|
|
731
|
+
}
|
|
732
|
+
onChipsFocus() {
|
|
733
|
+
// Tag mode has no panel to open.
|
|
734
|
+
if (this.hasChipSearch())
|
|
735
|
+
this.onSearchFocus();
|
|
736
|
+
}
|
|
737
|
+
onChipsPaste(event) {
|
|
738
|
+
// Splitting a paste into chips is tag-mode behaviour; a search query is
|
|
739
|
+
// pasted as-is.
|
|
740
|
+
if (!this.hasChipSearch())
|
|
741
|
+
this.onTagPaste(event);
|
|
742
|
+
}
|
|
743
|
+
onChipsBlur() {
|
|
744
|
+
if (this.hasChipSearch())
|
|
745
|
+
this.touch.emit();
|
|
746
|
+
else
|
|
747
|
+
this.onTagBlur();
|
|
748
|
+
}
|
|
615
749
|
// Search-mode handlers
|
|
616
750
|
onSearchInput(event) {
|
|
617
751
|
if (this.isDisabled())
|
|
@@ -620,11 +754,14 @@ class WrSelect {
|
|
|
620
754
|
this.searchQuery.set(value);
|
|
621
755
|
if (!this.open())
|
|
622
756
|
this.open.set(true);
|
|
623
|
-
// A new filter re-
|
|
624
|
-
//
|
|
757
|
+
// A new filter invalidates every index, so re-seed the cursor onto the first
|
|
758
|
+
// option that still matches. This must happen on BOTH paths: on the
|
|
759
|
+
// projected-`<wr-option>` path the registry keeps filtered-out options (they
|
|
760
|
+
// only self-hide via CSS), so a stale `activeIndex` would leave the cursor on
|
|
761
|
+
// a row the user can no longer see — and Enter would select it.
|
|
762
|
+
this.activeIndex.set(this.firstEnabled());
|
|
625
763
|
if (this.virtualActive()) {
|
|
626
764
|
this.scrollTop.set(0);
|
|
627
|
-
this.activeIndex.set(this.firstEnabled());
|
|
628
765
|
this.vlistElement?.scrollTo({ top: 0 });
|
|
629
766
|
}
|
|
630
767
|
}
|
|
@@ -655,6 +792,23 @@ class WrSelect {
|
|
|
655
792
|
return;
|
|
656
793
|
}
|
|
657
794
|
}
|
|
795
|
+
// Space belongs to the text field, not to the listbox. The button trigger
|
|
796
|
+
// uses it to pick the active option, but here swallowing it would make a
|
|
797
|
+
// multi-word query ("dump trucks") impossible to type.
|
|
798
|
+
if (event.key === ' ')
|
|
799
|
+
return;
|
|
800
|
+
// Backspace on an empty query drops the last chip, matching tag mode. The
|
|
801
|
+
// button-trigger path only does this while closed, and a chip-search input
|
|
802
|
+
// keeps the panel open, so it never reaches that branch.
|
|
803
|
+
if (event.key === 'Backspace' && this.hasChipSearch() && this.searchQuery() === '' && this.hasSelection()) {
|
|
804
|
+
const chips = this.selectedChips();
|
|
805
|
+
const last = chips[chips.length - 1];
|
|
806
|
+
if (last) {
|
|
807
|
+
event.preventDefault();
|
|
808
|
+
this.removeChip(last.value, event);
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
658
812
|
// Otherwise route through the same keyboard plumbing as the button trigger.
|
|
659
813
|
this.onTriggerKey(event);
|
|
660
814
|
}
|
|
@@ -671,6 +825,7 @@ class WrSelect {
|
|
|
671
825
|
...(ngDevMode ? [{ debugName: "searchDisplay" }] : /* istanbul ignore next */ []));
|
|
672
826
|
host = inject(ElementRef);
|
|
673
827
|
overlay = inject(WR_OVERLAY);
|
|
828
|
+
outsideClick = inject(WrOutsideClick);
|
|
674
829
|
responsiveConfig = inject(WR_RESPONSIVE_OVERLAYS);
|
|
675
830
|
vcr = inject(ViewContainerRef);
|
|
676
831
|
scrollStrategies = inject(ScrollStrategyOptions);
|
|
@@ -686,23 +841,43 @@ class WrSelect {
|
|
|
686
841
|
this.closeOverlay();
|
|
687
842
|
// Reset the search query when the panel closes so a re-open
|
|
688
843
|
// doesn't carry a stale filter.
|
|
689
|
-
if (this.
|
|
844
|
+
if (this.isSearchable())
|
|
690
845
|
this.searchQuery.set('');
|
|
691
846
|
}
|
|
692
847
|
});
|
|
693
|
-
//
|
|
848
|
+
// One debounced query stream feeds both the async loader and the public
|
|
849
|
+
// `searchChange` output, so they fire on exactly the same cadence.
|
|
850
|
+
// `debounce(() => timer(...))` rather than `debounceTime(this.debounceMs())`:
|
|
851
|
+
// this runs in the constructor, where inputs aren't bound yet, so reading
|
|
852
|
+
// the input eagerly would pin the delay to the default and ignore
|
|
853
|
+
// `[debounceMs]` forever.
|
|
854
|
+
// `skip(1)` sits BEFORE the debounce so it drops the mount-time replay of the
|
|
855
|
+
// empty query and nothing else. Downstream of `debounce` it would instead
|
|
856
|
+
// drop the first *settled* emission — and when a fast typist's first
|
|
857
|
+
// keystrokes coalesce with that initial `''`, the dropped emission is their
|
|
858
|
+
// query, not the replay.
|
|
859
|
+
const debouncedQuery = toObservable(this.searchQuery).pipe(skip(1), debounce(() => timer(this.debounceMs())));
|
|
860
|
+
debouncedQuery.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(query => {
|
|
861
|
+
if (this.isSearchable())
|
|
862
|
+
this.searchChange.emit(query);
|
|
863
|
+
});
|
|
864
|
+
// Async loader pipeline (searchable selects only). `switchMap` cancels
|
|
694
865
|
// in-flight requests when a new keystroke arrives.
|
|
695
|
-
|
|
696
|
-
.pipe(
|
|
866
|
+
debouncedQuery
|
|
867
|
+
.pipe(switchMap(query => {
|
|
697
868
|
const loader = this.loader();
|
|
698
|
-
if (!loader || !this.
|
|
699
|
-
this.
|
|
869
|
+
if (!loader || !this.isSearchable() || query.length < this.minChars()) {
|
|
870
|
+
this.loaderLoading.set(false);
|
|
700
871
|
return of([]);
|
|
701
872
|
}
|
|
702
|
-
this.
|
|
873
|
+
this.loaderLoading.set(true);
|
|
703
874
|
const result = loader(query);
|
|
704
875
|
const source = isObservable(result) ? result : from(Promise.resolve(result));
|
|
705
|
-
|
|
876
|
+
// A loader can hand back a long-lived observable (a store selector,
|
|
877
|
+
// say) that emits and never completes — so the first value lowers the
|
|
878
|
+
// flag. `finalize` still covers completion, errors and cancellation,
|
|
879
|
+
// including a loader that completes without emitting.
|
|
880
|
+
return source.pipe(tap(() => this.loaderLoading.set(false)), finalize(() => this.loaderLoading.set(false)));
|
|
706
881
|
}), takeUntilDestroyed(this.destroyRef))
|
|
707
882
|
.subscribe(items => this.loadedOptions.set(items));
|
|
708
883
|
// Resolve the single-mode trigger label whenever the value or
|
|
@@ -723,7 +898,7 @@ class WrSelect {
|
|
|
723
898
|
// Search mode: a value picked from a virtualized (or since-unmounted) row
|
|
724
899
|
// has no registered `<wr-option>`, so derive its label from `displayWith`
|
|
725
900
|
// — the same function that rendered the row.
|
|
726
|
-
this.selectedLabel.set(v != null && this.
|
|
901
|
+
this.selectedLabel.set(v != null && this.isSearchable() ? this.displayWith()(v) : null);
|
|
727
902
|
});
|
|
728
903
|
// Wire the virtual scroll listener + viewport/row measurement while the panel
|
|
729
904
|
// is open and virtualized. Re-runs when the panel opens/closes or the virtual
|
|
@@ -832,8 +1007,10 @@ class WrSelect {
|
|
|
832
1007
|
return;
|
|
833
1008
|
if (this.hasChips()) {
|
|
834
1009
|
this.value.set([]);
|
|
1010
|
+
// `isSearchTrigger()`, not `isSearch()` — a searchable single renders the
|
|
1011
|
+
// same × and would otherwise fall through to the no-op below.
|
|
835
1012
|
}
|
|
836
|
-
else if (this.
|
|
1013
|
+
else if (this.isSearchTrigger()) {
|
|
837
1014
|
this.value.set(null);
|
|
838
1015
|
this.searchQuery.set('');
|
|
839
1016
|
}
|
|
@@ -934,8 +1111,13 @@ class WrSelect {
|
|
|
934
1111
|
return;
|
|
935
1112
|
}
|
|
936
1113
|
// Multi mode: Backspace on closed trigger removes last chip. Tag
|
|
937
|
-
// mode has its own input handler — never reaches this branch.
|
|
938
|
-
|
|
1114
|
+
// mode has its own input handler — never reaches this branch. A
|
|
1115
|
+
// searchable multi does, so only claim Backspace once its query is empty
|
|
1116
|
+
// — otherwise it would eat a chip and a character at once.
|
|
1117
|
+
if (event.key === 'Backspace' &&
|
|
1118
|
+
this.isMulti() &&
|
|
1119
|
+
this.hasSelection() &&
|
|
1120
|
+
(!this.hasChipSearch() || this.searchQuery() === '')) {
|
|
939
1121
|
event.preventDefault();
|
|
940
1122
|
const chips = this.selectedChips();
|
|
941
1123
|
const last = chips[chips.length - 1];
|
|
@@ -976,7 +1158,10 @@ class WrSelect {
|
|
|
976
1158
|
break;
|
|
977
1159
|
}
|
|
978
1160
|
const list = this.registry();
|
|
979
|
-
|
|
1161
|
+
// Filtered-out options stay registered (they only self-hide via CSS), so
|
|
1162
|
+
// the hidden check is load-bearing: without it Enter can commit a row
|
|
1163
|
+
// that is not on screen.
|
|
1164
|
+
if (idx >= 0 && idx < list.length && !list[idx].disabled && !this.isOptionHidden(list[idx].getLabel())) {
|
|
980
1165
|
const id = list[idx].id;
|
|
981
1166
|
const el = this.overlayRef?.overlayElement.querySelector(`#${CSS.escape(id)}`);
|
|
982
1167
|
el?.click();
|
|
@@ -1097,8 +1282,8 @@ class WrSelect {
|
|
|
1097
1282
|
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
1098
1283
|
.subscribe(() => this.open.set(false));
|
|
1099
1284
|
}
|
|
1100
|
-
this.
|
|
1101
|
-
.outsidePointerEvents()
|
|
1285
|
+
this.outsideClick
|
|
1286
|
+
.outsidePointerEvents(this.overlayRef)
|
|
1102
1287
|
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
1103
1288
|
.subscribe(event => {
|
|
1104
1289
|
if (this.host.nativeElement.contains(event.target))
|
|
@@ -1122,13 +1307,13 @@ class WrSelect {
|
|
|
1122
1307
|
this.overlayRef = null;
|
|
1123
1308
|
}
|
|
1124
1309
|
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: [
|
|
1310
|
+
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
1311
|
{
|
|
1127
1312
|
provide: WR_SELECT,
|
|
1128
1313
|
// eslint-disable-next-line @angular-eslint/no-forward-ref
|
|
1129
1314
|
useExisting: forwardRef(() => WrSelect),
|
|
1130
1315
|
},
|
|
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 });
|
|
1316
|
+
], 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
1317
|
}
|
|
1133
1318
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: WrSelect, decorators: [{
|
|
1134
1319
|
type: Component,
|
|
@@ -1138,8 +1323,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImpor
|
|
|
1138
1323
|
// eslint-disable-next-line @angular-eslint/no-forward-ref
|
|
1139
1324
|
useExisting: forwardRef(() => WrSelect),
|
|
1140
1325
|
},
|
|
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 }] }] } });
|
|
1326
|
+
], 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" }]
|
|
1327
|
+
}], 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
1328
|
|
|
1144
1329
|
/**
|
|
1145
1330
|
* @license
|
|
@@ -1162,12 +1347,17 @@ class WrOptionGroup {
|
|
|
1162
1347
|
/** Section heading shown above the options. */
|
|
1163
1348
|
label = input.required(/* @ts-ignore */
|
|
1164
1349
|
...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
|
|
1350
|
+
/**
|
|
1351
|
+
* `role="group"` alone announces an unnamed group — the visible heading has to
|
|
1352
|
+
* be wired to it for the name to reach a screen reader.
|
|
1353
|
+
*/
|
|
1354
|
+
labelId = randomId('wr-option-group-label');
|
|
1165
1355
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: WrOptionGroup, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1166
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.0", type: WrOptionGroup, isStandalone: true, selector: "wr-option-group", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null } }, host: { attributes: { "role": "group" }, classAttribute: "wr-option-group" }, ngImport: i0, template: "<div class=\"wr-option-group__label\">{{ label() }}</div>\n<ng-content />\n", encapsulation: i0.ViewEncapsulation.None });
|
|
1356
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.0", type: WrOptionGroup, isStandalone: true, selector: "wr-option-group", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null } }, host: { attributes: { "role": "group" }, properties: { "attr.aria-labelledby": "labelId" }, classAttribute: "wr-option-group" }, ngImport: i0, template: "<div class=\"wr-option-group__label\" [id]=\"labelId\">{{ label() }}</div>\n<ng-content />\n", encapsulation: i0.ViewEncapsulation.None });
|
|
1167
1357
|
}
|
|
1168
1358
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: WrOptionGroup, decorators: [{
|
|
1169
1359
|
type: Component,
|
|
1170
|
-
args: [{ selector: 'wr-option-group', encapsulation: ViewEncapsulation.None, host: { class: 'wr-option-group', role: 'group' }, template: "<div class=\"wr-option-group__label\">{{ label() }}</div>\n<ng-content />\n" }]
|
|
1360
|
+
args: [{ selector: 'wr-option-group', encapsulation: ViewEncapsulation.None, host: { class: 'wr-option-group', role: 'group', '[attr.aria-labelledby]': 'labelId' }, template: "<div class=\"wr-option-group__label\" [id]=\"labelId\">{{ label() }}</div>\n<ng-content />\n" }]
|
|
1171
1361
|
}], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }] } });
|
|
1172
1362
|
|
|
1173
1363
|
/**
|