@cdevhub/ngx-tw 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +78 -15
- package/fesm2022/cdevhub-ngx-tw-accordion.mjs +16 -5
- package/fesm2022/cdevhub-ngx-tw-accordion.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-avatar.mjs +26 -15
- package/fesm2022/cdevhub-ngx-tw-avatar.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-badge.mjs +17 -5
- package/fesm2022/cdevhub-ngx-tw-badge.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-breadcrumbs.mjs +27 -6
- package/fesm2022/cdevhub-ngx-tw-breadcrumbs.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-button.mjs +23 -11
- package/fesm2022/cdevhub-ngx-tw-button.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-calendar-luxon.mjs +5 -5
- package/fesm2022/cdevhub-ngx-tw-calendar-luxon.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-calendar-testing.mjs +3 -3
- package/fesm2022/cdevhub-ngx-tw-calendar-testing.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-calendar.mjs +427 -122
- package/fesm2022/cdevhub-ngx-tw-calendar.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-card.mjs +21 -11
- package/fesm2022/cdevhub-ngx-tw-card.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-carousel.mjs +215 -96
- package/fesm2022/cdevhub-ngx-tw-carousel.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-checkbox.mjs +108 -64
- package/fesm2022/cdevhub-ngx-tw-checkbox.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-code-block.mjs +18 -6
- package/fesm2022/cdevhub-ngx-tw-code-block.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-collapsible.mjs +55 -30
- package/fesm2022/cdevhub-ngx-tw-collapsible.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-combobox-testing.mjs +218 -0
- package/fesm2022/cdevhub-ngx-tw-combobox-testing.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-combobox.mjs +236 -211
- package/fesm2022/cdevhub-ngx-tw-combobox.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-command-palette-testing.mjs +200 -0
- package/fesm2022/cdevhub-ngx-tw-command-palette-testing.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-command-palette.mjs +34 -7
- package/fesm2022/cdevhub-ngx-tw-command-palette.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-core.mjs +135 -15
- package/fesm2022/cdevhub-ngx-tw-core.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-date-picker-testing.mjs +203 -0
- package/fesm2022/cdevhub-ngx-tw-date-picker-testing.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-date-picker.mjs +160 -102
- package/fesm2022/cdevhub-ngx-tw-date-picker.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-date-range-picker-testing.mjs +209 -0
- package/fesm2022/cdevhub-ngx-tw-date-range-picker-testing.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-date-range-picker.mjs +182 -107
- package/fesm2022/cdevhub-ngx-tw-date-range-picker.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-dialog-testing.mjs +122 -0
- package/fesm2022/cdevhub-ngx-tw-dialog-testing.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-dialog.mjs +11 -2
- package/fesm2022/cdevhub-ngx-tw-dialog.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-empty-state.mjs +1 -1
- package/fesm2022/cdevhub-ngx-tw-empty-state.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-file-upload-testing.mjs +149 -0
- package/fesm2022/cdevhub-ngx-tw-file-upload-testing.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-file-upload.mjs +57 -67
- package/fesm2022/cdevhub-ngx-tw-file-upload.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-flip-card.mjs +42 -24
- package/fesm2022/cdevhub-ngx-tw-flip-card.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-form-field.mjs +114 -21
- package/fesm2022/cdevhub-ngx-tw-form-field.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-icon.mjs +16 -6
- package/fesm2022/cdevhub-ngx-tw-icon.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-input.mjs +52 -61
- package/fesm2022/cdevhub-ngx-tw-input.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-item.mjs +28 -4
- package/fesm2022/cdevhub-ngx-tw-item.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-menu-testing.mjs +168 -0
- package/fesm2022/cdevhub-ngx-tw-menu-testing.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-menu.mjs +58 -21
- package/fesm2022/cdevhub-ngx-tw-menu.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-number-input.mjs +34 -1
- package/fesm2022/cdevhub-ngx-tw-number-input.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-paginator.mjs +95 -45
- package/fesm2022/cdevhub-ngx-tw-paginator.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-popover.mjs +81 -32
- package/fesm2022/cdevhub-ngx-tw-popover.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-progress-bar.mjs +1 -1
- package/fesm2022/cdevhub-ngx-tw-progress-bar.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-radio.mjs +131 -98
- package/fesm2022/cdevhub-ngx-tw-radio.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-segmented-control.mjs +207 -42
- package/fesm2022/cdevhub-ngx-tw-segmented-control.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-select-testing.mjs +165 -0
- package/fesm2022/cdevhub-ngx-tw-select-testing.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-select.mjs +386 -210
- package/fesm2022/cdevhub-ngx-tw-select.mjs.map +1 -1
- package/fesm2022/{cdevhub-ngx-tw-sheet-sheet-renderer-ByyNluUd.mjs → cdevhub-ngx-tw-sheet-sheet-renderer-CqVJwgmp.mjs} +3 -3
- package/fesm2022/cdevhub-ngx-tw-sheet-sheet-renderer-CqVJwgmp.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-sheet-testing.mjs +129 -0
- package/fesm2022/cdevhub-ngx-tw-sheet-testing.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-sheet.mjs +23 -7
- package/fesm2022/cdevhub-ngx-tw-sheet.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-slider.mjs +89 -54
- package/fesm2022/cdevhub-ngx-tw-slider.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-sort.mjs +122 -17
- package/fesm2022/cdevhub-ngx-tw-sort.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-split.mjs +72 -19
- package/fesm2022/cdevhub-ngx-tw-split.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-stat.mjs +63 -25
- package/fesm2022/cdevhub-ngx-tw-stat.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-stepper.mjs +35 -8
- package/fesm2022/cdevhub-ngx-tw-stepper.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-switch.mjs +54 -48
- package/fesm2022/cdevhub-ngx-tw-switch.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-tab-nav.mjs +12 -15
- package/fesm2022/cdevhub-ngx-tw-tab-nav.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-table.mjs +74 -13
- package/fesm2022/cdevhub-ngx-tw-table.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-tabs.mjs +84 -30
- package/fesm2022/cdevhub-ngx-tw-tabs.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-tags-input-testing.mjs +172 -0
- package/fesm2022/cdevhub-ngx-tw-tags-input-testing.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-tags-input.mjs +107 -71
- package/fesm2022/cdevhub-ngx-tw-tags-input.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-textarea.mjs +41 -9
- package/fesm2022/cdevhub-ngx-tw-textarea.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-theme.mjs +257 -45
- package/fesm2022/cdevhub-ngx-tw-theme.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-time-picker-testing.mjs +201 -0
- package/fesm2022/cdevhub-ngx-tw-time-picker-testing.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-time-picker.mjs +184 -77
- package/fesm2022/cdevhub-ngx-tw-time-picker.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-timeline.mjs +110 -27
- package/fesm2022/cdevhub-ngx-tw-timeline.mjs.map +1 -1
- package/fesm2022/{cdevhub-ngx-tw-toast-toast-renderer-DSu4YoTy.mjs → cdevhub-ngx-tw-toast-toast-renderer-BlDxemsU.mjs} +107 -19
- package/fesm2022/cdevhub-ngx-tw-toast-toast-renderer-BlDxemsU.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-toast.mjs +21 -13
- package/fesm2022/cdevhub-ngx-tw-toast.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-tooltip.mjs +102 -15
- package/fesm2022/cdevhub-ngx-tw-tooltip.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-transfer-testing.mjs +219 -0
- package/fesm2022/cdevhub-ngx-tw-transfer-testing.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-transfer.mjs +88 -69
- package/fesm2022/cdevhub-ngx-tw-transfer.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-tree.mjs +68 -19
- package/fesm2022/cdevhub-ngx-tw-tree.mjs.map +1 -1
- package/index.json +1 -1
- package/package.json +63 -2
- package/theme/_base.css +13 -0
- package/theme/_dark.css +111 -36
- package/theme/_high-contrast-dark.css +367 -0
- package/theme/_high-contrast.css +13 -2
- package/theme/_light.css +332 -0
- package/theme/_semantic.css +60 -26
- package/theme/_typography.css +17 -0
- package/theme/index.css +86 -7
- package/types/cdevhub-ngx-tw-accordion.d.ts +12 -4
- package/types/cdevhub-ngx-tw-avatar.d.ts +10 -3
- package/types/cdevhub-ngx-tw-button.d.ts +1 -0
- package/types/cdevhub-ngx-tw-calendar-luxon.d.ts +2 -2
- package/types/cdevhub-ngx-tw-calendar-testing.d.ts +1 -1
- package/types/cdevhub-ngx-tw-calendar.d.ts +179 -73
- package/types/cdevhub-ngx-tw-card.d.ts +11 -3
- package/types/cdevhub-ngx-tw-carousel.d.ts +89 -20
- package/types/cdevhub-ngx-tw-checkbox.d.ts +40 -12
- package/types/cdevhub-ngx-tw-code-block.d.ts +11 -2
- package/types/cdevhub-ngx-tw-collapsible.d.ts +32 -5
- package/types/cdevhub-ngx-tw-combobox-testing.d.ts +157 -0
- package/types/cdevhub-ngx-tw-combobox.d.ts +94 -53
- package/types/cdevhub-ngx-tw-command-palette-testing.d.ts +136 -0
- package/types/cdevhub-ngx-tw-command-palette.d.ts +33 -5
- package/types/cdevhub-ngx-tw-core.d.ts +152 -21
- package/types/cdevhub-ngx-tw-date-picker-testing.d.ts +141 -0
- package/types/cdevhub-ngx-tw-date-picker.d.ts +37 -29
- package/types/cdevhub-ngx-tw-date-range-picker-testing.d.ts +148 -0
- package/types/cdevhub-ngx-tw-date-range-picker.d.ts +36 -29
- package/types/cdevhub-ngx-tw-dialog-testing.d.ts +98 -0
- package/types/cdevhub-ngx-tw-dialog.d.ts +12 -3
- package/types/cdevhub-ngx-tw-file-upload-testing.d.ts +120 -0
- package/types/cdevhub-ngx-tw-file-upload.d.ts +8 -10
- package/types/cdevhub-ngx-tw-flip-card.d.ts +18 -11
- package/types/cdevhub-ngx-tw-form-field.d.ts +19 -2
- package/types/cdevhub-ngx-tw-icon.d.ts +3 -3
- package/types/cdevhub-ngx-tw-input.d.ts +5 -8
- package/types/cdevhub-ngx-tw-item.d.ts +2 -0
- package/types/cdevhub-ngx-tw-menu-testing.d.ts +131 -0
- package/types/cdevhub-ngx-tw-menu.d.ts +2 -2
- package/types/cdevhub-ngx-tw-number-input.d.ts +20 -0
- package/types/cdevhub-ngx-tw-paginator.d.ts +58 -24
- package/types/cdevhub-ngx-tw-popover.d.ts +41 -10
- package/types/cdevhub-ngx-tw-progress-bar.d.ts +1 -1
- package/types/cdevhub-ngx-tw-radio.d.ts +68 -25
- package/types/cdevhub-ngx-tw-segmented-control.d.ts +79 -16
- package/types/cdevhub-ngx-tw-select-testing.d.ts +114 -0
- package/types/cdevhub-ngx-tw-select.d.ts +104 -33
- package/types/cdevhub-ngx-tw-sheet-testing.d.ts +104 -0
- package/types/cdevhub-ngx-tw-sheet.d.ts +19 -3
- package/types/cdevhub-ngx-tw-slider.d.ts +79 -25
- package/types/cdevhub-ngx-tw-sort.d.ts +45 -4
- package/types/cdevhub-ngx-tw-split.d.ts +32 -2
- package/types/cdevhub-ngx-tw-stat.d.ts +14 -4
- package/types/cdevhub-ngx-tw-stepper.d.ts +16 -2
- package/types/cdevhub-ngx-tw-switch.d.ts +35 -13
- package/types/cdevhub-ngx-tw-tab-nav.d.ts +7 -6
- package/types/cdevhub-ngx-tw-table.d.ts +53 -10
- package/types/cdevhub-ngx-tw-tabs.d.ts +41 -10
- package/types/cdevhub-ngx-tw-tags-input-testing.d.ts +135 -0
- package/types/cdevhub-ngx-tw-tags-input.d.ts +26 -11
- package/types/cdevhub-ngx-tw-textarea.d.ts +3 -2
- package/types/cdevhub-ngx-tw-theme.d.ts +245 -39
- package/types/cdevhub-ngx-tw-time-picker-testing.d.ts +129 -0
- package/types/cdevhub-ngx-tw-time-picker.d.ts +76 -19
- package/types/cdevhub-ngx-tw-timeline.d.ts +36 -12
- package/types/cdevhub-ngx-tw-toast.d.ts +20 -6
- package/types/cdevhub-ngx-tw-tooltip.d.ts +28 -6
- package/types/cdevhub-ngx-tw-transfer-testing.d.ts +152 -0
- package/types/cdevhub-ngx-tw-transfer.d.ts +21 -8
- package/types/cdevhub-ngx-tw-tree.d.ts +6 -1
- package/fesm2022/cdevhub-ngx-tw-sheet-sheet-renderer-ByyNluUd.mjs.map +0 -1
- package/fesm2022/cdevhub-ngx-tw-toast-toast-renderer-DSu4YoTy.mjs.map +0 -1
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
2
|
import { inject, ElementRef, signal, computed, ChangeDetectionStrategy, Component, TemplateRef, Directive, input, model, output, contentChild, viewChild, ViewContainerRef, Injector, DestroyRef, linkedSignal, effect, untracked, forwardRef } from '@angular/core';
|
|
3
|
-
import { NgControl
|
|
4
|
-
import {
|
|
5
|
-
import { merge } from 'rxjs';
|
|
6
|
-
import { TW_ERROR_STATE_MATCHER, buildSelectLikePositions, resolveSelectScrollStrategy, consumeOverlayEscape } from '@cdevhub/ngx-tw/core';
|
|
3
|
+
import { NgControl } from '@angular/forms';
|
|
4
|
+
import { PickerOverlayCoordinator, wireErrorState, resolveSelectScrollStrategy, buildSelectLikePositions } from '@cdevhub/ngx-tw/core';
|
|
7
5
|
import { Overlay } from '@angular/cdk/overlay';
|
|
8
|
-
import { ComponentPortal } from '@angular/cdk/portal';
|
|
9
6
|
import { Platform } from '@angular/cdk/platform';
|
|
10
7
|
import { FocusMonitor, LiveAnnouncer } from '@angular/cdk/a11y';
|
|
11
8
|
import { tv } from 'tailwind-variants';
|
|
@@ -340,8 +337,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
|
|
|
340
337
|
}] });
|
|
341
338
|
|
|
342
339
|
// ── Constants ──
|
|
343
|
-
/** Duration of leave animation (ms) — matches scale-out/fade-out in theme/_base.css. */
|
|
344
|
-
const ANIMATION_DURATION = 120;
|
|
345
340
|
/** Debounce window (ms) for LiveAnnouncer result announcements while typing. */
|
|
346
341
|
const ANNOUNCE_DEBOUNCE = 200;
|
|
347
342
|
/** Sentinel for an unresolved write-value awaiting late-arriving options. */
|
|
@@ -383,20 +378,29 @@ const comboboxVariants = tv({
|
|
|
383
378
|
// variants below paint this ring per the `color`/`errorState` axis.
|
|
384
379
|
trigger: 'flex w-full items-center gap-1.5 rounded-md border border-border bg-surface text-fg transition-colors duration-normal motion-reduce:transition-none hover:border-border-strong focus-within:outline-2 focus-within:outline-offset-2',
|
|
385
380
|
input: 'flex-1 min-w-0 bg-transparent outline-none placeholder:text-fg-subtle disabled:cursor-not-allowed',
|
|
386
|
-
|
|
381
|
+
// `size-6` (24px) is the WCAG 2.2 SC 2.5.8 target-size floor and the `xs`
|
|
382
|
+
// step of the square-interactive scale. It does NOT scale with `size`:
|
|
383
|
+
// the clear sits inside the trigger, whose smallest pinned height is
|
|
384
|
+
// 24px (`xs` -> `h-6`), so any larger step would overflow the smallest
|
|
385
|
+
// trigger. The floor is what the success criterion asks for — which is
|
|
386
|
+
// also why `xs` no longer shrinks it to `size-4`.
|
|
387
|
+
clearButton: 'inline-flex items-center justify-center shrink-0 size-6 rounded-md text-fg-muted hover:text-fg hover:bg-surface-muted transition-colors duration-normal motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500',
|
|
387
388
|
chevron: 'shrink-0 text-fg-muted transition-transform duration-normal motion-reduce:transition-none',
|
|
388
389
|
spinner: 'shrink-0 text-fg-muted animate-spin',
|
|
389
390
|
},
|
|
390
391
|
variants: {
|
|
392
|
+
// Trigger height is PINNED (docs/vertical-rhythm.md §1-3): the box is
|
|
393
|
+
// always one line, so it declares its border-box height and carries no
|
|
394
|
+
// vertical padding. Horizontal padding and the font size are unchanged.
|
|
391
395
|
size: {
|
|
392
396
|
// xs density: chevron and spinner use `size-3.5` (14px) — the codified
|
|
393
397
|
// half-step between size-3 and size-4 that lines up with text-xs metric
|
|
394
|
-
// inside a
|
|
395
|
-
xs: { trigger: 'px-2
|
|
396
|
-
sm: { trigger: 'px-3
|
|
397
|
-
md: { trigger: 'px-4
|
|
398
|
-
lg: { trigger: 'px-5
|
|
399
|
-
xl: { trigger: 'px-6
|
|
398
|
+
// inside a 24px trigger.
|
|
399
|
+
xs: { trigger: 'px-2 text-xs h-6', chevron: 'size-3.5', spinner: 'size-3.5' },
|
|
400
|
+
sm: { trigger: 'px-3 text-sm h-8', chevron: 'size-4', spinner: 'size-4' },
|
|
401
|
+
md: { trigger: 'px-4 text-sm h-9', chevron: 'size-4', spinner: 'size-4' },
|
|
402
|
+
lg: { trigger: 'px-5 text-base h-11', chevron: 'size-5', spinner: 'size-5' },
|
|
403
|
+
xl: { trigger: 'px-6 text-base h-12', chevron: 'size-5', spinner: 'size-5' },
|
|
400
404
|
},
|
|
401
405
|
color: {
|
|
402
406
|
primary: { trigger: 'focus-within:outline-primary-500' },
|
|
@@ -416,9 +420,14 @@ const comboboxVariants = tv({
|
|
|
416
420
|
true: { chevron: 'rotate-180' },
|
|
417
421
|
false: {},
|
|
418
422
|
},
|
|
423
|
+
// `naked` = wrapped in a `tw-form-field`, which owns the box: its
|
|
424
|
+
// controlWrapper draws the border and the vertical padding. `h-auto`
|
|
425
|
+
// releases the pinned height (declared after `size`, so twMerge keeps it
|
|
426
|
+
// — the same ordering `px-0` already relies on) so the trigger
|
|
427
|
+
// contributes only its line box and the field's own rhythm governs.
|
|
419
428
|
naked: {
|
|
420
429
|
true: {
|
|
421
|
-
trigger: 'border-0 bg-transparent rounded-none px-0 py-0 hover:border-0 focus-within:outline-0 focus-within:outline-offset-0',
|
|
430
|
+
trigger: 'border-0 bg-transparent rounded-none px-0 py-0 h-auto hover:border-0 focus-within:outline-0 focus-within:outline-offset-0',
|
|
422
431
|
},
|
|
423
432
|
false: {},
|
|
424
433
|
},
|
|
@@ -513,115 +522,109 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
|
|
|
513
522
|
*/
|
|
514
523
|
class ComboboxComponent {
|
|
515
524
|
// ── Inputs ──
|
|
516
|
-
/** Array of options to render in the popover. Accepts plain records or `TwComboboxOption<T>`. */
|
|
525
|
+
/** Array of options to render in the popover. Accepts plain records or `TwComboboxOption<T>`. Defaults to an empty array. */
|
|
517
526
|
options = input([], /* @ts-ignore */
|
|
518
527
|
...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
|
|
519
|
-
/** Accessor returning the visible label for an option. Used by the default filter and the trigger label resolver. */
|
|
528
|
+
/** Accessor returning the visible label for an option. Used by the default filter and the trigger label resolver. Defaults to reading `.label`, falling back to `String(.value)`. */
|
|
520
529
|
optionLabel = input(defaultOptionLabel, /* @ts-ignore */
|
|
521
530
|
...(ngDevMode ? [{ debugName: "optionLabel" }] : /* istanbul ignore next */ []));
|
|
522
|
-
/** Accessor returning the value emitted via `valueCommit` when this option is picked. */
|
|
531
|
+
/** Accessor returning the value emitted via `valueCommit` when this option is picked. Defaults to reading `.value`. */
|
|
523
532
|
optionValue = input(defaultOptionValue, /* @ts-ignore */
|
|
524
533
|
...(ngDevMode ? [{ debugName: "optionValue" }] : /* istanbul ignore next */ []));
|
|
525
|
-
/** Accessor returning whether an option is non-interactive. */
|
|
534
|
+
/** Accessor returning whether an option is non-interactive. Defaults to reading `.disabled`. */
|
|
526
535
|
optionDisabled = input(defaultOptionDisabled, /* @ts-ignore */
|
|
527
536
|
...(ngDevMode ? [{ debugName: "optionDisabled" }] : /* istanbul ignore next */ []));
|
|
528
|
-
/** Accessor returning a group label. Options sharing a group render under a labelled `role="group"` region. */
|
|
537
|
+
/** Accessor returning a group label. Options sharing a group render under a labelled `role="group"` region. Defaults to reading `.group`. */
|
|
529
538
|
optionGroup = input(defaultOptionGroup, /* @ts-ignore */
|
|
530
539
|
...(ngDevMode ? [{ debugName: "optionGroup" }] : /* istanbul ignore next */ []));
|
|
531
|
-
/** Accessor returning an optional secondary description rendered under the label in the default option row. */
|
|
540
|
+
/** Accessor returning an optional secondary description rendered under the label in the default option row. Defaults to reading `.description`. */
|
|
532
541
|
optionDescription = input(defaultOptionDescription, /* @ts-ignore */
|
|
533
542
|
...(ngDevMode ? [{ debugName: "optionDescription" }] : /* istanbul ignore next */ []));
|
|
534
543
|
/** Filter function applied client-side whenever `inputValue` changes. Pass `null` to disable client filtering (async mode). Defaults to case-insensitive `startsWith` on the label. */
|
|
535
544
|
filterFn = input(defaultStartsWithFilter, /* @ts-ignore */
|
|
536
545
|
...(ngDevMode ? [{ debugName: "filterFn" }] : /* istanbul ignore next */ []));
|
|
537
|
-
/** When `true`, free-text commits are rejected — the input reverts to the last committed label on blur. */
|
|
546
|
+
/** When `true`, free-text commits are rejected — the input reverts to the last committed label on blur. Defaults to `false`. */
|
|
538
547
|
strict = input(false, /* @ts-ignore */
|
|
539
548
|
...(ngDevMode ? [{ debugName: "strict" }] : /* istanbul ignore next */ []));
|
|
540
|
-
/** Placeholder shown when the input is empty. */
|
|
549
|
+
/** Placeholder shown when the input is empty. Defaults to `undefined`. */
|
|
541
550
|
placeholder = input(undefined, /* @ts-ignore */
|
|
542
551
|
...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
|
|
543
|
-
/** Disables the input and prevents the popover from opening. */
|
|
552
|
+
/** Disables the input and prevents the popover from opening. Defaults to `false`. Alias: `disabled`. */
|
|
544
553
|
disabledInput = input(false, { ...(ngDevMode ? { debugName: "disabledInput" } : /* istanbul ignore next */ {}), alias: 'disabled' });
|
|
545
|
-
/** Sets `aria-required="true"` on the input. */
|
|
554
|
+
/** Sets `aria-required="true"` on the input and the `*` marker on a wrapping `tw-form-field`. Also inferred from `Validators.required` on a bound control, so a reactive/template-driven form does not have to state it twice. Defaults to `false`. */
|
|
546
555
|
requiredInput = input(false, { ...(ngDevMode ? { debugName: "requiredInput" } : /* istanbul ignore next */ {}), alias: 'required' });
|
|
547
|
-
/** Controls trigger padding and font size per the inline padding scale. */
|
|
556
|
+
/** Controls trigger padding and font size per the inline padding scale. Defaults to `'md'`. */
|
|
548
557
|
size = input('md', /* @ts-ignore */
|
|
549
558
|
...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
|
|
550
|
-
/** Semantic color for the focus ring. */
|
|
559
|
+
/** Semantic color for the focus ring. Defaults to `'primary'`. */
|
|
551
560
|
color = input('primary', /* @ts-ignore */
|
|
552
561
|
...(ngDevMode ? [{ debugName: "color" }] : /* istanbul ignore next */ []));
|
|
553
|
-
/** Whether the trailing chevron affordance is rendered. */
|
|
554
|
-
// TRUE-default: the chevron signals the dropdown affordance; without it the
|
|
555
|
-
// combobox reads as a plain input. Opt-out is for inline search inputs.
|
|
562
|
+
/** Whether the trailing chevron affordance is rendered. Defaults to `true` — the chevron signals the dropdown affordance; the special case is an inline search input that opts out. */
|
|
556
563
|
showChevron = input(true, /* @ts-ignore */
|
|
557
564
|
...(ngDevMode ? [{ debugName: "showChevron" }] : /* istanbul ignore next */ []));
|
|
558
|
-
/** Whether the inline clear (×) button appears while `inputValue` is non-empty. */
|
|
559
|
-
// TRUE-default: the clear affordance is the expected combobox UX once a value
|
|
560
|
-
// is set; opt-out is for required-only flows.
|
|
565
|
+
/** Whether the inline clear (×) button appears while `inputValue` is non-empty. Defaults to `true` — clearing a typed value is the expected combobox gesture; the special case is a required-only flow. */
|
|
561
566
|
clearable = input(true, /* @ts-ignore */
|
|
562
567
|
...(ngDevMode ? [{ debugName: "clearable" }] : /* istanbul ignore next */ []));
|
|
563
|
-
/** When `true`, shows an in-popover spinner and an inline spinner in the trigger while the popover is open. */
|
|
568
|
+
/** When `true`, shows an in-popover spinner and an inline spinner in the trigger while the popover is open. Defaults to `false`. */
|
|
564
569
|
loading = input(false, /* @ts-ignore */
|
|
565
570
|
...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
|
|
566
|
-
/** Debounce window (ms) before `queryChange` emits. Local filtering is not debounced. */
|
|
571
|
+
/** Debounce window (ms) before `queryChange` emits. Local filtering is not debounced. Defaults to `150`. */
|
|
567
572
|
queryDebounce = input(150, /* @ts-ignore */
|
|
568
573
|
...(ngDevMode ? [{ debugName: "queryDebounce" }] : /* istanbul ignore next */ []));
|
|
569
|
-
/** Minimum query length before the popover opens automatically. `0` opens on focus. */
|
|
574
|
+
/** Minimum query length before the popover opens automatically. `0` opens on focus. Defaults to `0`. */
|
|
570
575
|
minQueryLength = input(0, /* @ts-ignore */
|
|
571
576
|
...(ngDevMode ? [{ debugName: "minQueryLength" }] : /* istanbul ignore next */ []));
|
|
572
|
-
/** Whether the popover opens automatically when the input receives focus. */
|
|
573
|
-
// TRUE-default: clicking into a combobox opens the dropdown — standard select-
|
|
574
|
-
// like UX. Opt-out is for command-palette-style flows triggered only by typing.
|
|
577
|
+
/** Whether the popover opens automatically when the input receives focus. Defaults to `true` — clicking into a combobox opens the dropdown, matching select-like UX; the special case is a flow that should open only once the user types. */
|
|
575
578
|
openOnFocus = input(true, /* @ts-ignore */
|
|
576
579
|
...(ngDevMode ? [{ debugName: "openOnFocus" }] : /* istanbul ignore next */ []));
|
|
577
|
-
/** Maximum height (px) of the popover scroll region. */
|
|
580
|
+
/** Maximum height (px) of the popover scroll region. Defaults to `256`. */
|
|
578
581
|
panelMaxHeight = input(256, /* @ts-ignore */
|
|
579
582
|
...(ngDevMode ? [{ debugName: "panelMaxHeight" }] : /* istanbul ignore next */ []));
|
|
580
|
-
/** Overlay width strategy. `'trigger'` matches input width; `'auto'` lets content decide; a number is applied as px; a string is passed as a CSS length. */
|
|
583
|
+
/** Overlay width strategy. `'trigger'` matches input width; `'auto'` lets content decide; a number is applied as px; a string is passed as a CSS length. Defaults to `'trigger'`. */
|
|
581
584
|
panelWidth = input('trigger', /* @ts-ignore */
|
|
582
585
|
...(ngDevMode ? [{ debugName: "panelWidth" }] : /* istanbul ignore next */ []));
|
|
583
|
-
/** Extra class(es) appended to the overlay panel for consumer customization. */
|
|
586
|
+
/** Extra class(es) appended to the overlay panel for consumer customization. Defaults to an empty string. */
|
|
584
587
|
panelClass = input('', /* @ts-ignore */
|
|
585
588
|
...(ngDevMode ? [{ debugName: "panelClass" }] : /* istanbul ignore next */ []));
|
|
586
|
-
/** CDK overlay scroll strategy. */
|
|
589
|
+
/** CDK overlay scroll strategy. Defaults to `'reposition'`. */
|
|
587
590
|
scrollStrategy = input('reposition', /* @ts-ignore */
|
|
588
591
|
...(ngDevMode ? [{ debugName: "scrollStrategy" }] : /* istanbul ignore next */ []));
|
|
589
|
-
/** Pixel offset between the input and the popover. */
|
|
592
|
+
/** Pixel offset between the input and the popover. Defaults to `4`. */
|
|
590
593
|
offset = input(4, /* @ts-ignore */
|
|
591
594
|
...(ngDevMode ? [{ debugName: "offset" }] : /* istanbul ignore next */ []));
|
|
592
|
-
/** Fallback empty-state message when no `*twComboboxEmpty` template is projected. */
|
|
595
|
+
/** Fallback empty-state message when no `*twComboboxEmpty` template is projected. Defaults to `'No results'`. */
|
|
593
596
|
emptyMessage = input('No results', /* @ts-ignore */
|
|
594
597
|
...(ngDevMode ? [{ debugName: "emptyMessage" }] : /* istanbul ignore next */ []));
|
|
595
|
-
/** Equality comparator used to reconcile `value` with options during `writeValue`. */
|
|
598
|
+
/** Equality comparator used to reconcile `value` with options during `writeValue`. Defaults to `Object.is`. */
|
|
596
599
|
compareWith = input(Object.is, /* @ts-ignore */
|
|
597
600
|
...(ngDevMode ? [{ debugName: "compareWith" }] : /* istanbul ignore next */ []));
|
|
598
|
-
/** Accessible name for the combobox input. */
|
|
601
|
+
/** Accessible name for the combobox input. Defaults to `undefined`. Alias: `aria-label`. */
|
|
599
602
|
ariaLabel = input(undefined, { ...(ngDevMode ? { debugName: "ariaLabel" } : /* istanbul ignore next */ {}), alias: 'aria-label' });
|
|
600
|
-
/** ID of an external label element. */
|
|
603
|
+
/** ID of an external label element. Defaults to `undefined`. Alias: `aria-labelledby`. */
|
|
601
604
|
ariaLabelledby = input(undefined, { ...(ngDevMode ? { debugName: "ariaLabelledby" } : /* istanbul ignore next */ {}), alias: 'aria-labelledby' });
|
|
602
|
-
/** ID of an external descriptor element. */
|
|
605
|
+
/** ID of an external descriptor element. Defaults to `undefined`. Alias: `aria-describedby`. */
|
|
603
606
|
ariaDescribedby = input(undefined, { ...(ngDevMode ? { debugName: "ariaDescribedby" } : /* istanbul ignore next */ {}), alias: 'aria-describedby' });
|
|
604
607
|
/** Per-instance override of the {@link ErrorStateMatcher}. When omitted, the combobox uses the `TW_ERROR_STATE_MATCHER` token's value. */
|
|
605
608
|
errorStateMatcher = input(undefined, /* @ts-ignore */
|
|
606
609
|
...(ngDevMode ? [{ debugName: "errorStateMatcher" }] : /* istanbul ignore next */ []));
|
|
607
610
|
// ── Models ──
|
|
608
|
-
/** Two-way bound committed value. May be an option's value (`T`), a typed string (free-text mode), or `null`. */
|
|
611
|
+
/** Two-way bound committed value. May be an option's value (`T`), a typed string (free-text mode), or `null`. Defaults to `null`. */
|
|
609
612
|
value = model(null, /* @ts-ignore */
|
|
610
613
|
...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
|
|
611
|
-
/** Two-way bound visible text in the input. Bound separately from `value` so async consumers can drive the query. */
|
|
614
|
+
/** Two-way bound visible text in the input. Bound separately from `value` so async consumers can drive the query. Writes back on every keystroke, on commit, and on clear. Defaults to `''`. */
|
|
612
615
|
inputValue = model('', /* @ts-ignore */
|
|
613
616
|
...(ngDevMode ? [{ debugName: "inputValue" }] : /* istanbul ignore next */ []));
|
|
614
|
-
/** Two-way bound open state of the popover. */
|
|
617
|
+
/** Two-way bound open state of the popover. Defaults to `false`. */
|
|
615
618
|
open = model(false, /* @ts-ignore */
|
|
616
619
|
...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
|
|
617
620
|
// ── Outputs ──
|
|
618
|
-
/** Fires after the query text changes, debounced by `queryDebounce`. Async-mode consumers subscribe to this to fetch results. */
|
|
621
|
+
/** Fires after the query text changes, debounced by `queryDebounce`. Async-mode consumers subscribe to this to fetch results. Payload is the current input text, untrimmed. */
|
|
619
622
|
queryChange = output();
|
|
620
|
-
/** Fires when the user picks an option from the list (not on free-text commit). */
|
|
623
|
+
/** Fires when the user picks an option from the list (not on free-text commit). Payload carries the raw option record plus its resolved `value` and `label`. */
|
|
621
624
|
optionSelected = output();
|
|
622
625
|
/** Fires whenever `value` changes, with a `source` discriminator distinguishing option / free-text / reset / programmatic origin. */
|
|
623
626
|
valueCommit = output();
|
|
624
|
-
/** Fires when the popover
|
|
627
|
+
/** Fires when the popover finishes opening or closing. Payload carries the new `open` state and the trigger element the overlay is anchored to. */
|
|
625
628
|
openedChange = output();
|
|
626
629
|
// ── Content queries ──
|
|
627
630
|
/** @internal */
|
|
@@ -640,6 +643,7 @@ class ComboboxComponent {
|
|
|
640
643
|
...(ngDevMode ? [{ debugName: "triggerSurfaceRef" }] : /* istanbul ignore next */ []));
|
|
641
644
|
// ── Injected deps ──
|
|
642
645
|
overlayService = inject(Overlay);
|
|
646
|
+
coordinator = inject(PickerOverlayCoordinator);
|
|
643
647
|
elementRef = inject((ElementRef));
|
|
644
648
|
viewContainerRef = inject(ViewContainerRef);
|
|
645
649
|
injector = inject(Injector);
|
|
@@ -649,9 +653,6 @@ class ComboboxComponent {
|
|
|
649
653
|
platform = inject(Platform);
|
|
650
654
|
formField = inject(TW_FORM_FIELD, { optional: true });
|
|
651
655
|
ngControl = inject(NgControl, { optional: true, self: true });
|
|
652
|
-
parentForm = inject(NgForm, { optional: true });
|
|
653
|
-
parentFormGroup = inject(FormGroupDirective, { optional: true });
|
|
654
|
-
defaultMatcher = inject(TW_ERROR_STATE_MATCHER);
|
|
655
656
|
// ── Identity ──
|
|
656
657
|
uid = nextComboboxId++;
|
|
657
658
|
/** @internal */
|
|
@@ -671,10 +672,12 @@ class ComboboxComponent {
|
|
|
671
672
|
...(ngDevMode ? [{ debugName: "focusedSignal" }] : /* istanbul ignore next */ []));
|
|
672
673
|
cvaDisabled = signal(false, /* @ts-ignore */
|
|
673
674
|
...(ngDevMode ? [{ debugName: "cvaDisabled" }] : /* istanbul ignore next */ []));
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
675
|
+
/** @internal Shared `errorState` / `required` / `errors` derivation — see `wireErrorState`. */
|
|
676
|
+
errorWiring = wireErrorState({
|
|
677
|
+
ngControl: () => this.ngControl,
|
|
678
|
+
matcher: () => this.errorStateMatcher(),
|
|
679
|
+
required: () => this.requiredInput(),
|
|
680
|
+
});
|
|
678
681
|
describedByIdsSignal = signal([], /* @ts-ignore */
|
|
679
682
|
...(ngDevMode ? [{ debugName: "describedByIdsSignal" }] : /* istanbul ignore next */ []));
|
|
680
683
|
labelledByIdsSignal = signal([], /* @ts-ignore */
|
|
@@ -684,22 +687,34 @@ class ComboboxComponent {
|
|
|
684
687
|
...(ngDevMode ? [{ debugName: "lastCommittedLabel" }] : /* istanbul ignore next */ []));
|
|
685
688
|
pendingWriteValue = signal(UNRESOLVED, /* @ts-ignore */
|
|
686
689
|
...(ngDevMode ? [{ debugName: "pendingWriteValue" }] : /* istanbul ignore next */ []));
|
|
687
|
-
closingSignal = signal(false, /* @ts-ignore */
|
|
688
|
-
...(ngDevMode ? [{ debugName: "closingSignal" }] : /* istanbul ignore next */ []));
|
|
689
690
|
/** Re-runs the input handler once after IME composition ends. */
|
|
690
691
|
composingPendingValue = null;
|
|
691
692
|
onChange = () => { };
|
|
692
693
|
onTouched = () => { };
|
|
693
|
-
overlayRef = null;
|
|
694
|
-
overlayInstanceSignal = signal(null, /* @ts-ignore */
|
|
695
|
-
...(ngDevMode ? [{ debugName: "overlayInstanceSignal" }] : /* istanbul ignore next */ []));
|
|
696
|
-
closeTimer = null;
|
|
697
694
|
queryDebounceTimer = null;
|
|
698
695
|
announceTimer = null;
|
|
696
|
+
/**
|
|
697
|
+
* Watches the host for size changes so `panelWidth="trigger"` keeps tracking
|
|
698
|
+
* while the panel is up. Minted per open and disconnected on close, because
|
|
699
|
+
* the `OverlayRef` it feeds is disposed on close (see `openOverlay`).
|
|
700
|
+
*/
|
|
699
701
|
resizeObserver = null;
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
702
|
+
// Overlay bookkeeping is deliberately held in plain fields, not signals — same
|
|
703
|
+
// shape as popover and command-palette. A signal here would be track-read by the
|
|
704
|
+
// open/close lifecycle effect that also writes it, which is the cycle shape
|
|
705
|
+
// CLAUDE.md forbids. The only reactive bit is `isAttached`, written by the
|
|
706
|
+
// lifecycle effect and read only by the state-push effect below, so the two
|
|
707
|
+
// never form a loop.
|
|
708
|
+
overlayInstance = null;
|
|
709
|
+
closing = false;
|
|
710
|
+
/**
|
|
711
|
+
* Flips true once the overlay component is attached; the sole trigger for the
|
|
712
|
+
* state-push effect. Load-bearing: the close path leaves `activeIndex` and the
|
|
713
|
+
* rest of the pushed state untouched, so on a reopen no data signal changes and
|
|
714
|
+
* nothing else would wake that effect — the fresh panel would render empty.
|
|
715
|
+
*/
|
|
716
|
+
isAttached = signal(false, /* @ts-ignore */
|
|
717
|
+
...(ngDevMode ? [{ debugName: "isAttached" }] : /* istanbul ignore next */ []));
|
|
703
718
|
// ── Derived state ──
|
|
704
719
|
/** @internal */
|
|
705
720
|
isDisabled = computed(() => this.disabledInput() || this.cvaDisabled(), /* @ts-ignore */
|
|
@@ -817,15 +832,7 @@ class ComboboxComponent {
|
|
|
817
832
|
}, /* @ts-ignore */
|
|
818
833
|
...(ngDevMode ? [{ debugName: "resolvedLabelledBy" }] : /* istanbul ignore next */ []));
|
|
819
834
|
/** @internal Error-state per the configured `ErrorStateMatcher`. Reads the bound `NgControl.invalid` through the matcher. */
|
|
820
|
-
errorState =
|
|
821
|
-
this._ngControlRev();
|
|
822
|
-
this._formSubmitRev();
|
|
823
|
-
const matcher = this.errorStateMatcher() ?? this.defaultMatcher;
|
|
824
|
-
const form = this.parentFormGroup ??
|
|
825
|
-
this.parentForm;
|
|
826
|
-
return matcher.isErrorState(this.ngControl?.control ?? null, form);
|
|
827
|
-
}, /* @ts-ignore */
|
|
828
|
-
...(ngDevMode ? [{ debugName: "errorState" }] : /* istanbul ignore next */ []));
|
|
835
|
+
errorState = this.errorWiring.errorState;
|
|
829
836
|
// ── tv() output ──
|
|
830
837
|
variantResult = computed(() => comboboxVariants({
|
|
831
838
|
size: this.size(),
|
|
@@ -864,9 +871,18 @@ class ComboboxComponent {
|
|
|
864
871
|
empty = this.isEmpty;
|
|
865
872
|
/** @internal */
|
|
866
873
|
disabled = this.isDisabled;
|
|
867
|
-
/**
|
|
868
|
-
|
|
869
|
-
|
|
874
|
+
/**
|
|
875
|
+
* @internal Resolved required state: the `required` input OR'd with
|
|
876
|
+
* `Validators.required` on a bound `NgControl`. Without the validator arm the
|
|
877
|
+
* form-field `*` marker (`FormFieldComponent.isRequired`) and the input's
|
|
878
|
+
* `aria-required` silently vanish under reactive/template-driven forms, while
|
|
879
|
+
* signal forms shows them — `cvaControlCreate` writes the `required` *input*
|
|
880
|
+
* directly from the field state and never consults validators. The OR keeps
|
|
881
|
+
* both branches true at once.
|
|
882
|
+
*/
|
|
883
|
+
required = this.errorWiring.required;
|
|
884
|
+
/** @internal Active validation errors map from the bound `NgControl`. Drives `[twError match="…"]` inside a wrapping `tw-form-field`; without it the form-field's key set is permanently empty and every `match`ed error stays hidden. */
|
|
885
|
+
errors = this.errorWiring.errors;
|
|
870
886
|
/** @internal */
|
|
871
887
|
controlType = 'combobox';
|
|
872
888
|
/** @internal */
|
|
@@ -885,30 +901,33 @@ class ComboboxComponent {
|
|
|
885
901
|
this.ngControl.valueAccessor = this;
|
|
886
902
|
}
|
|
887
903
|
// Mirror parent `open` model into overlay lifecycle.
|
|
904
|
+
// Only `open` and `isDisabled` are read in the tracked phase; the lifecycle
|
|
905
|
+
// calls run inside `untracked()` because they both read and write panel state
|
|
906
|
+
// (openOverlay writes activeIndex and reads visibleOptions).
|
|
888
907
|
effect(() => {
|
|
889
908
|
const shouldOpen = this.open();
|
|
890
909
|
const disabled = this.isDisabled();
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
this.
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
this.
|
|
900
|
-
|
|
910
|
+
untracked(() => {
|
|
911
|
+
if (disabled && this.overlayInstance) {
|
|
912
|
+
this.closeOverlay({ silent: false });
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
915
|
+
if (shouldOpen && !this.overlayInstance && !disabled && !this.closing) {
|
|
916
|
+
this.openOverlay();
|
|
917
|
+
}
|
|
918
|
+
else if (!shouldOpen && this.overlayInstance && !this.closing) {
|
|
919
|
+
this.closeOverlay({ silent: false });
|
|
920
|
+
}
|
|
921
|
+
});
|
|
901
922
|
});
|
|
902
923
|
// Push state into overlay component whenever anything relevant changes.
|
|
924
|
+
// `isAttached` is the trigger — it is written by the lifecycle effect above
|
|
925
|
+
// and never written here, so the two effects cannot feed each other.
|
|
903
926
|
// While the panel is animating out, freeze content updates so list mutations
|
|
904
927
|
// (e.g. the input filtering down after commitOption) don't visually flicker
|
|
905
928
|
// the panel during the leave animation.
|
|
906
929
|
effect(() => {
|
|
907
|
-
const
|
|
908
|
-
if (!instance)
|
|
909
|
-
return;
|
|
910
|
-
if (this.closingSignal())
|
|
911
|
-
return;
|
|
930
|
+
const attached = this.isAttached();
|
|
912
931
|
const renderedRows = this.renderedRows();
|
|
913
932
|
const activeIndex = this.activeIndex();
|
|
914
933
|
const panelMaxHeight = this.panelMaxHeight();
|
|
@@ -920,7 +939,14 @@ class ComboboxComponent {
|
|
|
920
939
|
const emptyTemplate = this.emptyTemplateChild()?.templateRef;
|
|
921
940
|
const loadingTemplate = this.loadingTemplateChild()?.templateRef;
|
|
922
941
|
const customPanelClass = this.resolvePanelClass();
|
|
942
|
+
// Tracked so the selection snapshot below refreshes when either changes.
|
|
943
|
+
// The snapshot replaced a live closure — see the `isSelected` push.
|
|
944
|
+
const visibleCount = this.visibleOptions().length;
|
|
945
|
+
void this.value();
|
|
923
946
|
untracked(() => {
|
|
947
|
+
const instance = this.overlayInstance;
|
|
948
|
+
if (!attached || !instance || this.closing)
|
|
949
|
+
return;
|
|
924
950
|
instance.renderedRows.set(renderedRows);
|
|
925
951
|
instance.activeIndex.set(activeIndex);
|
|
926
952
|
instance.panelMaxHeight.set(panelMaxHeight);
|
|
@@ -935,7 +961,19 @@ class ComboboxComponent {
|
|
|
935
961
|
instance.listboxId.set(this.listboxId);
|
|
936
962
|
instance.optionIdFn.set(this.optionId);
|
|
937
963
|
instance.groupHeaderIdFn.set(this.groupHeaderId);
|
|
938
|
-
|
|
964
|
+
// A SNAPSHOT, not a live closure. `renderedRows` is frozen while the
|
|
965
|
+
// panel animates out (the guard above), and each frozen row carries its
|
|
966
|
+
// index into the visible list at freeze time. A closure reading
|
|
967
|
+
// `visibleOptions()` at call time would resolve those stale indices
|
|
968
|
+
// against the current, already-refiltered list — so committing an option
|
|
969
|
+
// marked the WRONG row `aria-selected="true"` for the ~120ms leave
|
|
970
|
+
// window. Snapshotting here binds the flags to the same generation of
|
|
971
|
+
// the list as the rows they describe.
|
|
972
|
+
const selectedFlags = new Array(visibleCount);
|
|
973
|
+
for (let i = 0; i < visibleCount; i++) {
|
|
974
|
+
selectedFlags[i] = this.isVisibleOptionSelected(i);
|
|
975
|
+
}
|
|
976
|
+
instance.isSelected.set((index) => selectedFlags[index] ?? false);
|
|
939
977
|
});
|
|
940
978
|
});
|
|
941
979
|
// Late-arriving options reconciler for the async writeValue race.
|
|
@@ -996,20 +1034,21 @@ class ComboboxComponent {
|
|
|
996
1034
|
this.onTouched();
|
|
997
1035
|
// Blur often flips `touched` on the bound `NgControl`; bump the
|
|
998
1036
|
// revision so `errorState` recomputes.
|
|
999
|
-
this.
|
|
1037
|
+
this.errorWiring.bump();
|
|
1000
1038
|
}
|
|
1001
1039
|
});
|
|
1040
|
+
// The overlay itself is torn down by `PickerOverlayCoordinator`, which
|
|
1041
|
+
// registers its own `DestroyRef.onDestroy` during this component's field
|
|
1042
|
+
// initialisation — i.e. before this hook — and disposes the `OverlayRef`,
|
|
1043
|
+
// cancels the close timer and completes the per-open streams there.
|
|
1002
1044
|
this.destroyRef.onDestroy(() => {
|
|
1003
1045
|
monitorSub.unsubscribe();
|
|
1004
1046
|
this.focusMonitor.stopMonitoring(this.elementRef);
|
|
1005
|
-
this.clearCloseTimer();
|
|
1006
1047
|
this.clearQueryDebounceTimer();
|
|
1007
1048
|
this.clearAnnounceTimer();
|
|
1008
1049
|
this.resizeObserver?.disconnect();
|
|
1009
1050
|
this.resizeObserver = null;
|
|
1010
|
-
this.
|
|
1011
|
-
this.overlayRef = null;
|
|
1012
|
-
this.overlayInstanceSignal.set(null);
|
|
1051
|
+
this.overlayInstance = null;
|
|
1013
1052
|
});
|
|
1014
1053
|
}
|
|
1015
1054
|
// ── Public methods ──
|
|
@@ -1208,7 +1247,15 @@ class ComboboxComponent {
|
|
|
1208
1247
|
event.preventDefault();
|
|
1209
1248
|
event.stopPropagation();
|
|
1210
1249
|
}
|
|
1211
|
-
/**
|
|
1250
|
+
/**
|
|
1251
|
+
* @internal
|
|
1252
|
+
*
|
|
1253
|
+
* Reached by mouse and — since the button left `tabindex="-1"` — by Enter or
|
|
1254
|
+
* Space from the keyboard, both of which a native `<button>` delivers here
|
|
1255
|
+
* as a click. Clearing unmounts this button (`showClearButton()` goes
|
|
1256
|
+
* false), so focus is handed back to the input rather than falling to
|
|
1257
|
+
* `<body>` (SC 2.4.3).
|
|
1258
|
+
*/
|
|
1212
1259
|
onClearClick(event) {
|
|
1213
1260
|
event.preventDefault();
|
|
1214
1261
|
event.stopPropagation();
|
|
@@ -1376,13 +1423,60 @@ class ComboboxComponent {
|
|
|
1376
1423
|
}
|
|
1377
1424
|
}
|
|
1378
1425
|
// ── Overlay lifecycle ──
|
|
1426
|
+
/**
|
|
1427
|
+
* Builds and attaches the panel through {@link PickerOverlayCoordinator}.
|
|
1428
|
+
*
|
|
1429
|
+
* The coordinator disposes the `OverlayRef` on close, so every value read
|
|
1430
|
+
* here — `offset`, `scrollStrategy`, `panelClass`, and the position
|
|
1431
|
+
* strategy's origin — is re-read on each open rather than frozen at the
|
|
1432
|
+
* first one. That replaces the previous `refreshPositionConfig()` /
|
|
1433
|
+
* `applyScrollStrategy()` pair, which could only refresh the two inputs it
|
|
1434
|
+
* knew about on a reused ref.
|
|
1435
|
+
*
|
|
1436
|
+
* `focusTrap: false` is load-bearing: this is an `aria-activedescendant`
|
|
1437
|
+
* combobox, so DOM focus never leaves the `<input>` and a trap's anchor
|
|
1438
|
+
* elements would inject tab stops around a panel with nothing focusable in it.
|
|
1439
|
+
*/
|
|
1379
1440
|
openOverlay() {
|
|
1380
|
-
this.
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1441
|
+
const result = this.coordinator.open({
|
|
1442
|
+
origin: this.triggerSurfaceRef() ?? this.elementRef,
|
|
1443
|
+
portalComponent: ComboboxOverlayComponent,
|
|
1444
|
+
viewContainerRef: this.viewContainerRef,
|
|
1445
|
+
injector: this.injector,
|
|
1446
|
+
positions: buildSelectLikePositions(this.offset()),
|
|
1447
|
+
scrollStrategy: resolveSelectScrollStrategy(this.scrollStrategy(), this.overlayService),
|
|
1448
|
+
panelClass: 'tw-combobox-panel',
|
|
1449
|
+
focusTrap: false,
|
|
1450
|
+
});
|
|
1451
|
+
if (!result)
|
|
1452
|
+
return;
|
|
1453
|
+
const instance = result.instance;
|
|
1454
|
+
instance.onOptionPick.set((index) => {
|
|
1455
|
+
const visible = this.visibleOptions();
|
|
1456
|
+
if (index < 0 || index >= visible.length)
|
|
1457
|
+
return;
|
|
1458
|
+
const opt = visible[index];
|
|
1459
|
+
if (opt.disabled)
|
|
1460
|
+
return;
|
|
1461
|
+
this.commitOption(opt, 'option');
|
|
1462
|
+
});
|
|
1463
|
+
instance.onOptionHover.set((index) => {
|
|
1464
|
+
const visible = this.visibleOptions();
|
|
1465
|
+
if (index < 0 || index >= visible.length)
|
|
1466
|
+
return;
|
|
1467
|
+
if (visible[index].disabled)
|
|
1468
|
+
return;
|
|
1469
|
+
this.activeIndex.set(index);
|
|
1470
|
+
});
|
|
1471
|
+
this.overlayInstance = instance;
|
|
1472
|
+
this.installResizeObserver();
|
|
1473
|
+
this.updateOverlaySize();
|
|
1474
|
+
this.subscribePerOpen();
|
|
1384
1475
|
// Reset active to first enabled when opening.
|
|
1385
1476
|
this.activeIndex.set(this.firstEnabledIndex());
|
|
1477
|
+
// Set last: this is what wakes the state-push effect, and it must not fire
|
|
1478
|
+
// until the instance exists and the per-open state has been reset.
|
|
1479
|
+
this.isAttached.set(true);
|
|
1386
1480
|
this.liveAnnouncer.announce(`${this.visibleOptions().length} suggestions available`, 'polite');
|
|
1387
1481
|
this.openedChange.emit({
|
|
1388
1482
|
open: true,
|
|
@@ -1390,47 +1484,29 @@ class ComboboxComponent {
|
|
|
1390
1484
|
});
|
|
1391
1485
|
}
|
|
1392
1486
|
closeOverlay(_opts) {
|
|
1393
|
-
if (this.
|
|
1487
|
+
if (this.closing || !this.overlayInstance)
|
|
1394
1488
|
return;
|
|
1395
|
-
this.
|
|
1489
|
+
this.closing = true;
|
|
1396
1490
|
this.clearAnnounceTimer();
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1491
|
+
// The coordinator runs the leave animation, then detaches and disposes;
|
|
1492
|
+
// this callback fires once the overlay is fully gone.
|
|
1493
|
+
this.coordinator.close(() => {
|
|
1494
|
+
// Safe to drop the observer: the next open mints a fresh one against the
|
|
1495
|
+
// fresh OverlayRef. This used to be the SRP F-01 bug — the observer was
|
|
1496
|
+
// disconnected here while the reused ref made `ensureOverlay()`
|
|
1497
|
+
// early-return, so it was never re-installed. Under dispose-on-close the
|
|
1498
|
+
// install is unconditional, so that shape cannot come back.
|
|
1403
1499
|
this.resizeObserver?.disconnect();
|
|
1404
1500
|
this.resizeObserver = null;
|
|
1501
|
+
this.overlayInstance = null;
|
|
1502
|
+
this.isAttached.set(false);
|
|
1405
1503
|
untracked(() => this.open.set(false));
|
|
1406
|
-
this.
|
|
1504
|
+
this.closing = false;
|
|
1407
1505
|
this.openedChange.emit({
|
|
1408
1506
|
open: false,
|
|
1409
1507
|
trigger: this.inputRef()?.nativeElement ?? this.elementRef.nativeElement,
|
|
1410
1508
|
});
|
|
1411
|
-
}, ANIMATION_DURATION);
|
|
1412
|
-
}
|
|
1413
|
-
ensureOverlay() {
|
|
1414
|
-
if (this.overlayRef) {
|
|
1415
|
-
this.updateOverlaySize();
|
|
1416
|
-
return;
|
|
1417
|
-
}
|
|
1418
|
-
const positionStrategy = this.overlayService
|
|
1419
|
-
.position()
|
|
1420
|
-
.flexibleConnectedTo(this.triggerSurfaceRef()?.nativeElement ?? this.elementRef.nativeElement)
|
|
1421
|
-
.withPositions(buildSelectLikePositions(this.offset()))
|
|
1422
|
-
.withFlexibleDimensions(false)
|
|
1423
|
-
.withPush(false)
|
|
1424
|
-
.withViewportMargin(8);
|
|
1425
|
-
this.overlayRef = this.overlayService.create({
|
|
1426
|
-
positionStrategy,
|
|
1427
|
-
scrollStrategy: resolveSelectScrollStrategy(this.scrollStrategy(), this.overlayService),
|
|
1428
|
-
hasBackdrop: true,
|
|
1429
|
-
backdropClass: 'cdk-overlay-transparent-backdrop',
|
|
1430
|
-
panelClass: 'tw-combobox-panel',
|
|
1431
1509
|
});
|
|
1432
|
-
this.installResizeObserver();
|
|
1433
|
-
this.updateOverlaySize();
|
|
1434
1510
|
}
|
|
1435
1511
|
installResizeObserver() {
|
|
1436
1512
|
if (!this.platform.isBrowser || typeof ResizeObserver === 'undefined')
|
|
@@ -1441,85 +1517,48 @@ class ComboboxComponent {
|
|
|
1441
1517
|
this.resizeObserver.observe(target);
|
|
1442
1518
|
}
|
|
1443
1519
|
updateOverlaySize() {
|
|
1444
|
-
|
|
1520
|
+
const overlayRef = this.coordinator.ref();
|
|
1521
|
+
if (!overlayRef)
|
|
1445
1522
|
return;
|
|
1446
1523
|
const width = this.panelWidth();
|
|
1447
1524
|
if (width === 'trigger') {
|
|
1448
1525
|
const anchor = this.triggerSurfaceRef()?.nativeElement ?? this.elementRef.nativeElement;
|
|
1449
1526
|
const rect = anchor.getBoundingClientRect();
|
|
1450
|
-
|
|
1527
|
+
overlayRef.updateSize({ width: rect.width });
|
|
1451
1528
|
}
|
|
1452
1529
|
else if (width === 'auto') {
|
|
1453
|
-
|
|
1454
|
-
}
|
|
1455
|
-
else if (typeof width === 'number') {
|
|
1456
|
-
this.overlayRef.updateSize({ width });
|
|
1530
|
+
overlayRef.updateSize({ width: undefined, minWidth: undefined });
|
|
1457
1531
|
}
|
|
1458
1532
|
else {
|
|
1459
|
-
|
|
1533
|
+
overlayRef.updateSize({ width });
|
|
1460
1534
|
}
|
|
1461
1535
|
}
|
|
1462
|
-
attachOverlayComponent() {
|
|
1463
|
-
if (!this.overlayRef)
|
|
1464
|
-
return;
|
|
1465
|
-
const portal = new ComponentPortal(ComboboxOverlayComponent, this.viewContainerRef, this.injector);
|
|
1466
|
-
const ref = this.overlayRef.attach(portal);
|
|
1467
|
-
const instance = ref.instance;
|
|
1468
|
-
instance.onOptionPick.set((index) => {
|
|
1469
|
-
const visible = this.visibleOptions();
|
|
1470
|
-
if (index < 0 || index >= visible.length)
|
|
1471
|
-
return;
|
|
1472
|
-
const opt = visible[index];
|
|
1473
|
-
if (opt.disabled)
|
|
1474
|
-
return;
|
|
1475
|
-
this.commitOption(opt, 'option');
|
|
1476
|
-
});
|
|
1477
|
-
instance.onOptionHover.set((index) => {
|
|
1478
|
-
const visible = this.visibleOptions();
|
|
1479
|
-
if (index < 0 || index >= visible.length)
|
|
1480
|
-
return;
|
|
1481
|
-
if (visible[index].disabled)
|
|
1482
|
-
return;
|
|
1483
|
-
this.activeIndex.set(index);
|
|
1484
|
-
});
|
|
1485
|
-
this.overlayInstanceSignal.set(instance);
|
|
1486
|
-
}
|
|
1487
|
-
subscribeBackdrop() {
|
|
1488
|
-
if (!this.overlayRef)
|
|
1489
|
-
return;
|
|
1490
|
-
const sub = this.overlayRef.backdropClick().subscribe(() => {
|
|
1491
|
-
this.closePanel();
|
|
1492
|
-
});
|
|
1493
|
-
this.destroyRef.onDestroy(() => sub.unsubscribe());
|
|
1494
|
-
}
|
|
1495
1536
|
/**
|
|
1496
|
-
*
|
|
1537
|
+
* Registers the backdrop-click and Escape listeners for one open.
|
|
1538
|
+
*
|
|
1539
|
+
* The Escape listener rides the overlay's `keydownEvents()`. Mirrors
|
|
1497
1540
|
* `SelectComponent` so focus inside the overlay panel (e.g., if the user
|
|
1498
1541
|
* tabs into projected loading/empty templates) still dismisses cleanly.
|
|
1499
1542
|
* The combobox's own `<input>` keydown handler already short-circuits
|
|
1500
1543
|
* Escape when focus is on the input; this layer is the safety net for
|
|
1501
1544
|
* panel-internal focus.
|
|
1545
|
+
*
|
|
1546
|
+
* Both streams come from the coordinator, which completes them when the
|
|
1547
|
+
* overlay closes — so there is no per-open `Subscription` aggregate to keep
|
|
1548
|
+
* and no way for listeners to accumulate across opens.
|
|
1502
1549
|
*/
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
const teardown = consumeOverlayEscape(this.overlayRef, (event) => {
|
|
1550
|
+
subscribePerOpen() {
|
|
1551
|
+
this.coordinator.backdropClick$().subscribe(() => this.closePanel());
|
|
1552
|
+
this.coordinator.escape$().subscribe((event) => {
|
|
1507
1553
|
event.preventDefault();
|
|
1508
1554
|
this.inputValue.set(this.lastCommittedLabel());
|
|
1509
1555
|
this.closePanel();
|
|
1510
1556
|
});
|
|
1511
|
-
this.destroyRef.onDestroy(() => teardown());
|
|
1512
1557
|
}
|
|
1513
1558
|
resolvePanelClass() {
|
|
1514
1559
|
const raw = this.panelClass();
|
|
1515
1560
|
return Array.isArray(raw) ? raw.join(' ') : raw;
|
|
1516
1561
|
}
|
|
1517
|
-
clearCloseTimer() {
|
|
1518
|
-
if (this.closeTimer !== null) {
|
|
1519
|
-
clearTimeout(this.closeTimer);
|
|
1520
|
-
this.closeTimer = null;
|
|
1521
|
-
}
|
|
1522
|
-
}
|
|
1523
1562
|
// ── ControlValueAccessor ──
|
|
1524
1563
|
writeValue(value) {
|
|
1525
1564
|
if (value === null || value === undefined) {
|
|
@@ -1588,21 +1627,7 @@ class ComboboxComponent {
|
|
|
1588
1627
|
// NgControl's `control` is set by the parent FormControl* directive before
|
|
1589
1628
|
// children's `ngOnInit`. Subscribe here so errorState reacts to status/value
|
|
1590
1629
|
// changes on the bound control.
|
|
1591
|
-
|
|
1592
|
-
if (ctrl) {
|
|
1593
|
-
const streams = [ctrl.statusChanges, ctrl.valueChanges].filter((s) => !!s);
|
|
1594
|
-
if (streams.length) {
|
|
1595
|
-
merge(...streams)
|
|
1596
|
-
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
1597
|
-
.subscribe(() => this._ngControlRev.update((n) => n + 1));
|
|
1598
|
-
}
|
|
1599
|
-
}
|
|
1600
|
-
const submit = this.parentFormGroup?.ngSubmit ?? this.parentForm?.ngSubmit;
|
|
1601
|
-
if (submit) {
|
|
1602
|
-
submit
|
|
1603
|
-
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
1604
|
-
.subscribe(() => this._formSubmitRev.update((n) => n + 1));
|
|
1605
|
-
}
|
|
1630
|
+
this.errorWiring.connect();
|
|
1606
1631
|
}
|
|
1607
1632
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ComboboxComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1608
1633
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: ComboboxComponent, isStandalone: true, selector: "tw-combobox", 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 }, optionDescription: { classPropertyName: "optionDescription", publicName: "optionDescription", isSignal: true, isRequired: false, transformFunction: null }, filterFn: { classPropertyName: "filterFn", publicName: "filterFn", isSignal: true, isRequired: false, transformFunction: null }, strict: { classPropertyName: "strict", publicName: "strict", 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 }, showChevron: { classPropertyName: "showChevron", publicName: "showChevron", isSignal: true, isRequired: false, transformFunction: null }, clearable: { classPropertyName: "clearable", publicName: "clearable", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, queryDebounce: { classPropertyName: "queryDebounce", publicName: "queryDebounce", isSignal: true, isRequired: false, transformFunction: null }, minQueryLength: { classPropertyName: "minQueryLength", publicName: "minQueryLength", isSignal: true, isRequired: false, transformFunction: null }, openOnFocus: { classPropertyName: "openOnFocus", publicName: "openOnFocus", isSignal: true, isRequired: false, transformFunction: null }, panelMaxHeight: { classPropertyName: "panelMaxHeight", publicName: "panelMaxHeight", 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 }, 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 }, compareWith: { classPropertyName: "compareWith", publicName: "compareWith", 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 }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, inputValue: { classPropertyName: "inputValue", publicName: "inputValue", isSignal: true, isRequired: false, transformFunction: null }, open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", inputValue: "inputValueChange", open: "openChange", queryChange: "queryChange", optionSelected: "optionSelected", valueCommit: "valueCommit", openedChange: "openedChange" }, host: { properties: { "class": "rootClasses()" } }, providers: [
|
|
@@ -1610,6 +1635,7 @@ class ComboboxComponent {
|
|
|
1610
1635
|
provide: TW_FORM_FIELD_CONTROL,
|
|
1611
1636
|
useExisting: forwardRef(() => ComboboxComponent),
|
|
1612
1637
|
},
|
|
1638
|
+
PickerOverlayCoordinator,
|
|
1613
1639
|
], queries: [{ propertyName: "optionTemplateChild", first: true, predicate: ComboboxOptionTemplateDirective, descendants: true, isSignal: true }, { propertyName: "emptyTemplateChild", first: true, predicate: ComboboxEmptyTemplateDirective, descendants: true, isSignal: true }, { propertyName: "loadingTemplateChild", first: true, predicate: ComboboxLoadingTemplateDirective, descendants: true, isSignal: true }], viewQueries: [{ propertyName: "inputRef", first: true, predicate: ["inputEl"], descendants: true, isSignal: true }, { propertyName: "triggerSurfaceRef", first: true, predicate: ["triggerSurface"], descendants: true, isSignal: true }], ngImport: i0, template: `
|
|
1614
1640
|
<div #triggerSurface [class]="triggerClasses()">
|
|
1615
1641
|
<ng-content select="[twComboboxPrefix]" />
|
|
@@ -1625,7 +1651,7 @@ class ComboboxComponent {
|
|
|
1625
1651
|
[attr.aria-expanded]="open() ? 'true' : 'false'"
|
|
1626
1652
|
[attr.aria-controls]="open() ? listboxId : null"
|
|
1627
1653
|
[attr.aria-activedescendant]="open() ? (activeOptionId() || null) : null"
|
|
1628
|
-
[attr.aria-required]="
|
|
1654
|
+
[attr.aria-required]="required() ? 'true' : null"
|
|
1629
1655
|
[attr.aria-invalid]="errorState() ? 'true' : null"
|
|
1630
1656
|
[attr.aria-label]="ariaLabel() || null"
|
|
1631
1657
|
[attr.aria-labelledby]="resolvedLabelledBy() || null"
|
|
@@ -1664,7 +1690,6 @@ class ComboboxComponent {
|
|
|
1664
1690
|
@if (showClearButton()) {
|
|
1665
1691
|
<button
|
|
1666
1692
|
type="button"
|
|
1667
|
-
tabindex="-1"
|
|
1668
1693
|
[class]="clearButtonClasses()"
|
|
1669
1694
|
aria-label="Clear"
|
|
1670
1695
|
(mousedown)="onClearMousedown($event)"
|
|
@@ -1711,6 +1736,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
|
|
|
1711
1736
|
provide: TW_FORM_FIELD_CONTROL,
|
|
1712
1737
|
useExisting: forwardRef(() => ComboboxComponent),
|
|
1713
1738
|
},
|
|
1739
|
+
PickerOverlayCoordinator,
|
|
1714
1740
|
],
|
|
1715
1741
|
template: `
|
|
1716
1742
|
<div #triggerSurface [class]="triggerClasses()">
|
|
@@ -1727,7 +1753,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
|
|
|
1727
1753
|
[attr.aria-expanded]="open() ? 'true' : 'false'"
|
|
1728
1754
|
[attr.aria-controls]="open() ? listboxId : null"
|
|
1729
1755
|
[attr.aria-activedescendant]="open() ? (activeOptionId() || null) : null"
|
|
1730
|
-
[attr.aria-required]="
|
|
1756
|
+
[attr.aria-required]="required() ? 'true' : null"
|
|
1731
1757
|
[attr.aria-invalid]="errorState() ? 'true' : null"
|
|
1732
1758
|
[attr.aria-label]="ariaLabel() || null"
|
|
1733
1759
|
[attr.aria-labelledby]="resolvedLabelledBy() || null"
|
|
@@ -1766,7 +1792,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
|
|
|
1766
1792
|
@if (showClearButton()) {
|
|
1767
1793
|
<button
|
|
1768
1794
|
type="button"
|
|
1769
|
-
tabindex="-1"
|
|
1770
1795
|
[class]="clearButtonClasses()"
|
|
1771
1796
|
aria-label="Clear"
|
|
1772
1797
|
(mousedown)="onClearMousedown($event)"
|