@cdevhub/ngx-tw 0.2.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/LICENSE +21 -0
- package/README.md +134 -0
- package/fesm2022/cdevhub-ngx-tw-accordion.mjs +108 -0
- package/fesm2022/cdevhub-ngx-tw-accordion.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-alert.mjs +240 -0
- package/fesm2022/cdevhub-ngx-tw-alert.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-avatar.mjs +287 -0
- package/fesm2022/cdevhub-ngx-tw-avatar.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-badge.mjs +332 -0
- package/fesm2022/cdevhub-ngx-tw-badge.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-breadcrumbs.mjs +452 -0
- package/fesm2022/cdevhub-ngx-tw-breadcrumbs.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-button.mjs +227 -0
- package/fesm2022/cdevhub-ngx-tw-button.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-calendar-luxon.mjs +417 -0
- package/fesm2022/cdevhub-ngx-tw-calendar-luxon.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-calendar-testing.mjs +232 -0
- package/fesm2022/cdevhub-ngx-tw-calendar-testing.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-calendar.mjs +4482 -0
- package/fesm2022/cdevhub-ngx-tw-calendar.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-card.mjs +157 -0
- package/fesm2022/cdevhub-ngx-tw-card.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-carousel.mjs +1454 -0
- package/fesm2022/cdevhub-ngx-tw-carousel.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-checkbox.mjs +593 -0
- package/fesm2022/cdevhub-ngx-tw-checkbox.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-code-block.mjs +228 -0
- package/fesm2022/cdevhub-ngx-tw-code-block.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-collapsible.mjs +510 -0
- package/fesm2022/cdevhub-ngx-tw-collapsible.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-combobox.mjs +1721 -0
- package/fesm2022/cdevhub-ngx-tw-combobox.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-command-palette.mjs +721 -0
- package/fesm2022/cdevhub-ngx-tw-command-palette.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-core.mjs +861 -0
- package/fesm2022/cdevhub-ngx-tw-core.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-date-picker.mjs +1558 -0
- package/fesm2022/cdevhub-ngx-tw-date-picker.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-date-range-picker.mjs +1833 -0
- package/fesm2022/cdevhub-ngx-tw-date-range-picker.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-dialog.mjs +693 -0
- package/fesm2022/cdevhub-ngx-tw-dialog.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-empty-state.mjs +331 -0
- package/fesm2022/cdevhub-ngx-tw-empty-state.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-flip-card.mjs +289 -0
- package/fesm2022/cdevhub-ngx-tw-flip-card.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-form-field.mjs +572 -0
- package/fesm2022/cdevhub-ngx-tw-form-field.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-icon-lucide.mjs +36 -0
- package/fesm2022/cdevhub-ngx-tw-icon-lucide.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-icon.mjs +278 -0
- package/fesm2022/cdevhub-ngx-tw-icon.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-input.mjs +355 -0
- package/fesm2022/cdevhub-ngx-tw-input.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-item.mjs +241 -0
- package/fesm2022/cdevhub-ngx-tw-item.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-menu.mjs +433 -0
- package/fesm2022/cdevhub-ngx-tw-menu.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-paginator.mjs +691 -0
- package/fesm2022/cdevhub-ngx-tw-paginator.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-popover.mjs +678 -0
- package/fesm2022/cdevhub-ngx-tw-popover.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-progress-bar.mjs +335 -0
- package/fesm2022/cdevhub-ngx-tw-progress-bar.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-radio.mjs +708 -0
- package/fesm2022/cdevhub-ngx-tw-radio.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-segmented-control.mjs +319 -0
- package/fesm2022/cdevhub-ngx-tw-segmented-control.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-select.mjs +1722 -0
- package/fesm2022/cdevhub-ngx-tw-select.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-separator.mjs +115 -0
- package/fesm2022/cdevhub-ngx-tw-separator.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-sheet.mjs +768 -0
- package/fesm2022/cdevhub-ngx-tw-sheet.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-skeleton.mjs +149 -0
- package/fesm2022/cdevhub-ngx-tw-skeleton.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-slider.mjs +1147 -0
- package/fesm2022/cdevhub-ngx-tw-slider.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-sort.mjs +346 -0
- package/fesm2022/cdevhub-ngx-tw-sort.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-spinner.mjs +186 -0
- package/fesm2022/cdevhub-ngx-tw-spinner.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-split.mjs +1082 -0
- package/fesm2022/cdevhub-ngx-tw-split.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-stat.mjs +579 -0
- package/fesm2022/cdevhub-ngx-tw-stat.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-stepper.mjs +443 -0
- package/fesm2022/cdevhub-ngx-tw-stepper.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-switch.mjs +391 -0
- package/fesm2022/cdevhub-ngx-tw-switch.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-tab-nav.mjs +315 -0
- package/fesm2022/cdevhub-ngx-tw-tab-nav.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-table.mjs +994 -0
- package/fesm2022/cdevhub-ngx-tw-table.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-tabs.mjs +508 -0
- package/fesm2022/cdevhub-ngx-tw-tabs.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-textarea.mjs +147 -0
- package/fesm2022/cdevhub-ngx-tw-textarea.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-theme.mjs +178 -0
- package/fesm2022/cdevhub-ngx-tw-theme.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-time-picker.mjs +1344 -0
- package/fesm2022/cdevhub-ngx-tw-time-picker.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-timeline.mjs +1136 -0
- package/fesm2022/cdevhub-ngx-tw-timeline.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-toast.mjs +1311 -0
- package/fesm2022/cdevhub-ngx-tw-toast.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-tooltip.mjs +533 -0
- package/fesm2022/cdevhub-ngx-tw-tooltip.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw.mjs +61 -0
- package/fesm2022/cdevhub-ngx-tw.mjs.map +1 -0
- package/package.json +265 -0
- package/theme/_base.css +394 -0
- package/theme/_dark.css +510 -0
- package/theme/_high-contrast.css +290 -0
- package/theme/_semantic.css +317 -0
- package/theme/_typography.css +9 -0
- package/theme/index.css +54 -0
- package/types/cdevhub-ngx-tw-accordion.d.ts +48 -0
- package/types/cdevhub-ngx-tw-alert.d.ts +67 -0
- package/types/cdevhub-ngx-tw-avatar.d.ts +79 -0
- package/types/cdevhub-ngx-tw-badge.d.ts +94 -0
- package/types/cdevhub-ngx-tw-breadcrumbs.d.ts +227 -0
- package/types/cdevhub-ngx-tw-button.d.ts +74 -0
- package/types/cdevhub-ngx-tw-calendar-luxon.d.ts +200 -0
- package/types/cdevhub-ngx-tw-calendar-testing.d.ts +113 -0
- package/types/cdevhub-ngx-tw-calendar.d.ts +1704 -0
- package/types/cdevhub-ngx-tw-card.d.ts +48 -0
- package/types/cdevhub-ngx-tw-carousel.d.ts +406 -0
- package/types/cdevhub-ngx-tw-checkbox.d.ts +136 -0
- package/types/cdevhub-ngx-tw-code-block.d.ts +93 -0
- package/types/cdevhub-ngx-tw-collapsible.d.ts +151 -0
- package/types/cdevhub-ngx-tw-combobox.d.ts +389 -0
- package/types/cdevhub-ngx-tw-command-palette.d.ts +215 -0
- package/types/cdevhub-ngx-tw-core.d.ts +719 -0
- package/types/cdevhub-ngx-tw-date-picker.d.ts +317 -0
- package/types/cdevhub-ngx-tw-date-range-picker.d.ts +313 -0
- package/types/cdevhub-ngx-tw-dialog.d.ts +303 -0
- package/types/cdevhub-ngx-tw-empty-state.d.ts +104 -0
- package/types/cdevhub-ngx-tw-flip-card.d.ts +95 -0
- package/types/cdevhub-ngx-tw-form-field.d.ts +210 -0
- package/types/cdevhub-ngx-tw-icon-lucide.d.ts +29 -0
- package/types/cdevhub-ngx-tw-icon.d.ts +134 -0
- package/types/cdevhub-ngx-tw-input.d.ts +110 -0
- package/types/cdevhub-ngx-tw-item.d.ts +70 -0
- package/types/cdevhub-ngx-tw-menu.d.ts +99 -0
- package/types/cdevhub-ngx-tw-paginator.d.ts +305 -0
- package/types/cdevhub-ngx-tw-popover.d.ts +157 -0
- package/types/cdevhub-ngx-tw-progress-bar.d.ts +100 -0
- package/types/cdevhub-ngx-tw-radio.d.ts +174 -0
- package/types/cdevhub-ngx-tw-segmented-control.d.ts +66 -0
- package/types/cdevhub-ngx-tw-select.d.ts +384 -0
- package/types/cdevhub-ngx-tw-separator.d.ts +28 -0
- package/types/cdevhub-ngx-tw-sheet.d.ts +335 -0
- package/types/cdevhub-ngx-tw-skeleton.d.ts +52 -0
- package/types/cdevhub-ngx-tw-slider.d.ts +258 -0
- package/types/cdevhub-ngx-tw-sort.d.ts +136 -0
- package/types/cdevhub-ngx-tw-spinner.d.ts +50 -0
- package/types/cdevhub-ngx-tw-split.d.ts +265 -0
- package/types/cdevhub-ngx-tw-stat.d.ts +140 -0
- package/types/cdevhub-ngx-tw-stepper.d.ts +127 -0
- package/types/cdevhub-ngx-tw-switch.d.ts +88 -0
- package/types/cdevhub-ngx-tw-tab-nav.d.ts +107 -0
- package/types/cdevhub-ngx-tw-table.d.ts +479 -0
- package/types/cdevhub-ngx-tw-tabs.d.ts +121 -0
- package/types/cdevhub-ngx-tw-textarea.d.ts +62 -0
- package/types/cdevhub-ngx-tw-theme.d.ts +106 -0
- package/types/cdevhub-ngx-tw-time-picker.d.ts +303 -0
- package/types/cdevhub-ngx-tw-timeline.d.ts +278 -0
- package/types/cdevhub-ngx-tw-toast.d.ts +388 -0
- package/types/cdevhub-ngx-tw-tooltip.d.ts +95 -0
- package/types/cdevhub-ngx-tw.d.ts +49 -0
|
@@ -0,0 +1,1722 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { inject, ElementRef, signal, viewChild, computed, ChangeDetectionStrategy, Component, TemplateRef, Directive, input, model, output, contentChild, ViewContainerRef, Injector, DestroyRef, linkedSignal, effect, untracked, afterNextRender, isDevMode, forwardRef } from '@angular/core';
|
|
3
|
+
import { NgTemplateOutlet } from '@angular/common';
|
|
4
|
+
import { NgControl, NgForm, FormGroupDirective } from '@angular/forms';
|
|
5
|
+
import { Overlay } from '@angular/cdk/overlay';
|
|
6
|
+
import { ComponentPortal } from '@angular/cdk/portal';
|
|
7
|
+
import { Platform } from '@angular/cdk/platform';
|
|
8
|
+
import { FocusMonitor, LiveAnnouncer } from '@angular/cdk/a11y';
|
|
9
|
+
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
10
|
+
import { Subscription, merge } from 'rxjs';
|
|
11
|
+
import { tv } from 'tailwind-variants';
|
|
12
|
+
import { TW_ERROR_STATE_MATCHER, buildSelectLikePositions, resolveSelectScrollStrategy, consumeOverlayEscape } from '@cdevhub/ngx-tw/core';
|
|
13
|
+
import { FormFieldComponent, TW_FORM_FIELD_CONTROL } from '@cdevhub/ngx-tw/form-field';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Internal overlay panel for `tw-select`. Renders the header/search/listbox/empty/footer
|
|
17
|
+
* regions and dispatches interaction callbacks back to the parent `SelectComponent`.
|
|
18
|
+
* Not exported from the public API.
|
|
19
|
+
*/
|
|
20
|
+
class SelectOverlayComponent {
|
|
21
|
+
elementRef = inject((ElementRef));
|
|
22
|
+
// ── Config signals set by the parent component ──
|
|
23
|
+
/** @internal */
|
|
24
|
+
size = signal('md', ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
|
|
25
|
+
/** @internal */
|
|
26
|
+
color = signal('primary', ...(ngDevMode ? [{ debugName: "color" }] : /* istanbul ignore next */ []));
|
|
27
|
+
/** @internal */
|
|
28
|
+
multiple = signal(false, ...(ngDevMode ? [{ debugName: "multiple" }] : /* istanbul ignore next */ []));
|
|
29
|
+
/** @internal */
|
|
30
|
+
searchable = signal(false, ...(ngDevMode ? [{ debugName: "searchable" }] : /* istanbul ignore next */ []));
|
|
31
|
+
/** @internal */
|
|
32
|
+
panelMaxHeight = signal(256, ...(ngDevMode ? [{ debugName: "panelMaxHeight" }] : /* istanbul ignore next */ []));
|
|
33
|
+
/** @internal */
|
|
34
|
+
emptyMessage = signal('No results', ...(ngDevMode ? [{ debugName: "emptyMessage" }] : /* istanbul ignore next */ []));
|
|
35
|
+
/** @internal */
|
|
36
|
+
search = signal('', ...(ngDevMode ? [{ debugName: "search" }] : /* istanbul ignore next */ []));
|
|
37
|
+
/** @internal */
|
|
38
|
+
activeIndex = signal(-1, ...(ngDevMode ? [{ debugName: "activeIndex" }] : /* istanbul ignore next */ []));
|
|
39
|
+
/** @internal */
|
|
40
|
+
renderedRows = signal([], ...(ngDevMode ? [{ debugName: "renderedRows" }] : /* istanbul ignore next */ []));
|
|
41
|
+
/** @internal */
|
|
42
|
+
visibleOptions = signal([], ...(ngDevMode ? [{ debugName: "visibleOptions" }] : /* istanbul ignore next */ []));
|
|
43
|
+
/** @internal */
|
|
44
|
+
optionTemplate = signal(undefined, ...(ngDevMode ? [{ debugName: "optionTemplate" }] : /* istanbul ignore next */ []));
|
|
45
|
+
/** @internal */
|
|
46
|
+
emptyTemplate = signal(undefined, ...(ngDevMode ? [{ debugName: "emptyTemplate" }] : /* istanbul ignore next */ []));
|
|
47
|
+
/** @internal */
|
|
48
|
+
headerTemplate = signal(undefined, ...(ngDevMode ? [{ debugName: "headerTemplate" }] : /* istanbul ignore next */ []));
|
|
49
|
+
/** @internal */
|
|
50
|
+
footerTemplate = signal(undefined, ...(ngDevMode ? [{ debugName: "footerTemplate" }] : /* istanbul ignore next */ []));
|
|
51
|
+
/** @internal */
|
|
52
|
+
optionLabelFn = signal(() => '', ...(ngDevMode ? [{ debugName: "optionLabelFn" }] : /* istanbul ignore next */ []));
|
|
53
|
+
/** @internal */
|
|
54
|
+
selectedChecker = signal(() => false, ...(ngDevMode ? [{ debugName: "selectedChecker" }] : /* istanbul ignore next */ []));
|
|
55
|
+
/** @internal */
|
|
56
|
+
computeOptionClass = signal(() => '', ...(ngDevMode ? [{ debugName: "computeOptionClass" }] : /* istanbul ignore next */ []));
|
|
57
|
+
/** @internal */
|
|
58
|
+
checkmarkColorClass = signal('', ...(ngDevMode ? [{ debugName: "checkmarkColorClass" }] : /* istanbul ignore next */ []));
|
|
59
|
+
/** @internal */
|
|
60
|
+
panelClassValue = signal('', ...(ngDevMode ? [{ debugName: "panelClassValue" }] : /* istanbul ignore next */ []));
|
|
61
|
+
/** @internal */
|
|
62
|
+
listboxId = signal('', ...(ngDevMode ? [{ debugName: "listboxId" }] : /* istanbul ignore next */ []));
|
|
63
|
+
/** @internal */
|
|
64
|
+
searchInputId = signal('', ...(ngDevMode ? [{ debugName: "searchInputId" }] : /* istanbul ignore next */ []));
|
|
65
|
+
/** @internal */
|
|
66
|
+
optionIdFn = signal((i) => `option-${i}`, ...(ngDevMode ? [{ debugName: "optionIdFn" }] : /* istanbul ignore next */ []));
|
|
67
|
+
/** @internal */
|
|
68
|
+
leaving = signal(false, ...(ngDevMode ? [{ debugName: "leaving" }] : /* istanbul ignore next */ []));
|
|
69
|
+
// ── Callbacks set by the parent component ──
|
|
70
|
+
/** @internal */
|
|
71
|
+
onSearchInput = signal(() => { }, ...(ngDevMode ? [{ debugName: "onSearchInput" }] : /* istanbul ignore next */ []));
|
|
72
|
+
/** @internal */
|
|
73
|
+
onOptionSelect = signal(() => { }, ...(ngDevMode ? [{ debugName: "onOptionSelect" }] : /* istanbul ignore next */ []));
|
|
74
|
+
/** @internal */
|
|
75
|
+
onOptionActivate = signal(() => { }, ...(ngDevMode ? [{ debugName: "onOptionActivate" }] : /* istanbul ignore next */ []));
|
|
76
|
+
/** @internal */
|
|
77
|
+
onPanelKeydown = signal(() => { }, ...(ngDevMode ? [{ debugName: "onPanelKeydown" }] : /* istanbul ignore next */ []));
|
|
78
|
+
// ── View refs ──
|
|
79
|
+
searchInputRef = viewChild('searchInput', ...(ngDevMode ? [{ debugName: "searchInputRef" }] : /* istanbul ignore next */ []));
|
|
80
|
+
// ── Computed classes ──
|
|
81
|
+
/** @internal */
|
|
82
|
+
panelClasses = computed(() => {
|
|
83
|
+
const base = 'block w-full bg-surface-overlay border border-border rounded-lg shadow-md overflow-hidden flex flex-col';
|
|
84
|
+
const custom = this.panelClassValue();
|
|
85
|
+
return custom ? `${base} ${custom}` : base;
|
|
86
|
+
}, ...(ngDevMode ? [{ debugName: "panelClasses" }] : /* istanbul ignore next */ []));
|
|
87
|
+
/** @internal */
|
|
88
|
+
checkmarkClasses = computed(() => {
|
|
89
|
+
// xs/sm densities use `size-3.5` (14px) for the checkmark — the half-step
|
|
90
|
+
// is the only icon size that aligns with the surrounding text-xs metric.
|
|
91
|
+
const size = this.size() === 'xs' || this.size() === 'sm' ? 'size-3.5' : 'size-4';
|
|
92
|
+
return `${size} shrink-0 ml-auto ${this.checkmarkColorClass()}`;
|
|
93
|
+
}, ...(ngDevMode ? [{ debugName: "checkmarkClasses" }] : /* istanbul ignore next */ []));
|
|
94
|
+
// ── Template helpers ──
|
|
95
|
+
/** @internal */
|
|
96
|
+
isOptionDisabled(visibleIndex) {
|
|
97
|
+
const vis = this.visibleOptions();
|
|
98
|
+
if (visibleIndex < 0 || visibleIndex >= vis.length)
|
|
99
|
+
return false;
|
|
100
|
+
return vis[visibleIndex].disabled;
|
|
101
|
+
}
|
|
102
|
+
/** @internal */
|
|
103
|
+
buildOptionContext(visibleIndex, option) {
|
|
104
|
+
const v = this.visibleOptions()[visibleIndex];
|
|
105
|
+
return {
|
|
106
|
+
$implicit: option,
|
|
107
|
+
label: v?.label ?? '',
|
|
108
|
+
value: v?.value,
|
|
109
|
+
selected: this.selectedChecker()(visibleIndex),
|
|
110
|
+
active: this.activeIndex() === visibleIndex,
|
|
111
|
+
disabled: v?.disabled ?? false,
|
|
112
|
+
index: visibleIndex,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/** @internal track function for @for. */
|
|
116
|
+
trackRow(_index, row) {
|
|
117
|
+
if (row.kind === 'group-label')
|
|
118
|
+
return `g:${row.group}`;
|
|
119
|
+
return `o:${row.index}`;
|
|
120
|
+
}
|
|
121
|
+
// ── Interactions ──
|
|
122
|
+
/** @internal */
|
|
123
|
+
onSearchInputEvent(event) {
|
|
124
|
+
const target = event.target;
|
|
125
|
+
this.onSearchInput()(target.value);
|
|
126
|
+
}
|
|
127
|
+
/** @internal */
|
|
128
|
+
onSearchKeydown(event) {
|
|
129
|
+
this.onPanelKeydown()(event);
|
|
130
|
+
}
|
|
131
|
+
/** @internal */
|
|
132
|
+
onOptionClick(visibleIndex, event) {
|
|
133
|
+
event.stopPropagation();
|
|
134
|
+
event.preventDefault();
|
|
135
|
+
this.onOptionSelect()(visibleIndex);
|
|
136
|
+
}
|
|
137
|
+
/** @internal */
|
|
138
|
+
onOptionMouseEnter(visibleIndex) {
|
|
139
|
+
if (this.isOptionDisabled(visibleIndex))
|
|
140
|
+
return;
|
|
141
|
+
this.onOptionActivate()(visibleIndex);
|
|
142
|
+
}
|
|
143
|
+
/** @internal */
|
|
144
|
+
focusSearchInput() {
|
|
145
|
+
const el = this.searchInputRef()?.nativeElement;
|
|
146
|
+
el?.focus();
|
|
147
|
+
}
|
|
148
|
+
/** @internal Exposes the root overlay element for tests. */
|
|
149
|
+
getElement() {
|
|
150
|
+
return this.elementRef.nativeElement;
|
|
151
|
+
}
|
|
152
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: SelectOverlayComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
153
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: SelectOverlayComponent, isStandalone: true, selector: "tw-select-overlay", host: { properties: { "class": "panelClasses()", "animate.enter": "\"scale-in\"", "animate.leave": "\"scale-out\"" } }, viewQueries: [{ propertyName: "searchInputRef", first: true, predicate: ["searchInput"], descendants: true, isSignal: true }], ngImport: i0, template: `
|
|
154
|
+
@if (headerTemplate(); as tpl) {
|
|
155
|
+
<div class="border-b border-border p-2">
|
|
156
|
+
<ng-container *ngTemplateOutlet="tpl.templateRef" />
|
|
157
|
+
</div>
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
@if (searchable()) {
|
|
161
|
+
<div class="p-2 border-b border-border">
|
|
162
|
+
<input
|
|
163
|
+
#searchInput
|
|
164
|
+
type="search"
|
|
165
|
+
[id]="searchInputId()"
|
|
166
|
+
[attr.aria-label]="'Search'"
|
|
167
|
+
[attr.aria-controls]="listboxId()"
|
|
168
|
+
[value]="search()"
|
|
169
|
+
class="w-full px-3 py-1.5 rounded-md border border-border bg-surface text-fg placeholder:text-fg-subtle focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500 transition-colors duration-normal motion-reduce:transition-none text-sm"
|
|
170
|
+
placeholder="Search…"
|
|
171
|
+
(input)="onSearchInputEvent($event)"
|
|
172
|
+
(keydown)="onSearchKeydown($event)"
|
|
173
|
+
/>
|
|
174
|
+
</div>
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
<div
|
|
178
|
+
#listboxEl
|
|
179
|
+
role="listbox"
|
|
180
|
+
tabindex="-1"
|
|
181
|
+
[id]="listboxId()"
|
|
182
|
+
[attr.aria-multiselectable]="multiple() ? 'true' : null"
|
|
183
|
+
[style.maxHeight.px]="panelMaxHeight()"
|
|
184
|
+
class="flex-1 min-h-0 overflow-y-auto py-1 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500"
|
|
185
|
+
>
|
|
186
|
+
@if (visibleOptions().length === 0) {
|
|
187
|
+
@if (emptyTemplate(); as tpl) {
|
|
188
|
+
<ng-container *ngTemplateOutlet="tpl.templateRef; context: { $implicit: search() }" />
|
|
189
|
+
} @else {
|
|
190
|
+
<div class="p-4 text-center text-sm text-fg-muted">{{ emptyMessage() }}</div>
|
|
191
|
+
}
|
|
192
|
+
} @else {
|
|
193
|
+
@for (row of renderedRows(); track trackRow($index, row)) {
|
|
194
|
+
@if (row.kind === 'group-label') {
|
|
195
|
+
<div role="group" [attr.aria-label]="row.group">
|
|
196
|
+
<div class="px-3 py-1 text-xs font-semibold uppercase tracking-wide text-fg-subtle">
|
|
197
|
+
{{ row.group }}
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
} @else {
|
|
201
|
+
<div
|
|
202
|
+
role="option"
|
|
203
|
+
[id]="optionIdFn()(row.index)"
|
|
204
|
+
[attr.aria-selected]="selectedChecker()(row.index) ? 'true' : 'false'"
|
|
205
|
+
[attr.aria-disabled]="isOptionDisabled(row.index) || null"
|
|
206
|
+
[class]="
|
|
207
|
+
computeOptionClass()(
|
|
208
|
+
selectedChecker()(row.index),
|
|
209
|
+
activeIndex() === row.index,
|
|
210
|
+
isOptionDisabled(row.index)
|
|
211
|
+
)
|
|
212
|
+
"
|
|
213
|
+
(click)="onOptionClick(row.index, $event)"
|
|
214
|
+
(mouseenter)="onOptionMouseEnter(row.index)"
|
|
215
|
+
>
|
|
216
|
+
@if (optionTemplate(); as tpl) {
|
|
217
|
+
<ng-container
|
|
218
|
+
*ngTemplateOutlet="
|
|
219
|
+
tpl.templateRef;
|
|
220
|
+
context: buildOptionContext(row.index, row.option)
|
|
221
|
+
"
|
|
222
|
+
/>
|
|
223
|
+
} @else {
|
|
224
|
+
<span class="flex-1 min-w-0 truncate">{{ optionLabelFn()(row.option) }}</span>
|
|
225
|
+
@if (selectedChecker()(row.index)) {
|
|
226
|
+
<svg
|
|
227
|
+
viewBox="0 0 20 20"
|
|
228
|
+
fill="currentColor"
|
|
229
|
+
aria-hidden="true"
|
|
230
|
+
[class]="checkmarkClasses()"
|
|
231
|
+
>
|
|
232
|
+
<path
|
|
233
|
+
fill-rule="evenodd"
|
|
234
|
+
d="M16.704 5.29a1 1 0 0 1 .006 1.414l-7.2 7.3a1 1 0 0 1-1.42.007L3.29 9.296a1 1 0 0 1 1.42-1.41l3.794 3.82 6.79-6.886a1 1 0 0 1 1.41-.07Z"
|
|
235
|
+
clip-rule="evenodd"
|
|
236
|
+
/>
|
|
237
|
+
</svg>
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
</div>
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
</div>
|
|
245
|
+
|
|
246
|
+
@if (footerTemplate(); as tpl) {
|
|
247
|
+
<div class="border-t border-border p-2">
|
|
248
|
+
<ng-container *ngTemplateOutlet="tpl.templateRef" />
|
|
249
|
+
</div>
|
|
250
|
+
}
|
|
251
|
+
`, isInline: true, dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
252
|
+
}
|
|
253
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: SelectOverlayComponent, decorators: [{
|
|
254
|
+
type: Component,
|
|
255
|
+
args: [{
|
|
256
|
+
selector: 'tw-select-overlay',
|
|
257
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
258
|
+
imports: [NgTemplateOutlet],
|
|
259
|
+
host: {
|
|
260
|
+
'[class]': 'panelClasses()',
|
|
261
|
+
'[animate.enter]': '"scale-in"',
|
|
262
|
+
'[animate.leave]': '"scale-out"',
|
|
263
|
+
},
|
|
264
|
+
template: `
|
|
265
|
+
@if (headerTemplate(); as tpl) {
|
|
266
|
+
<div class="border-b border-border p-2">
|
|
267
|
+
<ng-container *ngTemplateOutlet="tpl.templateRef" />
|
|
268
|
+
</div>
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
@if (searchable()) {
|
|
272
|
+
<div class="p-2 border-b border-border">
|
|
273
|
+
<input
|
|
274
|
+
#searchInput
|
|
275
|
+
type="search"
|
|
276
|
+
[id]="searchInputId()"
|
|
277
|
+
[attr.aria-label]="'Search'"
|
|
278
|
+
[attr.aria-controls]="listboxId()"
|
|
279
|
+
[value]="search()"
|
|
280
|
+
class="w-full px-3 py-1.5 rounded-md border border-border bg-surface text-fg placeholder:text-fg-subtle focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500 transition-colors duration-normal motion-reduce:transition-none text-sm"
|
|
281
|
+
placeholder="Search…"
|
|
282
|
+
(input)="onSearchInputEvent($event)"
|
|
283
|
+
(keydown)="onSearchKeydown($event)"
|
|
284
|
+
/>
|
|
285
|
+
</div>
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
<div
|
|
289
|
+
#listboxEl
|
|
290
|
+
role="listbox"
|
|
291
|
+
tabindex="-1"
|
|
292
|
+
[id]="listboxId()"
|
|
293
|
+
[attr.aria-multiselectable]="multiple() ? 'true' : null"
|
|
294
|
+
[style.maxHeight.px]="panelMaxHeight()"
|
|
295
|
+
class="flex-1 min-h-0 overflow-y-auto py-1 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500"
|
|
296
|
+
>
|
|
297
|
+
@if (visibleOptions().length === 0) {
|
|
298
|
+
@if (emptyTemplate(); as tpl) {
|
|
299
|
+
<ng-container *ngTemplateOutlet="tpl.templateRef; context: { $implicit: search() }" />
|
|
300
|
+
} @else {
|
|
301
|
+
<div class="p-4 text-center text-sm text-fg-muted">{{ emptyMessage() }}</div>
|
|
302
|
+
}
|
|
303
|
+
} @else {
|
|
304
|
+
@for (row of renderedRows(); track trackRow($index, row)) {
|
|
305
|
+
@if (row.kind === 'group-label') {
|
|
306
|
+
<div role="group" [attr.aria-label]="row.group">
|
|
307
|
+
<div class="px-3 py-1 text-xs font-semibold uppercase tracking-wide text-fg-subtle">
|
|
308
|
+
{{ row.group }}
|
|
309
|
+
</div>
|
|
310
|
+
</div>
|
|
311
|
+
} @else {
|
|
312
|
+
<div
|
|
313
|
+
role="option"
|
|
314
|
+
[id]="optionIdFn()(row.index)"
|
|
315
|
+
[attr.aria-selected]="selectedChecker()(row.index) ? 'true' : 'false'"
|
|
316
|
+
[attr.aria-disabled]="isOptionDisabled(row.index) || null"
|
|
317
|
+
[class]="
|
|
318
|
+
computeOptionClass()(
|
|
319
|
+
selectedChecker()(row.index),
|
|
320
|
+
activeIndex() === row.index,
|
|
321
|
+
isOptionDisabled(row.index)
|
|
322
|
+
)
|
|
323
|
+
"
|
|
324
|
+
(click)="onOptionClick(row.index, $event)"
|
|
325
|
+
(mouseenter)="onOptionMouseEnter(row.index)"
|
|
326
|
+
>
|
|
327
|
+
@if (optionTemplate(); as tpl) {
|
|
328
|
+
<ng-container
|
|
329
|
+
*ngTemplateOutlet="
|
|
330
|
+
tpl.templateRef;
|
|
331
|
+
context: buildOptionContext(row.index, row.option)
|
|
332
|
+
"
|
|
333
|
+
/>
|
|
334
|
+
} @else {
|
|
335
|
+
<span class="flex-1 min-w-0 truncate">{{ optionLabelFn()(row.option) }}</span>
|
|
336
|
+
@if (selectedChecker()(row.index)) {
|
|
337
|
+
<svg
|
|
338
|
+
viewBox="0 0 20 20"
|
|
339
|
+
fill="currentColor"
|
|
340
|
+
aria-hidden="true"
|
|
341
|
+
[class]="checkmarkClasses()"
|
|
342
|
+
>
|
|
343
|
+
<path
|
|
344
|
+
fill-rule="evenodd"
|
|
345
|
+
d="M16.704 5.29a1 1 0 0 1 .006 1.414l-7.2 7.3a1 1 0 0 1-1.42.007L3.29 9.296a1 1 0 0 1 1.42-1.41l3.794 3.82 6.79-6.886a1 1 0 0 1 1.41-.07Z"
|
|
346
|
+
clip-rule="evenodd"
|
|
347
|
+
/>
|
|
348
|
+
</svg>
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
</div>
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
</div>
|
|
356
|
+
|
|
357
|
+
@if (footerTemplate(); as tpl) {
|
|
358
|
+
<div class="border-t border-border p-2">
|
|
359
|
+
<ng-container *ngTemplateOutlet="tpl.templateRef" />
|
|
360
|
+
</div>
|
|
361
|
+
}
|
|
362
|
+
`,
|
|
363
|
+
}]
|
|
364
|
+
}], propDecorators: { searchInputRef: [{ type: i0.ViewChild, args: ['searchInput', { isSignal: true }] }] } });
|
|
365
|
+
|
|
366
|
+
// Duration for leave animation — matches theme/_base.css scale-out/fade-out.
|
|
367
|
+
const ANIMATION_DURATION = 120;
|
|
368
|
+
// ── tv() config ───────────────────────────────────────────────────
|
|
369
|
+
const selectVariants = tv({
|
|
370
|
+
slots: {
|
|
371
|
+
root: 'relative inline-block w-full',
|
|
372
|
+
trigger: 'w-full inline-flex items-center gap-2 text-fg cursor-pointer transition-[color,border-color,background-color,box-shadow] duration-normal motion-reduce:transition-none',
|
|
373
|
+
valueText: 'flex-1 min-w-0 text-left truncate',
|
|
374
|
+
placeholderText: 'flex-1 min-w-0 text-left truncate text-fg-subtle',
|
|
375
|
+
chevron: 'shrink-0 text-fg-muted transition-transform duration-normal motion-reduce:transition-none',
|
|
376
|
+
clearButton: 'inline-flex items-center justify-center shrink-0 rounded-md text-fg-muted hover:text-fg hover:bg-surface-muted transition-colors duration-normal motion-reduce:transition-none size-5 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500',
|
|
377
|
+
},
|
|
378
|
+
variants: {
|
|
379
|
+
size: {
|
|
380
|
+
// xs density: chevron uses `size-3.5` (14px) — half-step that lines up
|
|
381
|
+
// with text-xs inside the compact trigger.
|
|
382
|
+
xs: { trigger: 'px-2 py-1 text-xs', chevron: 'size-3.5' },
|
|
383
|
+
sm: { trigger: 'px-3 py-1.5 text-sm', chevron: 'size-4' },
|
|
384
|
+
md: { trigger: 'px-4 py-2 text-sm', chevron: 'size-4' },
|
|
385
|
+
lg: { trigger: 'px-5 py-2.5 text-base', chevron: 'size-5' },
|
|
386
|
+
xl: { trigger: 'px-6 py-3 text-base', chevron: 'size-5' },
|
|
387
|
+
},
|
|
388
|
+
variant: {
|
|
389
|
+
default: {
|
|
390
|
+
trigger: 'border border-border bg-surface rounded-md hover:border-border-strong focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500',
|
|
391
|
+
},
|
|
392
|
+
naked: {
|
|
393
|
+
trigger: 'bg-transparent border-0 rounded-none p-0 focus-visible:outline-none',
|
|
394
|
+
},
|
|
395
|
+
},
|
|
396
|
+
open: {
|
|
397
|
+
true: { chevron: 'rotate-180' },
|
|
398
|
+
false: { chevron: 'rotate-0' },
|
|
399
|
+
},
|
|
400
|
+
disabled: {
|
|
401
|
+
true: { root: 'opacity-50 pointer-events-none cursor-not-allowed' },
|
|
402
|
+
false: { root: '' },
|
|
403
|
+
},
|
|
404
|
+
focused: { true: {}, false: {} },
|
|
405
|
+
color: {
|
|
406
|
+
primary: {},
|
|
407
|
+
secondary: {},
|
|
408
|
+
accent: {},
|
|
409
|
+
neutral: {},
|
|
410
|
+
info: {},
|
|
411
|
+
success: {},
|
|
412
|
+
warning: {},
|
|
413
|
+
error: {},
|
|
414
|
+
},
|
|
415
|
+
},
|
|
416
|
+
compoundVariants: [
|
|
417
|
+
{ variant: 'default', focused: true, color: 'primary', class: { trigger: 'border-primary-500' } },
|
|
418
|
+
{ variant: 'default', focused: true, color: 'secondary', class: { trigger: 'border-secondary-500' } },
|
|
419
|
+
{ variant: 'default', focused: true, color: 'accent', class: { trigger: 'border-accent-500' } },
|
|
420
|
+
{ variant: 'default', focused: true, color: 'neutral', class: { trigger: 'border-border-strong' } },
|
|
421
|
+
{ variant: 'default', focused: true, color: 'info', class: { trigger: 'border-info-500' } },
|
|
422
|
+
{ variant: 'default', focused: true, color: 'success', class: { trigger: 'border-success-500' } },
|
|
423
|
+
{ variant: 'default', focused: true, color: 'warning', class: { trigger: 'border-warning-500' } },
|
|
424
|
+
{ variant: 'default', focused: true, color: 'error', class: { trigger: 'border-error-500' } },
|
|
425
|
+
],
|
|
426
|
+
defaultVariants: {
|
|
427
|
+
size: 'md',
|
|
428
|
+
variant: 'default',
|
|
429
|
+
open: false,
|
|
430
|
+
disabled: false,
|
|
431
|
+
focused: false,
|
|
432
|
+
color: 'primary',
|
|
433
|
+
},
|
|
434
|
+
}, { twMerge: true });
|
|
435
|
+
// ── Option-state color lookups ──
|
|
436
|
+
// Slot tokens own light/dark contrast — no `dark:`, no shade picks.
|
|
437
|
+
const OPTION_SELECTED_BG = {
|
|
438
|
+
primary: 'bg-primary-soft',
|
|
439
|
+
secondary: 'bg-secondary-soft',
|
|
440
|
+
accent: 'bg-accent-soft',
|
|
441
|
+
neutral: 'bg-neutral-soft',
|
|
442
|
+
info: 'bg-info-soft',
|
|
443
|
+
success: 'bg-success-soft',
|
|
444
|
+
warning: 'bg-warning-soft',
|
|
445
|
+
error: 'bg-error-soft',
|
|
446
|
+
};
|
|
447
|
+
const OPTION_CHECKMARK_COLOR = {
|
|
448
|
+
primary: 'text-primary-icon',
|
|
449
|
+
secondary: 'text-secondary-icon',
|
|
450
|
+
accent: 'text-accent-icon',
|
|
451
|
+
neutral: 'text-fg',
|
|
452
|
+
info: 'text-info-icon',
|
|
453
|
+
success: 'text-success-icon',
|
|
454
|
+
warning: 'text-warning-icon',
|
|
455
|
+
error: 'text-error-icon',
|
|
456
|
+
};
|
|
457
|
+
// ── Template directives (content-projected slots) ─────────────────
|
|
458
|
+
/** Structural directive projecting a custom template for each option in the panel. */
|
|
459
|
+
class SelectOptionTemplateDirective {
|
|
460
|
+
/** @internal */
|
|
461
|
+
templateRef = inject((TemplateRef));
|
|
462
|
+
/** @internal Enables strict typing of `let-` bindings in the template. */
|
|
463
|
+
static ngTemplateContextGuard(_dir, _ctx) {
|
|
464
|
+
return true;
|
|
465
|
+
}
|
|
466
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: SelectOptionTemplateDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
467
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.14", type: SelectOptionTemplateDirective, isStandalone: true, selector: "[twSelectOption]", ngImport: i0 });
|
|
468
|
+
}
|
|
469
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: SelectOptionTemplateDirective, decorators: [{
|
|
470
|
+
type: Directive,
|
|
471
|
+
args: [{ selector: '[twSelectOption]' }]
|
|
472
|
+
}] });
|
|
473
|
+
/** Structural directive projecting a custom template for the select's trigger content. */
|
|
474
|
+
class SelectTriggerTemplateDirective {
|
|
475
|
+
/** @internal */
|
|
476
|
+
templateRef = inject((TemplateRef));
|
|
477
|
+
/** @internal */
|
|
478
|
+
static ngTemplateContextGuard(_dir, _ctx) {
|
|
479
|
+
return true;
|
|
480
|
+
}
|
|
481
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: SelectTriggerTemplateDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
482
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.14", type: SelectTriggerTemplateDirective, isStandalone: true, selector: "[twSelectTrigger]", ngImport: i0 });
|
|
483
|
+
}
|
|
484
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: SelectTriggerTemplateDirective, decorators: [{
|
|
485
|
+
type: Directive,
|
|
486
|
+
args: [{ selector: '[twSelectTrigger]' }]
|
|
487
|
+
}] });
|
|
488
|
+
/** Structural directive projecting a custom template for the panel's empty state. */
|
|
489
|
+
class SelectEmptyTemplateDirective {
|
|
490
|
+
/** @internal */
|
|
491
|
+
templateRef = inject((TemplateRef));
|
|
492
|
+
/** @internal */
|
|
493
|
+
static ngTemplateContextGuard(_dir, _ctx) {
|
|
494
|
+
return true;
|
|
495
|
+
}
|
|
496
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: SelectEmptyTemplateDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
497
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.14", type: SelectEmptyTemplateDirective, isStandalone: true, selector: "[twSelectEmpty]", ngImport: i0 });
|
|
498
|
+
}
|
|
499
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: SelectEmptyTemplateDirective, decorators: [{
|
|
500
|
+
type: Directive,
|
|
501
|
+
args: [{ selector: '[twSelectEmpty]' }]
|
|
502
|
+
}] });
|
|
503
|
+
/** Structural directive projecting a custom header at the top of the panel. */
|
|
504
|
+
class SelectHeaderTemplateDirective {
|
|
505
|
+
/** @internal */
|
|
506
|
+
templateRef = inject((TemplateRef));
|
|
507
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: SelectHeaderTemplateDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
508
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.14", type: SelectHeaderTemplateDirective, isStandalone: true, selector: "[twSelectHeader]", ngImport: i0 });
|
|
509
|
+
}
|
|
510
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: SelectHeaderTemplateDirective, decorators: [{
|
|
511
|
+
type: Directive,
|
|
512
|
+
args: [{ selector: '[twSelectHeader]' }]
|
|
513
|
+
}] });
|
|
514
|
+
/** Structural directive projecting a custom footer at the bottom of the panel. */
|
|
515
|
+
class SelectFooterTemplateDirective {
|
|
516
|
+
/** @internal */
|
|
517
|
+
templateRef = inject((TemplateRef));
|
|
518
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: SelectFooterTemplateDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
519
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.14", type: SelectFooterTemplateDirective, isStandalone: true, selector: "[twSelectFooter]", ngImport: i0 });
|
|
520
|
+
}
|
|
521
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: SelectFooterTemplateDirective, decorators: [{
|
|
522
|
+
type: Directive,
|
|
523
|
+
args: [{ selector: '[twSelectFooter]' }]
|
|
524
|
+
}] });
|
|
525
|
+
// ── ID generator ──
|
|
526
|
+
let nextSelectId = 0;
|
|
527
|
+
// ── Accessor defaults ──
|
|
528
|
+
function defaultOptionLabel(o) {
|
|
529
|
+
const opt = o;
|
|
530
|
+
if (typeof opt.label === 'string')
|
|
531
|
+
return opt.label;
|
|
532
|
+
return String(opt.value ?? '');
|
|
533
|
+
}
|
|
534
|
+
function defaultOptionValue(o) {
|
|
535
|
+
return o.value;
|
|
536
|
+
}
|
|
537
|
+
function defaultOptionDisabled(o) {
|
|
538
|
+
return !!o.disabled;
|
|
539
|
+
}
|
|
540
|
+
function defaultOptionGroup(o) {
|
|
541
|
+
return o.group;
|
|
542
|
+
}
|
|
543
|
+
// ── SelectComponent ──
|
|
544
|
+
/**
|
|
545
|
+
* ARIA combobox with listbox popup. Supports single/multi selection, in-panel
|
|
546
|
+
* search filtering, custom option/trigger templates, and full integration with
|
|
547
|
+
* Angular forms (reactive, template-driven, signal-forms) plus `tw-form-field`.
|
|
548
|
+
*/
|
|
549
|
+
class SelectComponent {
|
|
550
|
+
/** Array of options to render in the panel. Accepts either `TwSelectOption<T>` objects or arbitrary records read via the accessor inputs. Defaults to an empty array. */
|
|
551
|
+
options = input([], ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
|
|
552
|
+
/** Accessor returning the visible label for an option. Override when passing arbitrary objects. */
|
|
553
|
+
optionLabel = input(defaultOptionLabel, ...(ngDevMode ? [{ debugName: "optionLabel" }] : /* istanbul ignore next */ []));
|
|
554
|
+
/** Accessor returning the value for an option. The result is what `value` / `valueChange` emit. */
|
|
555
|
+
optionValue = input(defaultOptionValue, ...(ngDevMode ? [{ debugName: "optionValue" }] : /* istanbul ignore next */ []));
|
|
556
|
+
/** Accessor returning the disabled state for an option. Defaults to reading `.disabled`. */
|
|
557
|
+
optionDisabled = input(defaultOptionDisabled, ...(ngDevMode ? [{ debugName: "optionDisabled" }] : /* istanbul ignore next */ []));
|
|
558
|
+
/** Accessor returning the group name for an option. Options sharing a group render under a labelled `role="group"` region. */
|
|
559
|
+
optionGroup = input(defaultOptionGroup, ...(ngDevMode ? [{ debugName: "optionGroup" }] : /* istanbul ignore next */ []));
|
|
560
|
+
/** When true, enables multi-selection. The `value` model becomes a `T[]` and the panel renders checkable options. Defaults to `false`. */
|
|
561
|
+
multiple = input(false, ...(ngDevMode ? [{ debugName: "multiple" }] : /* istanbul ignore next */ []));
|
|
562
|
+
/** When true, renders a search input at the top of the panel that filters options using `filterPredicate`. Defaults to `false`. */
|
|
563
|
+
searchable = input(false, ...(ngDevMode ? [{ debugName: "searchable" }] : /* istanbul ignore next */ []));
|
|
564
|
+
/** Custom filter function for the search input. Defaults to a case-insensitive substring match on the option label. */
|
|
565
|
+
filterPredicate = input((option, search) => {
|
|
566
|
+
const label = this.optionLabel()(option);
|
|
567
|
+
return label.toLowerCase().includes(search.toLowerCase());
|
|
568
|
+
}, ...(ngDevMode ? [{ debugName: "filterPredicate" }] : /* istanbul ignore next */ []));
|
|
569
|
+
/** Placeholder text shown in the trigger when no value is selected. */
|
|
570
|
+
placeholder = input(undefined, ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
|
|
571
|
+
/** When true, the trigger cannot be activated and the panel cannot open. Defaults to `false`. */
|
|
572
|
+
disabledInput = input(false, { ...(ngDevMode ? { debugName: "disabledInput" } : /* istanbul ignore next */ {}), alias: 'disabled' });
|
|
573
|
+
/** When true, exposes `aria-required="true"` on the trigger. Defaults to `false`. */
|
|
574
|
+
requiredInput = input(false, { ...(ngDevMode ? { debugName: "requiredInput" } : /* istanbul ignore next */ {}), alias: 'required' });
|
|
575
|
+
/** Controls trigger padding, font size, and panel option density. Defaults to `'md'`. */
|
|
576
|
+
size = input('md', ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
|
|
577
|
+
/** Semantic color for focused trigger border, active-option background, and checkmarks. Defaults to `'primary'`. */
|
|
578
|
+
color = input('primary', ...(ngDevMode ? [{ debugName: "color" }] : /* istanbul ignore next */ []));
|
|
579
|
+
/** Visual style of the trigger. When inside a `tw-form-field` and left unset, auto-resolves to `'naked'`. Otherwise defaults to `'default'`. */
|
|
580
|
+
variant = input(undefined, ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
|
|
581
|
+
/** Overlay panel width. `'trigger'` matches the trigger's measured width; `'auto'` lets content decide; a number is applied as pixels; a string is passed through as a CSS length. Defaults to `'trigger'`. */
|
|
582
|
+
panelWidth = input('trigger', ...(ngDevMode ? [{ debugName: "panelWidth" }] : /* istanbul ignore next */ []));
|
|
583
|
+
/** Extra class(es) applied to the overlay panel element. */
|
|
584
|
+
panelClass = input('', ...(ngDevMode ? [{ debugName: "panelClass" }] : /* istanbul ignore next */ []));
|
|
585
|
+
/** Maximum height of the listbox scroll region in pixels. Defaults to `256`. */
|
|
586
|
+
panelMaxHeight = input(256, ...(ngDevMode ? [{ debugName: "panelMaxHeight" }] : /* istanbul ignore next */ []));
|
|
587
|
+
/** Whether the panel closes after a selection is made. When unset, resolves to `true` for single-select and `false` for multi-select. */
|
|
588
|
+
closeOnSelect = input(undefined, ...(ngDevMode ? [{ debugName: "closeOnSelect" }] : /* istanbul ignore next */ []));
|
|
589
|
+
/** CDK scroll strategy for the overlay. Defaults to `'reposition'`. */
|
|
590
|
+
scrollStrategy = input('reposition', ...(ngDevMode ? [{ debugName: "scrollStrategy" }] : /* istanbul ignore next */ []));
|
|
591
|
+
/** Pixel distance between trigger and panel. Defaults to `4`. */
|
|
592
|
+
offset = input(4, ...(ngDevMode ? [{ debugName: "offset" }] : /* istanbul ignore next */ []));
|
|
593
|
+
/** Fallback message rendered when the filter yields no options and no `*twSelectEmpty` template is provided. */
|
|
594
|
+
emptyMessage = input('No results', ...(ngDevMode ? [{ debugName: "emptyMessage" }] : /* istanbul ignore next */ []));
|
|
595
|
+
/** Accessible name for the combobox trigger. */
|
|
596
|
+
ariaLabel = input(undefined, { ...(ngDevMode ? { debugName: "ariaLabel" } : /* istanbul ignore next */ {}), alias: 'aria-label' });
|
|
597
|
+
/** ID of an external element that labels the combobox. */
|
|
598
|
+
ariaLabelledby = input(undefined, { ...(ngDevMode ? { debugName: "ariaLabelledby" } : /* istanbul ignore next */ {}), alias: 'aria-labelledby' });
|
|
599
|
+
/** ID of an external element that describes the combobox. */
|
|
600
|
+
ariaDescribedby = input(undefined, { ...(ngDevMode ? { debugName: "ariaDescribedby" } : /* istanbul ignore next */ {}), alias: 'aria-describedby' });
|
|
601
|
+
/** Per-instance override of the {@link ErrorStateMatcher}. When omitted, the select uses the `TW_ERROR_STATE_MATCHER` token's value. */
|
|
602
|
+
errorStateMatcher = input(undefined, ...(ngDevMode ? [{ debugName: "errorStateMatcher" }] : /* istanbul ignore next */ []));
|
|
603
|
+
/** Equality comparator for option values. Defaults to `Object.is`. */
|
|
604
|
+
compareWith = input(Object.is, ...(ngDevMode ? [{ debugName: "compareWith" }] : /* istanbul ignore next */ []));
|
|
605
|
+
// ── Models (two-way) ──
|
|
606
|
+
/** Two-way bound selected value(s). Single-select: `T | null`. Multi-select: `T[]`. */
|
|
607
|
+
value = model(null, ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
|
|
608
|
+
/** Two-way bound open state of the panel. */
|
|
609
|
+
open = model(false, ...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
|
|
610
|
+
// ── Outputs ──
|
|
611
|
+
/** Fires when the panel's visibility finishes changing. */
|
|
612
|
+
openedChange = output();
|
|
613
|
+
/** Fires after any selection change, with `added`, `removed`, `previousValue`, and a `source` discriminator. */
|
|
614
|
+
selectionChange = output();
|
|
615
|
+
/** Fires whenever the search input changes (only when `searchable` is true). */
|
|
616
|
+
searchChange = output();
|
|
617
|
+
// ── Content queries ──
|
|
618
|
+
/** @internal */
|
|
619
|
+
triggerTemplateChild = contentChild(SelectTriggerTemplateDirective, ...(ngDevMode ? [{ debugName: "triggerTemplateChild" }] : /* istanbul ignore next */ []));
|
|
620
|
+
/** @internal */
|
|
621
|
+
optionTemplateChild = contentChild(SelectOptionTemplateDirective, ...(ngDevMode ? [{ debugName: "optionTemplateChild" }] : /* istanbul ignore next */ []));
|
|
622
|
+
/** @internal */
|
|
623
|
+
emptyTemplateChild = contentChild(SelectEmptyTemplateDirective, ...(ngDevMode ? [{ debugName: "emptyTemplateChild" }] : /* istanbul ignore next */ []));
|
|
624
|
+
/** @internal */
|
|
625
|
+
headerTemplateChild = contentChild(SelectHeaderTemplateDirective, ...(ngDevMode ? [{ debugName: "headerTemplateChild" }] : /* istanbul ignore next */ []));
|
|
626
|
+
/** @internal */
|
|
627
|
+
footerTemplateChild = contentChild(SelectFooterTemplateDirective, ...(ngDevMode ? [{ debugName: "footerTemplateChild" }] : /* istanbul ignore next */ []));
|
|
628
|
+
// ── Injected deps ──
|
|
629
|
+
overlayService = inject(Overlay);
|
|
630
|
+
elementRef = inject((ElementRef));
|
|
631
|
+
viewContainerRef = inject(ViewContainerRef);
|
|
632
|
+
injector = inject(Injector);
|
|
633
|
+
destroyRef = inject(DestroyRef);
|
|
634
|
+
focusMonitor = inject(FocusMonitor);
|
|
635
|
+
liveAnnouncer = inject(LiveAnnouncer);
|
|
636
|
+
platform = inject(Platform);
|
|
637
|
+
formField = inject(FormFieldComponent, { optional: true });
|
|
638
|
+
ngControl = inject(NgControl, { optional: true, self: true });
|
|
639
|
+
parentForm = inject(NgForm, { optional: true });
|
|
640
|
+
parentFormGroup = inject(FormGroupDirective, { optional: true });
|
|
641
|
+
defaultMatcher = inject(TW_ERROR_STATE_MATCHER);
|
|
642
|
+
// ── Identity ──
|
|
643
|
+
uid = nextSelectId++;
|
|
644
|
+
/** @internal */
|
|
645
|
+
hostId = `tw-select-${this.uid}`;
|
|
646
|
+
/** @internal */
|
|
647
|
+
listboxId = `${this.hostId}-listbox`;
|
|
648
|
+
/** @internal */
|
|
649
|
+
searchInputId = `${this.hostId}-search`;
|
|
650
|
+
/** @internal */
|
|
651
|
+
optionId = (index) => `${this.hostId}-option-${index}`;
|
|
652
|
+
// ── Internal state signals ──
|
|
653
|
+
/** @internal */
|
|
654
|
+
internalValue = linkedSignal(() => this.value(), ...(ngDevMode ? [{ debugName: "internalValue" }] : /* istanbul ignore next */ []));
|
|
655
|
+
/** @internal */
|
|
656
|
+
search = signal('', ...(ngDevMode ? [{ debugName: "search" }] : /* istanbul ignore next */ []));
|
|
657
|
+
/** @internal */
|
|
658
|
+
activeIndex = signal(-1, ...(ngDevMode ? [{ debugName: "activeIndex" }] : /* istanbul ignore next */ []));
|
|
659
|
+
/** @internal */
|
|
660
|
+
focusedSignal = signal(false, ...(ngDevMode ? [{ debugName: "focusedSignal" }] : /* istanbul ignore next */ []));
|
|
661
|
+
cvaDisabled = signal(false, ...(ngDevMode ? [{ debugName: "cvaDisabled" }] : /* istanbul ignore next */ []));
|
|
662
|
+
_ngControlRev = signal(0, ...(ngDevMode ? [{ debugName: "_ngControlRev" }] : /* istanbul ignore next */ []));
|
|
663
|
+
_formSubmitRev = signal(0, ...(ngDevMode ? [{ debugName: "_formSubmitRev" }] : /* istanbul ignore next */ []));
|
|
664
|
+
describedByIdsSignal = signal([], ...(ngDevMode ? [{ debugName: "describedByIdsSignal" }] : /* istanbul ignore next */ []));
|
|
665
|
+
labelledByIdsSignal = signal([], ...(ngDevMode ? [{ debugName: "labelledByIdsSignal" }] : /* istanbul ignore next */ []));
|
|
666
|
+
closingSignal = signal(false, ...(ngDevMode ? [{ debugName: "closingSignal" }] : /* istanbul ignore next */ []));
|
|
667
|
+
onChange = () => { };
|
|
668
|
+
onTouched = () => { };
|
|
669
|
+
overlayRef = null;
|
|
670
|
+
overlayInstanceSignal = signal(null, ...(ngDevMode ? [{ debugName: "overlayInstanceSignal" }] : /* istanbul ignore next */ []));
|
|
671
|
+
perOpenSubs = null;
|
|
672
|
+
closeTimer = null;
|
|
673
|
+
resizeObserver = null;
|
|
674
|
+
get overlayInstance() {
|
|
675
|
+
return this.overlayInstanceSignal();
|
|
676
|
+
}
|
|
677
|
+
// ── Derived state ──
|
|
678
|
+
/** @internal */
|
|
679
|
+
isDisabled = computed(() => this.disabledInput() || this.cvaDisabled(), ...(ngDevMode ? [{ debugName: "isDisabled" }] : /* istanbul ignore next */ []));
|
|
680
|
+
/** @internal Effective variant — auto-naked when wrapped in tw-form-field. */
|
|
681
|
+
resolvedVariant = computed(() => this.variant() ?? (this.formField ? 'naked' : 'default'), ...(ngDevMode ? [{ debugName: "resolvedVariant" }] : /* istanbul ignore next */ []));
|
|
682
|
+
/** @internal */
|
|
683
|
+
isEmpty = computed(() => {
|
|
684
|
+
const v = this.internalValue();
|
|
685
|
+
if (this.multiple())
|
|
686
|
+
return !Array.isArray(v) || v.length === 0;
|
|
687
|
+
return v === null || v === undefined;
|
|
688
|
+
}, ...(ngDevMode ? [{ debugName: "isEmpty" }] : /* istanbul ignore next */ []));
|
|
689
|
+
/** @internal Visible (filtered) options with resolved view data. */
|
|
690
|
+
visibleOptions = computed(() => {
|
|
691
|
+
const opts = this.options();
|
|
692
|
+
const labelFn = this.optionLabel();
|
|
693
|
+
const valueFn = this.optionValue();
|
|
694
|
+
const disabledFn = this.optionDisabled();
|
|
695
|
+
const groupFn = this.optionGroup();
|
|
696
|
+
const s = this.search().trim();
|
|
697
|
+
const predicate = this.filterPredicate();
|
|
698
|
+
const filtered = s && this.searchable() ? opts.filter((o) => predicate(o, s)) : opts;
|
|
699
|
+
return filtered.map((option) => ({
|
|
700
|
+
option,
|
|
701
|
+
label: labelFn(option),
|
|
702
|
+
value: valueFn(option),
|
|
703
|
+
disabled: disabledFn(option),
|
|
704
|
+
group: groupFn(option),
|
|
705
|
+
}));
|
|
706
|
+
}, ...(ngDevMode ? [{ debugName: "visibleOptions" }] : /* istanbul ignore next */ []));
|
|
707
|
+
/** @internal Rows rendered in the panel (group labels + options). */
|
|
708
|
+
renderedRows = computed(() => {
|
|
709
|
+
const visible = this.visibleOptions();
|
|
710
|
+
const rows = [];
|
|
711
|
+
let currentGroup;
|
|
712
|
+
visible.forEach((v, index) => {
|
|
713
|
+
if (v.group !== currentGroup) {
|
|
714
|
+
if (v.group !== undefined) {
|
|
715
|
+
rows.push({ kind: 'group-label', group: v.group });
|
|
716
|
+
}
|
|
717
|
+
currentGroup = v.group;
|
|
718
|
+
}
|
|
719
|
+
rows.push({ kind: 'option', option: v.option, index, group: v.group });
|
|
720
|
+
});
|
|
721
|
+
return rows;
|
|
722
|
+
}, ...(ngDevMode ? [{ debugName: "renderedRows" }] : /* istanbul ignore next */ []));
|
|
723
|
+
/** @internal Resolved option objects that match the current value. */
|
|
724
|
+
selectedOptions = computed(() => {
|
|
725
|
+
const v = this.internalValue();
|
|
726
|
+
if (v === null || v === undefined)
|
|
727
|
+
return [];
|
|
728
|
+
const cmp = this.compareWith();
|
|
729
|
+
const valueFn = this.optionValue();
|
|
730
|
+
const opts = this.options();
|
|
731
|
+
if (this.multiple()) {
|
|
732
|
+
const arr = Array.isArray(v) ? v : [];
|
|
733
|
+
return opts.filter((o) => arr.some((x) => cmp(valueFn(o), x)));
|
|
734
|
+
}
|
|
735
|
+
return opts.filter((o) => cmp(valueFn(o), v));
|
|
736
|
+
}, ...(ngDevMode ? [{ debugName: "selectedOptions" }] : /* istanbul ignore next */ []));
|
|
737
|
+
/** @internal */
|
|
738
|
+
defaultTriggerText = computed(() => {
|
|
739
|
+
const opts = this.selectedOptions();
|
|
740
|
+
const labelFn = this.optionLabel();
|
|
741
|
+
if (opts.length === 0)
|
|
742
|
+
return '';
|
|
743
|
+
return opts.map((o) => labelFn(o)).join(', ');
|
|
744
|
+
}, ...(ngDevMode ? [{ debugName: "defaultTriggerText" }] : /* istanbul ignore next */ []));
|
|
745
|
+
/** @internal */
|
|
746
|
+
showClearButton = computed(() => !this.isDisabled() && !this.isEmpty(), ...(ngDevMode ? [{ debugName: "showClearButton" }] : /* istanbul ignore next */ []));
|
|
747
|
+
/** @internal */
|
|
748
|
+
activeDescendantId = computed(() => {
|
|
749
|
+
if (!this.open())
|
|
750
|
+
return null;
|
|
751
|
+
const idx = this.activeIndex();
|
|
752
|
+
if (idx < 0 || idx >= this.visibleOptions().length)
|
|
753
|
+
return null;
|
|
754
|
+
return this.optionId(idx);
|
|
755
|
+
}, ...(ngDevMode ? [{ debugName: "activeDescendantId" }] : /* istanbul ignore next */ []));
|
|
756
|
+
/** @internal */
|
|
757
|
+
describedBy = computed(() => {
|
|
758
|
+
const extra = this.describedByIdsSignal();
|
|
759
|
+
const user = this.ariaDescribedby();
|
|
760
|
+
const merged = [...extra];
|
|
761
|
+
if (user)
|
|
762
|
+
merged.push(...user.split(/\s+/).filter(Boolean));
|
|
763
|
+
return merged.length ? merged.join(' ') : '';
|
|
764
|
+
}, ...(ngDevMode ? [{ debugName: "describedBy" }] : /* istanbul ignore next */ []));
|
|
765
|
+
/** @internal Merged `aria-labelledby`: form-field-pushed label ids + consumer-supplied `aria-labelledby`. */
|
|
766
|
+
labelledBy = computed(() => {
|
|
767
|
+
const extra = this.labelledByIdsSignal();
|
|
768
|
+
const user = this.ariaLabelledby();
|
|
769
|
+
const merged = [...extra];
|
|
770
|
+
if (user)
|
|
771
|
+
merged.push(...user.split(/\s+/).filter(Boolean));
|
|
772
|
+
return merged.length ? merged.join(' ') : '';
|
|
773
|
+
}, ...(ngDevMode ? [{ debugName: "labelledBy" }] : /* istanbul ignore next */ []));
|
|
774
|
+
/** @internal Resolved close-on-select behaviour. */
|
|
775
|
+
resolvedCloseOnSelect = computed(() => {
|
|
776
|
+
const explicit = this.closeOnSelect();
|
|
777
|
+
if (explicit !== undefined)
|
|
778
|
+
return explicit;
|
|
779
|
+
return !this.multiple();
|
|
780
|
+
}, ...(ngDevMode ? [{ debugName: "resolvedCloseOnSelect" }] : /* istanbul ignore next */ []));
|
|
781
|
+
// ── tv() output ──
|
|
782
|
+
variantResult = computed(() => selectVariants({
|
|
783
|
+
size: this.size(),
|
|
784
|
+
variant: this.resolvedVariant(),
|
|
785
|
+
open: this.open(),
|
|
786
|
+
disabled: this.isDisabled(),
|
|
787
|
+
focused: this.focusedSignal(),
|
|
788
|
+
color: this.color(),
|
|
789
|
+
}), ...(ngDevMode ? [{ debugName: "variantResult" }] : /* istanbul ignore next */ []));
|
|
790
|
+
/** @internal */
|
|
791
|
+
rootClasses = computed(() => this.variantResult().root(), ...(ngDevMode ? [{ debugName: "rootClasses" }] : /* istanbul ignore next */ []));
|
|
792
|
+
/** @internal */
|
|
793
|
+
triggerClasses = computed(() => this.variantResult().trigger(), ...(ngDevMode ? [{ debugName: "triggerClasses" }] : /* istanbul ignore next */ []));
|
|
794
|
+
/** @internal */
|
|
795
|
+
valueTextClasses = computed(() => this.variantResult().valueText(), ...(ngDevMode ? [{ debugName: "valueTextClasses" }] : /* istanbul ignore next */ []));
|
|
796
|
+
/** @internal */
|
|
797
|
+
placeholderTextClasses = computed(() => this.variantResult().placeholderText(), ...(ngDevMode ? [{ debugName: "placeholderTextClasses" }] : /* istanbul ignore next */ []));
|
|
798
|
+
/** @internal */
|
|
799
|
+
chevronClasses = computed(() => this.variantResult().chevron(), ...(ngDevMode ? [{ debugName: "chevronClasses" }] : /* istanbul ignore next */ []));
|
|
800
|
+
/** @internal */
|
|
801
|
+
clearButtonClasses = computed(() => this.variantResult().clearButton(), ...(ngDevMode ? [{ debugName: "clearButtonClasses" }] : /* istanbul ignore next */ []));
|
|
802
|
+
/** @internal */
|
|
803
|
+
optionSizeClass = computed(() => {
|
|
804
|
+
switch (this.size()) {
|
|
805
|
+
case 'xs':
|
|
806
|
+
return 'px-2 py-1 text-xs gap-1.5';
|
|
807
|
+
case 'sm':
|
|
808
|
+
return 'px-3 py-1.5 text-sm';
|
|
809
|
+
case 'md':
|
|
810
|
+
return 'px-3 py-2 text-sm';
|
|
811
|
+
case 'lg':
|
|
812
|
+
return 'px-4 py-2.5 text-base';
|
|
813
|
+
case 'xl':
|
|
814
|
+
return 'px-4 py-3 text-base';
|
|
815
|
+
}
|
|
816
|
+
}, ...(ngDevMode ? [{ debugName: "optionSizeClass" }] : /* istanbul ignore next */ []));
|
|
817
|
+
/** @internal */
|
|
818
|
+
checkmarkColorClass = computed(() => OPTION_CHECKMARK_COLOR[this.color()], ...(ngDevMode ? [{ debugName: "checkmarkColorClass" }] : /* istanbul ignore next */ []));
|
|
819
|
+
/** @internal Class string applied to each option row. */
|
|
820
|
+
computeOptionClass = (selected, active, disabled) => {
|
|
821
|
+
const sizeClass = this.optionSizeClass();
|
|
822
|
+
const color = this.color();
|
|
823
|
+
const base = 'relative flex items-center cursor-pointer select-none text-fg gap-2 transition-colors duration-normal motion-reduce:transition-none';
|
|
824
|
+
const parts = [base, sizeClass];
|
|
825
|
+
if (disabled) {
|
|
826
|
+
parts.push('opacity-50 pointer-events-none');
|
|
827
|
+
}
|
|
828
|
+
if (selected) {
|
|
829
|
+
parts.push(OPTION_SELECTED_BG[color]);
|
|
830
|
+
}
|
|
831
|
+
else if (active) {
|
|
832
|
+
parts.push('bg-surface-muted');
|
|
833
|
+
}
|
|
834
|
+
else {
|
|
835
|
+
parts.push('hover:bg-surface-muted');
|
|
836
|
+
}
|
|
837
|
+
return parts.join(' ');
|
|
838
|
+
};
|
|
839
|
+
// ── FormFieldControl impl (readonly signals) ──
|
|
840
|
+
/** @internal */
|
|
841
|
+
id = computed(() => this.hostId, ...(ngDevMode ? [{ debugName: "id" }] : /* istanbul ignore next */ []));
|
|
842
|
+
/** @internal */
|
|
843
|
+
focused = this.focusedSignal.asReadonly();
|
|
844
|
+
/** @internal */
|
|
845
|
+
empty = this.isEmpty;
|
|
846
|
+
/** @internal */
|
|
847
|
+
disabled = this.isDisabled;
|
|
848
|
+
/** @internal */
|
|
849
|
+
required = computed(() => this.requiredInput(), ...(ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []));
|
|
850
|
+
/** @internal Error-state per the configured `ErrorStateMatcher`. Reads the bound `NgControl.invalid` through the matcher. */
|
|
851
|
+
errorState = computed(() => {
|
|
852
|
+
this._ngControlRev();
|
|
853
|
+
this._formSubmitRev();
|
|
854
|
+
const matcher = this.errorStateMatcher() ?? this.defaultMatcher;
|
|
855
|
+
const form = this.parentFormGroup ??
|
|
856
|
+
this.parentForm;
|
|
857
|
+
return matcher.isErrorState(this.ngControl?.control ?? null, form);
|
|
858
|
+
}, ...(ngDevMode ? [{ debugName: "errorState" }] : /* istanbul ignore next */ []));
|
|
859
|
+
/** @internal */
|
|
860
|
+
controlType = 'select';
|
|
861
|
+
/** @internal */
|
|
862
|
+
userAriaDescribedBy = computed(() => this.ariaDescribedby(), ...(ngDevMode ? [{ debugName: "userAriaDescribedBy" }] : /* istanbul ignore next */ []));
|
|
863
|
+
/** @internal */
|
|
864
|
+
userAriaLabelledby = computed(() => this.ariaLabelledby(), ...(ngDevMode ? [{ debugName: "userAriaLabelledby" }] : /* istanbul ignore next */ []));
|
|
865
|
+
/** @internal Public read-only view of the current value for FormFieldControl. */
|
|
866
|
+
// Overriding `value` typing from the model by using an explicit computed that matches the abstract signature.
|
|
867
|
+
// (The model `value` property is a WritableSignal<T | readonly T[] | null>; FormFieldControl needs a Signal<T | readonly T[] | null>.
|
|
868
|
+
// A WritableSignal is assignable to Signal, so we just cast once.)
|
|
869
|
+
// ── Constructor ──
|
|
870
|
+
constructor() {
|
|
871
|
+
// Material-style CVA wiring: declare ourselves as the value accessor on any
|
|
872
|
+
// host-level `NgControl` (FormControlDirective, NgModel, etc.). This avoids
|
|
873
|
+
// the circular-DI that a static `NG_VALUE_ACCESSOR` provider would create
|
|
874
|
+
// because `NgControl` is injected with `self: true` on the same element.
|
|
875
|
+
if (this.ngControl) {
|
|
876
|
+
this.ngControl.valueAccessor = this;
|
|
877
|
+
}
|
|
878
|
+
// Mirror parent `open` model into overlay lifecycle.
|
|
879
|
+
effect(() => {
|
|
880
|
+
const shouldOpen = this.open();
|
|
881
|
+
const disabled = this.isDisabled();
|
|
882
|
+
if (disabled && this.overlayInstance) {
|
|
883
|
+
this.closeOverlay();
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
if (shouldOpen && !this.overlayInstance && !disabled && !this.closingSignal()) {
|
|
887
|
+
this.openOverlay();
|
|
888
|
+
}
|
|
889
|
+
else if (!shouldOpen && this.overlayInstance && !this.closingSignal()) {
|
|
890
|
+
this.closeOverlay();
|
|
891
|
+
}
|
|
892
|
+
});
|
|
893
|
+
// Push state into overlay component whenever anything relevant changes.
|
|
894
|
+
// Reads happen in the tracked phase; writes to the overlay instance are
|
|
895
|
+
// wrapped in `untracked()` so they never feed back into this effect.
|
|
896
|
+
effect(() => {
|
|
897
|
+
const instance = this.overlayInstance;
|
|
898
|
+
if (!instance)
|
|
899
|
+
return;
|
|
900
|
+
const size = this.size();
|
|
901
|
+
const color = this.color();
|
|
902
|
+
const multiple = this.multiple();
|
|
903
|
+
const searchable = this.searchable();
|
|
904
|
+
const panelMaxHeight = this.panelMaxHeight();
|
|
905
|
+
const emptyMessage = this.emptyMessage();
|
|
906
|
+
const search = this.search();
|
|
907
|
+
const activeIndex = this.activeIndex();
|
|
908
|
+
const renderedRows = this.renderedRows();
|
|
909
|
+
const visibleOptions = this.visibleOptions();
|
|
910
|
+
const optionTemplate = this.optionTemplateChild();
|
|
911
|
+
const emptyTemplate = this.emptyTemplateChild();
|
|
912
|
+
const headerTemplate = this.headerTemplateChild();
|
|
913
|
+
const footerTemplate = this.footerTemplateChild();
|
|
914
|
+
const optionLabelFn = this.optionLabel();
|
|
915
|
+
const checkmarkColorClass = this.checkmarkColorClass();
|
|
916
|
+
const panelClassValue = this.resolvePanelClass();
|
|
917
|
+
untracked(() => {
|
|
918
|
+
instance.size.set(size);
|
|
919
|
+
instance.color.set(color);
|
|
920
|
+
instance.multiple.set(multiple);
|
|
921
|
+
instance.searchable.set(searchable);
|
|
922
|
+
instance.panelMaxHeight.set(panelMaxHeight);
|
|
923
|
+
instance.emptyMessage.set(emptyMessage);
|
|
924
|
+
instance.search.set(search);
|
|
925
|
+
instance.activeIndex.set(activeIndex);
|
|
926
|
+
instance.renderedRows.set(renderedRows);
|
|
927
|
+
instance.visibleOptions.set(visibleOptions);
|
|
928
|
+
instance.optionTemplate.set(optionTemplate);
|
|
929
|
+
instance.emptyTemplate.set(emptyTemplate);
|
|
930
|
+
instance.headerTemplate.set(headerTemplate);
|
|
931
|
+
instance.footerTemplate.set(footerTemplate);
|
|
932
|
+
instance.optionLabelFn.set(optionLabelFn);
|
|
933
|
+
instance.selectedChecker.set((index) => this.isVisibleOptionSelected(index));
|
|
934
|
+
instance.computeOptionClass.set(this.computeOptionClass);
|
|
935
|
+
instance.checkmarkColorClass.set(checkmarkColorClass);
|
|
936
|
+
instance.panelClassValue.set(panelClassValue);
|
|
937
|
+
instance.listboxId.set(this.listboxId);
|
|
938
|
+
instance.searchInputId.set(this.searchInputId);
|
|
939
|
+
instance.optionIdFn.set(this.optionId);
|
|
940
|
+
});
|
|
941
|
+
});
|
|
942
|
+
// Emit searchChange whenever the user types.
|
|
943
|
+
effect(() => {
|
|
944
|
+
const s = this.search();
|
|
945
|
+
const count = this.visibleOptions().length;
|
|
946
|
+
untracked(() => {
|
|
947
|
+
if (this.overlayInstance) {
|
|
948
|
+
this.searchChange.emit({ search: s, visibleCount: count });
|
|
949
|
+
}
|
|
950
|
+
});
|
|
951
|
+
});
|
|
952
|
+
// Dev-mode accessible-name warning.
|
|
953
|
+
afterNextRender(() => {
|
|
954
|
+
if (!isDevMode())
|
|
955
|
+
return;
|
|
956
|
+
const hasLabel = !!this.ariaLabel() ||
|
|
957
|
+
!!this.ariaLabelledby() ||
|
|
958
|
+
!!this.formField?.labelChild() ||
|
|
959
|
+
!!this.triggerTemplateChild();
|
|
960
|
+
if (!hasLabel) {
|
|
961
|
+
console.warn('[tw-select] The select has no accessible name. Set aria-label, aria-labelledby, wrap in <tw-form-field> with a <label twLabel>, or project a *twSelectTrigger template.');
|
|
962
|
+
}
|
|
963
|
+
});
|
|
964
|
+
// Monitor focus on the trigger (and descendant overlay) for focused state.
|
|
965
|
+
const monitorSub = this.focusMonitor
|
|
966
|
+
.monitor(this.elementRef, true)
|
|
967
|
+
.subscribe((origin) => {
|
|
968
|
+
const wasFocused = this.focusedSignal();
|
|
969
|
+
this.focusedSignal.set(!!origin);
|
|
970
|
+
if (!origin) {
|
|
971
|
+
this.onTouched();
|
|
972
|
+
if (wasFocused) {
|
|
973
|
+
// Blur often flips `touched` on the bound `NgControl`; bump the
|
|
974
|
+
// revision so `errorState` recomputes.
|
|
975
|
+
this._ngControlRev.update((n) => n + 1);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
});
|
|
979
|
+
this.destroyRef.onDestroy(() => {
|
|
980
|
+
monitorSub.unsubscribe();
|
|
981
|
+
this.focusMonitor.stopMonitoring(this.elementRef);
|
|
982
|
+
this.clearCloseTimer();
|
|
983
|
+
this.perOpenSubs?.unsubscribe();
|
|
984
|
+
this.resizeObserver?.disconnect();
|
|
985
|
+
this.resizeObserver = null;
|
|
986
|
+
this.overlayRef?.dispose();
|
|
987
|
+
this.overlayRef = null;
|
|
988
|
+
this.overlayInstanceSignal.set(null);
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
// ── Public methods ──
|
|
992
|
+
/** Opens the overlay panel. No-op when disabled or already open. */
|
|
993
|
+
openPanel() {
|
|
994
|
+
if (this.isDisabled() || this.open())
|
|
995
|
+
return;
|
|
996
|
+
this.open.set(true);
|
|
997
|
+
}
|
|
998
|
+
/** Closes the overlay panel. No-op when already closed. */
|
|
999
|
+
closePanel() {
|
|
1000
|
+
if (!this.open())
|
|
1001
|
+
return;
|
|
1002
|
+
this.open.set(false);
|
|
1003
|
+
}
|
|
1004
|
+
/** Toggles the panel's open state. */
|
|
1005
|
+
toggle() {
|
|
1006
|
+
if (this.open()) {
|
|
1007
|
+
this.closePanel();
|
|
1008
|
+
}
|
|
1009
|
+
else {
|
|
1010
|
+
this.openPanel();
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
/** Clears the current selection and emits `selectionChange` with `source: 'reset'`. */
|
|
1014
|
+
clear() {
|
|
1015
|
+
const previous = this.internalValue();
|
|
1016
|
+
const removed = this.selectedOptions().map((o) => this.optionValue()(o));
|
|
1017
|
+
const next = this.multiple() ? [] : null;
|
|
1018
|
+
this.internalValue.set(next);
|
|
1019
|
+
this.value.set(next);
|
|
1020
|
+
this.onChange(next);
|
|
1021
|
+
this.selectionChange.emit({
|
|
1022
|
+
value: next,
|
|
1023
|
+
previousValue: previous,
|
|
1024
|
+
added: [],
|
|
1025
|
+
removed,
|
|
1026
|
+
source: 'reset',
|
|
1027
|
+
});
|
|
1028
|
+
}
|
|
1029
|
+
/** @internal Whether a visible-option index is currently selected. */
|
|
1030
|
+
isVisibleOptionSelected(index) {
|
|
1031
|
+
const visible = this.visibleOptions();
|
|
1032
|
+
if (index < 0 || index >= visible.length)
|
|
1033
|
+
return false;
|
|
1034
|
+
return this.isValueSelected(visible[index].value);
|
|
1035
|
+
}
|
|
1036
|
+
/** @internal */
|
|
1037
|
+
isValueSelected(value) {
|
|
1038
|
+
const current = this.internalValue();
|
|
1039
|
+
if (current === null || current === undefined)
|
|
1040
|
+
return false;
|
|
1041
|
+
const cmp = this.compareWith();
|
|
1042
|
+
if (this.multiple()) {
|
|
1043
|
+
const arr = Array.isArray(current) ? current : [];
|
|
1044
|
+
return arr.some((x) => cmp(x, value));
|
|
1045
|
+
}
|
|
1046
|
+
return cmp(current, value);
|
|
1047
|
+
}
|
|
1048
|
+
/** @internal */
|
|
1049
|
+
selectByVisibleIndex(index, source = 'user') {
|
|
1050
|
+
const visible = this.visibleOptions();
|
|
1051
|
+
if (index < 0 || index >= visible.length)
|
|
1052
|
+
return;
|
|
1053
|
+
const target = visible[index];
|
|
1054
|
+
if (target.disabled)
|
|
1055
|
+
return;
|
|
1056
|
+
this.commitSelection(target.value, target.label, source);
|
|
1057
|
+
}
|
|
1058
|
+
// ── Trigger interactions ──
|
|
1059
|
+
/** @internal */
|
|
1060
|
+
onTriggerClick() {
|
|
1061
|
+
if (this.isDisabled())
|
|
1062
|
+
return;
|
|
1063
|
+
this.toggle();
|
|
1064
|
+
}
|
|
1065
|
+
/** @internal */
|
|
1066
|
+
onClearClick(event) {
|
|
1067
|
+
event.stopPropagation();
|
|
1068
|
+
event.preventDefault();
|
|
1069
|
+
this.clear();
|
|
1070
|
+
}
|
|
1071
|
+
/** @internal */
|
|
1072
|
+
onClearKeydown(event) {
|
|
1073
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
1074
|
+
event.preventDefault();
|
|
1075
|
+
event.stopPropagation();
|
|
1076
|
+
this.clear();
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
/** @internal */
|
|
1080
|
+
onTriggerKeydown(event) {
|
|
1081
|
+
if (this.isDisabled())
|
|
1082
|
+
return;
|
|
1083
|
+
this.handleKeydown(event);
|
|
1084
|
+
}
|
|
1085
|
+
/** @internal Handles keyboard events from the trigger AND the search input inside the overlay. */
|
|
1086
|
+
handleKeydown(event) {
|
|
1087
|
+
const key = event.key;
|
|
1088
|
+
const isOpen = this.open();
|
|
1089
|
+
const searchMode = this.searchable() && isOpen;
|
|
1090
|
+
if (event.altKey && (key === 'ArrowDown' || key === 'ArrowUp')) {
|
|
1091
|
+
event.preventDefault();
|
|
1092
|
+
if (key === 'ArrowDown' && !isOpen)
|
|
1093
|
+
this.openPanel();
|
|
1094
|
+
else if (key === 'ArrowUp' && isOpen)
|
|
1095
|
+
this.closePanel();
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
switch (key) {
|
|
1099
|
+
case 'Enter': {
|
|
1100
|
+
if (!isOpen) {
|
|
1101
|
+
event.preventDefault();
|
|
1102
|
+
this.openPanel();
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
event.preventDefault();
|
|
1106
|
+
const idx = this.activeIndex();
|
|
1107
|
+
if (idx >= 0) {
|
|
1108
|
+
this.selectByVisibleIndex(idx, 'user');
|
|
1109
|
+
}
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
case ' ':
|
|
1113
|
+
case 'Spacebar': {
|
|
1114
|
+
if (searchMode)
|
|
1115
|
+
return;
|
|
1116
|
+
event.preventDefault();
|
|
1117
|
+
if (!isOpen) {
|
|
1118
|
+
this.openPanel();
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
1121
|
+
const idx = this.activeIndex();
|
|
1122
|
+
if (idx >= 0) {
|
|
1123
|
+
this.selectByVisibleIndex(idx, 'user');
|
|
1124
|
+
}
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
case 'ArrowDown': {
|
|
1128
|
+
event.preventDefault();
|
|
1129
|
+
if (!isOpen) {
|
|
1130
|
+
this.openPanel();
|
|
1131
|
+
}
|
|
1132
|
+
else {
|
|
1133
|
+
this.moveActive(1);
|
|
1134
|
+
}
|
|
1135
|
+
return;
|
|
1136
|
+
}
|
|
1137
|
+
case 'ArrowUp': {
|
|
1138
|
+
event.preventDefault();
|
|
1139
|
+
if (!isOpen) {
|
|
1140
|
+
this.openPanel();
|
|
1141
|
+
this.moveActiveTo(this.visibleOptions().length - 1, -1);
|
|
1142
|
+
}
|
|
1143
|
+
else {
|
|
1144
|
+
this.moveActive(-1);
|
|
1145
|
+
}
|
|
1146
|
+
return;
|
|
1147
|
+
}
|
|
1148
|
+
case 'Home': {
|
|
1149
|
+
if (searchMode)
|
|
1150
|
+
return;
|
|
1151
|
+
event.preventDefault();
|
|
1152
|
+
if (!isOpen)
|
|
1153
|
+
this.openPanel();
|
|
1154
|
+
this.moveActiveTo(0, 1);
|
|
1155
|
+
return;
|
|
1156
|
+
}
|
|
1157
|
+
case 'End': {
|
|
1158
|
+
if (searchMode)
|
|
1159
|
+
return;
|
|
1160
|
+
event.preventDefault();
|
|
1161
|
+
if (!isOpen)
|
|
1162
|
+
this.openPanel();
|
|
1163
|
+
this.moveActiveTo(this.visibleOptions().length - 1, -1);
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
case 'PageDown': {
|
|
1167
|
+
if (!isOpen)
|
|
1168
|
+
return;
|
|
1169
|
+
event.preventDefault();
|
|
1170
|
+
this.moveActive(10);
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1173
|
+
case 'PageUp': {
|
|
1174
|
+
if (!isOpen)
|
|
1175
|
+
return;
|
|
1176
|
+
event.preventDefault();
|
|
1177
|
+
this.moveActive(-10);
|
|
1178
|
+
return;
|
|
1179
|
+
}
|
|
1180
|
+
case 'Escape': {
|
|
1181
|
+
if (isOpen) {
|
|
1182
|
+
event.preventDefault();
|
|
1183
|
+
this.closePanel();
|
|
1184
|
+
}
|
|
1185
|
+
return;
|
|
1186
|
+
}
|
|
1187
|
+
case 'Tab': {
|
|
1188
|
+
if (isOpen) {
|
|
1189
|
+
this.closePanel();
|
|
1190
|
+
}
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
1193
|
+
default:
|
|
1194
|
+
if (!searchMode && key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey) {
|
|
1195
|
+
this.applyTypeAhead(key);
|
|
1196
|
+
}
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
// ── Type-ahead (non-search mode) ──
|
|
1201
|
+
typeAheadBuffer = '';
|
|
1202
|
+
typeAheadTimer = null;
|
|
1203
|
+
applyTypeAhead(char) {
|
|
1204
|
+
if (this.typeAheadTimer !== null)
|
|
1205
|
+
clearTimeout(this.typeAheadTimer);
|
|
1206
|
+
this.typeAheadBuffer += char.toLowerCase();
|
|
1207
|
+
const labelFn = this.optionLabel();
|
|
1208
|
+
const visible = this.visibleOptions();
|
|
1209
|
+
const match = visible.findIndex((o) => labelFn(o.option).toLowerCase().startsWith(this.typeAheadBuffer));
|
|
1210
|
+
if (match >= 0) {
|
|
1211
|
+
if (!this.open())
|
|
1212
|
+
this.openPanel();
|
|
1213
|
+
this.activeIndex.set(match);
|
|
1214
|
+
this.scrollActiveIntoView();
|
|
1215
|
+
}
|
|
1216
|
+
this.typeAheadTimer = setTimeout(() => {
|
|
1217
|
+
this.typeAheadBuffer = '';
|
|
1218
|
+
this.typeAheadTimer = null;
|
|
1219
|
+
}, 400);
|
|
1220
|
+
}
|
|
1221
|
+
// ── Active-index navigation ──
|
|
1222
|
+
moveActive(delta) {
|
|
1223
|
+
const visible = this.visibleOptions();
|
|
1224
|
+
if (visible.length === 0) {
|
|
1225
|
+
this.activeIndex.set(-1);
|
|
1226
|
+
return;
|
|
1227
|
+
}
|
|
1228
|
+
const start = this.activeIndex();
|
|
1229
|
+
const direction = delta > 0 ? 1 : -1;
|
|
1230
|
+
const steps = Math.abs(delta);
|
|
1231
|
+
let current = start;
|
|
1232
|
+
for (let i = 0; i < steps; i++) {
|
|
1233
|
+
const next = this.findEnabledFrom(current + direction, direction);
|
|
1234
|
+
if (next === -1)
|
|
1235
|
+
break;
|
|
1236
|
+
current = next;
|
|
1237
|
+
}
|
|
1238
|
+
if (current !== start && current >= 0) {
|
|
1239
|
+
this.activeIndex.set(current);
|
|
1240
|
+
this.scrollActiveIntoView();
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
moveActiveTo(startIndex, direction) {
|
|
1244
|
+
const next = this.findEnabledFrom(startIndex, direction);
|
|
1245
|
+
if (next >= 0) {
|
|
1246
|
+
this.activeIndex.set(next);
|
|
1247
|
+
this.scrollActiveIntoView();
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
findEnabledFrom(start, direction) {
|
|
1251
|
+
const visible = this.visibleOptions();
|
|
1252
|
+
let i = start;
|
|
1253
|
+
while (i >= 0 && i < visible.length) {
|
|
1254
|
+
if (!visible[i].disabled)
|
|
1255
|
+
return i;
|
|
1256
|
+
i += direction;
|
|
1257
|
+
}
|
|
1258
|
+
return -1;
|
|
1259
|
+
}
|
|
1260
|
+
/** @internal */
|
|
1261
|
+
initActiveIndexOnOpen() {
|
|
1262
|
+
const visible = this.visibleOptions();
|
|
1263
|
+
if (visible.length === 0) {
|
|
1264
|
+
this.activeIndex.set(-1);
|
|
1265
|
+
return;
|
|
1266
|
+
}
|
|
1267
|
+
const selectedIdx = visible.findIndex((v) => this.isValueSelected(v.value));
|
|
1268
|
+
if (selectedIdx >= 0 && !visible[selectedIdx].disabled) {
|
|
1269
|
+
this.activeIndex.set(selectedIdx);
|
|
1270
|
+
}
|
|
1271
|
+
else {
|
|
1272
|
+
this.activeIndex.set(this.findEnabledFrom(0, 1));
|
|
1273
|
+
}
|
|
1274
|
+
this.scrollActiveIntoView();
|
|
1275
|
+
}
|
|
1276
|
+
scrollActiveIntoView() {
|
|
1277
|
+
if (!this.platform.isBrowser)
|
|
1278
|
+
return;
|
|
1279
|
+
queueMicrotask(() => {
|
|
1280
|
+
const id = this.activeDescendantId();
|
|
1281
|
+
if (!id)
|
|
1282
|
+
return;
|
|
1283
|
+
const el = document.getElementById(id);
|
|
1284
|
+
if (el && typeof el.scrollIntoView === 'function') {
|
|
1285
|
+
el.scrollIntoView({ block: 'nearest', behavior: 'auto' });
|
|
1286
|
+
}
|
|
1287
|
+
});
|
|
1288
|
+
}
|
|
1289
|
+
// ── Selection commit ──
|
|
1290
|
+
commitSelection(value, label, source) {
|
|
1291
|
+
const previous = this.internalValue();
|
|
1292
|
+
if (this.multiple()) {
|
|
1293
|
+
const cmp = this.compareWith();
|
|
1294
|
+
const currentArr = Array.isArray(previous) ? [...previous] : [];
|
|
1295
|
+
const existingIdx = currentArr.findIndex((x) => cmp(x, value));
|
|
1296
|
+
let added = [];
|
|
1297
|
+
let removed = [];
|
|
1298
|
+
if (existingIdx >= 0) {
|
|
1299
|
+
removed = [currentArr[existingIdx]];
|
|
1300
|
+
currentArr.splice(existingIdx, 1);
|
|
1301
|
+
}
|
|
1302
|
+
else {
|
|
1303
|
+
added = [value];
|
|
1304
|
+
currentArr.push(value);
|
|
1305
|
+
}
|
|
1306
|
+
this.internalValue.set(currentArr);
|
|
1307
|
+
this.value.set(currentArr);
|
|
1308
|
+
if (source === 'user') {
|
|
1309
|
+
this.onChange(currentArr);
|
|
1310
|
+
this.liveAnnouncer.announce(`${label} ${added.length ? 'selected' : 'deselected'}`, 'polite');
|
|
1311
|
+
}
|
|
1312
|
+
this.selectionChange.emit({
|
|
1313
|
+
value: currentArr,
|
|
1314
|
+
previousValue: previous,
|
|
1315
|
+
added,
|
|
1316
|
+
removed,
|
|
1317
|
+
source,
|
|
1318
|
+
});
|
|
1319
|
+
}
|
|
1320
|
+
else {
|
|
1321
|
+
const cmp = this.compareWith();
|
|
1322
|
+
const prevSingle = previous;
|
|
1323
|
+
if (prevSingle !== null && prevSingle !== undefined && cmp(prevSingle, value)) {
|
|
1324
|
+
if (this.resolvedCloseOnSelect())
|
|
1325
|
+
this.closePanel();
|
|
1326
|
+
return;
|
|
1327
|
+
}
|
|
1328
|
+
this.internalValue.set(value);
|
|
1329
|
+
this.value.set(value);
|
|
1330
|
+
if (source === 'user')
|
|
1331
|
+
this.onChange(value);
|
|
1332
|
+
this.selectionChange.emit({
|
|
1333
|
+
value,
|
|
1334
|
+
previousValue: previous,
|
|
1335
|
+
added: [value],
|
|
1336
|
+
removed: prevSingle !== null && prevSingle !== undefined ? [prevSingle] : [],
|
|
1337
|
+
source,
|
|
1338
|
+
});
|
|
1339
|
+
if (this.resolvedCloseOnSelect())
|
|
1340
|
+
this.closePanel();
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
// ── Overlay lifecycle ──
|
|
1344
|
+
openOverlay() {
|
|
1345
|
+
this.ensureOverlay();
|
|
1346
|
+
this.attachOverlayComponent();
|
|
1347
|
+
this.subscribePerOpen();
|
|
1348
|
+
this.search.set('');
|
|
1349
|
+
this.initActiveIndexOnOpen();
|
|
1350
|
+
queueMicrotask(() => {
|
|
1351
|
+
if (this.searchable()) {
|
|
1352
|
+
this.overlayInstance?.focusSearchInput();
|
|
1353
|
+
}
|
|
1354
|
+
});
|
|
1355
|
+
this.openedChange.emit({ open: true, trigger: this.elementRef.nativeElement });
|
|
1356
|
+
}
|
|
1357
|
+
closeOverlay() {
|
|
1358
|
+
if (this.closingSignal() || !this.overlayInstance)
|
|
1359
|
+
return;
|
|
1360
|
+
this.closingSignal.set(true);
|
|
1361
|
+
this.overlayInstance.leaving.set(true);
|
|
1362
|
+
this.elementRef.nativeElement.querySelector('button')?.focus();
|
|
1363
|
+
this.closeTimer = setTimeout(() => {
|
|
1364
|
+
this.closeTimer = null;
|
|
1365
|
+
if (this.overlayRef?.hasAttached()) {
|
|
1366
|
+
this.overlayRef.detach();
|
|
1367
|
+
}
|
|
1368
|
+
this.perOpenSubs?.unsubscribe();
|
|
1369
|
+
this.perOpenSubs = null;
|
|
1370
|
+
this.overlayInstanceSignal.set(null);
|
|
1371
|
+
this.search.set('');
|
|
1372
|
+
this.activeIndex.set(-1);
|
|
1373
|
+
untracked(() => this.open.set(false));
|
|
1374
|
+
this.closingSignal.set(false);
|
|
1375
|
+
this.openedChange.emit({ open: false, trigger: this.elementRef.nativeElement });
|
|
1376
|
+
}, ANIMATION_DURATION);
|
|
1377
|
+
}
|
|
1378
|
+
ensureOverlay() {
|
|
1379
|
+
if (this.overlayRef) {
|
|
1380
|
+
this.updateOverlaySize();
|
|
1381
|
+
return;
|
|
1382
|
+
}
|
|
1383
|
+
const positionStrategy = this.overlayService
|
|
1384
|
+
.position()
|
|
1385
|
+
.flexibleConnectedTo(this.elementRef)
|
|
1386
|
+
.withPositions(buildSelectLikePositions(this.offset()))
|
|
1387
|
+
.withFlexibleDimensions(false)
|
|
1388
|
+
.withPush(false)
|
|
1389
|
+
.withViewportMargin(8);
|
|
1390
|
+
this.overlayRef = this.overlayService.create({
|
|
1391
|
+
positionStrategy,
|
|
1392
|
+
scrollStrategy: resolveSelectScrollStrategy(this.scrollStrategy(), this.overlayService),
|
|
1393
|
+
hasBackdrop: true,
|
|
1394
|
+
backdropClass: 'cdk-overlay-transparent-backdrop',
|
|
1395
|
+
panelClass: 'tw-select-panel',
|
|
1396
|
+
});
|
|
1397
|
+
this.installResizeObserver();
|
|
1398
|
+
this.updateOverlaySize();
|
|
1399
|
+
}
|
|
1400
|
+
installResizeObserver() {
|
|
1401
|
+
if (!this.platform.isBrowser || typeof ResizeObserver === 'undefined')
|
|
1402
|
+
return;
|
|
1403
|
+
this.resizeObserver?.disconnect();
|
|
1404
|
+
this.resizeObserver = new ResizeObserver(() => this.updateOverlaySize());
|
|
1405
|
+
this.resizeObserver.observe(this.elementRef.nativeElement);
|
|
1406
|
+
}
|
|
1407
|
+
updateOverlaySize() {
|
|
1408
|
+
if (!this.overlayRef)
|
|
1409
|
+
return;
|
|
1410
|
+
const width = this.panelWidth();
|
|
1411
|
+
if (width === 'trigger') {
|
|
1412
|
+
const button = this.elementRef.nativeElement.querySelector('button');
|
|
1413
|
+
const rect = (button ?? this.elementRef.nativeElement).getBoundingClientRect();
|
|
1414
|
+
this.overlayRef.updateSize({ width: rect.width });
|
|
1415
|
+
}
|
|
1416
|
+
else if (width === 'auto') {
|
|
1417
|
+
this.overlayRef.updateSize({ width: undefined, minWidth: undefined });
|
|
1418
|
+
}
|
|
1419
|
+
else if (typeof width === 'number') {
|
|
1420
|
+
this.overlayRef.updateSize({ width });
|
|
1421
|
+
}
|
|
1422
|
+
else {
|
|
1423
|
+
this.overlayRef.updateSize({ width });
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
attachOverlayComponent() {
|
|
1427
|
+
if (!this.overlayRef)
|
|
1428
|
+
return;
|
|
1429
|
+
const portal = new ComponentPortal(SelectOverlayComponent, this.viewContainerRef, this.injector);
|
|
1430
|
+
const ref = this.overlayRef.attach(portal);
|
|
1431
|
+
const instance = ref.instance;
|
|
1432
|
+
instance.onSearchInput.set((v) => this.search.set(v));
|
|
1433
|
+
instance.onOptionSelect.set((i) => this.selectByVisibleIndex(i, 'user'));
|
|
1434
|
+
instance.onOptionActivate.set((i) => this.activeIndex.set(i));
|
|
1435
|
+
instance.onPanelKeydown.set((e) => this.handleKeydown(e));
|
|
1436
|
+
this.overlayInstanceSignal.set(instance);
|
|
1437
|
+
}
|
|
1438
|
+
subscribePerOpen() {
|
|
1439
|
+
this.perOpenSubs?.unsubscribe();
|
|
1440
|
+
this.perOpenSubs = new Subscription();
|
|
1441
|
+
if (!this.overlayRef)
|
|
1442
|
+
return;
|
|
1443
|
+
this.perOpenSubs.add(this.overlayRef
|
|
1444
|
+
.backdropClick()
|
|
1445
|
+
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
1446
|
+
.subscribe(() => this.closePanel()));
|
|
1447
|
+
const teardownEscape = consumeOverlayEscape(this.overlayRef, (event) => {
|
|
1448
|
+
event.preventDefault();
|
|
1449
|
+
this.closePanel();
|
|
1450
|
+
});
|
|
1451
|
+
this.perOpenSubs.add(() => teardownEscape());
|
|
1452
|
+
}
|
|
1453
|
+
resolvePanelClass() {
|
|
1454
|
+
const raw = this.panelClass();
|
|
1455
|
+
return Array.isArray(raw) ? raw.join(' ') : raw;
|
|
1456
|
+
}
|
|
1457
|
+
clearCloseTimer() {
|
|
1458
|
+
if (this.closeTimer !== null) {
|
|
1459
|
+
clearTimeout(this.closeTimer);
|
|
1460
|
+
this.closeTimer = null;
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
/** @internal */
|
|
1464
|
+
buildTriggerContext() {
|
|
1465
|
+
return {
|
|
1466
|
+
$implicit: this.internalValue(),
|
|
1467
|
+
open: this.open(),
|
|
1468
|
+
empty: this.isEmpty(),
|
|
1469
|
+
selectedOptions: this.selectedOptions(),
|
|
1470
|
+
};
|
|
1471
|
+
}
|
|
1472
|
+
// ── ControlValueAccessor ──
|
|
1473
|
+
writeValue(value) {
|
|
1474
|
+
const previous = this.internalValue();
|
|
1475
|
+
let normalised;
|
|
1476
|
+
if (this.multiple()) {
|
|
1477
|
+
if (Array.isArray(value))
|
|
1478
|
+
normalised = value;
|
|
1479
|
+
else if (value === null || value === undefined)
|
|
1480
|
+
normalised = [];
|
|
1481
|
+
else
|
|
1482
|
+
normalised = [value];
|
|
1483
|
+
}
|
|
1484
|
+
else {
|
|
1485
|
+
if (Array.isArray(value))
|
|
1486
|
+
normalised = value.length ? value[0] : null;
|
|
1487
|
+
else
|
|
1488
|
+
normalised = value ?? null;
|
|
1489
|
+
}
|
|
1490
|
+
this.internalValue.set(normalised);
|
|
1491
|
+
this.value.set(normalised);
|
|
1492
|
+
this.selectionChange.emit({
|
|
1493
|
+
value: normalised,
|
|
1494
|
+
previousValue: previous,
|
|
1495
|
+
added: [],
|
|
1496
|
+
removed: [],
|
|
1497
|
+
source: 'programmatic',
|
|
1498
|
+
});
|
|
1499
|
+
}
|
|
1500
|
+
registerOnChange(fn) {
|
|
1501
|
+
this.onChange = fn;
|
|
1502
|
+
}
|
|
1503
|
+
registerOnTouched(fn) {
|
|
1504
|
+
this.onTouched = fn;
|
|
1505
|
+
}
|
|
1506
|
+
setDisabledState(isDisabled) {
|
|
1507
|
+
this.cvaDisabled.set(isDisabled);
|
|
1508
|
+
}
|
|
1509
|
+
// ── FormFieldControl methods ──
|
|
1510
|
+
/** @internal */
|
|
1511
|
+
setDescribedByIds(ids) {
|
|
1512
|
+
this.describedByIdsSignal.set([...ids]);
|
|
1513
|
+
}
|
|
1514
|
+
/** @internal Receives the merged `aria-labelledby` ids from the wrapping form-field. The trigger needs explicit labelledby because it's a non-native combobox button — `<label for>` does not reach it. */
|
|
1515
|
+
setLabelledByIds(ids) {
|
|
1516
|
+
this.labelledByIdsSignal.set([...ids]);
|
|
1517
|
+
}
|
|
1518
|
+
/** @internal */
|
|
1519
|
+
onContainerClick(event) {
|
|
1520
|
+
if (this.isDisabled())
|
|
1521
|
+
return;
|
|
1522
|
+
const btn = this.elementRef.nativeElement.querySelector('button');
|
|
1523
|
+
btn?.focus();
|
|
1524
|
+
if (!event.defaultPrevented) {
|
|
1525
|
+
this.openPanel();
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
ngOnInit() {
|
|
1529
|
+
// NgControl's `control` is set by the parent FormControl* directive before
|
|
1530
|
+
// children's `ngOnInit`. Subscribe here so errorState reacts to status/value
|
|
1531
|
+
// changes on the bound control.
|
|
1532
|
+
const ctrl = this.ngControl?.control;
|
|
1533
|
+
if (ctrl) {
|
|
1534
|
+
const streams = [ctrl.statusChanges, ctrl.valueChanges].filter((s) => !!s);
|
|
1535
|
+
if (streams.length) {
|
|
1536
|
+
merge(...streams)
|
|
1537
|
+
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
1538
|
+
.subscribe(() => this._ngControlRev.update((n) => n + 1));
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
const submit = this.parentFormGroup?.ngSubmit ?? this.parentForm?.ngSubmit;
|
|
1542
|
+
if (submit) {
|
|
1543
|
+
submit
|
|
1544
|
+
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
1545
|
+
.subscribe(() => this._formSubmitRev.update((n) => n + 1));
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: SelectComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1549
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: SelectComponent, isStandalone: true, selector: "tw-select", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, optionLabel: { classPropertyName: "optionLabel", publicName: "optionLabel", isSignal: true, isRequired: false, transformFunction: null }, optionValue: { classPropertyName: "optionValue", publicName: "optionValue", isSignal: true, isRequired: false, transformFunction: null }, optionDisabled: { classPropertyName: "optionDisabled", publicName: "optionDisabled", isSignal: true, isRequired: false, transformFunction: null }, optionGroup: { classPropertyName: "optionGroup", publicName: "optionGroup", isSignal: true, isRequired: false, transformFunction: null }, multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, searchable: { classPropertyName: "searchable", publicName: "searchable", isSignal: true, isRequired: false, transformFunction: null }, filterPredicate: { classPropertyName: "filterPredicate", publicName: "filterPredicate", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, disabledInput: { classPropertyName: "disabledInput", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, requiredInput: { classPropertyName: "requiredInput", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, panelWidth: { classPropertyName: "panelWidth", publicName: "panelWidth", isSignal: true, isRequired: false, transformFunction: null }, panelClass: { classPropertyName: "panelClass", publicName: "panelClass", isSignal: true, isRequired: false, transformFunction: null }, panelMaxHeight: { classPropertyName: "panelMaxHeight", publicName: "panelMaxHeight", isSignal: true, isRequired: false, transformFunction: null }, closeOnSelect: { classPropertyName: "closeOnSelect", publicName: "closeOnSelect", isSignal: true, isRequired: false, transformFunction: null }, scrollStrategy: { classPropertyName: "scrollStrategy", publicName: "scrollStrategy", isSignal: true, isRequired: false, transformFunction: null }, offset: { classPropertyName: "offset", publicName: "offset", isSignal: true, isRequired: false, transformFunction: null }, emptyMessage: { classPropertyName: "emptyMessage", publicName: "emptyMessage", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "aria-label", isSignal: true, isRequired: false, transformFunction: null }, ariaLabelledby: { classPropertyName: "ariaLabelledby", publicName: "aria-labelledby", isSignal: true, isRequired: false, transformFunction: null }, ariaDescribedby: { classPropertyName: "ariaDescribedby", publicName: "aria-describedby", isSignal: true, isRequired: false, transformFunction: null }, errorStateMatcher: { classPropertyName: "errorStateMatcher", publicName: "errorStateMatcher", isSignal: true, isRequired: false, transformFunction: null }, compareWith: { classPropertyName: "compareWith", publicName: "compareWith", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", open: "openChange", openedChange: "openedChange", selectionChange: "selectionChange", searchChange: "searchChange" }, host: { properties: { "class": "rootClasses()" } }, providers: [
|
|
1550
|
+
{
|
|
1551
|
+
provide: TW_FORM_FIELD_CONTROL,
|
|
1552
|
+
useExisting: forwardRef(() => SelectComponent),
|
|
1553
|
+
},
|
|
1554
|
+
], queries: [{ propertyName: "triggerTemplateChild", first: true, predicate: SelectTriggerTemplateDirective, descendants: true, isSignal: true }, { propertyName: "optionTemplateChild", first: true, predicate: SelectOptionTemplateDirective, descendants: true, isSignal: true }, { propertyName: "emptyTemplateChild", first: true, predicate: SelectEmptyTemplateDirective, descendants: true, isSignal: true }, { propertyName: "headerTemplateChild", first: true, predicate: SelectHeaderTemplateDirective, descendants: true, isSignal: true }, { propertyName: "footerTemplateChild", first: true, predicate: SelectFooterTemplateDirective, descendants: true, isSignal: true }], ngImport: i0, template: `
|
|
1555
|
+
<button
|
|
1556
|
+
#triggerButton
|
|
1557
|
+
type="button"
|
|
1558
|
+
[id]="hostId"
|
|
1559
|
+
[class]="triggerClasses()"
|
|
1560
|
+
[attr.role]="'combobox'"
|
|
1561
|
+
[attr.aria-haspopup]="'listbox'"
|
|
1562
|
+
[attr.aria-expanded]="open() ? 'true' : 'false'"
|
|
1563
|
+
[attr.aria-controls]="listboxId"
|
|
1564
|
+
[attr.aria-activedescendant]="activeDescendantId() || null"
|
|
1565
|
+
[attr.aria-autocomplete]="searchable() ? 'list' : 'none'"
|
|
1566
|
+
[attr.aria-label]="ariaLabel() || null"
|
|
1567
|
+
[attr.aria-labelledby]="labelledBy() || null"
|
|
1568
|
+
[attr.aria-describedby]="describedBy() || null"
|
|
1569
|
+
[attr.aria-required]="requiredInput() || null"
|
|
1570
|
+
[attr.aria-invalid]="errorState() || null"
|
|
1571
|
+
[attr.aria-disabled]="isDisabled() || null"
|
|
1572
|
+
[disabled]="isDisabled()"
|
|
1573
|
+
[attr.data-variant]="resolvedVariant()"
|
|
1574
|
+
(click)="onTriggerClick()"
|
|
1575
|
+
(keydown)="onTriggerKeydown($event)"
|
|
1576
|
+
>
|
|
1577
|
+
@if (triggerTemplateChild(); as tpl) {
|
|
1578
|
+
<span [class]="valueTextClasses()">
|
|
1579
|
+
<ng-container
|
|
1580
|
+
*ngTemplateOutlet="tpl.templateRef; context: buildTriggerContext()"
|
|
1581
|
+
/>
|
|
1582
|
+
</span>
|
|
1583
|
+
} @else if (isEmpty()) {
|
|
1584
|
+
<span [class]="placeholderTextClasses()">{{ placeholder() || '\u00A0' }}</span>
|
|
1585
|
+
} @else {
|
|
1586
|
+
<span [class]="valueTextClasses()">{{ defaultTriggerText() }}</span>
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
@if (showClearButton()) {
|
|
1590
|
+
<span
|
|
1591
|
+
role="button"
|
|
1592
|
+
tabindex="-1"
|
|
1593
|
+
[class]="clearButtonClasses()"
|
|
1594
|
+
[attr.aria-label]="'Clear selection'"
|
|
1595
|
+
(click)="onClearClick($event)"
|
|
1596
|
+
(keydown)="onClearKeydown($event)"
|
|
1597
|
+
>
|
|
1598
|
+
<svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" class="size-3">
|
|
1599
|
+
<path
|
|
1600
|
+
fill-rule="evenodd"
|
|
1601
|
+
d="M10 8.586 4.707 3.293a1 1 0 0 0-1.414 1.414L8.586 10l-5.293 5.293a1 1 0 1 0 1.414 1.414L10 11.414l5.293 5.293a1 1 0 0 0 1.414-1.414L11.414 10l5.293-5.293a1 1 0 0 0-1.414-1.414L10 8.586Z"
|
|
1602
|
+
clip-rule="evenodd"
|
|
1603
|
+
/>
|
|
1604
|
+
</svg>
|
|
1605
|
+
</span>
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
<svg
|
|
1609
|
+
viewBox="0 0 20 20"
|
|
1610
|
+
fill="currentColor"
|
|
1611
|
+
aria-hidden="true"
|
|
1612
|
+
[class]="chevronClasses()"
|
|
1613
|
+
>
|
|
1614
|
+
<path
|
|
1615
|
+
fill-rule="evenodd"
|
|
1616
|
+
d="M5.23 7.21a.75.75 0 0 1 1.06.02L10 11.06l3.71-3.83a.75.75 0 1 1 1.08 1.04l-4.25 4.39a.75.75 0 0 1-1.08 0L5.21 8.27a.75.75 0 0 1 .02-1.06Z"
|
|
1617
|
+
clip-rule="evenodd"
|
|
1618
|
+
/>
|
|
1619
|
+
</svg>
|
|
1620
|
+
</button>
|
|
1621
|
+
|
|
1622
|
+
<span hidden>
|
|
1623
|
+
<ng-content />
|
|
1624
|
+
</span>
|
|
1625
|
+
`, isInline: true, dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1626
|
+
}
|
|
1627
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: SelectComponent, decorators: [{
|
|
1628
|
+
type: Component,
|
|
1629
|
+
args: [{
|
|
1630
|
+
selector: 'tw-select',
|
|
1631
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1632
|
+
imports: [NgTemplateOutlet],
|
|
1633
|
+
providers: [
|
|
1634
|
+
{
|
|
1635
|
+
provide: TW_FORM_FIELD_CONTROL,
|
|
1636
|
+
useExisting: forwardRef(() => SelectComponent),
|
|
1637
|
+
},
|
|
1638
|
+
],
|
|
1639
|
+
template: `
|
|
1640
|
+
<button
|
|
1641
|
+
#triggerButton
|
|
1642
|
+
type="button"
|
|
1643
|
+
[id]="hostId"
|
|
1644
|
+
[class]="triggerClasses()"
|
|
1645
|
+
[attr.role]="'combobox'"
|
|
1646
|
+
[attr.aria-haspopup]="'listbox'"
|
|
1647
|
+
[attr.aria-expanded]="open() ? 'true' : 'false'"
|
|
1648
|
+
[attr.aria-controls]="listboxId"
|
|
1649
|
+
[attr.aria-activedescendant]="activeDescendantId() || null"
|
|
1650
|
+
[attr.aria-autocomplete]="searchable() ? 'list' : 'none'"
|
|
1651
|
+
[attr.aria-label]="ariaLabel() || null"
|
|
1652
|
+
[attr.aria-labelledby]="labelledBy() || null"
|
|
1653
|
+
[attr.aria-describedby]="describedBy() || null"
|
|
1654
|
+
[attr.aria-required]="requiredInput() || null"
|
|
1655
|
+
[attr.aria-invalid]="errorState() || null"
|
|
1656
|
+
[attr.aria-disabled]="isDisabled() || null"
|
|
1657
|
+
[disabled]="isDisabled()"
|
|
1658
|
+
[attr.data-variant]="resolvedVariant()"
|
|
1659
|
+
(click)="onTriggerClick()"
|
|
1660
|
+
(keydown)="onTriggerKeydown($event)"
|
|
1661
|
+
>
|
|
1662
|
+
@if (triggerTemplateChild(); as tpl) {
|
|
1663
|
+
<span [class]="valueTextClasses()">
|
|
1664
|
+
<ng-container
|
|
1665
|
+
*ngTemplateOutlet="tpl.templateRef; context: buildTriggerContext()"
|
|
1666
|
+
/>
|
|
1667
|
+
</span>
|
|
1668
|
+
} @else if (isEmpty()) {
|
|
1669
|
+
<span [class]="placeholderTextClasses()">{{ placeholder() || '\u00A0' }}</span>
|
|
1670
|
+
} @else {
|
|
1671
|
+
<span [class]="valueTextClasses()">{{ defaultTriggerText() }}</span>
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
@if (showClearButton()) {
|
|
1675
|
+
<span
|
|
1676
|
+
role="button"
|
|
1677
|
+
tabindex="-1"
|
|
1678
|
+
[class]="clearButtonClasses()"
|
|
1679
|
+
[attr.aria-label]="'Clear selection'"
|
|
1680
|
+
(click)="onClearClick($event)"
|
|
1681
|
+
(keydown)="onClearKeydown($event)"
|
|
1682
|
+
>
|
|
1683
|
+
<svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" class="size-3">
|
|
1684
|
+
<path
|
|
1685
|
+
fill-rule="evenodd"
|
|
1686
|
+
d="M10 8.586 4.707 3.293a1 1 0 0 0-1.414 1.414L8.586 10l-5.293 5.293a1 1 0 1 0 1.414 1.414L10 11.414l5.293 5.293a1 1 0 0 0 1.414-1.414L11.414 10l5.293-5.293a1 1 0 0 0-1.414-1.414L10 8.586Z"
|
|
1687
|
+
clip-rule="evenodd"
|
|
1688
|
+
/>
|
|
1689
|
+
</svg>
|
|
1690
|
+
</span>
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
<svg
|
|
1694
|
+
viewBox="0 0 20 20"
|
|
1695
|
+
fill="currentColor"
|
|
1696
|
+
aria-hidden="true"
|
|
1697
|
+
[class]="chevronClasses()"
|
|
1698
|
+
>
|
|
1699
|
+
<path
|
|
1700
|
+
fill-rule="evenodd"
|
|
1701
|
+
d="M5.23 7.21a.75.75 0 0 1 1.06.02L10 11.06l3.71-3.83a.75.75 0 1 1 1.08 1.04l-4.25 4.39a.75.75 0 0 1-1.08 0L5.21 8.27a.75.75 0 0 1 .02-1.06Z"
|
|
1702
|
+
clip-rule="evenodd"
|
|
1703
|
+
/>
|
|
1704
|
+
</svg>
|
|
1705
|
+
</button>
|
|
1706
|
+
|
|
1707
|
+
<span hidden>
|
|
1708
|
+
<ng-content />
|
|
1709
|
+
</span>
|
|
1710
|
+
`,
|
|
1711
|
+
host: {
|
|
1712
|
+
'[class]': 'rootClasses()',
|
|
1713
|
+
},
|
|
1714
|
+
}]
|
|
1715
|
+
}], ctorParameters: () => [], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], optionLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionLabel", required: false }] }], optionValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionValue", required: false }] }], optionDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionDisabled", required: false }] }], optionGroup: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionGroup", required: false }] }], multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], searchable: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchable", required: false }] }], filterPredicate: [{ type: i0.Input, args: [{ isSignal: true, alias: "filterPredicate", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], disabledInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], requiredInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], panelWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "panelWidth", required: false }] }], panelClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "panelClass", required: false }] }], panelMaxHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "panelMaxHeight", required: false }] }], closeOnSelect: [{ type: i0.Input, args: [{ isSignal: true, alias: "closeOnSelect", required: false }] }], scrollStrategy: [{ type: i0.Input, args: [{ isSignal: true, alias: "scrollStrategy", required: false }] }], offset: [{ type: i0.Input, args: [{ isSignal: true, alias: "offset", required: false }] }], emptyMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyMessage", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-label", required: false }] }], ariaLabelledby: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-labelledby", required: false }] }], ariaDescribedby: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-describedby", required: false }] }], errorStateMatcher: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorStateMatcher", required: false }] }], compareWith: [{ type: i0.Input, args: [{ isSignal: true, alias: "compareWith", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }, { type: i0.Output, args: ["openChange"] }], openedChange: [{ type: i0.Output, args: ["openedChange"] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], searchChange: [{ type: i0.Output, args: ["searchChange"] }], triggerTemplateChild: [{ type: i0.ContentChild, args: [i0.forwardRef(() => SelectTriggerTemplateDirective), { isSignal: true }] }], optionTemplateChild: [{ type: i0.ContentChild, args: [i0.forwardRef(() => SelectOptionTemplateDirective), { isSignal: true }] }], emptyTemplateChild: [{ type: i0.ContentChild, args: [i0.forwardRef(() => SelectEmptyTemplateDirective), { isSignal: true }] }], headerTemplateChild: [{ type: i0.ContentChild, args: [i0.forwardRef(() => SelectHeaderTemplateDirective), { isSignal: true }] }], footerTemplateChild: [{ type: i0.ContentChild, args: [i0.forwardRef(() => SelectFooterTemplateDirective), { isSignal: true }] }] } });
|
|
1716
|
+
|
|
1717
|
+
/**
|
|
1718
|
+
* Generated bundle index. Do not edit.
|
|
1719
|
+
*/
|
|
1720
|
+
|
|
1721
|
+
export { SelectComponent, SelectEmptyTemplateDirective, SelectFooterTemplateDirective, SelectHeaderTemplateDirective, SelectOptionTemplateDirective, SelectTriggerTemplateDirective };
|
|
1722
|
+
//# sourceMappingURL=cdevhub-ngx-tw-select.mjs.map
|