ngwr 15.2.0 → 15.3.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/alert/styles/_index.scss +13 -5
- package/fesm2022/ngwr-alert.mjs +2 -2
- package/fesm2022/ngwr-alert.mjs.map +1 -1
- package/fesm2022/ngwr-breadcrumbs.mjs +18 -2
- package/fesm2022/ngwr-breadcrumbs.mjs.map +1 -1
- package/fesm2022/ngwr-hotkey.mjs +23 -1
- package/fesm2022/ngwr-hotkey.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-ar.mjs +2 -0
- package/fesm2022/ngwr-i18n-ar.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-cs.mjs +2 -0
- package/fesm2022/ngwr-i18n-cs.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-de.mjs +2 -0
- package/fesm2022/ngwr-i18n-de.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-en.mjs +2 -0
- package/fesm2022/ngwr-i18n-en.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-es.mjs +2 -0
- package/fesm2022/ngwr-i18n-es.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-fr.mjs +2 -0
- package/fesm2022/ngwr-i18n-fr.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-he.mjs +2 -0
- package/fesm2022/ngwr-i18n-he.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-hi.mjs +2 -0
- package/fesm2022/ngwr-i18n-hi.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-id.mjs +2 -0
- package/fesm2022/ngwr-i18n-id.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-it.mjs +2 -0
- package/fesm2022/ngwr-i18n-it.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-ja.mjs +2 -0
- package/fesm2022/ngwr-i18n-ja.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-ko.mjs +2 -0
- package/fesm2022/ngwr-i18n-ko.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-nl.mjs +2 -0
- package/fesm2022/ngwr-i18n-nl.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-pl.mjs +2 -0
- package/fesm2022/ngwr-i18n-pl.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-pt.mjs +2 -0
- package/fesm2022/ngwr-i18n-pt.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-ru.mjs +2 -0
- package/fesm2022/ngwr-i18n-ru.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-sv.mjs +2 -0
- package/fesm2022/ngwr-i18n-sv.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-tr.mjs +2 -0
- package/fesm2022/ngwr-i18n-tr.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-uk.mjs +2 -0
- package/fesm2022/ngwr-i18n-uk.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-vi.mjs +2 -0
- package/fesm2022/ngwr-i18n-vi.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-zh-tw.mjs +2 -0
- package/fesm2022/ngwr-i18n-zh-tw.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-zh.mjs +2 -0
- package/fesm2022/ngwr-i18n-zh.mjs.map +1 -1
- package/fesm2022/ngwr-pagination.mjs +3 -0
- package/fesm2022/ngwr-pagination.mjs.map +1 -1
- package/fesm2022/ngwr-pipes.mjs +5 -2
- package/fesm2022/ngwr-pipes.mjs.map +1 -1
- package/fesm2022/ngwr-schema-form.mjs +3 -0
- package/fesm2022/ngwr-schema-form.mjs.map +1 -1
- package/fesm2022/ngwr-select-testing.mjs +16 -0
- package/fesm2022/ngwr-select-testing.mjs.map +1 -1
- package/fesm2022/ngwr-select.mjs +218 -38
- package/fesm2022/ngwr-select.mjs.map +1 -1
- package/fesm2022/ngwr-statistic.mjs +9 -5
- package/fesm2022/ngwr-statistic.mjs.map +1 -1
- package/fesm2022/ngwr-version.mjs +1 -1
- package/fesm2022/ngwr-version.mjs.map +1 -1
- package/i18n/ar.json +2 -0
- package/i18n/cs.json +2 -0
- package/i18n/de.json +2 -0
- package/i18n/en.json +2 -0
- package/i18n/es.json +2 -0
- package/i18n/fr.json +2 -0
- package/i18n/he.json +2 -0
- package/i18n/hi.json +2 -0
- package/i18n/id.json +2 -0
- package/i18n/it.json +2 -0
- package/i18n/ja.json +2 -0
- package/i18n/ko.json +2 -0
- package/i18n/nl.json +2 -0
- package/i18n/pl.json +2 -0
- package/i18n/pt.json +2 -0
- package/i18n/ru.json +2 -0
- package/i18n/sv.json +2 -0
- package/i18n/tr.json +2 -0
- package/i18n/uk.json +2 -0
- package/i18n/vi.json +2 -0
- package/i18n/zh-TW.json +2 -0
- package/i18n/zh.json +2 -0
- package/llms-full.txt +3 -3
- package/package.json +1 -1
- package/schematics/use/symbol-map.json +1 -0
- package/select/styles/_index.scss +23 -1
- package/skills/ngwr/references/catalog.md +1 -1
- package/types/ngwr-alert.d.ts +3 -1
- package/types/ngwr-breadcrumbs.d.ts +15 -0
- package/types/ngwr-hotkey.d.ts +14 -2
- package/types/ngwr-select-testing.d.ts +9 -0
- package/types/ngwr-select.d.ts +133 -2
- package/types/ngwr-sidebar.d.ts +1 -1
- package/types/ngwr-version.d.ts +1 -1
package/fesm2022/ngwr-select.mjs
CHANGED
|
@@ -4,13 +4,52 @@ import { coerceBooleanProperty, coerceNumberProperty } from "@angular/cdk/coerci
|
|
|
4
4
|
import { NgTemplateOutlet, isPlatformBrowser } from "@angular/common";
|
|
5
5
|
import { useI18nFormatter, useI18nText } from "ngwr/i18n";
|
|
6
6
|
import { isComposing, randomId, toClassList } from "ngwr/utils";
|
|
7
|
+
import { WrSpinner } from "ngwr/spinner";
|
|
7
8
|
import { Directionality } from "@angular/cdk/bidi";
|
|
8
9
|
import { ScrollStrategyOptions } from "@angular/cdk/overlay";
|
|
9
10
|
import { TemplatePortal } from "@angular/cdk/portal";
|
|
10
11
|
import { takeUntilDestroyed, toObservable } from "@angular/core/rxjs-interop";
|
|
11
12
|
import { WR_FORM_FIELD, useFormFieldAria } from "ngwr/form";
|
|
12
13
|
import { WR_OVERLAY, WrOutsideClick, wrFollowDirection } from "ngwr/overlay";
|
|
13
|
-
import { catchError, debounce, defer, finalize, from, isObservable, of, skip, switchMap, tap, timer } from "rxjs";
|
|
14
|
+
import { Subject, catchError, debounce, defer, finalize, from, isObservable, map, merge, of, skip, switchMap, tap, timer } from "rxjs";
|
|
15
|
+
/**
|
|
16
|
+
* @license
|
|
17
|
+
*
|
|
18
|
+
* Use of this source code is governed by an MIT-style license that can be
|
|
19
|
+
* found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE
|
|
20
|
+
*/
|
|
21
|
+
var WrSelectLoadError = class WrSelectLoadError {
|
|
22
|
+
template = inject(TemplateRef);
|
|
23
|
+
static ngTemplateContextGuard(_dir, _ctx) {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
27
|
+
minVersion: "12.0.0",
|
|
28
|
+
version: "22.2.4",
|
|
29
|
+
ngImport: i0,
|
|
30
|
+
type: WrSelectLoadError,
|
|
31
|
+
deps: [],
|
|
32
|
+
target: i0.ɵɵFactoryTarget.Directive
|
|
33
|
+
});
|
|
34
|
+
static ɵdir = i0.ɵɵngDeclareDirective({
|
|
35
|
+
minVersion: "14.0.0",
|
|
36
|
+
version: "22.2.4",
|
|
37
|
+
type: WrSelectLoadError,
|
|
38
|
+
isStandalone: true,
|
|
39
|
+
selector: "ng-template[wrSelectLoadError]",
|
|
40
|
+
ngImport: i0
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
44
|
+
minVersion: "12.0.0",
|
|
45
|
+
version: "22.2.4",
|
|
46
|
+
ngImport: i0,
|
|
47
|
+
type: WrSelectLoadError,
|
|
48
|
+
decorators: [{
|
|
49
|
+
type: Directive,
|
|
50
|
+
args: [{ selector: "ng-template[wrSelectLoadError]" }]
|
|
51
|
+
}]
|
|
52
|
+
});
|
|
14
53
|
/**
|
|
15
54
|
* @license
|
|
16
55
|
*
|
|
@@ -370,8 +409,16 @@ var WrSelect = class WrSelect {
|
|
|
370
409
|
});
|
|
371
410
|
noResultsText = input(null, ...ngDevMode ? [{ debugName: "noResultsText" }] : /* istanbul ignore next */ []);
|
|
372
411
|
loadingText = input(null, ...ngDevMode ? [{ debugName: "loadingText" }] : /* istanbul ignore next */ []);
|
|
412
|
+
loadErrorText = input(null, ...ngDevMode ? [{ debugName: "loadErrorText" }] : /* istanbul ignore next */ []);
|
|
413
|
+
retryable = input(false, {
|
|
414
|
+
...ngDevMode ? { debugName: "retryable" } : /* istanbul ignore next */ {},
|
|
415
|
+
transform: coerceBooleanProperty
|
|
416
|
+
});
|
|
417
|
+
retryText = input(null, ...ngDevMode ? [{ debugName: "retryText" }] : /* istanbul ignore next */ []);
|
|
373
418
|
resolvedNoResults = useI18nText(this.noResultsText, "select.noResults", "No results");
|
|
374
419
|
resolvedLoading = useI18nText(this.loadingText, "select.loading", "Loading…");
|
|
420
|
+
resolvedLoadError = useI18nText(this.loadErrorText, "select.loadError", "Could not load results");
|
|
421
|
+
resolvedRetry = useI18nText(this.retryText, "select.retry", "Try again");
|
|
375
422
|
searchChange = output();
|
|
376
423
|
loading = input(false, {
|
|
377
424
|
...ngDevMode ? { debugName: "loading" } : /* istanbul ignore next */ {},
|
|
@@ -380,6 +427,11 @@ var WrSelect = class WrSelect {
|
|
|
380
427
|
loadedOptions = signal([], ...ngDevMode ? [{ debugName: "loadedOptions" }] : /* istanbul ignore next */ []);
|
|
381
428
|
loaderLoading = signal(false, ...ngDevMode ? [{ debugName: "loaderLoading" }] : /* istanbul ignore next */ []);
|
|
382
429
|
isLoading = computed(() => this.loading() || this.loaderLoading(), ...ngDevMode ? [{ debugName: "isLoading" }] : /* istanbul ignore next */ []);
|
|
430
|
+
failedQuery = signal(null, ...ngDevMode ? [{ debugName: "failedQuery" }] : /* istanbul ignore next */ []);
|
|
431
|
+
loadErrorValue = signal(null, ...ngDevMode ? [{ debugName: "loadErrorValue" }] : /* istanbul ignore next */ []);
|
|
432
|
+
retrying = signal(false, ...ngDevMode ? [{ debugName: "retrying" }] : /* istanbul ignore next */ []);
|
|
433
|
+
loadFailed = computed(() => this.failedQuery() === this.searchQuery(), ...ngDevMode ? [{ debugName: "loadFailed" }] : /* istanbul ignore next */ []);
|
|
434
|
+
retryRequests = new Subject();
|
|
383
435
|
separators = input(["Enter", ","], ...ngDevMode ? [{ debugName: "separators" }] : /* istanbul ignore next */ []);
|
|
384
436
|
allowDuplicates = input(false, {
|
|
385
437
|
...ngDevMode ? { debugName: "allowDuplicates" } : /* istanbul ignore next */ {},
|
|
@@ -418,6 +470,7 @@ var WrSelect = class WrSelect {
|
|
|
418
470
|
}, ...ngDevMode ? [{ debugName: "orderedRegistry" }] : /* istanbul ignore next */ []);
|
|
419
471
|
activeIndex = signal(-1, ...ngDevMode ? [{ debugName: "activeIndex" }] : /* istanbul ignore next */ []);
|
|
420
472
|
activeOptionId = computed(() => {
|
|
473
|
+
if (this.retryOffered() && !this.freeText()) return this.retryRowId;
|
|
421
474
|
const idx = this.activeIndex();
|
|
422
475
|
if (this.virtualActive()) {
|
|
423
476
|
const { start, end } = this.virtualWindow();
|
|
@@ -616,10 +669,25 @@ var WrSelect = class WrSelect {
|
|
|
616
669
|
}, ...ngDevMode ? [{ debugName: "visibleCount" }] : /* istanbul ignore next */ []);
|
|
617
670
|
meetsMinChars = computed(() => !this.isSearchable() || this.searchQuery().trim().length >= this.minChars(), ...ngDevMode ? [{ debugName: "meetsMinChars" }] : /* istanbul ignore next */ []);
|
|
618
671
|
hasNoResults = computed(() => {
|
|
619
|
-
if (!this.isSearchable() || !this.open() || this.isLoading()) return false;
|
|
672
|
+
if (!this.isSearchable() || !this.open() || this.isLoading() || this.loadFailed()) return false;
|
|
620
673
|
if (this.searchQuery().trim().length < this.minChars()) return false;
|
|
621
674
|
return this.visibleCount() === 0;
|
|
622
675
|
}, ...ngDevMode ? [{ debugName: "hasNoResults" }] : /* istanbul ignore next */ []);
|
|
676
|
+
showLoadError = computed(() => this.isSearchable() && this.open() && this.loadFailed() && (!this.isLoading() || this.retrying()), ...ngDevMode ? [{ debugName: "showLoadError" }] : /* istanbul ignore next */ []);
|
|
677
|
+
loadErrorTpl = contentChild(WrSelectLoadError, {
|
|
678
|
+
...ngDevMode ? { debugName: "loadErrorTpl" } : /* istanbul ignore next */ {},
|
|
679
|
+
descendants: false
|
|
680
|
+
});
|
|
681
|
+
loadErrorContext = computed(() => ({
|
|
682
|
+
$implicit: this.loadErrorValue(),
|
|
683
|
+
query: this.failedQuery() ?? "",
|
|
684
|
+
canRetry: this.retryOffered(),
|
|
685
|
+
retry: this.retryFn,
|
|
686
|
+
retrying: this.retrying()
|
|
687
|
+
}), ...ngDevMode ? [{ debugName: "loadErrorContext" }] : /* istanbul ignore next */ []);
|
|
688
|
+
retryFn = () => this.onLoadErrorClick();
|
|
689
|
+
retryOffered = computed(() => this.retryable() && this.showLoadError() && this.visibleCount() === 0, ...ngDevMode ? [{ debugName: "retryOffered" }] : /* istanbul ignore next */ []);
|
|
690
|
+
retryRowId = `${this.listboxId}-retry`;
|
|
623
691
|
isOptionHidden(label) {
|
|
624
692
|
if (!this.isSearchable() || !this.clientFilter()) return false;
|
|
625
693
|
const q = this.searchQuery().trim().toLowerCase();
|
|
@@ -709,6 +777,11 @@ var WrSelect = class WrSelect {
|
|
|
709
777
|
}
|
|
710
778
|
onSearchKey(event) {
|
|
711
779
|
if (this.imeOwnsKey(event)) return;
|
|
780
|
+
if (event.key === "Enter" && this.retryOffered() && !this.freeText()) {
|
|
781
|
+
event.preventDefault();
|
|
782
|
+
this.retryLoad();
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
712
785
|
if (event.key === "Enter" && this.freeText() && this.open()) {
|
|
713
786
|
const q = this.searchQuery().trim();
|
|
714
787
|
const idx = this.activeIndex();
|
|
@@ -769,17 +842,43 @@ var WrSelect = class WrSelect {
|
|
|
769
842
|
debouncedQuery.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((query) => {
|
|
770
843
|
if (this.isSearchable()) this.searchChange.emit(query);
|
|
771
844
|
});
|
|
772
|
-
debouncedQuery.pipe(
|
|
845
|
+
merge(debouncedQuery.pipe(map((query) => ({
|
|
846
|
+
query,
|
|
847
|
+
retry: false
|
|
848
|
+
}))), this.retryRequests.pipe(map(() => ({
|
|
849
|
+
query: this.failedQuery() ?? this.searchQuery(),
|
|
850
|
+
retry: true
|
|
851
|
+
})))).pipe(switchMap(({ query, retry }) => {
|
|
773
852
|
const loader = this.loader();
|
|
774
853
|
if (!loader || !this.isSearchable() || query.length < this.minChars()) {
|
|
775
854
|
this.loaderLoading.set(false);
|
|
855
|
+
this.retrying.set(false);
|
|
856
|
+
this.failedQuery.set(null);
|
|
776
857
|
return of([]);
|
|
777
858
|
}
|
|
859
|
+
this.retrying.set(retry);
|
|
860
|
+
if (!retry) this.failedQuery.set(null);
|
|
861
|
+
let settled = false;
|
|
778
862
|
this.loaderLoading.set(true);
|
|
779
863
|
return defer(() => {
|
|
780
864
|
const result = loader(query);
|
|
781
865
|
return isObservable(result) ? result : from(Promise.resolve(result));
|
|
782
|
-
}).pipe(tap(() =>
|
|
866
|
+
}).pipe(tap(() => {
|
|
867
|
+
settled = true;
|
|
868
|
+
this.loaderLoading.set(false);
|
|
869
|
+
this.failedQuery.set(null);
|
|
870
|
+
this.retrying.set(false);
|
|
871
|
+
}), catchError((error) => {
|
|
872
|
+
settled = true;
|
|
873
|
+
this.failedQuery.set(query);
|
|
874
|
+
this.loadErrorValue.set(error);
|
|
875
|
+
this.retrying.set(false);
|
|
876
|
+
return of([]);
|
|
877
|
+
}), finalize(() => {
|
|
878
|
+
this.loaderLoading.set(false);
|
|
879
|
+
this.retrying.set(false);
|
|
880
|
+
if (retry && !settled) this.failedQuery.set(null);
|
|
881
|
+
}));
|
|
783
882
|
}), takeUntilDestroyed(this.destroyRef)).subscribe((items) => this.loadedOptions.set(items));
|
|
784
883
|
effect((onCleanup) => {
|
|
785
884
|
if (!isPlatformBrowser(this.platformId)) return;
|
|
@@ -823,6 +922,13 @@ var WrSelect = class WrSelect {
|
|
|
823
922
|
if (this.isMulti()) return this.asArray(this.value()).includes(value);
|
|
824
923
|
return this.value() === value;
|
|
825
924
|
}
|
|
925
|
+
retryLoad() {
|
|
926
|
+
if (this.retrying()) return;
|
|
927
|
+
this.retryRequests.next();
|
|
928
|
+
}
|
|
929
|
+
onLoadErrorClick() {
|
|
930
|
+
if (this.retryOffered()) this.retryLoad();
|
|
931
|
+
}
|
|
826
932
|
selectOption(value) {
|
|
827
933
|
if (this.isReadonly()) return;
|
|
828
934
|
if (this.isMulti()) {
|
|
@@ -1257,6 +1363,27 @@ var WrSelect = class WrSelect {
|
|
|
1257
1363
|
isRequired: false,
|
|
1258
1364
|
transformFunction: null
|
|
1259
1365
|
},
|
|
1366
|
+
loadErrorText: {
|
|
1367
|
+
classPropertyName: "loadErrorText",
|
|
1368
|
+
publicName: "loadErrorText",
|
|
1369
|
+
isSignal: true,
|
|
1370
|
+
isRequired: false,
|
|
1371
|
+
transformFunction: null
|
|
1372
|
+
},
|
|
1373
|
+
retryable: {
|
|
1374
|
+
classPropertyName: "retryable",
|
|
1375
|
+
publicName: "retryable",
|
|
1376
|
+
isSignal: true,
|
|
1377
|
+
isRequired: false,
|
|
1378
|
+
transformFunction: null
|
|
1379
|
+
},
|
|
1380
|
+
retryText: {
|
|
1381
|
+
classPropertyName: "retryText",
|
|
1382
|
+
publicName: "retryText",
|
|
1383
|
+
isSignal: true,
|
|
1384
|
+
isRequired: false,
|
|
1385
|
+
transformFunction: null
|
|
1386
|
+
},
|
|
1260
1387
|
loading: {
|
|
1261
1388
|
classPropertyName: "loading",
|
|
1262
1389
|
publicName: "loading",
|
|
@@ -1332,17 +1459,26 @@ var WrSelect = class WrSelect {
|
|
|
1332
1459
|
provide: WR_SELECT,
|
|
1333
1460
|
useExisting: forwardRef(() => WrSelect)
|
|
1334
1461
|
}],
|
|
1335
|
-
queries: [
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1462
|
+
queries: [
|
|
1463
|
+
{
|
|
1464
|
+
propertyName: "defaultLeading",
|
|
1465
|
+
first: true,
|
|
1466
|
+
predicate: WrOptionLeading,
|
|
1467
|
+
isSignal: true
|
|
1468
|
+
},
|
|
1469
|
+
{
|
|
1470
|
+
propertyName: "projectedOptions",
|
|
1471
|
+
predicate: WrOption,
|
|
1472
|
+
descendants: true,
|
|
1473
|
+
isSignal: true
|
|
1474
|
+
},
|
|
1475
|
+
{
|
|
1476
|
+
propertyName: "loadErrorTpl",
|
|
1477
|
+
first: true,
|
|
1478
|
+
predicate: WrSelectLoadError,
|
|
1479
|
+
isSignal: true
|
|
1480
|
+
}
|
|
1481
|
+
],
|
|
1346
1482
|
viewQueries: [
|
|
1347
1483
|
{
|
|
1348
1484
|
propertyName: "panelTpl",
|
|
@@ -1375,26 +1511,35 @@ var WrSelect = class WrSelect {
|
|
|
1375
1511
|
}
|
|
1376
1512
|
],
|
|
1377
1513
|
ngImport: i0,
|
|
1378
|
-
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 — 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 <!-- The selected option's leading visual, before the field — the same class\n and the same `placement: 'value'` the button trigger uses, because it is\n the same surface and a consumer who styled one should not have to learn\n a second name. `searchLeading()` draws it only while the field is\n showing the VALUE, so it never stands in front of a query or a bare\n placeholder, and it sits OUTSIDE the input for the obvious reason: an\n `<input>` holds a string. `aria-hidden` keeps it out of the combobox's\n name, which is `resolvedAriaLabel()`, and out of its value, which is the\n input's own text. -->\n @if (searchLeading(); as leading) {\n <span class=\"wr-select__value-leading\" aria-hidden=\"true\">\n <ng-container\n [ngTemplateOutlet]=\"leading\"\n [ngTemplateOutletContext]=\"{ $implicit: value(), placement: 'value' }\"\n />\n </span>\n }\n <!-- `id` / `aria-describedby` / `aria-invalid` come from a surrounding\n `<wr-form-field>` and are null without one — see `controlId` in the\n component. They go on the tab stop, the only labelable element here.\n `aria-label` is the trigger's name and belongs on the same element for\n the same reason: this input carries `role=\"combobox\"`, and the four\n other trigger shapes all bind `resolvedAriaLabel()`. Without it a\n search select with no placeholder rendered a combobox with no name at\n all — the one shape where the documented `ariaLabel` input reached\n nothing. -->\n <!-- `aria-autocomplete` is static, and `list` rather than `both`: the panel\n is the only place a suggestion appears, nothing is written back into\n the field for the user. It describes a permanent CAPABILITY, which is\n why it does not track `open()` — the same reasoning `wr-mention`\n records at its own host binding. -->\n <input\n #searchInput\n type=\"text\"\n role=\"combobox\"\n class=\"wr-select__search-input\"\n autocomplete=\"off\"\n spellcheck=\"false\"\n [attr.id]=\"controlId()\"\n [attr.aria-label]=\"nameLabel()\"\n [attr.aria-labelledby]=\"labelledBy()\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.aria-invalid]=\"ariaInvalid()\"\n [disabled]=\"isDisabled()\"\n [attr.aria-haspopup]=\"'listbox'\"\n aria-autocomplete=\"list\"\n [attr.aria-expanded]=\"open()\"\n [attr.aria-controls]=\"listboxId\"\n [attr.aria-activedescendant]=\"open() ? activeOptionId() || null : null\"\n [attr.aria-readonly]=\"isReadonly() || null\"\n [readonly]=\"isReadonly()\"\n [placeholder]=\"resolvedPlaceholder()\"\n [value]=\"searchDisplay()\"\n (input)=\"onSearchInput($event)\"\n (focus)=\"onSearchFocus()\"\n (keydown)=\"onSearchKey($event)\"\n (blur)=\"onSearchBlur()\"\n />\n <!-- Pointer-only, and the `keydown.enter` / `keydown.space` pair this used to\n carry was dead code rather than the keyboard path it looked like: the ×\n is `tabindex=\"-1\"` with no roving owner, so no key event ever originates\n here to bubble into a handler. It stays unfocusable because the same\n span sits inside the BUTTON trigger further down, where a focusable\n child is `nested-interactive` — the defect the window taskbar's close\n button already taught this repo. The keyboard reaches the function\n instead: Backspace on the empty field, which `onSearchKey` routes to\n `clearAll` for a single and to the last chip for a multi. -->\n @if (clearable() && hasSelection() && !isDisabled() && !isReadonly()) {\n <span\n class=\"wr-select__clear\"\n role=\"button\"\n [attr.aria-label]=\"resolvedClear()\"\n tabindex=\"-1\"\n (click)=\"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 — 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 @if (chip.leading; as leading) {\n <span class=\"wr-select__chip-leading\" aria-hidden=\"true\">\n <ng-container\n [ngTemplateOutlet]=\"leading\"\n [ngTemplateOutletContext]=\"{ $implicit: chip.value, placement: 'chip' }\"\n />\n </span>\n }\n <span class=\"wr-select__chip-label\">{{ chip.label }}</span>\n @if (!isReadonly()) {\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($index, $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 </span>\n }\n @if (hiddenChipCount(); as extra) {\n <span class=\"wr-select__chip wr-select__chip--more\">{{ moreLabel({ count: extra }) }}</span>\n }\n <!-- At `maxItems` this input goes READONLY, never disabled: it is the only\n tab stop in the whole control (the chip × and the clear button are\n `tabindex=\"-1\"` spans), so disabling it left a full tag select with\n nothing focusable at all — and Backspace on an empty draft, the one\n keyboard way back UNDER the cap, is bound to this very element. It was\n never needed as enforcement either: `tryAddTag` already refuses past\n capacity. -->\n <!-- `aria-autocomplete` is conditional here, unlike the search input's:\n this field is only a combobox when a panel backs it. In plain tag\n mode there is no popup to suggest from, and claiming otherwise\n describes a control that does not exist. -->\n <input\n #tagInput\n type=\"text\"\n class=\"wr-select__tag-input\"\n autocomplete=\"off\"\n [attr.id]=\"controlId()\"\n [attr.aria-label]=\"nameLabel()\"\n [attr.aria-labelledby]=\"labelledBy()\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.aria-invalid]=\"ariaInvalid()\"\n spellcheck=\"false\"\n [value]=\"hasChipSearch() ? searchQuery() : draft()\"\n [disabled]=\"isDisabled()\"\n [readonly]=\"isReadonly() || (isTag() && atCapacity())\"\n [attr.aria-readonly]=\"hasChipSearch() && isReadonly() ? true : null\"\n [placeholder]=\"hasSelection() ? '' : resolvedPlaceholder()\"\n [attr.role]=\"hasChipSearch() ? 'combobox' : null\"\n [attr.aria-haspopup]=\"hasChipSearch() ? 'listbox' : null\"\n [attr.aria-autocomplete]=\"hasChipSearch() ? 'list' : 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() && !isReadonly()) {\n <span\n class=\"wr-select__clear\"\n role=\"button\"\n [attr.aria-label]=\"resolvedClear()\"\n tabindex=\"-1\"\n (click)=\"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 <!-- Only for the searchable MULTI, never for tag mode. The two share this\n trigger and differ in the one thing a chevron stands for: the searchable\n multi's input is a `role=\"combobox\"` with `aria-haspopup=\"listbox\"` and\n a panel of options behind it, while tag mode takes free text and has no\n popup at all. Folding the second into this branch left it as the only\n combobox shape in the component with no affordance saying it opens —\n `mode=\"search\"` has one, a searchable single has one, the button trigger\n has one, and this one rendered a bare field. -->\n @if (hasChipSearch()) {\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 }\n </div>\n} @else {\n <button\n #trigger\n type=\"button\"\n class=\"wr-select__trigger\"\n role=\"combobox\"\n [attr.id]=\"controlId()\"\n [attr.aria-label]=\"nameLabel()\"\n [attr.aria-labelledby]=\"labelledBy()\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.aria-invalid]=\"ariaInvalid()\"\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 [attr.aria-readonly]=\"isReadonly() || 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 <!-- A selected chip draws its option's leading visual — one copy per\n VISIBLE chip, so `maxTagCount` caps these too. -->\n @if (chip.leading; as leading) {\n <span class=\"wr-select__chip-leading\" aria-hidden=\"true\">\n <ng-container\n [ngTemplateOutlet]=\"leading\"\n [ngTemplateOutletContext]=\"{ $implicit: chip.value, placement: 'chip' }\"\n />\n </span>\n }\n <span class=\"wr-select__chip-label\">{{ chip.label }}</span>\n @if (!isReadonly()) {\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($index, $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 </span>\n }\n @if (hiddenChipCount(); as extra) {\n <span class=\"wr-select__chip wr-select__chip--more\">{{ moreLabel({ count: extra }) }}</span>\n }\n </span>\n } @else {\n <span class=\"wr-select__placeholder\">{{ resolvedPlaceholder() }}</span>\n }\n } @else {\n @if (selectedLabel(); as label) {\n <!-- Beside the label, not inside it: the harness and the accessible name\n both read `.wr-select__value`, and the visual says nothing either should. -->\n @if (selectedLeading(); as leading) {\n <span class=\"wr-select__value-leading\" aria-hidden=\"true\">\n <ng-container\n [ngTemplateOutlet]=\"leading\"\n [ngTemplateOutletContext]=\"{ $implicit: value(), placement: 'value' }\"\n />\n </span>\n }\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() && !isReadonly()) {\n <span\n class=\"wr-select__clear\"\n role=\"button\"\n [attr.aria-label]=\"resolvedClear()\"\n tabindex=\"-1\"\n (click)=\"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 [attr.aria-busy]=\"isLoading() ? 'true' : null\"\n >\n <!-- `aria-busy` belongs on the listbox, not on this row: it is the container\n whose required `option` children are temporarily missing, and a global\n ARIA attribute on a role-less child makes that child an owned element the\n listbox may not own (axe `aria-required-children`, critical). Plain text\n inside the listbox is fine — only ELEMENTS are checked. -->\n @if (isLoading()) {\n <div class=\"wr-select-panel__loading\">{{ 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 [class.wr-option--disabled]=\"atMaxItems() && !isSelected(entry.item)\"\n [attr.aria-selected]=\"isSelected(entry.item)\"\n [attr.aria-disabled]=\"atMaxItems() && !isSelected(entry.item) ? true : null\"\n [attr.aria-setsize]=\"filteredDynamicOptions().length\"\n [attr.aria-posinset]=\"entry.index + 1\"\n (click)=\"selectOption(entry.item)\"\n >\n <!-- Only the select-wide template reaches a virtual row — there is no\n `<wr-option>` here to declare one — and only rows inside the\n rendered window instantiate it. -->\n @if (optionLeading(); as leading) {\n <span class=\"wr-option__leading\" aria-hidden=\"true\">\n <ng-container\n [ngTemplateOutlet]=\"leading\"\n [ngTemplateOutletContext]=\"{ $implicit: entry.item, placement: 'option' }\"\n />\n </span>\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 — always present (a conditional `<ng-content>`\n would drop projected content). Empty whenever `virtualActive`. -->\n <ng-content />\n\n <!-- A listbox may only own `option` and `group` children, and as a bare `<div>`\n this row was the only thing in the tree when it showed — an\n `aria-required-children` critical. It cannot move out of the listbox the\n way the command palette's does: this element IS the listbox, and the id\n the trigger's `aria-controls` names is what gives it its accessible name.\n So it announces as a disabled option instead. Keyboard navigation skips\n it for free — arrow keys walk the registered `wr-option`s, and nothing\n registers this row. -->\n @if (hasNoResults()) {\n <div class=\"wr-select-panel__empty\" role=\"option\" aria-selected=\"false\" aria-disabled=\"true\">\n {{ resolvedNoResults() }}\n </div>\n }\n </div>\n</ng-template>\n",
|
|
1379
|
-
dependencies: [
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
"
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1514
|
+
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 — 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 <!-- The selected option's leading visual, before the field — the same class\n and the same `placement: 'value'` the button trigger uses, because it is\n the same surface and a consumer who styled one should not have to learn\n a second name. `searchLeading()` draws it only while the field is\n showing the VALUE, so it never stands in front of a query or a bare\n placeholder, and it sits OUTSIDE the input for the obvious reason: an\n `<input>` holds a string. `aria-hidden` keeps it out of the combobox's\n name, which is `resolvedAriaLabel()`, and out of its value, which is the\n input's own text. -->\n @if (searchLeading(); as leading) {\n <span class=\"wr-select__value-leading\" aria-hidden=\"true\">\n <ng-container\n [ngTemplateOutlet]=\"leading\"\n [ngTemplateOutletContext]=\"{ $implicit: value(), placement: 'value' }\"\n />\n </span>\n }\n <!-- `id` / `aria-describedby` / `aria-invalid` come from a surrounding\n `<wr-form-field>` and are null without one — see `controlId` in the\n component. They go on the tab stop, the only labelable element here.\n `aria-label` is the trigger's name and belongs on the same element for\n the same reason: this input carries `role=\"combobox\"`, and the four\n other trigger shapes all bind `resolvedAriaLabel()`. Without it a\n search select with no placeholder rendered a combobox with no name at\n all — the one shape where the documented `ariaLabel` input reached\n nothing. -->\n <!-- `aria-autocomplete` is static, and `list` rather than `both`: the panel\n is the only place a suggestion appears, nothing is written back into\n the field for the user. It describes a permanent CAPABILITY, which is\n why it does not track `open()` — the same reasoning `wr-mention`\n records at its own host binding. -->\n <input\n #searchInput\n type=\"text\"\n role=\"combobox\"\n class=\"wr-select__search-input\"\n autocomplete=\"off\"\n spellcheck=\"false\"\n [attr.id]=\"controlId()\"\n [attr.aria-label]=\"nameLabel()\"\n [attr.aria-labelledby]=\"labelledBy()\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.aria-invalid]=\"ariaInvalid()\"\n [disabled]=\"isDisabled()\"\n [attr.aria-haspopup]=\"'listbox'\"\n aria-autocomplete=\"list\"\n [attr.aria-expanded]=\"open()\"\n [attr.aria-controls]=\"listboxId\"\n [attr.aria-activedescendant]=\"open() ? activeOptionId() || null : null\"\n [attr.aria-readonly]=\"isReadonly() || null\"\n [readonly]=\"isReadonly()\"\n [placeholder]=\"resolvedPlaceholder()\"\n [value]=\"searchDisplay()\"\n (input)=\"onSearchInput($event)\"\n (focus)=\"onSearchFocus()\"\n (keydown)=\"onSearchKey($event)\"\n (blur)=\"onSearchBlur()\"\n />\n <!-- Pointer-only, and the `keydown.enter` / `keydown.space` pair this used to\n carry was dead code rather than the keyboard path it looked like: the ×\n is `tabindex=\"-1\"` with no roving owner, so no key event ever originates\n here to bubble into a handler. It stays unfocusable because the same\n span sits inside the BUTTON trigger further down, where a focusable\n child is `nested-interactive` — the defect the window taskbar's close\n button already taught this repo. The keyboard reaches the function\n instead: Backspace on the empty field, which `onSearchKey` routes to\n `clearAll` for a single and to the last chip for a multi. -->\n @if (clearable() && hasSelection() && !isDisabled() && !isReadonly()) {\n <span\n class=\"wr-select__clear\"\n role=\"button\"\n [attr.aria-label]=\"resolvedClear()\"\n tabindex=\"-1\"\n (click)=\"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 — 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 @if (chip.leading; as leading) {\n <span class=\"wr-select__chip-leading\" aria-hidden=\"true\">\n <ng-container\n [ngTemplateOutlet]=\"leading\"\n [ngTemplateOutletContext]=\"{ $implicit: chip.value, placement: 'chip' }\"\n />\n </span>\n }\n <span class=\"wr-select__chip-label\">{{ chip.label }}</span>\n @if (!isReadonly()) {\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($index, $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 </span>\n }\n @if (hiddenChipCount(); as extra) {\n <span class=\"wr-select__chip wr-select__chip--more\">{{ moreLabel({ count: extra }) }}</span>\n }\n <!-- At `maxItems` this input goes READONLY, never disabled: it is the only\n tab stop in the whole control (the chip × and the clear button are\n `tabindex=\"-1\"` spans), so disabling it left a full tag select with\n nothing focusable at all — and Backspace on an empty draft, the one\n keyboard way back UNDER the cap, is bound to this very element. It was\n never needed as enforcement either: `tryAddTag` already refuses past\n capacity. -->\n <!-- `aria-autocomplete` is conditional here, unlike the search input's:\n this field is only a combobox when a panel backs it. In plain tag\n mode there is no popup to suggest from, and claiming otherwise\n describes a control that does not exist. -->\n <input\n #tagInput\n type=\"text\"\n class=\"wr-select__tag-input\"\n autocomplete=\"off\"\n [attr.id]=\"controlId()\"\n [attr.aria-label]=\"nameLabel()\"\n [attr.aria-labelledby]=\"labelledBy()\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.aria-invalid]=\"ariaInvalid()\"\n spellcheck=\"false\"\n [value]=\"hasChipSearch() ? searchQuery() : draft()\"\n [disabled]=\"isDisabled()\"\n [readonly]=\"isReadonly() || (isTag() && atCapacity())\"\n [attr.aria-readonly]=\"hasChipSearch() && isReadonly() ? true : null\"\n [placeholder]=\"hasSelection() ? '' : resolvedPlaceholder()\"\n [attr.role]=\"hasChipSearch() ? 'combobox' : null\"\n [attr.aria-haspopup]=\"hasChipSearch() ? 'listbox' : null\"\n [attr.aria-autocomplete]=\"hasChipSearch() ? 'list' : 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() && !isReadonly()) {\n <span\n class=\"wr-select__clear\"\n role=\"button\"\n [attr.aria-label]=\"resolvedClear()\"\n tabindex=\"-1\"\n (click)=\"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 <!-- Only for the searchable MULTI, never for tag mode. The two share this\n trigger and differ in the one thing a chevron stands for: the searchable\n multi's input is a `role=\"combobox\"` with `aria-haspopup=\"listbox\"` and\n a panel of options behind it, while tag mode takes free text and has no\n popup at all. Folding the second into this branch left it as the only\n combobox shape in the component with no affordance saying it opens —\n `mode=\"search\"` has one, a searchable single has one, the button trigger\n has one, and this one rendered a bare field. -->\n @if (hasChipSearch()) {\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 }\n </div>\n} @else {\n <button\n #trigger\n type=\"button\"\n class=\"wr-select__trigger\"\n role=\"combobox\"\n [attr.id]=\"controlId()\"\n [attr.aria-label]=\"nameLabel()\"\n [attr.aria-labelledby]=\"labelledBy()\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.aria-invalid]=\"ariaInvalid()\"\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 [attr.aria-readonly]=\"isReadonly() || 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 <!-- A selected chip draws its option's leading visual — one copy per\n VISIBLE chip, so `maxTagCount` caps these too. -->\n @if (chip.leading; as leading) {\n <span class=\"wr-select__chip-leading\" aria-hidden=\"true\">\n <ng-container\n [ngTemplateOutlet]=\"leading\"\n [ngTemplateOutletContext]=\"{ $implicit: chip.value, placement: 'chip' }\"\n />\n </span>\n }\n <span class=\"wr-select__chip-label\">{{ chip.label }}</span>\n @if (!isReadonly()) {\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($index, $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 </span>\n }\n @if (hiddenChipCount(); as extra) {\n <span class=\"wr-select__chip wr-select__chip--more\">{{ moreLabel({ count: extra }) }}</span>\n }\n </span>\n } @else {\n <span class=\"wr-select__placeholder\">{{ resolvedPlaceholder() }}</span>\n }\n } @else {\n @if (selectedLabel(); as label) {\n <!-- Beside the label, not inside it: the harness and the accessible name\n both read `.wr-select__value`, and the visual says nothing either should. -->\n @if (selectedLeading(); as leading) {\n <span class=\"wr-select__value-leading\" aria-hidden=\"true\">\n <ng-container\n [ngTemplateOutlet]=\"leading\"\n [ngTemplateOutletContext]=\"{ $implicit: value(), placement: 'value' }\"\n />\n </span>\n }\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() && !isReadonly()) {\n <span\n class=\"wr-select__clear\"\n role=\"button\"\n [attr.aria-label]=\"resolvedClear()\"\n tabindex=\"-1\"\n (click)=\"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 [attr.aria-busy]=\"isLoading() ? 'true' : null\"\n >\n <!-- `aria-busy` belongs on the listbox, not on this row: it is the container\n whose required `option` children are temporarily missing, and a global\n ARIA attribute on a role-less child makes that child an owned element the\n listbox may not own (axe `aria-required-children`, critical). Plain text\n inside the listbox is fine — only ELEMENTS are checked. -->\n <!-- While the error row is up, through a retry, it carries the loader. -->\n @if (isLoading() && !showLoadError()) {\n <div class=\"wr-select-panel__loading\">{{ 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 [class.wr-option--disabled]=\"atMaxItems() && !isSelected(entry.item)\"\n [attr.aria-selected]=\"isSelected(entry.item)\"\n [attr.aria-disabled]=\"atMaxItems() && !isSelected(entry.item) ? true : null\"\n [attr.aria-setsize]=\"filteredDynamicOptions().length\"\n [attr.aria-posinset]=\"entry.index + 1\"\n (click)=\"selectOption(entry.item)\"\n >\n <!-- Only the select-wide template reaches a virtual row — there is no\n `<wr-option>` here to declare one — and only rows inside the\n rendered window instantiate it. -->\n @if (optionLeading(); as leading) {\n <span class=\"wr-option__leading\" aria-hidden=\"true\">\n <ng-container\n [ngTemplateOutlet]=\"leading\"\n [ngTemplateOutletContext]=\"{ $implicit: entry.item, placement: 'option' }\"\n />\n </span>\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 — always present (a conditional `<ng-content>`\n would drop projected content). Empty whenever `virtualActive`. -->\n <ng-content />\n\n <!-- A listbox may only own `option` and `group` children, and as a bare `<div>`\n this row was the only thing in the tree when it showed — an\n `aria-required-children` critical. It cannot move out of the listbox the\n way the command palette's does: this element IS the listbox, and the id\n the trigger's `aria-controls` names is what gives it its accessible name.\n So it announces as a disabled option instead. Keyboard navigation skips\n it for free — arrow keys walk the registered `wr-option`s, and nothing\n registers this row. -->\n @if (hasNoResults()) {\n <div class=\"wr-select-panel__empty\" role=\"option\" aria-selected=\"false\" aria-disabled=\"true\">\n {{ resolvedNoResults() }}\n </div>\n }\n\n <!-- A failed load, in place of \"no results\". An option for the same reason as\n that row. While it offers a retry it is the option that runs the loader\n again: the field's aria-activedescendant names it, Enter there reaches it,\n and mousedown keeps focus in the field. The retry is DRAWN as a button but\n cannot be one, since a button inside an option is nested interactive\n content; it wears `.wr-btn` and takes the click. -->\n @if (showLoadError()) {\n <div\n class=\"wr-select-panel__error\"\n [class.wr-select-panel__error--retryable]=\"retryOffered()\"\n [class.wr-select-panel__error--active]=\"retryOffered() && !freeText()\"\n role=\"option\"\n aria-selected=\"false\"\n [attr.id]=\"retryOffered() ? retryRowId : null\"\n [attr.aria-disabled]=\"retryOffered() ? null : true\"\n (mousedown)=\"$event.preventDefault()\"\n >\n @if (loadErrorTpl(); as custom) {\n <ng-container [ngTemplateOutlet]=\"custom.template\" [ngTemplateOutletContext]=\"loadErrorContext()\" />\n } @else {\n {{ resolvedLoadError() }}\n @if (retryOffered()) {\n <!-- The spinner as `wr-btn` draws it while loading: hidden from the\n accessibility tree, since the row already reads the retry label. -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <span\n class=\"wr-btn wr-btn--sm wr-select-panel__retry\"\n [class.wr-btn--loading]=\"retrying()\"\n (click)=\"onLoadErrorClick()\"\n >\n @if (retrying()) {\n <wr-spinner class=\"wr-btn__spin\" size=\"sm\" aria-hidden=\"true\" />\n }\n <span class=\"wr-btn__label\">{{ resolvedRetry() }}</span>\n </span>\n }\n }\n </div>\n }\n </div>\n</ng-template>\n",
|
|
1515
|
+
dependencies: [
|
|
1516
|
+
{
|
|
1517
|
+
kind: "component",
|
|
1518
|
+
type: WrOption,
|
|
1519
|
+
selector: "wr-option",
|
|
1520
|
+
inputs: [
|
|
1521
|
+
"value",
|
|
1522
|
+
"disabled",
|
|
1523
|
+
"label"
|
|
1524
|
+
]
|
|
1525
|
+
},
|
|
1526
|
+
{
|
|
1527
|
+
kind: "directive",
|
|
1528
|
+
type: NgTemplateOutlet,
|
|
1529
|
+
selector: "[ngTemplateOutlet]",
|
|
1530
|
+
inputs: [
|
|
1531
|
+
"ngTemplateOutletContext",
|
|
1532
|
+
"ngTemplateOutlet",
|
|
1533
|
+
"ngTemplateOutletInjector"
|
|
1534
|
+
]
|
|
1535
|
+
},
|
|
1536
|
+
{
|
|
1537
|
+
kind: "component",
|
|
1538
|
+
type: WrSpinner,
|
|
1539
|
+
selector: "wr-spinner",
|
|
1540
|
+
inputs: ["size", "ariaLabel"]
|
|
1541
|
+
}
|
|
1542
|
+
],
|
|
1398
1543
|
encapsulation: i0.ViewEncapsulation.None
|
|
1399
1544
|
});
|
|
1400
1545
|
};
|
|
@@ -1409,12 +1554,16 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
1409
1554
|
selector: "wr-select",
|
|
1410
1555
|
encapsulation: ViewEncapsulation.None,
|
|
1411
1556
|
host: { "[class]": "classes()" },
|
|
1412
|
-
imports: [
|
|
1557
|
+
imports: [
|
|
1558
|
+
WrOption,
|
|
1559
|
+
NgTemplateOutlet,
|
|
1560
|
+
WrSpinner
|
|
1561
|
+
],
|
|
1413
1562
|
providers: [{
|
|
1414
1563
|
provide: WR_SELECT,
|
|
1415
1564
|
useExisting: forwardRef(() => WrSelect)
|
|
1416
1565
|
}],
|
|
1417
|
-
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 — 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 <!-- The selected option's leading visual, before the field — the same class\n and the same `placement: 'value'` the button trigger uses, because it is\n the same surface and a consumer who styled one should not have to learn\n a second name. `searchLeading()` draws it only while the field is\n showing the VALUE, so it never stands in front of a query or a bare\n placeholder, and it sits OUTSIDE the input for the obvious reason: an\n `<input>` holds a string. `aria-hidden` keeps it out of the combobox's\n name, which is `resolvedAriaLabel()`, and out of its value, which is the\n input's own text. -->\n @if (searchLeading(); as leading) {\n <span class=\"wr-select__value-leading\" aria-hidden=\"true\">\n <ng-container\n [ngTemplateOutlet]=\"leading\"\n [ngTemplateOutletContext]=\"{ $implicit: value(), placement: 'value' }\"\n />\n </span>\n }\n <!-- `id` / `aria-describedby` / `aria-invalid` come from a surrounding\n `<wr-form-field>` and are null without one — see `controlId` in the\n component. They go on the tab stop, the only labelable element here.\n `aria-label` is the trigger's name and belongs on the same element for\n the same reason: this input carries `role=\"combobox\"`, and the four\n other trigger shapes all bind `resolvedAriaLabel()`. Without it a\n search select with no placeholder rendered a combobox with no name at\n all — the one shape where the documented `ariaLabel` input reached\n nothing. -->\n <!-- `aria-autocomplete` is static, and `list` rather than `both`: the panel\n is the only place a suggestion appears, nothing is written back into\n the field for the user. It describes a permanent CAPABILITY, which is\n why it does not track `open()` — the same reasoning `wr-mention`\n records at its own host binding. -->\n <input\n #searchInput\n type=\"text\"\n role=\"combobox\"\n class=\"wr-select__search-input\"\n autocomplete=\"off\"\n spellcheck=\"false\"\n [attr.id]=\"controlId()\"\n [attr.aria-label]=\"nameLabel()\"\n [attr.aria-labelledby]=\"labelledBy()\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.aria-invalid]=\"ariaInvalid()\"\n [disabled]=\"isDisabled()\"\n [attr.aria-haspopup]=\"'listbox'\"\n aria-autocomplete=\"list\"\n [attr.aria-expanded]=\"open()\"\n [attr.aria-controls]=\"listboxId\"\n [attr.aria-activedescendant]=\"open() ? activeOptionId() || null : null\"\n [attr.aria-readonly]=\"isReadonly() || null\"\n [readonly]=\"isReadonly()\"\n [placeholder]=\"resolvedPlaceholder()\"\n [value]=\"searchDisplay()\"\n (input)=\"onSearchInput($event)\"\n (focus)=\"onSearchFocus()\"\n (keydown)=\"onSearchKey($event)\"\n (blur)=\"onSearchBlur()\"\n />\n <!-- Pointer-only, and the `keydown.enter` / `keydown.space` pair this used to\n carry was dead code rather than the keyboard path it looked like: the ×\n is `tabindex=\"-1\"` with no roving owner, so no key event ever originates\n here to bubble into a handler. It stays unfocusable because the same\n span sits inside the BUTTON trigger further down, where a focusable\n child is `nested-interactive` — the defect the window taskbar's close\n button already taught this repo. The keyboard reaches the function\n instead: Backspace on the empty field, which `onSearchKey` routes to\n `clearAll` for a single and to the last chip for a multi. -->\n @if (clearable() && hasSelection() && !isDisabled() && !isReadonly()) {\n <span\n class=\"wr-select__clear\"\n role=\"button\"\n [attr.aria-label]=\"resolvedClear()\"\n tabindex=\"-1\"\n (click)=\"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 — 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 @if (chip.leading; as leading) {\n <span class=\"wr-select__chip-leading\" aria-hidden=\"true\">\n <ng-container\n [ngTemplateOutlet]=\"leading\"\n [ngTemplateOutletContext]=\"{ $implicit: chip.value, placement: 'chip' }\"\n />\n </span>\n }\n <span class=\"wr-select__chip-label\">{{ chip.label }}</span>\n @if (!isReadonly()) {\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($index, $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 </span>\n }\n @if (hiddenChipCount(); as extra) {\n <span class=\"wr-select__chip wr-select__chip--more\">{{ moreLabel({ count: extra }) }}</span>\n }\n <!-- At `maxItems` this input goes READONLY, never disabled: it is the only\n tab stop in the whole control (the chip × and the clear button are\n `tabindex=\"-1\"` spans), so disabling it left a full tag select with\n nothing focusable at all — and Backspace on an empty draft, the one\n keyboard way back UNDER the cap, is bound to this very element. It was\n never needed as enforcement either: `tryAddTag` already refuses past\n capacity. -->\n <!-- `aria-autocomplete` is conditional here, unlike the search input's:\n this field is only a combobox when a panel backs it. In plain tag\n mode there is no popup to suggest from, and claiming otherwise\n describes a control that does not exist. -->\n <input\n #tagInput\n type=\"text\"\n class=\"wr-select__tag-input\"\n autocomplete=\"off\"\n [attr.id]=\"controlId()\"\n [attr.aria-label]=\"nameLabel()\"\n [attr.aria-labelledby]=\"labelledBy()\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.aria-invalid]=\"ariaInvalid()\"\n spellcheck=\"false\"\n [value]=\"hasChipSearch() ? searchQuery() : draft()\"\n [disabled]=\"isDisabled()\"\n [readonly]=\"isReadonly() || (isTag() && atCapacity())\"\n [attr.aria-readonly]=\"hasChipSearch() && isReadonly() ? true : null\"\n [placeholder]=\"hasSelection() ? '' : resolvedPlaceholder()\"\n [attr.role]=\"hasChipSearch() ? 'combobox' : null\"\n [attr.aria-haspopup]=\"hasChipSearch() ? 'listbox' : null\"\n [attr.aria-autocomplete]=\"hasChipSearch() ? 'list' : 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() && !isReadonly()) {\n <span\n class=\"wr-select__clear\"\n role=\"button\"\n [attr.aria-label]=\"resolvedClear()\"\n tabindex=\"-1\"\n (click)=\"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 <!-- Only for the searchable MULTI, never for tag mode. The two share this\n trigger and differ in the one thing a chevron stands for: the searchable\n multi's input is a `role=\"combobox\"` with `aria-haspopup=\"listbox\"` and\n a panel of options behind it, while tag mode takes free text and has no\n popup at all. Folding the second into this branch left it as the only\n combobox shape in the component with no affordance saying it opens —\n `mode=\"search\"` has one, a searchable single has one, the button trigger\n has one, and this one rendered a bare field. -->\n @if (hasChipSearch()) {\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 }\n </div>\n} @else {\n <button\n #trigger\n type=\"button\"\n class=\"wr-select__trigger\"\n role=\"combobox\"\n [attr.id]=\"controlId()\"\n [attr.aria-label]=\"nameLabel()\"\n [attr.aria-labelledby]=\"labelledBy()\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.aria-invalid]=\"ariaInvalid()\"\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 [attr.aria-readonly]=\"isReadonly() || 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 <!-- A selected chip draws its option's leading visual — one copy per\n VISIBLE chip, so `maxTagCount` caps these too. -->\n @if (chip.leading; as leading) {\n <span class=\"wr-select__chip-leading\" aria-hidden=\"true\">\n <ng-container\n [ngTemplateOutlet]=\"leading\"\n [ngTemplateOutletContext]=\"{ $implicit: chip.value, placement: 'chip' }\"\n />\n </span>\n }\n <span class=\"wr-select__chip-label\">{{ chip.label }}</span>\n @if (!isReadonly()) {\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($index, $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 </span>\n }\n @if (hiddenChipCount(); as extra) {\n <span class=\"wr-select__chip wr-select__chip--more\">{{ moreLabel({ count: extra }) }}</span>\n }\n </span>\n } @else {\n <span class=\"wr-select__placeholder\">{{ resolvedPlaceholder() }}</span>\n }\n } @else {\n @if (selectedLabel(); as label) {\n <!-- Beside the label, not inside it: the harness and the accessible name\n both read `.wr-select__value`, and the visual says nothing either should. -->\n @if (selectedLeading(); as leading) {\n <span class=\"wr-select__value-leading\" aria-hidden=\"true\">\n <ng-container\n [ngTemplateOutlet]=\"leading\"\n [ngTemplateOutletContext]=\"{ $implicit: value(), placement: 'value' }\"\n />\n </span>\n }\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() && !isReadonly()) {\n <span\n class=\"wr-select__clear\"\n role=\"button\"\n [attr.aria-label]=\"resolvedClear()\"\n tabindex=\"-1\"\n (click)=\"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 [attr.aria-busy]=\"isLoading() ? 'true' : null\"\n >\n <!-- `aria-busy` belongs on the listbox, not on this row: it is the container\n whose required `option` children are temporarily missing, and a global\n ARIA attribute on a role-less child makes that child an owned element the\n listbox may not own (axe `aria-required-children`, critical). Plain text\n inside the listbox is fine — only ELEMENTS are checked. -->\n @if (isLoading()) {\n <div class=\"wr-select-panel__loading\">{{ 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 [class.wr-option--disabled]=\"atMaxItems() && !isSelected(entry.item)\"\n [attr.aria-selected]=\"isSelected(entry.item)\"\n [attr.aria-disabled]=\"atMaxItems() && !isSelected(entry.item) ? true : null\"\n [attr.aria-setsize]=\"filteredDynamicOptions().length\"\n [attr.aria-posinset]=\"entry.index + 1\"\n (click)=\"selectOption(entry.item)\"\n >\n <!-- Only the select-wide template reaches a virtual row — there is no\n `<wr-option>` here to declare one — and only rows inside the\n rendered window instantiate it. -->\n @if (optionLeading(); as leading) {\n <span class=\"wr-option__leading\" aria-hidden=\"true\">\n <ng-container\n [ngTemplateOutlet]=\"leading\"\n [ngTemplateOutletContext]=\"{ $implicit: entry.item, placement: 'option' }\"\n />\n </span>\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 — always present (a conditional `<ng-content>`\n would drop projected content). Empty whenever `virtualActive`. -->\n <ng-content />\n\n <!-- A listbox may only own `option` and `group` children, and as a bare `<div>`\n this row was the only thing in the tree when it showed — an\n `aria-required-children` critical. It cannot move out of the listbox the\n way the command palette's does: this element IS the listbox, and the id\n the trigger's `aria-controls` names is what gives it its accessible name.\n So it announces as a disabled option instead. Keyboard navigation skips\n it for free — arrow keys walk the registered `wr-option`s, and nothing\n registers this row. -->\n @if (hasNoResults()) {\n <div class=\"wr-select-panel__empty\" role=\"option\" aria-selected=\"false\" aria-disabled=\"true\">\n {{ resolvedNoResults() }}\n </div>\n }\n </div>\n</ng-template>\n"
|
|
1566
|
+
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 — 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 <!-- The selected option's leading visual, before the field — the same class\n and the same `placement: 'value'` the button trigger uses, because it is\n the same surface and a consumer who styled one should not have to learn\n a second name. `searchLeading()` draws it only while the field is\n showing the VALUE, so it never stands in front of a query or a bare\n placeholder, and it sits OUTSIDE the input for the obvious reason: an\n `<input>` holds a string. `aria-hidden` keeps it out of the combobox's\n name, which is `resolvedAriaLabel()`, and out of its value, which is the\n input's own text. -->\n @if (searchLeading(); as leading) {\n <span class=\"wr-select__value-leading\" aria-hidden=\"true\">\n <ng-container\n [ngTemplateOutlet]=\"leading\"\n [ngTemplateOutletContext]=\"{ $implicit: value(), placement: 'value' }\"\n />\n </span>\n }\n <!-- `id` / `aria-describedby` / `aria-invalid` come from a surrounding\n `<wr-form-field>` and are null without one — see `controlId` in the\n component. They go on the tab stop, the only labelable element here.\n `aria-label` is the trigger's name and belongs on the same element for\n the same reason: this input carries `role=\"combobox\"`, and the four\n other trigger shapes all bind `resolvedAriaLabel()`. Without it a\n search select with no placeholder rendered a combobox with no name at\n all — the one shape where the documented `ariaLabel` input reached\n nothing. -->\n <!-- `aria-autocomplete` is static, and `list` rather than `both`: the panel\n is the only place a suggestion appears, nothing is written back into\n the field for the user. It describes a permanent CAPABILITY, which is\n why it does not track `open()` — the same reasoning `wr-mention`\n records at its own host binding. -->\n <input\n #searchInput\n type=\"text\"\n role=\"combobox\"\n class=\"wr-select__search-input\"\n autocomplete=\"off\"\n spellcheck=\"false\"\n [attr.id]=\"controlId()\"\n [attr.aria-label]=\"nameLabel()\"\n [attr.aria-labelledby]=\"labelledBy()\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.aria-invalid]=\"ariaInvalid()\"\n [disabled]=\"isDisabled()\"\n [attr.aria-haspopup]=\"'listbox'\"\n aria-autocomplete=\"list\"\n [attr.aria-expanded]=\"open()\"\n [attr.aria-controls]=\"listboxId\"\n [attr.aria-activedescendant]=\"open() ? activeOptionId() || null : null\"\n [attr.aria-readonly]=\"isReadonly() || null\"\n [readonly]=\"isReadonly()\"\n [placeholder]=\"resolvedPlaceholder()\"\n [value]=\"searchDisplay()\"\n (input)=\"onSearchInput($event)\"\n (focus)=\"onSearchFocus()\"\n (keydown)=\"onSearchKey($event)\"\n (blur)=\"onSearchBlur()\"\n />\n <!-- Pointer-only, and the `keydown.enter` / `keydown.space` pair this used to\n carry was dead code rather than the keyboard path it looked like: the ×\n is `tabindex=\"-1\"` with no roving owner, so no key event ever originates\n here to bubble into a handler. It stays unfocusable because the same\n span sits inside the BUTTON trigger further down, where a focusable\n child is `nested-interactive` — the defect the window taskbar's close\n button already taught this repo. The keyboard reaches the function\n instead: Backspace on the empty field, which `onSearchKey` routes to\n `clearAll` for a single and to the last chip for a multi. -->\n @if (clearable() && hasSelection() && !isDisabled() && !isReadonly()) {\n <span\n class=\"wr-select__clear\"\n role=\"button\"\n [attr.aria-label]=\"resolvedClear()\"\n tabindex=\"-1\"\n (click)=\"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 — 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 @if (chip.leading; as leading) {\n <span class=\"wr-select__chip-leading\" aria-hidden=\"true\">\n <ng-container\n [ngTemplateOutlet]=\"leading\"\n [ngTemplateOutletContext]=\"{ $implicit: chip.value, placement: 'chip' }\"\n />\n </span>\n }\n <span class=\"wr-select__chip-label\">{{ chip.label }}</span>\n @if (!isReadonly()) {\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($index, $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 </span>\n }\n @if (hiddenChipCount(); as extra) {\n <span class=\"wr-select__chip wr-select__chip--more\">{{ moreLabel({ count: extra }) }}</span>\n }\n <!-- At `maxItems` this input goes READONLY, never disabled: it is the only\n tab stop in the whole control (the chip × and the clear button are\n `tabindex=\"-1\"` spans), so disabling it left a full tag select with\n nothing focusable at all — and Backspace on an empty draft, the one\n keyboard way back UNDER the cap, is bound to this very element. It was\n never needed as enforcement either: `tryAddTag` already refuses past\n capacity. -->\n <!-- `aria-autocomplete` is conditional here, unlike the search input's:\n this field is only a combobox when a panel backs it. In plain tag\n mode there is no popup to suggest from, and claiming otherwise\n describes a control that does not exist. -->\n <input\n #tagInput\n type=\"text\"\n class=\"wr-select__tag-input\"\n autocomplete=\"off\"\n [attr.id]=\"controlId()\"\n [attr.aria-label]=\"nameLabel()\"\n [attr.aria-labelledby]=\"labelledBy()\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.aria-invalid]=\"ariaInvalid()\"\n spellcheck=\"false\"\n [value]=\"hasChipSearch() ? searchQuery() : draft()\"\n [disabled]=\"isDisabled()\"\n [readonly]=\"isReadonly() || (isTag() && atCapacity())\"\n [attr.aria-readonly]=\"hasChipSearch() && isReadonly() ? true : null\"\n [placeholder]=\"hasSelection() ? '' : resolvedPlaceholder()\"\n [attr.role]=\"hasChipSearch() ? 'combobox' : null\"\n [attr.aria-haspopup]=\"hasChipSearch() ? 'listbox' : null\"\n [attr.aria-autocomplete]=\"hasChipSearch() ? 'list' : 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() && !isReadonly()) {\n <span\n class=\"wr-select__clear\"\n role=\"button\"\n [attr.aria-label]=\"resolvedClear()\"\n tabindex=\"-1\"\n (click)=\"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 <!-- Only for the searchable MULTI, never for tag mode. The two share this\n trigger and differ in the one thing a chevron stands for: the searchable\n multi's input is a `role=\"combobox\"` with `aria-haspopup=\"listbox\"` and\n a panel of options behind it, while tag mode takes free text and has no\n popup at all. Folding the second into this branch left it as the only\n combobox shape in the component with no affordance saying it opens —\n `mode=\"search\"` has one, a searchable single has one, the button trigger\n has one, and this one rendered a bare field. -->\n @if (hasChipSearch()) {\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 }\n </div>\n} @else {\n <button\n #trigger\n type=\"button\"\n class=\"wr-select__trigger\"\n role=\"combobox\"\n [attr.id]=\"controlId()\"\n [attr.aria-label]=\"nameLabel()\"\n [attr.aria-labelledby]=\"labelledBy()\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.aria-invalid]=\"ariaInvalid()\"\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 [attr.aria-readonly]=\"isReadonly() || 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 <!-- A selected chip draws its option's leading visual — one copy per\n VISIBLE chip, so `maxTagCount` caps these too. -->\n @if (chip.leading; as leading) {\n <span class=\"wr-select__chip-leading\" aria-hidden=\"true\">\n <ng-container\n [ngTemplateOutlet]=\"leading\"\n [ngTemplateOutletContext]=\"{ $implicit: chip.value, placement: 'chip' }\"\n />\n </span>\n }\n <span class=\"wr-select__chip-label\">{{ chip.label }}</span>\n @if (!isReadonly()) {\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($index, $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 </span>\n }\n @if (hiddenChipCount(); as extra) {\n <span class=\"wr-select__chip wr-select__chip--more\">{{ moreLabel({ count: extra }) }}</span>\n }\n </span>\n } @else {\n <span class=\"wr-select__placeholder\">{{ resolvedPlaceholder() }}</span>\n }\n } @else {\n @if (selectedLabel(); as label) {\n <!-- Beside the label, not inside it: the harness and the accessible name\n both read `.wr-select__value`, and the visual says nothing either should. -->\n @if (selectedLeading(); as leading) {\n <span class=\"wr-select__value-leading\" aria-hidden=\"true\">\n <ng-container\n [ngTemplateOutlet]=\"leading\"\n [ngTemplateOutletContext]=\"{ $implicit: value(), placement: 'value' }\"\n />\n </span>\n }\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() && !isReadonly()) {\n <span\n class=\"wr-select__clear\"\n role=\"button\"\n [attr.aria-label]=\"resolvedClear()\"\n tabindex=\"-1\"\n (click)=\"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 [attr.aria-busy]=\"isLoading() ? 'true' : null\"\n >\n <!-- `aria-busy` belongs on the listbox, not on this row: it is the container\n whose required `option` children are temporarily missing, and a global\n ARIA attribute on a role-less child makes that child an owned element the\n listbox may not own (axe `aria-required-children`, critical). Plain text\n inside the listbox is fine — only ELEMENTS are checked. -->\n <!-- While the error row is up, through a retry, it carries the loader. -->\n @if (isLoading() && !showLoadError()) {\n <div class=\"wr-select-panel__loading\">{{ 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 [class.wr-option--disabled]=\"atMaxItems() && !isSelected(entry.item)\"\n [attr.aria-selected]=\"isSelected(entry.item)\"\n [attr.aria-disabled]=\"atMaxItems() && !isSelected(entry.item) ? true : null\"\n [attr.aria-setsize]=\"filteredDynamicOptions().length\"\n [attr.aria-posinset]=\"entry.index + 1\"\n (click)=\"selectOption(entry.item)\"\n >\n <!-- Only the select-wide template reaches a virtual row — there is no\n `<wr-option>` here to declare one — and only rows inside the\n rendered window instantiate it. -->\n @if (optionLeading(); as leading) {\n <span class=\"wr-option__leading\" aria-hidden=\"true\">\n <ng-container\n [ngTemplateOutlet]=\"leading\"\n [ngTemplateOutletContext]=\"{ $implicit: entry.item, placement: 'option' }\"\n />\n </span>\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 — always present (a conditional `<ng-content>`\n would drop projected content). Empty whenever `virtualActive`. -->\n <ng-content />\n\n <!-- A listbox may only own `option` and `group` children, and as a bare `<div>`\n this row was the only thing in the tree when it showed — an\n `aria-required-children` critical. It cannot move out of the listbox the\n way the command palette's does: this element IS the listbox, and the id\n the trigger's `aria-controls` names is what gives it its accessible name.\n So it announces as a disabled option instead. Keyboard navigation skips\n it for free — arrow keys walk the registered `wr-option`s, and nothing\n registers this row. -->\n @if (hasNoResults()) {\n <div class=\"wr-select-panel__empty\" role=\"option\" aria-selected=\"false\" aria-disabled=\"true\">\n {{ resolvedNoResults() }}\n </div>\n }\n\n <!-- A failed load, in place of \"no results\". An option for the same reason as\n that row. While it offers a retry it is the option that runs the loader\n again: the field's aria-activedescendant names it, Enter there reaches it,\n and mousedown keeps focus in the field. The retry is DRAWN as a button but\n cannot be one, since a button inside an option is nested interactive\n content; it wears `.wr-btn` and takes the click. -->\n @if (showLoadError()) {\n <div\n class=\"wr-select-panel__error\"\n [class.wr-select-panel__error--retryable]=\"retryOffered()\"\n [class.wr-select-panel__error--active]=\"retryOffered() && !freeText()\"\n role=\"option\"\n aria-selected=\"false\"\n [attr.id]=\"retryOffered() ? retryRowId : null\"\n [attr.aria-disabled]=\"retryOffered() ? null : true\"\n (mousedown)=\"$event.preventDefault()\"\n >\n @if (loadErrorTpl(); as custom) {\n <ng-container [ngTemplateOutlet]=\"custom.template\" [ngTemplateOutletContext]=\"loadErrorContext()\" />\n } @else {\n {{ resolvedLoadError() }}\n @if (retryOffered()) {\n <!-- The spinner as `wr-btn` draws it while loading: hidden from the\n accessibility tree, since the row already reads the retry label. -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <span\n class=\"wr-btn wr-btn--sm wr-select-panel__retry\"\n [class.wr-btn--loading]=\"retrying()\"\n (click)=\"onLoadErrorClick()\"\n >\n @if (retrying()) {\n <wr-spinner class=\"wr-btn__spin\" size=\"sm\" aria-hidden=\"true\" />\n }\n <span class=\"wr-btn__label\">{{ resolvedRetry() }}</span>\n </span>\n }\n }\n </div>\n }\n </div>\n</ng-template>\n"
|
|
1418
1567
|
}]
|
|
1419
1568
|
}],
|
|
1420
1569
|
ctorParameters: () => [],
|
|
@@ -1606,6 +1755,30 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
1606
1755
|
required: false
|
|
1607
1756
|
}]
|
|
1608
1757
|
}],
|
|
1758
|
+
loadErrorText: [{
|
|
1759
|
+
type: i0.Input,
|
|
1760
|
+
args: [{
|
|
1761
|
+
isSignal: true,
|
|
1762
|
+
alias: "loadErrorText",
|
|
1763
|
+
required: false
|
|
1764
|
+
}]
|
|
1765
|
+
}],
|
|
1766
|
+
retryable: [{
|
|
1767
|
+
type: i0.Input,
|
|
1768
|
+
args: [{
|
|
1769
|
+
isSignal: true,
|
|
1770
|
+
alias: "retryable",
|
|
1771
|
+
required: false
|
|
1772
|
+
}]
|
|
1773
|
+
}],
|
|
1774
|
+
retryText: [{
|
|
1775
|
+
type: i0.Input,
|
|
1776
|
+
args: [{
|
|
1777
|
+
isSignal: true,
|
|
1778
|
+
alias: "retryText",
|
|
1779
|
+
required: false
|
|
1780
|
+
}]
|
|
1781
|
+
}],
|
|
1609
1782
|
searchChange: [{
|
|
1610
1783
|
type: i0.Output,
|
|
1611
1784
|
args: ["searchChange"]
|
|
@@ -1703,6 +1876,13 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
1703
1876
|
isSignal: true
|
|
1704
1877
|
}]
|
|
1705
1878
|
}],
|
|
1879
|
+
loadErrorTpl: [{
|
|
1880
|
+
type: i0.ContentChild,
|
|
1881
|
+
args: [i0.forwardRef(() => WrSelectLoadError), {
|
|
1882
|
+
descendants: false,
|
|
1883
|
+
isSignal: true
|
|
1884
|
+
}]
|
|
1885
|
+
}],
|
|
1706
1886
|
panelTpl: [{
|
|
1707
1887
|
type: i0.ViewChild,
|
|
1708
1888
|
args: ["panelTpl", {
|
|
@@ -1791,6 +1971,6 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
1791
1971
|
}]
|
|
1792
1972
|
}] }
|
|
1793
1973
|
});
|
|
1794
|
-
export { WR_SELECT, WrOption, WrOptionGroup, WrOptionLeading, WrSelect };
|
|
1974
|
+
export { WR_SELECT, WrOption, WrOptionGroup, WrOptionLeading, WrSelect, WrSelectLoadError };
|
|
1795
1975
|
|
|
1796
1976
|
//# sourceMappingURL=ngwr-select.mjs.map
|