@radix-ng/primitives 1.0.0-beta.3 → 1.0.0-beta.4
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 +1 -1
- package/fesm2022/radix-ng-primitives-accordion.mjs +5 -3
- package/fesm2022/radix-ng-primitives-accordion.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-alert-dialog.mjs +3 -2
- package/fesm2022/radix-ng-primitives-alert-dialog.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-autocomplete.mjs +617 -659
- package/fesm2022/radix-ng-primitives-autocomplete.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-calendar.mjs +5 -3
- package/fesm2022/radix-ng-primitives-calendar.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-combobox.mjs +1305 -572
- package/fesm2022/radix-ng-primitives-combobox.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-config.mjs +13 -4
- package/fesm2022/radix-ng-primitives-config.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-context-menu.mjs +51 -10
- package/fesm2022/radix-ng-primitives-context-menu.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-core.mjs +1345 -64
- package/fesm2022/radix-ng-primitives-core.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-date-field.mjs +5 -3
- package/fesm2022/radix-ng-primitives-date-field.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-dialog.mjs +240 -112
- package/fesm2022/radix-ng-primitives-dialog.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-direction-provider.mjs +70 -0
- package/fesm2022/radix-ng-primitives-direction-provider.mjs.map +1 -0
- package/fesm2022/radix-ng-primitives-dismissable-layer.mjs +519 -184
- package/fesm2022/radix-ng-primitives-dismissable-layer.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-drawer.mjs +3 -3
- package/fesm2022/radix-ng-primitives-drawer.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-field.mjs +3 -2
- package/fesm2022/radix-ng-primitives-field.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-floating-focus-manager.mjs +517 -0
- package/fesm2022/radix-ng-primitives-floating-focus-manager.mjs.map +1 -0
- package/fesm2022/radix-ng-primitives-focus-scope.mjs +296 -70
- package/fesm2022/radix-ng-primitives-focus-scope.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-menu.mjs +861 -286
- package/fesm2022/radix-ng-primitives-menu.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-menubar.mjs +32 -4
- package/fesm2022/radix-ng-primitives-menubar.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-navigation-menu.mjs +144 -159
- package/fesm2022/radix-ng-primitives-navigation-menu.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-popover.mjs +220 -205
- package/fesm2022/radix-ng-primitives-popover.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-popper.mjs +94 -51
- package/fesm2022/radix-ng-primitives-popper.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-presence.mjs +1 -1
- package/fesm2022/radix-ng-primitives-presence.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-preview-card.mjs +141 -173
- package/fesm2022/radix-ng-primitives-preview-card.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-roving-focus.mjs +4 -2
- package/fesm2022/radix-ng-primitives-roving-focus.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-scroll-area.mjs +5 -4
- package/fesm2022/radix-ng-primitives-scroll-area.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-select.mjs +211 -156
- package/fesm2022/radix-ng-primitives-select.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-slider.mjs +5 -3
- package/fesm2022/radix-ng-primitives-slider.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-stepper.mjs +5 -3
- package/fesm2022/radix-ng-primitives-stepper.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-time-field.mjs +5 -3
- package/fesm2022/radix-ng-primitives-time-field.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-toast.mjs +15 -36
- package/fesm2022/radix-ng-primitives-toast.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-toggle-group.mjs +5 -3
- package/fesm2022/radix-ng-primitives-toggle-group.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-toolbar.mjs +5 -3
- package/fesm2022/radix-ng-primitives-toolbar.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-tooltip.mjs +73 -110
- package/fesm2022/radix-ng-primitives-tooltip.mjs.map +1 -1
- package/package.json +10 -1
- package/types/radix-ng-primitives-accordion.d.ts +4 -3
- package/types/radix-ng-primitives-autocomplete.d.ts +217 -152
- package/types/radix-ng-primitives-calendar.d.ts +5 -3
- package/types/radix-ng-primitives-combobox.d.ts +672 -283
- package/types/radix-ng-primitives-config.d.ts +1 -1
- package/types/radix-ng-primitives-context-menu.d.ts +15 -5
- package/types/radix-ng-primitives-core.d.ts +762 -14
- package/types/radix-ng-primitives-date-field.d.ts +3 -2
- package/types/radix-ng-primitives-dialog.d.ts +77 -32
- package/types/radix-ng-primitives-direction-provider.d.ts +41 -0
- package/types/radix-ng-primitives-dismissable-layer.d.ts +147 -99
- package/types/radix-ng-primitives-field.d.ts +1 -0
- package/types/radix-ng-primitives-floating-focus-manager.d.ts +175 -0
- package/types/radix-ng-primitives-focus-scope.d.ts +132 -1
- package/types/radix-ng-primitives-menu.d.ts +186 -103
- package/types/radix-ng-primitives-navigation-menu.d.ts +37 -75
- package/types/radix-ng-primitives-popover.d.ts +59 -92
- package/types/radix-ng-primitives-popper.d.ts +39 -9
- package/types/radix-ng-primitives-preview-card.d.ts +39 -72
- package/types/radix-ng-primitives-roving-focus.d.ts +7 -6
- package/types/radix-ng-primitives-scroll-area.d.ts +2 -2
- package/types/radix-ng-primitives-select.d.ts +145 -108
- package/types/radix-ng-primitives-slider.d.ts +5 -4
- package/types/radix-ng-primitives-stepper.d.ts +4 -3
- package/types/radix-ng-primitives-time-field.d.ts +3 -2
- package/types/radix-ng-primitives-toast.d.ts +7 -7
- package/types/radix-ng-primitives-toggle-group.d.ts +5 -4
- package/types/radix-ng-primitives-toolbar.d.ts +3 -2
- package/types/radix-ng-primitives-tooltip.d.ts +24 -67
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { Directive, inject, booleanAttribute, Injector, model, input,
|
|
2
|
+
import { Directive, signal, computed, untracked, effect, inject, booleanAttribute, Injector, ElementRef, model, input, numberAttribute, output, isDevMode, DestroyRef, ChangeDetectionStrategy, Component, afterNextRender, afterRenderEffect, NgModule } from '@angular/core';
|
|
3
3
|
import * as i1 from '@radix-ng/primitives/popper';
|
|
4
|
-
import { RdxPopperAnchor, RdxPopperArrow, RdxPopper, injectPopperContentWrapperContext, RdxPopperContent
|
|
4
|
+
import { RdxPopperAnchor, RdxPopperArrow, RdxPopper, RdxPopperContentWrapper, provideRdxPopperContentWrapper, provideRdxPopperContentConfig, injectPopperContentWrapperContext, RdxPopperContent } from '@radix-ng/primitives/popper';
|
|
5
5
|
import { NG_VALUE_ACCESSOR } from '@angular/forms';
|
|
6
|
-
import
|
|
6
|
+
import * as i2 from '@radix-ng/primitives/core';
|
|
7
|
+
import { useFilter, injectId, useListHighlight, useTransitionStatus, createContext, createFloatingRootContext, isItemEqualToValue, isNullish, itemToStringLabel, createCancelableChangeEventDetails, provideFloatingTree, provideFloatingRootContext, setupInternalBackdrop, RDX_FLOATING_ROOT_CONTEXT, RDX_FLOATING_REGISTRATION, useAnchoredScrollLock, RdxFloatingNodeRegistration, rdxDevError } from '@radix-ng/primitives/core';
|
|
8
|
+
import { injectDirection } from '@radix-ng/primitives/direction-provider';
|
|
7
9
|
import * as i1$1 from '@radix-ng/primitives/dismissable-layer';
|
|
8
|
-
import {
|
|
10
|
+
import { RdxFloatingInsideElement, RdxDismiss } from '@radix-ng/primitives/dismissable-layer';
|
|
9
11
|
import { injectFieldRootContext } from '@radix-ng/primitives/field';
|
|
10
12
|
import * as i1$2 from '@radix-ng/primitives/portal';
|
|
11
13
|
import { RdxPortalPresence } from '@radix-ng/primitives/portal';
|
|
@@ -52,60 +54,606 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
|
|
|
52
54
|
}]
|
|
53
55
|
}] });
|
|
54
56
|
|
|
57
|
+
/**
|
|
58
|
+
* The shared Combobox engine (ADR 0014): item registry, filtering, highlight-model navigation,
|
|
59
|
+
* open/close transition, and the reactive effects that tie them together — everything Combobox and
|
|
60
|
+
* Autocomplete have in common. Value/selection semantics, open orchestration, and forms integration
|
|
61
|
+
* stay in each root; the root configures the engine and reads its state for the DI context.
|
|
62
|
+
*
|
|
63
|
+
* Must be called in an injection context (it runs effects and `inject`-based hooks).
|
|
64
|
+
*
|
|
65
|
+
* @internal Not part of the public API — exported only for the autocomplete entry; may change without notice.
|
|
66
|
+
*/
|
|
67
|
+
function useComboboxEngine(config) {
|
|
68
|
+
const { injector } = config;
|
|
69
|
+
const defaultFilter = useFilter();
|
|
70
|
+
const listId = injectId(config.listIdPrefix);
|
|
71
|
+
const labelId = signal(undefined, ...(ngDevMode ? [{ debugName: "labelId" }] : /* istanbul ignore next */ []));
|
|
72
|
+
const inputElement = signal(null, ...(ngDevMode ? [{ debugName: "inputElement" }] : /* istanbul ignore next */ []));
|
|
73
|
+
// Where the text input lives relative to the popup (Base UI's `inputInsidePopup`), reported by the
|
|
74
|
+
// input on mount from its positioner ancestor. Drives the Trigger's role:
|
|
75
|
+
// - `outside` → the input is the tab stop; the Trigger is a `tabindex="-1"` toggle.
|
|
76
|
+
// - `inside` → the Trigger is the focusable `role="combobox"` control (`aria-haspopup="dialog"`).
|
|
77
|
+
// - `unknown` → no input has mounted yet (e.g. the input lives in a not-yet-opened popup); the
|
|
78
|
+
// Trigger stays a normal focusable button so it is reachable by `Tab` before the first open.
|
|
79
|
+
const inputLayout = signal('unknown', ...(ngDevMode ? [{ debugName: "inputLayout" }] : /* istanbul ignore next */ []));
|
|
80
|
+
// Whether the popup was opened by a touch interaction. When the input lives inside the popup, a
|
|
81
|
+
// touch-open focuses the popup (not the input) so Android doesn't raise the virtual keyboard
|
|
82
|
+
// (Base UI; iOS handles this itself). Reset to `false` on keyboard/mouse opens.
|
|
83
|
+
const openedByTouch = signal(false, ...(ngDevMode ? [{ debugName: "openedByTouch" }] : /* istanbul ignore next */ []));
|
|
84
|
+
// Whether the popup directive is currently mounted (open through the close/exit animation, until the
|
|
85
|
+
// presence machine unmounts it). Distinguishes "Escape closed the popup" (still mounted this tick)
|
|
86
|
+
// from "Escape on an already-closed combobox" (unmounted) — Base UI's `mounted`, since `open()`
|
|
87
|
+
// flips synchronously when the input's Escape handler (or the dismiss mechanism) closes the popup.
|
|
88
|
+
const popupMounted = signal(false, ...(ngDevMode ? [{ debugName: "popupMounted" }] : /* istanbul ignore next */ []));
|
|
89
|
+
let triggerElement = null;
|
|
90
|
+
// Tracks whether the last interaction was the keyboard, so the highlight doesn't jump to an item
|
|
91
|
+
// the cursor happens to rest on when arrow-key navigation scrolls the list under a still pointer.
|
|
92
|
+
let keyboardActive = false;
|
|
93
|
+
const _items = signal([], ...(ngDevMode ? [{ debugName: "_items" }] : /* istanbul ignore next */ []));
|
|
94
|
+
const orderedItems = computed(() => {
|
|
95
|
+
const items = [..._items()];
|
|
96
|
+
return items.sort((a, b) => domOrder(a.element, b.element));
|
|
97
|
+
}, ...(ngDevMode ? [{ debugName: "orderedItems" }] : /* istanbul ignore next */ []));
|
|
98
|
+
const matchesFilter = (item) => {
|
|
99
|
+
if (!config.filteringEnabled()) {
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
const filter = config.filter();
|
|
103
|
+
if (filter === null) {
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
const query = config.query();
|
|
107
|
+
// Custom filter: Base UI shape `(value, query, itemToString)`. Default: locale-aware contains
|
|
108
|
+
// on the item's own text (the element content), no value→text round-trip.
|
|
109
|
+
return filter
|
|
110
|
+
? filter(item.value(), query, config.itemToString)
|
|
111
|
+
: defaultFilter.contains(item.textValue(), query);
|
|
112
|
+
};
|
|
113
|
+
const visibleItems = computed(() => {
|
|
114
|
+
const matching = orderedItems().filter((item) => matchesFilter(item));
|
|
115
|
+
const limit = config.limit();
|
|
116
|
+
return limit >= 0 ? matching.slice(0, limit) : matching;
|
|
117
|
+
}, ...(ngDevMode ? [{ debugName: "visibleItems" }] : /* istanbul ignore next */ []));
|
|
118
|
+
const visibleSet = computed(() => new Set(visibleItems()), ...(ngDevMode ? [{ debugName: "visibleSet" }] : /* istanbul ignore next */ []));
|
|
119
|
+
const isVisible = (item) => visibleSet().has(item);
|
|
120
|
+
const filteredItems = computed(() => {
|
|
121
|
+
const data = config.items();
|
|
122
|
+
if (data === undefined) {
|
|
123
|
+
return visibleItems().map((item) => item.value());
|
|
124
|
+
}
|
|
125
|
+
const limit = config.limit();
|
|
126
|
+
const cap = (arr) => (limit >= 0 ? arr.slice(0, limit) : arr);
|
|
127
|
+
if (!config.filteringEnabled()) {
|
|
128
|
+
return cap(data);
|
|
129
|
+
}
|
|
130
|
+
const filter = config.filter();
|
|
131
|
+
if (filter === null) {
|
|
132
|
+
return cap(data);
|
|
133
|
+
}
|
|
134
|
+
const query = config.query();
|
|
135
|
+
if (!query) {
|
|
136
|
+
return cap(data);
|
|
137
|
+
}
|
|
138
|
+
// Virtualized: no DOM to read text from, so resolve each value through `itemToString`.
|
|
139
|
+
return cap(data.filter((value) => filter
|
|
140
|
+
? filter(value, query, config.itemToString)
|
|
141
|
+
: defaultFilter.contains(config.itemToString(value), query)));
|
|
142
|
+
}, ...(ngDevMode ? [{ debugName: "filteredItems" }] : /* istanbul ignore next */ []));
|
|
143
|
+
const visibleCount = computed(() => (config.virtualized() ? filteredItems().length : visibleItems().length), ...(ngDevMode ? [{ debugName: "visibleCount" }] : /* istanbul ignore next */ []));
|
|
144
|
+
const highlight = useListHighlight({
|
|
145
|
+
items: orderedItems,
|
|
146
|
+
isNavigable: (item) => isVisible(item) && !item.disabled(),
|
|
147
|
+
getId: (item) => item.id,
|
|
148
|
+
loop: config.loopFocus,
|
|
149
|
+
injector
|
|
150
|
+
});
|
|
151
|
+
const highlightedItem = highlight.highlightedItem;
|
|
152
|
+
const highlightedIndex = signal(-1, ...(ngDevMode ? [{ debugName: "highlightedIndex" }] : /* istanbul ignore next */ []));
|
|
153
|
+
const highlightReason = signal('none', ...(ngDevMode ? [{ debugName: "highlightReason" }] : /* istanbul ignore next */ []));
|
|
154
|
+
const itemId = (index) => `${listId}-item-${index}`;
|
|
155
|
+
const activeId = computed(() => {
|
|
156
|
+
if (config.virtualized()) {
|
|
157
|
+
const index = highlightedIndex();
|
|
158
|
+
return index >= 0 ? itemId(index) : undefined;
|
|
159
|
+
}
|
|
160
|
+
return highlight.activeId();
|
|
161
|
+
}, ...(ngDevMode ? [{ debugName: "activeId" }] : /* istanbul ignore next */ []));
|
|
162
|
+
// `'first-match'` highlights the first item whose label prefix-matches the query (inline modes),
|
|
163
|
+
// so inline completion lands on a real prefix even when the list is static.
|
|
164
|
+
const pendingHighlightEdge = signal(null, ...(ngDevMode ? [{ debugName: "pendingHighlightEdge" }] : /* istanbul ignore next */ []));
|
|
165
|
+
// Inline completion (autocomplete `both` / `inline`): a transient preview of the highlighted item's
|
|
166
|
+
// label mirrored into the input. `null` when off. `suppressInline` skips it for a deleting edit.
|
|
167
|
+
const inlinePreview = signal(null, ...(ngDevMode ? [{ debugName: "inlinePreview" }] : /* istanbul ignore next */ []));
|
|
168
|
+
let suppressInline = false;
|
|
169
|
+
/** The first visible, navigable item whose label starts with the query (for inline completion). */
|
|
170
|
+
const firstMatchItem = () => {
|
|
171
|
+
const query = config.query();
|
|
172
|
+
if (!query) {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
const lower = query.toLowerCase();
|
|
176
|
+
return (visibleItems().find((item) => !item.disabled() && item.textValue().toLowerCase().startsWith(lower)) ?? null);
|
|
177
|
+
};
|
|
178
|
+
/** The first visible, navigable item (auto-highlight fallback when no prefix match exists). */
|
|
179
|
+
const firstVisibleNavigable = () => visibleItems().find((item) => !item.disabled()) ?? null;
|
|
180
|
+
/** Resolves a pending edge to a virtualized index (`'first-match'` → first prefix match, else 0). */
|
|
181
|
+
const resolveVirtualizedEdge = (edge, count) => {
|
|
182
|
+
if (edge === 'last') {
|
|
183
|
+
return count - 1;
|
|
184
|
+
}
|
|
185
|
+
if (edge === 'first-match') {
|
|
186
|
+
const query = config.query();
|
|
187
|
+
if (query) {
|
|
188
|
+
const lower = query.toLowerCase();
|
|
189
|
+
const index = filteredItems().findIndex((value) => config.itemToString(value).toLowerCase().startsWith(lower));
|
|
190
|
+
if (index >= 0) {
|
|
191
|
+
return index;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return 0;
|
|
196
|
+
};
|
|
197
|
+
/** The active highlight's label, DOM-ref or virtualized (resolved from the index). `null` when none. */
|
|
198
|
+
const activeLabel = () => {
|
|
199
|
+
if (config.virtualized()) {
|
|
200
|
+
const index = highlightedIndex();
|
|
201
|
+
const value = index >= 0 ? filteredItems()[index] : undefined;
|
|
202
|
+
return value === undefined ? null : config.itemToString(value);
|
|
203
|
+
}
|
|
204
|
+
return highlightedItem()?.textValue() ?? null;
|
|
205
|
+
};
|
|
206
|
+
const recomputeInlinePreview = (label, query, reason) => {
|
|
207
|
+
// Pointer hover must not rewrite the input (matches Base UI); only typing / keyboard nav complete it.
|
|
208
|
+
if (!config.inlineMode() || suppressInline || !label || reason === 'pointer') {
|
|
209
|
+
inlinePreview.set(null);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (query && label.toLowerCase().startsWith(query.toLowerCase())) {
|
|
213
|
+
// Type-ahead: keep the typed prefix (preserving its casing) and complete the rest.
|
|
214
|
+
inlinePreview.set(query + label.slice(query.length));
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
// Keyboard navigation to a non-prefix item: show its full label so the input reflects it.
|
|
218
|
+
if (reason === 'keyboard') {
|
|
219
|
+
inlinePreview.set(label);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
inlinePreview.set(null);
|
|
223
|
+
};
|
|
224
|
+
const transition = useTransitionStatus(config.onOpenChangeComplete);
|
|
225
|
+
// --- effects ---
|
|
226
|
+
// Emit open changes and drive the open/close transition (skip the initial run).
|
|
227
|
+
let previousOpen = untracked(config.open);
|
|
228
|
+
effect(() => {
|
|
229
|
+
const open = config.open();
|
|
230
|
+
if (open === previousOpen) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
previousOpen = open;
|
|
234
|
+
untracked(() => {
|
|
235
|
+
// Drop a deferred open-edge highlight when the popup closes — otherwise a pending
|
|
236
|
+
// 'first' (e.g. ArrowDown opened an empty list, then it closed) would unexpectedly
|
|
237
|
+
// highlight on the next plain open, even without autoHighlight.
|
|
238
|
+
if (!open) {
|
|
239
|
+
pendingHighlightEdge.set(null);
|
|
240
|
+
}
|
|
241
|
+
config.onOpenChange(open);
|
|
242
|
+
transition.start(open);
|
|
243
|
+
});
|
|
244
|
+
}, { injector });
|
|
245
|
+
// Emit highlight changes (skip the initial run). Tracks both the DOM-ref highlight and the
|
|
246
|
+
// virtualized index; only one is active per mode, so the other never fires spuriously.
|
|
247
|
+
let highlightInitialized = false;
|
|
248
|
+
effect(() => {
|
|
249
|
+
const item = highlightedItem();
|
|
250
|
+
const index = highlightedIndex();
|
|
251
|
+
if (!highlightInitialized) {
|
|
252
|
+
highlightInitialized = true;
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
untracked(() => {
|
|
256
|
+
if (config.virtualized()) {
|
|
257
|
+
const value = index >= 0 ? (filteredItems()[index] ?? null) : null;
|
|
258
|
+
// No active highlight (e.g. filtering pushed the index out of range) carries no
|
|
259
|
+
// interaction — report 'none', not a stale 'keyboard'/'pointer' reason.
|
|
260
|
+
config.onItemHighlighted({ value, index, reason: value === null ? 'none' : highlightReason() });
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
const value = item ? item.value() : null;
|
|
264
|
+
const itemIndex = item ? visibleItems().indexOf(item) : -1;
|
|
265
|
+
// DOM-mode self-heal in `useListHighlight` clears `highlighted` without touching
|
|
266
|
+
// `highlightReason`; treat a null highlight as 'none' so the emit isn't mis-reported.
|
|
267
|
+
config.onItemHighlighted({
|
|
268
|
+
value,
|
|
269
|
+
index: itemIndex,
|
|
270
|
+
reason: item === null ? 'none' : highlightReason()
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
}, { injector });
|
|
275
|
+
// Apply a deferred open-edge highlight once items (DOM refs) or filtered data have registered.
|
|
276
|
+
effect(() => {
|
|
277
|
+
const edge = pendingHighlightEdge();
|
|
278
|
+
const count = config.virtualized() ? filteredItems().length : orderedItems().length;
|
|
279
|
+
if (!config.open() || edge === null || count === 0) {
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
untracked(() => {
|
|
283
|
+
// Programmatic move — reset the reason so the emit reports 'none', not a stale interaction.
|
|
284
|
+
highlightReason.set('none');
|
|
285
|
+
if (config.virtualized()) {
|
|
286
|
+
highlightedIndex.set(resolveVirtualizedEdge(edge, count));
|
|
287
|
+
}
|
|
288
|
+
else if (edge === 'last') {
|
|
289
|
+
highlight.last();
|
|
290
|
+
}
|
|
291
|
+
else if (edge === 'first-match') {
|
|
292
|
+
highlight.set(firstMatchItem() ?? firstVisibleNavigable());
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
highlight.first();
|
|
296
|
+
}
|
|
297
|
+
pendingHighlightEdge.set(null);
|
|
298
|
+
});
|
|
299
|
+
}, { injector });
|
|
300
|
+
// Inline completion: mirror the active item's label into the input. Tracks the DOM-ref highlight,
|
|
301
|
+
// the virtualized index, the query, the reason, and `inlineMode` — so virtualized navigation and a
|
|
302
|
+
// `both → list` mode switch both recompute (and clear) the preview. No-op (null) when off (combobox).
|
|
303
|
+
effect(() => {
|
|
304
|
+
config.inlineMode();
|
|
305
|
+
highlightedItem();
|
|
306
|
+
highlightedIndex();
|
|
307
|
+
const query = config.query();
|
|
308
|
+
const reason = highlightReason();
|
|
309
|
+
untracked(() => recomputeInlinePreview(activeLabel(), query, reason));
|
|
310
|
+
}, { injector });
|
|
311
|
+
// autoHighlight 'always': keep the first navigable item highlighted whenever the popup is open.
|
|
312
|
+
effect(() => {
|
|
313
|
+
orderedItems();
|
|
314
|
+
visibleCount();
|
|
315
|
+
if (config.autoHighlightMode() === 'always' && config.open()) {
|
|
316
|
+
untracked(() => {
|
|
317
|
+
if (config.virtualized()) {
|
|
318
|
+
const length = filteredItems().length;
|
|
319
|
+
const index = highlightedIndex();
|
|
320
|
+
if ((index < 0 || index >= length) && length > 0) {
|
|
321
|
+
highlightReason.set('none');
|
|
322
|
+
highlightedIndex.set(0);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
else if (highlightedItem() === null) {
|
|
326
|
+
highlightReason.set('none');
|
|
327
|
+
highlight.first();
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
}, { injector });
|
|
332
|
+
// Virtualized self-heal: clear a highlight that filtering has pushed out of range.
|
|
333
|
+
effect(() => {
|
|
334
|
+
if (!config.virtualized()) {
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
const length = filteredItems().length;
|
|
338
|
+
untracked(() => {
|
|
339
|
+
const index = highlightedIndex();
|
|
340
|
+
if (index >= length && index !== -1) {
|
|
341
|
+
highlightReason.set('none');
|
|
342
|
+
highlightedIndex.set(-1);
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
}, { injector });
|
|
346
|
+
// --- navigation facade (mode-aware: index-based when virtualized, else DOM-ref) ---
|
|
347
|
+
const stepIndex = (direction) => {
|
|
348
|
+
const length = filteredItems().length;
|
|
349
|
+
if (length === 0) {
|
|
350
|
+
highlightedIndex.set(-1);
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
const current = highlightedIndex();
|
|
354
|
+
if (current < 0) {
|
|
355
|
+
highlightedIndex.set(direction === 1 ? 0 : length - 1);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
let next = current + direction;
|
|
359
|
+
const loop = config.loopFocus();
|
|
360
|
+
if (next < 0) {
|
|
361
|
+
next = loop ? length - 1 : 0;
|
|
362
|
+
}
|
|
363
|
+
else if (next >= length) {
|
|
364
|
+
next = loop ? 0 : length - 1;
|
|
365
|
+
}
|
|
366
|
+
highlightedIndex.set(next);
|
|
367
|
+
};
|
|
368
|
+
// --- grid navigation (DOM-ref mode only) ---
|
|
369
|
+
/** Visible items grouped into rows by their enclosing row element (DOM order). */
|
|
370
|
+
const gridRows = () => {
|
|
371
|
+
const rows = new Map();
|
|
372
|
+
for (const item of visibleItems()) {
|
|
373
|
+
const key = config.rowOf(item.element);
|
|
374
|
+
const bucket = rows.get(key);
|
|
375
|
+
if (bucket) {
|
|
376
|
+
bucket.push(item);
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
rows.set(key, [item]);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return [...rows.values()];
|
|
383
|
+
};
|
|
384
|
+
/** Grid vertical move: keep the column index, jump to the adjacent row (wraps when looping). */
|
|
385
|
+
const gridVertical = (direction) => {
|
|
386
|
+
const rows = gridRows();
|
|
387
|
+
if (rows.length === 0) {
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
const current = highlightedItem();
|
|
391
|
+
if (!current) {
|
|
392
|
+
const row = direction === 1 ? rows[0] : rows[rows.length - 1];
|
|
393
|
+
highlight.set(row[0] ?? null);
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
let rowIndex = rows.findIndex((row) => row.includes(current));
|
|
397
|
+
const col = rowIndex >= 0 ? rows[rowIndex].indexOf(current) : 0;
|
|
398
|
+
const loop = config.loopFocus();
|
|
399
|
+
rowIndex += direction;
|
|
400
|
+
if (rowIndex < 0) {
|
|
401
|
+
rowIndex = loop ? rows.length - 1 : 0;
|
|
402
|
+
}
|
|
403
|
+
else if (rowIndex >= rows.length) {
|
|
404
|
+
rowIndex = loop ? 0 : rows.length - 1;
|
|
405
|
+
}
|
|
406
|
+
const targetRow = rows[rowIndex];
|
|
407
|
+
highlight.set(targetRow[Math.min(col, targetRow.length - 1)] ?? null);
|
|
408
|
+
};
|
|
409
|
+
return {
|
|
410
|
+
listId,
|
|
411
|
+
labelId: labelId.asReadonly(),
|
|
412
|
+
setLabelId(id) {
|
|
413
|
+
labelId.set(id);
|
|
414
|
+
},
|
|
415
|
+
inputElement: inputElement.asReadonly(),
|
|
416
|
+
setInputElement(el) {
|
|
417
|
+
inputElement.set(el);
|
|
418
|
+
},
|
|
419
|
+
inputLayout: inputLayout.asReadonly(),
|
|
420
|
+
setInputLayout(layout) {
|
|
421
|
+
inputLayout.set(layout);
|
|
422
|
+
},
|
|
423
|
+
openedByTouch: openedByTouch.asReadonly(),
|
|
424
|
+
setOpenedByTouch(value) {
|
|
425
|
+
openedByTouch.set(value);
|
|
426
|
+
},
|
|
427
|
+
popupMounted: popupMounted.asReadonly(),
|
|
428
|
+
setPopupMounted(value) {
|
|
429
|
+
popupMounted.set(value);
|
|
430
|
+
},
|
|
431
|
+
get triggerElement() {
|
|
432
|
+
return triggerElement;
|
|
433
|
+
},
|
|
434
|
+
setTrigger(el) {
|
|
435
|
+
triggerElement = el;
|
|
436
|
+
},
|
|
437
|
+
orderedItems,
|
|
438
|
+
visibleItems,
|
|
439
|
+
visibleCount,
|
|
440
|
+
filteredItems,
|
|
441
|
+
isVisible,
|
|
442
|
+
registerItem(item) {
|
|
443
|
+
_items.update((items) => [...items, item]);
|
|
444
|
+
},
|
|
445
|
+
unregisterItem(item) {
|
|
446
|
+
_items.update((items) => items.filter((i) => i !== item));
|
|
447
|
+
},
|
|
448
|
+
highlight,
|
|
449
|
+
highlightedItem,
|
|
450
|
+
highlightedIndex: highlightedIndex.asReadonly(),
|
|
451
|
+
activeId,
|
|
452
|
+
itemId,
|
|
453
|
+
setHighlightReason(reason) {
|
|
454
|
+
highlightReason.set(reason);
|
|
455
|
+
},
|
|
456
|
+
setPendingHighlightEdge(edge) {
|
|
457
|
+
pendingHighlightEdge.set(edge);
|
|
458
|
+
},
|
|
459
|
+
/** Transient inline-completion preview (autocomplete inline modes); `null` when inactive. */
|
|
460
|
+
inlinePreview: inlinePreview.asReadonly(),
|
|
461
|
+
/** Suppress inline completion for the current edit (e.g. while a delete key is held). */
|
|
462
|
+
setSuppressInline(value) {
|
|
463
|
+
suppressInline = value;
|
|
464
|
+
},
|
|
465
|
+
/** Clear the inline preview synchronously (on select / clear / close, before the effect runs). */
|
|
466
|
+
clearInlinePreview() {
|
|
467
|
+
inlinePreview.set(null);
|
|
468
|
+
},
|
|
469
|
+
highlightNext(reason = 'keyboard') {
|
|
470
|
+
highlightReason.set(reason);
|
|
471
|
+
if (config.virtualized()) {
|
|
472
|
+
stepIndex(1);
|
|
473
|
+
}
|
|
474
|
+
else if (config.grid()) {
|
|
475
|
+
gridVertical(1);
|
|
476
|
+
}
|
|
477
|
+
else {
|
|
478
|
+
highlight.next();
|
|
479
|
+
}
|
|
480
|
+
},
|
|
481
|
+
highlightPrevious(reason = 'keyboard') {
|
|
482
|
+
highlightReason.set(reason);
|
|
483
|
+
if (config.virtualized()) {
|
|
484
|
+
stepIndex(-1);
|
|
485
|
+
}
|
|
486
|
+
else if (config.grid()) {
|
|
487
|
+
gridVertical(-1);
|
|
488
|
+
}
|
|
489
|
+
else {
|
|
490
|
+
highlight.previous();
|
|
491
|
+
}
|
|
492
|
+
},
|
|
493
|
+
// Grid horizontal moves (next/previous cell in DOM order). No-op outside grid mode.
|
|
494
|
+
highlightNextColumn(reason = 'keyboard') {
|
|
495
|
+
if (!config.grid() || config.virtualized()) {
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
highlightReason.set(reason);
|
|
499
|
+
highlight.next();
|
|
500
|
+
},
|
|
501
|
+
highlightPreviousColumn(reason = 'keyboard') {
|
|
502
|
+
if (!config.grid() || config.virtualized()) {
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
highlightReason.set(reason);
|
|
506
|
+
highlight.previous();
|
|
507
|
+
},
|
|
508
|
+
highlightFirst(reason = 'keyboard') {
|
|
509
|
+
highlightReason.set(reason);
|
|
510
|
+
if (config.virtualized()) {
|
|
511
|
+
highlightedIndex.set(filteredItems().length > 0 ? 0 : -1);
|
|
512
|
+
}
|
|
513
|
+
else {
|
|
514
|
+
highlight.first();
|
|
515
|
+
}
|
|
516
|
+
},
|
|
517
|
+
highlightLast(reason = 'keyboard') {
|
|
518
|
+
highlightReason.set(reason);
|
|
519
|
+
if (config.virtualized()) {
|
|
520
|
+
const length = filteredItems().length;
|
|
521
|
+
highlightedIndex.set(length > 0 ? length - 1 : -1);
|
|
522
|
+
}
|
|
523
|
+
else {
|
|
524
|
+
highlight.last();
|
|
525
|
+
}
|
|
526
|
+
},
|
|
527
|
+
highlightIndex(index, reason) {
|
|
528
|
+
if (index < 0 || index >= filteredItems().length) {
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
highlightReason.set(reason);
|
|
532
|
+
highlightedIndex.set(index);
|
|
533
|
+
},
|
|
534
|
+
setHighlight(item, reason) {
|
|
535
|
+
highlightReason.set(reason);
|
|
536
|
+
highlight.set(item);
|
|
537
|
+
},
|
|
538
|
+
clearHighlightState() {
|
|
539
|
+
highlight.clear();
|
|
540
|
+
highlightedIndex.set(-1);
|
|
541
|
+
},
|
|
542
|
+
isKeyboardActive() {
|
|
543
|
+
return keyboardActive;
|
|
544
|
+
},
|
|
545
|
+
setKeyboardActive(value) {
|
|
546
|
+
keyboardActive = value;
|
|
547
|
+
},
|
|
548
|
+
transitionStatus: transition.status,
|
|
549
|
+
registerTransitionElement: transition.registerElement,
|
|
550
|
+
focusInput() {
|
|
551
|
+
inputElement()?.focus();
|
|
552
|
+
},
|
|
553
|
+
selectInputText() {
|
|
554
|
+
inputElement()?.select();
|
|
555
|
+
},
|
|
556
|
+
/**
|
|
557
|
+
* Restore focus after a selection: the input when it sits outside the popup, otherwise the
|
|
558
|
+
* trigger. Skipped when the consumer moved focus during the `onValueChange` callback — pass the
|
|
559
|
+
* `document.activeElement` captured *before* the emit so we don't clobber a consumer's choice
|
|
560
|
+
* (e.g. focusing an external message field after inserting an emoji).
|
|
561
|
+
*/
|
|
562
|
+
restoreFocusAfterSelect(previousActiveElement) {
|
|
563
|
+
if (typeof document !== 'undefined' && document.activeElement !== previousActiveElement) {
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
const input = inputElement();
|
|
567
|
+
if (input && !input.closest(config.popupSelector)) {
|
|
568
|
+
input.focus();
|
|
569
|
+
}
|
|
570
|
+
else {
|
|
571
|
+
triggerElement?.focus();
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
/** DOM-order comparator for two elements (precedes → -1, follows → 1). */
|
|
577
|
+
function domOrder(a, b) {
|
|
578
|
+
const position = a.compareDocumentPosition(b);
|
|
579
|
+
if (position & Node.DOCUMENT_POSITION_FOLLOWING) {
|
|
580
|
+
return -1;
|
|
581
|
+
}
|
|
582
|
+
if (position & Node.DOCUMENT_POSITION_PRECEDING) {
|
|
583
|
+
return 1;
|
|
584
|
+
}
|
|
585
|
+
return 0;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// The engine stays private to the root (it owns mutable internals); the context factory — a free
|
|
589
|
+
// function, so it can't reach a `private` field — reads it through this registry instead.
|
|
590
|
+
const engineRegistry = new WeakMap();
|
|
55
591
|
const context = () => {
|
|
56
592
|
const root = inject(RdxComboboxRoot);
|
|
593
|
+
const engine = engineRegistry.get(root);
|
|
57
594
|
return {
|
|
58
|
-
listId:
|
|
59
|
-
labelId:
|
|
60
|
-
setLabelId: (id) =>
|
|
595
|
+
listId: engine.listId,
|
|
596
|
+
labelId: engine.labelId,
|
|
597
|
+
setLabelId: (id) => engine.setLabelId(id),
|
|
61
598
|
dir: root.dir,
|
|
62
599
|
value: root.value,
|
|
63
600
|
inputValue: root.inputValue,
|
|
64
601
|
open: root.open,
|
|
602
|
+
present: root.present,
|
|
65
603
|
multiple: root.multiple,
|
|
66
604
|
selectionMode: root.mode,
|
|
67
605
|
disabledState: root.disabledState,
|
|
68
|
-
readonly: root.
|
|
606
|
+
readonly: root.readOnly,
|
|
69
607
|
requiredState: root.requiredState,
|
|
70
608
|
openOnInputClick: root.openOnInputClick,
|
|
71
609
|
modal: root.modal,
|
|
72
610
|
virtualized: root.virtualized,
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
611
|
+
grid: root.grid,
|
|
612
|
+
filteredItems: engine.filteredItems,
|
|
613
|
+
highlightedItem: engine.highlightedItem,
|
|
614
|
+
highlightedIndex: engine.highlightedIndex,
|
|
615
|
+
activeId: engine.activeId,
|
|
616
|
+
itemId: (index) => engine.itemId(index),
|
|
617
|
+
isKeyboardActive: () => engine.isKeyboardActive(),
|
|
618
|
+
setKeyboardActive: (value) => engine.setKeyboardActive(value),
|
|
619
|
+
transitionStatus: engine.transitionStatus,
|
|
620
|
+
registerTransitionElement: engine.registerTransitionElement,
|
|
621
|
+
visibleCount: engine.visibleCount,
|
|
622
|
+
isVisible: (item) => engine.isVisible(item),
|
|
84
623
|
isSelected: (value) => root.isSelected(value),
|
|
85
|
-
registerItem: (item) =>
|
|
86
|
-
unregisterItem: (item) =>
|
|
87
|
-
highlight:
|
|
88
|
-
highlightNext: () =>
|
|
89
|
-
highlightPrevious: () =>
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
624
|
+
registerItem: (item) => engine.registerItem(item),
|
|
625
|
+
unregisterItem: (item) => engine.unregisterItem(item),
|
|
626
|
+
highlight: engine.highlight,
|
|
627
|
+
highlightNext: () => engine.highlightNext('keyboard'),
|
|
628
|
+
highlightPrevious: () => engine.highlightPrevious('keyboard'),
|
|
629
|
+
highlightNextColumn: () => engine.highlightNextColumn('keyboard'),
|
|
630
|
+
highlightPreviousColumn: () => engine.highlightPreviousColumn('keyboard'),
|
|
631
|
+
highlightFirst: () => engine.highlightFirst('keyboard'),
|
|
632
|
+
highlightLast: () => engine.highlightLast('keyboard'),
|
|
633
|
+
highlightIndex: (index, reason) => engine.highlightIndex(index, reason),
|
|
634
|
+
setHighlight: (item, reason) => engine.setHighlight(item, reason),
|
|
635
|
+
clearHighlight: () => engine.clearHighlightState(),
|
|
95
636
|
highlightItemOnHover: root.highlightItemOnHover,
|
|
96
637
|
keepHighlight: root.keepHighlight,
|
|
97
|
-
inputElement:
|
|
98
|
-
setInputElement: (el) =>
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
638
|
+
inputElement: engine.inputElement,
|
|
639
|
+
setInputElement: (el) => engine.setInputElement(el),
|
|
640
|
+
inputLayout: engine.inputLayout,
|
|
641
|
+
setInputLayout: (layout) => engine.setInputLayout(layout),
|
|
642
|
+
openedByTouch: engine.openedByTouch,
|
|
643
|
+
setOpenedByTouch: (value) => engine.setOpenedByTouch(value),
|
|
644
|
+
popupMounted: engine.popupMounted,
|
|
645
|
+
setPopupMounted: (value) => engine.setPopupMounted(value),
|
|
646
|
+
registerTrigger: (el) => engine.setTrigger(el),
|
|
647
|
+
focusInput: () => engine.focusInput(),
|
|
648
|
+
openPopup: (reason, event) => root.setOpen(true, reason, event),
|
|
649
|
+
openForBrowse: (reason, event) => root.openForBrowse(reason, event),
|
|
650
|
+
closePopup: (revert = true, reason, event) => root.closePopup(revert, reason, event),
|
|
104
651
|
setInputValue: (value) => root.setInputValue(value),
|
|
105
|
-
openAndHighlight: (edge) => root.openAndHighlight(edge),
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
652
|
+
openAndHighlight: (edge, reason, event) => root.openAndHighlight(edge, reason, event),
|
|
653
|
+
navigateByKeyboard: (direction, event) => root.navigateByKeyboard(direction, event),
|
|
654
|
+
select: (item, event) => root.handleSelect(item, event),
|
|
655
|
+
selectIndex: (index, event) => root.selectIndex(index, event),
|
|
656
|
+
selectHighlighted: (event) => root.selectHighlighted(event),
|
|
109
657
|
clearSelection: () => root.clearSelection(),
|
|
110
658
|
removeValue: (value) => root.removeValue(value),
|
|
111
659
|
removeLastValue: () => root.removeLastValue(),
|
|
@@ -128,15 +676,45 @@ function coerceAutoHighlight(value) {
|
|
|
128
676
|
return value === 'always' || value === 'input-change' ? value : booleanAttribute(value);
|
|
129
677
|
}
|
|
130
678
|
/**
|
|
131
|
-
* Root of a Combobox — a filterable select. Owns selection, input text, open state,
|
|
132
|
-
* highlight-model navigation
|
|
133
|
-
*
|
|
679
|
+
* Root of a Combobox — a filterable select. Owns selection, input text, open state, and the forms
|
|
680
|
+
* integration, and delegates filtering / highlight-model navigation / the open-close transition to the
|
|
681
|
+
* shared {@link useComboboxEngine} (ADR 0014). Exposes everything to the parts through
|
|
682
|
+
* {@link RdxComboboxRootContext}. Implements `ControlValueAccessor`.
|
|
134
683
|
*
|
|
135
684
|
* @group Components
|
|
136
685
|
*/
|
|
137
686
|
class RdxComboboxRoot {
|
|
687
|
+
/** The list element id referenced by `aria-controls` / `aria-activedescendant` (engine-backed). */
|
|
688
|
+
get listId() {
|
|
689
|
+
return this.engine.listId;
|
|
690
|
+
}
|
|
691
|
+
/** The currently highlighted item (engine-backed; read by parts and tests). */
|
|
692
|
+
get highlightedItem() {
|
|
693
|
+
return this.engine.highlightedItem;
|
|
694
|
+
}
|
|
695
|
+
/** Number of items the list currently shows (engine-backed). */
|
|
696
|
+
get visibleCount() {
|
|
697
|
+
return this.engine.visibleCount;
|
|
698
|
+
}
|
|
699
|
+
/** The filtered item values an external virtualizer should render (engine-backed). */
|
|
700
|
+
get filteredItems() {
|
|
701
|
+
return this.engine.filteredItems;
|
|
702
|
+
}
|
|
703
|
+
/** Highlighted index into {@link filteredItems} in virtualized mode (engine-backed). */
|
|
704
|
+
get highlightedIndex() {
|
|
705
|
+
return this.engine.highlightedIndex;
|
|
706
|
+
}
|
|
707
|
+
/** The active option's id for `aria-activedescendant` (engine-backed). */
|
|
708
|
+
get activeId() {
|
|
709
|
+
return this.engine.activeId;
|
|
710
|
+
}
|
|
138
711
|
constructor() {
|
|
139
712
|
this.injector = inject(Injector);
|
|
713
|
+
/** Per-popup floating root context (ADR 0015) — `open` / `triggers` / reference for the dismissal engine. */
|
|
714
|
+
this.floatingContext = createFloatingRootContext({
|
|
715
|
+
ownerDocument: inject(ElementRef).nativeElement.ownerDocument,
|
|
716
|
+
open: () => this.open()
|
|
717
|
+
});
|
|
140
718
|
/** Selected value(s). A single value in single mode, an array in `multiple` mode. */
|
|
141
719
|
this.value = model(null, ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
|
|
142
720
|
/** Initial value when uncontrolled. */
|
|
@@ -161,11 +739,12 @@ class RdxComboboxRoot {
|
|
|
161
739
|
/** In `'none'` mode, whether pressing an item fills the input with its label. */
|
|
162
740
|
this.fillInputOnItemPress = input(true, { ...(ngDevMode ? { debugName: "fillInputOnItemPress" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
163
741
|
/** Text direction. */
|
|
164
|
-
this.
|
|
742
|
+
this.dirInput = input(undefined, { ...(ngDevMode ? { debugName: "dirInput" } : /* istanbul ignore next */ {}), alias: 'dir' });
|
|
743
|
+
this.dir = injectDirection(this.dirInput);
|
|
165
744
|
/** Whether the combobox is disabled. */
|
|
166
745
|
this.disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
167
|
-
/** Whether the combobox is read-only. */
|
|
168
|
-
this.
|
|
746
|
+
/** Whether the combobox is read-only. Base UI prop name. */
|
|
747
|
+
this.readOnly = input(false, { ...(ngDevMode ? { debugName: "readOnly" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
169
748
|
/** Whether a value is required (for forms). */
|
|
170
749
|
this.required = input(false, { ...(ngDevMode ? { debugName: "required" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
171
750
|
/** Whether keyboard navigation wraps at the list boundaries. */
|
|
@@ -228,8 +807,14 @@ class RdxComboboxRoot {
|
|
|
228
807
|
* items outside the rendered window are not skipped by keyboard navigation.
|
|
229
808
|
*/
|
|
230
809
|
this.virtualized = input(false, { ...(ngDevMode ? { debugName: "virtualized" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
231
|
-
/**
|
|
232
|
-
|
|
810
|
+
/**
|
|
811
|
+
* Whether the list is laid out as a 2D grid: `ArrowUp`/`ArrowDown` move between rows (keeping the
|
|
812
|
+
* column), `ArrowLeft`/`ArrowRight` move within a row. Wrap items in `RdxComboboxRow`; the list
|
|
813
|
+
* switches to `role="grid"`. Not supported together with {@link virtualized}.
|
|
814
|
+
*/
|
|
815
|
+
this.grid = input(false, { ...(ngDevMode ? { debugName: "grid" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
816
|
+
/** How item values are compared for equality (a comparator function or an object key). Base UI prop name. */
|
|
817
|
+
this.isItemEqualToValue = input(...(ngDevMode ? [undefined, { debugName: "isItemEqualToValue" }] : /* istanbul ignore next */ []));
|
|
233
818
|
/** Converts a value to its display label. Defaults to the matching item's text. */
|
|
234
819
|
this.itemToStringLabel = input(...(ngDevMode ? [undefined, { debugName: "itemToStringLabel" }] : /* istanbul ignore next */ []));
|
|
235
820
|
/** Emits when the selection changes. */
|
|
@@ -245,97 +830,51 @@ class RdxComboboxRoot {
|
|
|
245
830
|
this.onItemHighlighted = output();
|
|
246
831
|
/** Emits after the open/close transition (including any exit animation) finishes. */
|
|
247
832
|
this.onOpenChangeComplete = output();
|
|
248
|
-
this.transition = useTransitionStatus((open) => this.onOpenChangeComplete.emit(open));
|
|
249
|
-
/** Open/close transition phase, for `data-starting-style` / `data-ending-style`. */
|
|
250
|
-
this.transitionStatus = this.transition.status;
|
|
251
|
-
/** Registers the popup element whose animation determines transition completion. */
|
|
252
|
-
this.registerTransitionElement = this.transition.registerElement;
|
|
253
|
-
this.listId = injectId('rdx-combobox-list-');
|
|
254
|
-
this.labelId = signal(undefined, ...(ngDevMode ? [{ debugName: "labelId" }] : /* istanbul ignore next */ []));
|
|
255
|
-
this.inputElement = signal(null, ...(ngDevMode ? [{ debugName: "inputElement" }] : /* istanbul ignore next */ []));
|
|
256
833
|
this.cvaDisabled = signal(false, ...(ngDevMode ? [{ debugName: "cvaDisabled" }] : /* istanbul ignore next */ []));
|
|
257
834
|
this.disabledState = computed(() => this.disabled() || this.cvaDisabled(), ...(ngDevMode ? [{ debugName: "disabledState" }] : /* istanbul ignore next */ []));
|
|
258
835
|
this.requiredState = computed(() => this.required(), ...(ngDevMode ? [{ debugName: "requiredState" }] : /* istanbul ignore next */ []));
|
|
259
|
-
this.
|
|
836
|
+
this.preventUnmountOnClose = signal(false, ...(ngDevMode ? [{ debugName: "preventUnmountOnClose" }] : /* istanbul ignore next */ []));
|
|
837
|
+
this.present = computed(() => this.open() || this.preventUnmountOnClose(), ...(ngDevMode ? [{ debugName: "present" }] : /* istanbul ignore next */ []));
|
|
260
838
|
/**
|
|
261
839
|
* Whether the input text is a fresh user query rather than the current selection's label. While
|
|
262
840
|
* `false` (just opened, or showing a selected label), the list is unfiltered so the user can
|
|
263
841
|
* browse; it flips `true` on the first keystroke.
|
|
264
842
|
*/
|
|
265
843
|
this.typed = signal(false, ...(ngDevMode ? [{ debugName: "typed" }] : /* istanbul ignore next */ []));
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
this.filteredItems = computed(() => {
|
|
294
|
-
const data = this.items();
|
|
295
|
-
if (data === undefined) {
|
|
296
|
-
return this.visibleItems().map((item) => item.value());
|
|
297
|
-
}
|
|
298
|
-
const limit = this.limit();
|
|
299
|
-
const cap = (arr) => (limit >= 0 ? arr.slice(0, limit) : arr);
|
|
300
|
-
const filter = this.filter();
|
|
301
|
-
if (filter === null) {
|
|
302
|
-
return cap(data);
|
|
303
|
-
}
|
|
304
|
-
const query = this.typed() ? (this.inputValue() ?? '') : '';
|
|
305
|
-
if (!query) {
|
|
306
|
-
return cap(data);
|
|
307
|
-
}
|
|
308
|
-
const matcher = filter ?? this.defaultFilter.contains;
|
|
309
|
-
return cap(data.filter((value) => matcher(this.textFor(value), query)));
|
|
310
|
-
}, ...(ngDevMode ? [{ debugName: "filteredItems" }] : /* istanbul ignore next */ []));
|
|
311
|
-
this.visibleCount = computed(() => this.virtualized() ? this.filteredItems().length : this.visibleItems().length, ...(ngDevMode ? [{ debugName: "visibleCount" }] : /* istanbul ignore next */ []));
|
|
312
|
-
this.highlight = useListHighlight({
|
|
313
|
-
items: this.orderedItems,
|
|
314
|
-
isNavigable: (item) => this.isVisible(item) && !item.disabled(),
|
|
315
|
-
getId: (item) => item.id,
|
|
316
|
-
loop: this.loopFocus,
|
|
317
|
-
injector: this.injector
|
|
844
|
+
/** The active query: the typed text once the user starts typing, otherwise empty (browse mode). */
|
|
845
|
+
this.query = computed(() => (this.typed() ? (this.inputValue() ?? '') : ''), ...(ngDevMode ? [{ debugName: "query" }] : /* istanbul ignore next */ []));
|
|
846
|
+
/** Built-in filtering always applies for combobox (the `none` filter is handled inside the engine). */
|
|
847
|
+
this.filteringEnabled = signal(true, ...(ngDevMode ? [{ debugName: "filteringEnabled" }] : /* istanbul ignore next */ []));
|
|
848
|
+
/** Combobox never inline-completes (that's an autocomplete mode). */
|
|
849
|
+
this.noInline = signal(false, ...(ngDevMode ? [{ debugName: "noInline" }] : /* istanbul ignore next */ []));
|
|
850
|
+
/** The shared engine: item registry, filtering, highlight navigation, and the open-close transition. */
|
|
851
|
+
this.engine = useComboboxEngine({
|
|
852
|
+
injector: this.injector,
|
|
853
|
+
listIdPrefix: 'rdx-combobox-list-',
|
|
854
|
+
popupSelector: '[rdxComboboxPopup]',
|
|
855
|
+
open: this.open,
|
|
856
|
+
query: this.query,
|
|
857
|
+
filteringEnabled: this.filteringEnabled,
|
|
858
|
+
loopFocus: this.loopFocus,
|
|
859
|
+
autoHighlightMode: this.autoHighlightMode,
|
|
860
|
+
virtualized: this.virtualized,
|
|
861
|
+
items: this.items,
|
|
862
|
+
filter: this.filter,
|
|
863
|
+
limit: this.limit,
|
|
864
|
+
grid: this.grid,
|
|
865
|
+
rowOf: (element) => element.closest('[rdxComboboxRow]'),
|
|
866
|
+
inlineMode: this.noInline,
|
|
867
|
+
itemToString: (value) => this.labelFor(value),
|
|
868
|
+
onItemHighlighted: (details) => this.onItemHighlighted.emit(details),
|
|
869
|
+
onOpenChange: () => { },
|
|
870
|
+
onOpenChangeComplete: (open) => this.onOpenChangeComplete.emit(open)
|
|
318
871
|
});
|
|
319
|
-
this.highlightedItem = this.highlight.highlightedItem;
|
|
320
|
-
/** Highlighted index into {@link filteredItems} in virtualized mode (`-1` when cleared). */
|
|
321
|
-
this.highlightedIndex = signal(-1, ...(ngDevMode ? [{ debugName: "highlightedIndex" }] : /* istanbul ignore next */ []));
|
|
322
|
-
/** Why the highlight last moved; read when emitting {@link onItemHighlighted}. */
|
|
323
|
-
this.highlightReason = signal('none', ...(ngDevMode ? [{ debugName: "highlightReason" }] : /* istanbul ignore next */ []));
|
|
324
|
-
this.activeId = computed(() => {
|
|
325
|
-
if (this.virtualized()) {
|
|
326
|
-
const index = this.highlightedIndex();
|
|
327
|
-
return index >= 0 ? this.itemId(index) : undefined;
|
|
328
|
-
}
|
|
329
|
-
return this.highlight.activeId();
|
|
330
|
-
}, ...(ngDevMode ? [{ debugName: "activeId" }] : /* istanbul ignore next */ []));
|
|
331
|
-
/** Edge to highlight once the list has mounted (items register asynchronously after opening). */
|
|
332
|
-
this.pendingHighlightEdge = signal(null, ...(ngDevMode ? [{ debugName: "pendingHighlightEdge" }] : /* istanbul ignore next */ []));
|
|
333
|
-
// Tracks whether the last interaction was the keyboard, so the highlight doesn't jump to an item
|
|
334
|
-
// the cursor happens to rest on when arrow-key navigation scrolls the list under a still pointer.
|
|
335
|
-
this.keyboardActive = false;
|
|
336
|
-
/** The trigger element, used as a focus fallback when the input lives inside the popup. */
|
|
337
|
-
this.triggerElement = null;
|
|
338
872
|
this.chipsFocusLast = null;
|
|
873
|
+
// Expose the (private) engine to the context factory, which is a free function.
|
|
874
|
+
engineRegistry.set(this, this.engine);
|
|
875
|
+
// Keep the dismissal reference in sync with the input (the anchor) so a press / focus on it counts
|
|
876
|
+
// as "inside" and never dismisses (ADR 0015).
|
|
877
|
+
effect(() => this.floatingContext.setReferenceElement(this.engine.inputElement() ?? null));
|
|
339
878
|
// Apply uncontrolled defaults once.
|
|
340
879
|
effect(() => {
|
|
341
880
|
const initial = this.defaultValue();
|
|
@@ -348,105 +887,6 @@ class RdxComboboxRoot {
|
|
|
348
887
|
this.open.set(true);
|
|
349
888
|
}
|
|
350
889
|
});
|
|
351
|
-
// Emit open changes and drive the open/close transition (skip the initial run).
|
|
352
|
-
let previousOpen = untracked(this.open);
|
|
353
|
-
effect(() => {
|
|
354
|
-
const open = this.open();
|
|
355
|
-
if (open === previousOpen) {
|
|
356
|
-
return;
|
|
357
|
-
}
|
|
358
|
-
previousOpen = open;
|
|
359
|
-
untracked(() => {
|
|
360
|
-
this.onOpenChange.emit(open);
|
|
361
|
-
this.transition.start(open);
|
|
362
|
-
});
|
|
363
|
-
});
|
|
364
|
-
// Emit highlight changes (skip the initial run). Tracks both the DOM-ref highlight and the
|
|
365
|
-
// virtualized index; only one is active per mode, so the other never fires spuriously.
|
|
366
|
-
let highlightInitialized = false;
|
|
367
|
-
effect(() => {
|
|
368
|
-
const item = this.highlightedItem();
|
|
369
|
-
const index = this.highlightedIndex();
|
|
370
|
-
if (!highlightInitialized) {
|
|
371
|
-
highlightInitialized = true;
|
|
372
|
-
return;
|
|
373
|
-
}
|
|
374
|
-
untracked(() => {
|
|
375
|
-
const reason = this.highlightReason();
|
|
376
|
-
if (this.virtualized()) {
|
|
377
|
-
const value = index >= 0 ? (this.filteredItems()[index] ?? null) : null;
|
|
378
|
-
this.onItemHighlighted.emit({ value, index, reason });
|
|
379
|
-
}
|
|
380
|
-
else {
|
|
381
|
-
const value = item ? item.value() : null;
|
|
382
|
-
const itemIndex = item ? this.visibleItems().indexOf(item) : -1;
|
|
383
|
-
this.onItemHighlighted.emit({ value, index: itemIndex, reason });
|
|
384
|
-
}
|
|
385
|
-
});
|
|
386
|
-
});
|
|
387
|
-
// Apply a deferred open-edge highlight once items (DOM refs) or filtered data have registered.
|
|
388
|
-
effect(() => {
|
|
389
|
-
const edge = this.pendingHighlightEdge();
|
|
390
|
-
const count = this.virtualized() ? this.filteredItems().length : this.orderedItems().length;
|
|
391
|
-
if (!this.open() || edge === null || count === 0) {
|
|
392
|
-
return;
|
|
393
|
-
}
|
|
394
|
-
untracked(() => {
|
|
395
|
-
// Programmatic move — reset the reason in both modes so the emit reports 'none', not a
|
|
396
|
-
// stale 'keyboard'/'pointer' left by the previous user interaction.
|
|
397
|
-
this.highlightReason.set('none');
|
|
398
|
-
if (this.virtualized()) {
|
|
399
|
-
this.highlightedIndex.set(edge === 'first' ? 0 : count - 1);
|
|
400
|
-
}
|
|
401
|
-
else if (edge === 'first') {
|
|
402
|
-
this.highlight.first();
|
|
403
|
-
}
|
|
404
|
-
else {
|
|
405
|
-
this.highlight.last();
|
|
406
|
-
}
|
|
407
|
-
this.pendingHighlightEdge.set(null);
|
|
408
|
-
});
|
|
409
|
-
});
|
|
410
|
-
// autoHighlight 'always': keep the first navigable item highlighted whenever the popup is
|
|
411
|
-
// open. `visibleCount` re-runs this when filtering changes; the current highlight is read
|
|
412
|
-
// untracked to re-establish a highlight only after the self-heal clears it (no loop).
|
|
413
|
-
effect(() => {
|
|
414
|
-
this.orderedItems();
|
|
415
|
-
this.visibleCount();
|
|
416
|
-
if (this.autoHighlightMode() === 'always' && this.open()) {
|
|
417
|
-
untracked(() => {
|
|
418
|
-
if (this.virtualized()) {
|
|
419
|
-
// Re-seed when the index is cleared OR has fallen out of range, so this works
|
|
420
|
-
// regardless of whether the self-heal effect ran first (no ordering dependency).
|
|
421
|
-
const length = this.filteredItems().length;
|
|
422
|
-
const index = this.highlightedIndex();
|
|
423
|
-
if ((index < 0 || index >= length) && length > 0) {
|
|
424
|
-
this.highlightReason.set('none');
|
|
425
|
-
this.highlightedIndex.set(0);
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
else if (this.highlightedItem() === null) {
|
|
429
|
-
this.highlightReason.set('none');
|
|
430
|
-
this.highlight.first();
|
|
431
|
-
}
|
|
432
|
-
});
|
|
433
|
-
}
|
|
434
|
-
});
|
|
435
|
-
// Virtualized self-heal: clear a highlight that filtering has pushed out of range, so
|
|
436
|
-
// `activeId` never references an index past the end of the filtered list.
|
|
437
|
-
effect(() => {
|
|
438
|
-
if (!this.virtualized()) {
|
|
439
|
-
return;
|
|
440
|
-
}
|
|
441
|
-
const length = this.filteredItems().length;
|
|
442
|
-
untracked(() => {
|
|
443
|
-
const index = this.highlightedIndex();
|
|
444
|
-
if (index >= length && index !== -1) {
|
|
445
|
-
this.highlightReason.set('none');
|
|
446
|
-
this.highlightedIndex.set(-1);
|
|
447
|
-
}
|
|
448
|
-
});
|
|
449
|
-
});
|
|
450
890
|
// Virtualized object values can't be labelled from the DOM (items aren't registered) — without
|
|
451
891
|
// `itemToStringLabel`, selection/revert fall back to a generic label. Warn once in dev.
|
|
452
892
|
if (isDevMode()) {
|
|
@@ -464,46 +904,41 @@ class RdxComboboxRoot {
|
|
|
464
904
|
}
|
|
465
905
|
}
|
|
466
906
|
/** Opens the popup for browsing (resets the query to "pristine" and selects the input text). */
|
|
467
|
-
openForBrowse() {
|
|
907
|
+
openForBrowse(reason = 'none', event = new Event('combobox.open-change')) {
|
|
468
908
|
if (!this.open()) {
|
|
469
909
|
this.typed.set(false);
|
|
470
910
|
}
|
|
471
|
-
this.setOpen(true);
|
|
472
|
-
this.selectInputText();
|
|
911
|
+
this.setOpen(true, reason, event);
|
|
912
|
+
this.engine.selectInputText();
|
|
473
913
|
if (this.autoHighlightMode() === 'always') {
|
|
474
|
-
this.
|
|
914
|
+
this.engine.setPendingHighlightEdge('first');
|
|
475
915
|
}
|
|
476
916
|
}
|
|
477
917
|
/** Opens the popup and highlights the given edge once the list mounts. */
|
|
478
|
-
openAndHighlight(edge) {
|
|
918
|
+
openAndHighlight(edge, reason = 'list-navigation', event = new Event('combobox.open-change')) {
|
|
479
919
|
if (!this.open()) {
|
|
480
920
|
this.typed.set(false);
|
|
481
921
|
}
|
|
482
|
-
this.setOpen(true);
|
|
483
|
-
this.selectInputText();
|
|
484
|
-
this.
|
|
922
|
+
this.setOpen(true, reason, event);
|
|
923
|
+
this.engine.selectInputText();
|
|
924
|
+
this.engine.setPendingHighlightEdge(edge);
|
|
485
925
|
}
|
|
486
|
-
/**
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
926
|
+
/**
|
|
927
|
+
* Keyboard list navigation shared by the input and the chips: opens the popup and highlights the
|
|
928
|
+
* leading/trailing edge when closed, otherwise steps the highlight. `direction` is `1` (down) or
|
|
929
|
+
* `-1` (up). Centralized so the input and chip key handlers can't drift apart.
|
|
930
|
+
*/
|
|
931
|
+
navigateByKeyboard(direction, event = new Event('combobox.open-change')) {
|
|
932
|
+
this.engine.setKeyboardActive(true);
|
|
933
|
+
if (!this.open()) {
|
|
934
|
+
this.openAndHighlight(direction === 1 ? 'first' : 'last', 'list-navigation', event);
|
|
935
|
+
}
|
|
936
|
+
else if (direction === 1) {
|
|
937
|
+
this.engine.highlightNext();
|
|
938
|
+
}
|
|
939
|
+
else {
|
|
940
|
+
this.engine.highlightPrevious();
|
|
491
941
|
}
|
|
492
|
-
// Until the user types a fresh query, show the whole list (the input may still hold the
|
|
493
|
-
// selected item's label, which must not filter everything down to just that item).
|
|
494
|
-
const query = this.typed() ? (this.inputValue() ?? '') : '';
|
|
495
|
-
const matcher = filter ?? this.defaultFilter.contains;
|
|
496
|
-
return matcher(item.textValue(), query);
|
|
497
|
-
}
|
|
498
|
-
/** Whether the item is shown in the list (matches the query and is within `limit`). */
|
|
499
|
-
isVisible(item) {
|
|
500
|
-
return this.visibleSet().has(item);
|
|
501
|
-
}
|
|
502
|
-
isKeyboardActive() {
|
|
503
|
-
return this.keyboardActive;
|
|
504
|
-
}
|
|
505
|
-
setKeyboardActive(value) {
|
|
506
|
-
this.keyboardActive = value;
|
|
507
942
|
}
|
|
508
943
|
isSelected(value) {
|
|
509
944
|
if (this.mode() === 'none') {
|
|
@@ -511,28 +946,34 @@ class RdxComboboxRoot {
|
|
|
511
946
|
}
|
|
512
947
|
const current = this.value();
|
|
513
948
|
if (this.multiple()) {
|
|
514
|
-
return Array.isArray(current) && current.some((v) => isItemEqualToValue(v, value, this.
|
|
949
|
+
return Array.isArray(current) && current.some((v) => isItemEqualToValue(v, value, this.isItemEqualToValue()));
|
|
515
950
|
}
|
|
516
|
-
return !isNullish(current) && isItemEqualToValue(current, value, this.
|
|
517
|
-
}
|
|
518
|
-
registerItem(item) {
|
|
519
|
-
this._items.update((items) => [...items, item]);
|
|
520
|
-
}
|
|
521
|
-
unregisterItem(item) {
|
|
522
|
-
this._items.update((items) => items.filter((i) => i !== item));
|
|
951
|
+
return !isNullish(current) && isItemEqualToValue(current, value, this.isItemEqualToValue());
|
|
523
952
|
}
|
|
524
|
-
setOpen(open) {
|
|
525
|
-
if (
|
|
526
|
-
return;
|
|
953
|
+
setOpen(open, reason = 'none', event = new Event('combobox.open-change')) {
|
|
954
|
+
if (open === this.open()) {
|
|
955
|
+
return true;
|
|
956
|
+
}
|
|
957
|
+
if (open && (this.disabledState() || this.readOnly())) {
|
|
958
|
+
return false;
|
|
527
959
|
}
|
|
960
|
+
const change = this.createOpenChangeEvent(open, reason, event);
|
|
961
|
+
this.onOpenChange.emit(change.payload);
|
|
962
|
+
if (change.eventDetails.isCanceled()) {
|
|
963
|
+
return false;
|
|
964
|
+
}
|
|
965
|
+
this.preventUnmountOnClose.set(open ? false : change.shouldPreventUnmountOnClose());
|
|
528
966
|
this.open.set(open);
|
|
967
|
+
return true;
|
|
529
968
|
}
|
|
530
|
-
closePopup(revert = true) {
|
|
969
|
+
closePopup(revert = true, reason = 'none', event = new Event('combobox.open-change')) {
|
|
531
970
|
if (!this.open()) {
|
|
532
971
|
return;
|
|
533
972
|
}
|
|
534
|
-
this.
|
|
535
|
-
|
|
973
|
+
if (!this.setOpen(false, reason, event)) {
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
this.engine.clearHighlightState();
|
|
536
977
|
if (revert) {
|
|
537
978
|
this.revertInputValue();
|
|
538
979
|
}
|
|
@@ -543,9 +984,19 @@ class RdxComboboxRoot {
|
|
|
543
984
|
this.inputValue.set(value);
|
|
544
985
|
this.typed.set(true);
|
|
545
986
|
this.onInputValueChange.emit(value);
|
|
987
|
+
// Base UI: emptying the field clears a single selection — but only when the input is OUTSIDE the
|
|
988
|
+
// popup. With the input inside the popup, the search box and the committed value are independent,
|
|
989
|
+
// so clearing the search must not deselect. (multiple keeps its chips; `none` has no committed
|
|
990
|
+
// value.) The guarded `commitValue` is a no-op when read-only / disabled.
|
|
991
|
+
if (value === '' &&
|
|
992
|
+
this.mode() === 'single' &&
|
|
993
|
+
!isNullish(this.value()) &&
|
|
994
|
+
this.engine.inputLayout() !== 'inside') {
|
|
995
|
+
this.commitValue(null);
|
|
996
|
+
}
|
|
546
997
|
// Auto-highlight the first match as the query changes (deferred so it lands after items mount).
|
|
547
998
|
if (this.autoHighlightMode() !== 'off') {
|
|
548
|
-
this.
|
|
999
|
+
this.engine.setPendingHighlightEdge('first');
|
|
549
1000
|
}
|
|
550
1001
|
}
|
|
551
1002
|
/** Sets the input text programmatically (a selection label / revert) — not a user query. */
|
|
@@ -554,10 +1005,6 @@ class RdxComboboxRoot {
|
|
|
554
1005
|
this.typed.set(false);
|
|
555
1006
|
this.onInputValueChange.emit(value);
|
|
556
1007
|
}
|
|
557
|
-
/** Selects all input text so the next keystroke replaces a stale selection label. */
|
|
558
|
-
selectInputText() {
|
|
559
|
-
this.inputElement()?.select();
|
|
560
|
-
}
|
|
561
1008
|
/** Resets the input text to the current selection's label (single mode) or empty. */
|
|
562
1009
|
revertInputValue() {
|
|
563
1010
|
if (this.multiple()) {
|
|
@@ -572,37 +1019,31 @@ class RdxComboboxRoot {
|
|
|
572
1019
|
if (custom) {
|
|
573
1020
|
return custom(value);
|
|
574
1021
|
}
|
|
575
|
-
const item = this.orderedItems().find((i) => isItemEqualToValue(i.value(), value, this.
|
|
1022
|
+
const item = this.engine.orderedItems().find((i) => isItemEqualToValue(i.value(), value, this.isItemEqualToValue()));
|
|
576
1023
|
return item ? item.textValue() : itemToStringLabel(value);
|
|
577
1024
|
}
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
const custom = this.itemToStringLabel();
|
|
581
|
-
return custom ? custom(value) : itemToStringLabel(value);
|
|
582
|
-
}
|
|
583
|
-
/** Deterministic id for the item at `index` in virtualized mode (matches `aria-activedescendant`). */
|
|
584
|
-
itemId(index) {
|
|
585
|
-
return `${this.listId}-item-${index}`;
|
|
586
|
-
}
|
|
587
|
-
handleSelect(item) {
|
|
588
|
-
if (this.disabledState() || this.readonly() || item.disabled()) {
|
|
1025
|
+
handleSelect(item, event = new Event('combobox.item-press')) {
|
|
1026
|
+
if (this.disabledState() || this.readOnly() || item.disabled()) {
|
|
589
1027
|
return;
|
|
590
1028
|
}
|
|
591
|
-
this.handleSelectValue(item.value(), item.textValue() || this.labelFor(item.value()));
|
|
1029
|
+
this.handleSelectValue(item.value(), item.textValue() || this.labelFor(item.value()), event);
|
|
592
1030
|
}
|
|
593
1031
|
/** Selects the filtered item at `index` (virtualized mode). The label comes from {@link labelFor}. */
|
|
594
|
-
selectIndex(index) {
|
|
595
|
-
if (this.disabledState() || this.
|
|
1032
|
+
selectIndex(index, event = new Event('combobox.item-press')) {
|
|
1033
|
+
if (this.disabledState() || this.readOnly()) {
|
|
596
1034
|
return;
|
|
597
1035
|
}
|
|
598
|
-
const value = this.filteredItems()[index];
|
|
1036
|
+
const value = this.engine.filteredItems()[index];
|
|
599
1037
|
if (value === undefined) {
|
|
600
1038
|
return;
|
|
601
1039
|
}
|
|
602
|
-
this.handleSelectValue(value, this.labelFor(value));
|
|
1040
|
+
this.handleSelectValue(value, this.labelFor(value), event);
|
|
603
1041
|
}
|
|
604
1042
|
/** Commits a selection from a resolved value/label, independent of whether a DOM item exists. */
|
|
605
|
-
handleSelectValue(value, textValue) {
|
|
1043
|
+
handleSelectValue(value, textValue, event = new Event('combobox.item-press')) {
|
|
1044
|
+
// Capture focus *before* emitting `onValueChange` so focus restoration can be skipped when the
|
|
1045
|
+
// consumer moves focus in that callback (e.g. focusing an external field after an emoji press).
|
|
1046
|
+
const activeBefore = typeof document !== 'undefined' ? document.activeElement : null;
|
|
606
1047
|
if (this.mode() === 'none') {
|
|
607
1048
|
// No value is committed; `onValueChange` fires as a pointer/keyboard activation signal so
|
|
608
1049
|
// command-palette consumers can react. Optionally fill the input, then close.
|
|
@@ -610,15 +1051,14 @@ class RdxComboboxRoot {
|
|
|
610
1051
|
if (this.fillInputOnItemPress()) {
|
|
611
1052
|
this.setLabel(textValue);
|
|
612
1053
|
}
|
|
613
|
-
this.
|
|
614
|
-
this.
|
|
615
|
-
this.restoreFocusAfterSelect();
|
|
1054
|
+
this.closePopup(false, 'item-press', event);
|
|
1055
|
+
this.engine.restoreFocusAfterSelect(activeBefore);
|
|
616
1056
|
this.maybeSubmit();
|
|
617
1057
|
return;
|
|
618
1058
|
}
|
|
619
1059
|
if (this.multiple()) {
|
|
620
1060
|
const current = Array.isArray(this.value()) ? [...this.value()] : [];
|
|
621
|
-
const index = current.findIndex((v) => isItemEqualToValue(v, value, this.
|
|
1061
|
+
const index = current.findIndex((v) => isItemEqualToValue(v, value, this.isItemEqualToValue()));
|
|
622
1062
|
if (index === -1) {
|
|
623
1063
|
current.push(value);
|
|
624
1064
|
}
|
|
@@ -627,124 +1067,57 @@ class RdxComboboxRoot {
|
|
|
627
1067
|
}
|
|
628
1068
|
this.commitValue(current);
|
|
629
1069
|
this.setLabel('');
|
|
630
|
-
|
|
1070
|
+
// Keep the input focused for adding more values — unless the consumer moved focus.
|
|
1071
|
+
if (typeof document === 'undefined' || document.activeElement === activeBefore) {
|
|
1072
|
+
this.engine.focusInput();
|
|
1073
|
+
}
|
|
631
1074
|
}
|
|
632
1075
|
else {
|
|
633
1076
|
this.commitValue(value);
|
|
634
1077
|
this.setLabel(textValue);
|
|
635
|
-
this.
|
|
636
|
-
this.
|
|
637
|
-
this.restoreFocusAfterSelect();
|
|
1078
|
+
this.closePopup(false, 'item-press', event);
|
|
1079
|
+
this.engine.restoreFocusAfterSelect(activeBefore);
|
|
638
1080
|
}
|
|
639
1081
|
this.maybeSubmit();
|
|
640
1082
|
}
|
|
641
1083
|
/** Requests submit of the closest form when `submitOnItemClick` is enabled. */
|
|
642
1084
|
maybeSubmit() {
|
|
643
1085
|
if (this.submitOnItemClick()) {
|
|
644
|
-
this.inputElement()?.form?.requestSubmit?.();
|
|
1086
|
+
this.engine.inputElement()?.form?.requestSubmit?.();
|
|
645
1087
|
}
|
|
646
1088
|
}
|
|
647
|
-
selectHighlighted() {
|
|
1089
|
+
selectHighlighted(event = new Event('combobox.item-press')) {
|
|
648
1090
|
if (this.virtualized()) {
|
|
649
|
-
const index = this.highlightedIndex();
|
|
1091
|
+
const index = this.engine.highlightedIndex();
|
|
650
1092
|
if (index >= 0) {
|
|
651
|
-
this.selectIndex(index);
|
|
1093
|
+
this.selectIndex(index, event);
|
|
652
1094
|
}
|
|
653
1095
|
return;
|
|
654
1096
|
}
|
|
655
|
-
const item = this.highlightedItem();
|
|
1097
|
+
const item = this.engine.highlightedItem();
|
|
656
1098
|
if (item) {
|
|
657
|
-
this.handleSelect(item);
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
// --- Highlight navigation facade (mode-aware: index-based when virtualized, else DOM-ref) ---
|
|
661
|
-
highlightNext(reason = 'keyboard') {
|
|
662
|
-
this.highlightReason.set(reason);
|
|
663
|
-
if (this.virtualized()) {
|
|
664
|
-
this.stepIndex(1);
|
|
665
|
-
}
|
|
666
|
-
else {
|
|
667
|
-
this.highlight.next();
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
highlightPrevious(reason = 'keyboard') {
|
|
671
|
-
this.highlightReason.set(reason);
|
|
672
|
-
if (this.virtualized()) {
|
|
673
|
-
this.stepIndex(-1);
|
|
674
|
-
}
|
|
675
|
-
else {
|
|
676
|
-
this.highlight.previous();
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
highlightFirst(reason = 'keyboard') {
|
|
680
|
-
this.highlightReason.set(reason);
|
|
681
|
-
if (this.virtualized()) {
|
|
682
|
-
this.highlightedIndex.set(this.filteredItems().length > 0 ? 0 : -1);
|
|
683
|
-
}
|
|
684
|
-
else {
|
|
685
|
-
this.highlight.first();
|
|
686
|
-
}
|
|
687
|
-
}
|
|
688
|
-
highlightLast(reason = 'keyboard') {
|
|
689
|
-
this.highlightReason.set(reason);
|
|
690
|
-
if (this.virtualized()) {
|
|
691
|
-
const length = this.filteredItems().length;
|
|
692
|
-
this.highlightedIndex.set(length > 0 ? length - 1 : -1);
|
|
693
|
-
}
|
|
694
|
-
else {
|
|
695
|
-
this.highlight.last();
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
|
-
/** Highlights a specific index in virtualized mode (e.g. pointer hover). Ignored if out of range. */
|
|
699
|
-
highlightIndex(index, reason) {
|
|
700
|
-
if (index < 0 || index >= this.filteredItems().length) {
|
|
701
|
-
return;
|
|
1099
|
+
this.handleSelect(item, event);
|
|
702
1100
|
}
|
|
703
|
-
this.highlightReason.set(reason);
|
|
704
|
-
this.highlightedIndex.set(index);
|
|
705
1101
|
}
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
this.
|
|
709
|
-
this.highlight.set(item);
|
|
710
|
-
}
|
|
711
|
-
/** Clears whichever highlight model is active. */
|
|
712
|
-
clearHighlightState() {
|
|
713
|
-
this.highlight.clear();
|
|
714
|
-
this.highlightedIndex.set(-1);
|
|
715
|
-
}
|
|
716
|
-
/** Steps the virtualized highlight index by `direction`, wrapping when {@link loopFocus}. */
|
|
717
|
-
stepIndex(direction) {
|
|
718
|
-
const length = this.filteredItems().length;
|
|
719
|
-
if (length === 0) {
|
|
720
|
-
this.highlightedIndex.set(-1);
|
|
721
|
-
return;
|
|
722
|
-
}
|
|
723
|
-
const current = this.highlightedIndex();
|
|
724
|
-
if (current < 0) {
|
|
725
|
-
this.highlightedIndex.set(direction === 1 ? 0 : length - 1);
|
|
1102
|
+
clearSelection() {
|
|
1103
|
+
// Read-only / disabled comboboxes are not user-mutable (Base UI blocks Clear here too).
|
|
1104
|
+
if (this.disabledState() || this.readOnly()) {
|
|
726
1105
|
return;
|
|
727
1106
|
}
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
if (
|
|
731
|
-
|
|
732
|
-
}
|
|
733
|
-
else if (next >= length) {
|
|
734
|
-
next = loop ? 0 : length - 1;
|
|
1107
|
+
// In `none` mode there is no committed value to clear — only the input text. Otherwise reset
|
|
1108
|
+
// the selection. Also drop any highlight (Base UI resets the active/selected indices here).
|
|
1109
|
+
if (this.mode() !== 'none') {
|
|
1110
|
+
this.commitValue(this.multiple() ? [] : null);
|
|
735
1111
|
}
|
|
736
|
-
this.highlightedIndex.set(next);
|
|
737
|
-
}
|
|
738
|
-
clearSelection() {
|
|
739
|
-
this.commitValue(this.multiple() ? [] : null);
|
|
740
1112
|
this.setLabel('');
|
|
741
|
-
this.
|
|
1113
|
+
this.engine.clearHighlightState();
|
|
1114
|
+
this.engine.focusInput();
|
|
742
1115
|
}
|
|
743
1116
|
removeValue(value) {
|
|
744
1117
|
if (!this.multiple() || !Array.isArray(this.value())) {
|
|
745
1118
|
return;
|
|
746
1119
|
}
|
|
747
|
-
const next = this.value().filter((v) => !isItemEqualToValue(v, value, this.
|
|
1120
|
+
const next = this.value().filter((v) => !isItemEqualToValue(v, value, this.isItemEqualToValue()));
|
|
748
1121
|
this.commitValue(next);
|
|
749
1122
|
}
|
|
750
1123
|
removeLastValue() {
|
|
@@ -757,21 +1130,7 @@ class RdxComboboxRoot {
|
|
|
757
1130
|
}
|
|
758
1131
|
}
|
|
759
1132
|
focusInput() {
|
|
760
|
-
this.
|
|
761
|
-
}
|
|
762
|
-
/**
|
|
763
|
-
* Restores focus after a selection closes the popup, so the keyboard can reopen it. When the
|
|
764
|
-
* input lives inside the popup it is about to unmount, so focus goes to the trigger instead;
|
|
765
|
-
* otherwise it returns to the input. Done synchronously while the input is still in the DOM.
|
|
766
|
-
*/
|
|
767
|
-
restoreFocusAfterSelect() {
|
|
768
|
-
const input = this.inputElement();
|
|
769
|
-
if (input && !input.closest('[rdxComboboxPopup]')) {
|
|
770
|
-
input.focus();
|
|
771
|
-
}
|
|
772
|
-
else {
|
|
773
|
-
this.triggerElement?.focus();
|
|
774
|
-
}
|
|
1133
|
+
this.engine.focusInput();
|
|
775
1134
|
}
|
|
776
1135
|
/** Registered by `RdxComboboxChips` so the input can hand keyboard focus to the chips. */
|
|
777
1136
|
registerChipsNav(fn) {
|
|
@@ -784,11 +1143,40 @@ class RdxComboboxRoot {
|
|
|
784
1143
|
markAsTouched() {
|
|
785
1144
|
this.onTouched?.();
|
|
786
1145
|
}
|
|
1146
|
+
/**
|
|
1147
|
+
* The single guarded value-mutation point for all user-driven changes (selection toggle, Clear,
|
|
1148
|
+
* chip removal, Backspace). Read-only / disabled comboboxes never mutate here — programmatic form
|
|
1149
|
+
* writes go through {@link writeValue}, which is intentionally not guarded. (ADR 0014, Finding 1.)
|
|
1150
|
+
*/
|
|
787
1151
|
commitValue(value) {
|
|
1152
|
+
if (this.disabledState() || this.readOnly()) {
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
788
1155
|
this.value.set(value);
|
|
789
1156
|
this.onValueChange.emit(value);
|
|
790
1157
|
this.onChange?.(value);
|
|
791
1158
|
}
|
|
1159
|
+
createOpenChangeEvent(open, reason, event) {
|
|
1160
|
+
const change = createCancelableChangeEventDetails(reason, event, this.resolveOpenChangeTrigger(event));
|
|
1161
|
+
return {
|
|
1162
|
+
payload: {
|
|
1163
|
+
open,
|
|
1164
|
+
reason,
|
|
1165
|
+
event: change.eventDetails.event,
|
|
1166
|
+
trigger: change.eventDetails.trigger,
|
|
1167
|
+
eventDetails: change.eventDetails
|
|
1168
|
+
},
|
|
1169
|
+
eventDetails: change.eventDetails,
|
|
1170
|
+
shouldPreventUnmountOnClose: change.shouldPreventUnmountOnClose
|
|
1171
|
+
};
|
|
1172
|
+
}
|
|
1173
|
+
resolveOpenChangeTrigger(event) {
|
|
1174
|
+
const target = event.target;
|
|
1175
|
+
if (target instanceof HTMLElement) {
|
|
1176
|
+
return target;
|
|
1177
|
+
}
|
|
1178
|
+
return this.engine.triggerElement ?? this.engine.inputElement() ?? undefined;
|
|
1179
|
+
}
|
|
792
1180
|
// ControlValueAccessor
|
|
793
1181
|
writeValue(value) {
|
|
794
1182
|
untracked(() => this.value.set(value));
|
|
@@ -803,9 +1191,12 @@ class RdxComboboxRoot {
|
|
|
803
1191
|
this.cvaDisabled.set(isDisabled);
|
|
804
1192
|
}
|
|
805
1193
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxRoot, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
806
|
-
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxComboboxRoot, isStandalone: true, selector: "[rdxComboboxRoot]", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, defaultValue: { classPropertyName: "defaultValue", publicName: "defaultValue", 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 }, defaultOpen: { classPropertyName: "defaultOpen", publicName: "defaultOpen", isSignal: true, isRequired: false, transformFunction: null }, multipleInput: { classPropertyName: "multipleInput", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, selectionMode: { classPropertyName: "selectionMode", publicName: "selectionMode", isSignal: true, isRequired: false, transformFunction: null }, fillInputOnItemPress: { classPropertyName: "fillInputOnItemPress", publicName: "fillInputOnItemPress", isSignal: true, isRequired: false, transformFunction: null },
|
|
1194
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxComboboxRoot, isStandalone: true, selector: "[rdxComboboxRoot]", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, defaultValue: { classPropertyName: "defaultValue", publicName: "defaultValue", 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 }, defaultOpen: { classPropertyName: "defaultOpen", publicName: "defaultOpen", isSignal: true, isRequired: false, transformFunction: null }, multipleInput: { classPropertyName: "multipleInput", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, selectionMode: { classPropertyName: "selectionMode", publicName: "selectionMode", isSignal: true, isRequired: false, transformFunction: null }, fillInputOnItemPress: { classPropertyName: "fillInputOnItemPress", publicName: "fillInputOnItemPress", isSignal: true, isRequired: false, transformFunction: null }, dirInput: { classPropertyName: "dirInput", publicName: "dir", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, readOnly: { classPropertyName: "readOnly", publicName: "readOnly", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, loopFocus: { classPropertyName: "loopFocus", publicName: "loopFocus", isSignal: true, isRequired: false, transformFunction: null }, autoHighlight: { classPropertyName: "autoHighlight", publicName: "autoHighlight", isSignal: true, isRequired: false, transformFunction: null }, highlightItemOnHover: { classPropertyName: "highlightItemOnHover", publicName: "highlightItemOnHover", isSignal: true, isRequired: false, transformFunction: null }, keepHighlight: { classPropertyName: "keepHighlight", publicName: "keepHighlight", isSignal: true, isRequired: false, transformFunction: null }, openOnInputClick: { classPropertyName: "openOnInputClick", publicName: "openOnInputClick", isSignal: true, isRequired: false, transformFunction: null }, modal: { classPropertyName: "modal", publicName: "modal", isSignal: true, isRequired: false, transformFunction: null }, submitOnItemClick: { classPropertyName: "submitOnItemClick", publicName: "submitOnItemClick", isSignal: true, isRequired: false, transformFunction: null }, filter: { classPropertyName: "filter", publicName: "filter", isSignal: true, isRequired: false, transformFunction: null }, limit: { classPropertyName: "limit", publicName: "limit", isSignal: true, isRequired: false, transformFunction: null }, items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: false, transformFunction: null }, virtualized: { classPropertyName: "virtualized", publicName: "virtualized", isSignal: true, isRequired: false, transformFunction: null }, grid: { classPropertyName: "grid", publicName: "grid", isSignal: true, isRequired: false, transformFunction: null }, isItemEqualToValue: { classPropertyName: "isItemEqualToValue", publicName: "isItemEqualToValue", isSignal: true, isRequired: false, transformFunction: null }, itemToStringLabel: { classPropertyName: "itemToStringLabel", publicName: "itemToStringLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", inputValue: "inputValueChange", open: "openChange", onValueChange: "onValueChange", onInputValueChange: "onInputValueChange", onOpenChange: "onOpenChange", onItemHighlighted: "onItemHighlighted", onOpenChangeComplete: "onOpenChangeComplete" }, host: { properties: { "attr.data-disabled": "disabledState() ? \"\" : undefined" } }, providers: [
|
|
807
1195
|
provideComboboxRootContext(context),
|
|
808
|
-
{ provide: NG_VALUE_ACCESSOR, useExisting: RdxComboboxRoot, multi: true }
|
|
1196
|
+
{ provide: NG_VALUE_ACCESSOR, useExisting: RdxComboboxRoot, multi: true },
|
|
1197
|
+
// New floating foundation (ADR 0015/0017) — the dismissal capability reads this shared context.
|
|
1198
|
+
provideFloatingTree(),
|
|
1199
|
+
provideFloatingRootContext(() => inject(RdxComboboxRoot).floatingContext)
|
|
809
1200
|
], exportAs: ["rdxComboboxRoot"], hostDirectives: [{ directive: i1.RdxPopper }], ngImport: i0 }); }
|
|
810
1201
|
}
|
|
811
1202
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxRoot, decorators: [{
|
|
@@ -815,14 +1206,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
|
|
|
815
1206
|
exportAs: 'rdxComboboxRoot',
|
|
816
1207
|
providers: [
|
|
817
1208
|
provideComboboxRootContext(context),
|
|
818
|
-
{ provide: NG_VALUE_ACCESSOR, useExisting: RdxComboboxRoot, multi: true }
|
|
1209
|
+
{ provide: NG_VALUE_ACCESSOR, useExisting: RdxComboboxRoot, multi: true },
|
|
1210
|
+
// New floating foundation (ADR 0015/0017) — the dismissal capability reads this shared context.
|
|
1211
|
+
provideFloatingTree(),
|
|
1212
|
+
provideFloatingRootContext(() => inject(RdxComboboxRoot).floatingContext)
|
|
819
1213
|
],
|
|
820
1214
|
hostDirectives: [RdxPopper],
|
|
821
1215
|
host: {
|
|
822
1216
|
'[attr.data-disabled]': 'disabledState() ? "" : undefined'
|
|
823
1217
|
}
|
|
824
1218
|
}]
|
|
825
|
-
}], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], defaultValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "defaultValue", required: false }] }], inputValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "inputValue", required: false }] }, { type: i0.Output, args: ["inputValueChange"] }], open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }, { type: i0.Output, args: ["openChange"] }], defaultOpen: [{ type: i0.Input, args: [{ isSignal: true, alias: "defaultOpen", required: false }] }], multipleInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], selectionMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectionMode", required: false }] }], fillInputOnItemPress: [{ type: i0.Input, args: [{ isSignal: true, alias: "fillInputOnItemPress", required: false }] }],
|
|
1219
|
+
}], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], defaultValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "defaultValue", required: false }] }], inputValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "inputValue", required: false }] }, { type: i0.Output, args: ["inputValueChange"] }], open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }, { type: i0.Output, args: ["openChange"] }], defaultOpen: [{ type: i0.Input, args: [{ isSignal: true, alias: "defaultOpen", required: false }] }], multipleInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], selectionMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectionMode", required: false }] }], fillInputOnItemPress: [{ type: i0.Input, args: [{ isSignal: true, alias: "fillInputOnItemPress", required: false }] }], dirInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "dir", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], readOnly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readOnly", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], loopFocus: [{ type: i0.Input, args: [{ isSignal: true, alias: "loopFocus", required: false }] }], autoHighlight: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoHighlight", required: false }] }], highlightItemOnHover: [{ type: i0.Input, args: [{ isSignal: true, alias: "highlightItemOnHover", required: false }] }], keepHighlight: [{ type: i0.Input, args: [{ isSignal: true, alias: "keepHighlight", required: false }] }], openOnInputClick: [{ type: i0.Input, args: [{ isSignal: true, alias: "openOnInputClick", required: false }] }], modal: [{ type: i0.Input, args: [{ isSignal: true, alias: "modal", required: false }] }], submitOnItemClick: [{ type: i0.Input, args: [{ isSignal: true, alias: "submitOnItemClick", required: false }] }], filter: [{ type: i0.Input, args: [{ isSignal: true, alias: "filter", required: false }] }], limit: [{ type: i0.Input, args: [{ isSignal: true, alias: "limit", required: false }] }], items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: false }] }], virtualized: [{ type: i0.Input, args: [{ isSignal: true, alias: "virtualized", required: false }] }], grid: [{ type: i0.Input, args: [{ isSignal: true, alias: "grid", required: false }] }], isItemEqualToValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "isItemEqualToValue", required: false }] }], itemToStringLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemToStringLabel", required: false }] }], onValueChange: [{ type: i0.Output, args: ["onValueChange"] }], onInputValueChange: [{ type: i0.Output, args: ["onInputValueChange"] }], onOpenChange: [{ type: i0.Output, args: ["onOpenChange"] }], onItemHighlighted: [{ type: i0.Output, args: ["onItemHighlighted"] }], onOpenChangeComplete: [{ type: i0.Output, args: ["onOpenChangeComplete"] }] } });
|
|
826
1220
|
|
|
827
1221
|
/**
|
|
828
1222
|
* An overlay rendered beneath the popup in `modal` mode. Place it inside the portal/presence; style
|
|
@@ -836,7 +1230,7 @@ class RdxComboboxBackdrop {
|
|
|
836
1230
|
this.rootContext = injectComboboxRootContext();
|
|
837
1231
|
}
|
|
838
1232
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxBackdrop, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
839
|
-
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxComboboxBackdrop, isStandalone: true, selector: "[rdxComboboxBackdrop]", host: { attributes: { "
|
|
1233
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxComboboxBackdrop, isStandalone: true, selector: "[rdxComboboxBackdrop]", host: { attributes: { "role": "presentation" }, properties: { "attr.data-state": "rootContext.open() ? \"open\" : \"closed\"", "attr.data-open": "rootContext.open() ? \"\" : undefined", "attr.data-closed": "rootContext.open() ? undefined : \"\"" } }, exportAs: ["rdxComboboxBackdrop"], ngImport: i0 }); }
|
|
840
1234
|
}
|
|
841
1235
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxBackdrop, decorators: [{
|
|
842
1236
|
type: Directive,
|
|
@@ -844,7 +1238,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
|
|
|
844
1238
|
selector: '[rdxComboboxBackdrop]',
|
|
845
1239
|
exportAs: 'rdxComboboxBackdrop',
|
|
846
1240
|
host: {
|
|
847
|
-
|
|
1241
|
+
// A decorative overlay — Base UI marks it `role="presentation"` (excluded from the a11y tree).
|
|
1242
|
+
role: 'presentation',
|
|
848
1243
|
'[attr.data-state]': 'rootContext.open() ? "open" : "closed"',
|
|
849
1244
|
'[attr.data-open]': 'rootContext.open() ? "" : undefined',
|
|
850
1245
|
'[attr.data-closed]': 'rootContext.open() ? undefined : ""'
|
|
@@ -854,7 +1249,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
|
|
|
854
1249
|
|
|
855
1250
|
/**
|
|
856
1251
|
* Container for the selected-value chips in `multiple` mode. Sits before the input and coordinates
|
|
857
|
-
* arrow-key navigation across the chips (the chips themselves handle the key events).
|
|
1252
|
+
* arrow-key navigation across the chips (the chips themselves handle the key events). Uses
|
|
1253
|
+
* `role="toolbar"` (Base UI): it keeps NVDA in focus mode while arrow-navigating the chips, where a
|
|
1254
|
+
* plain `list` would drop into browse mode.
|
|
858
1255
|
*
|
|
859
1256
|
* @group Components
|
|
860
1257
|
*/
|
|
@@ -879,16 +1276,16 @@ class RdxComboboxChips {
|
|
|
879
1276
|
return true;
|
|
880
1277
|
}
|
|
881
1278
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxChips, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
882
|
-
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxComboboxChips, isStandalone: true, selector: "[rdxComboboxChips]", host: { attributes: { "role": "
|
|
1279
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxComboboxChips, isStandalone: true, selector: "[rdxComboboxChips]", host: { attributes: { "role": "toolbar" } }, exportAs: ["rdxComboboxChips"], hostDirectives: [{ directive: i1$1.RdxFloatingInsideElement }], ngImport: i0 }); }
|
|
883
1280
|
}
|
|
884
1281
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxChips, decorators: [{
|
|
885
1282
|
type: Directive,
|
|
886
1283
|
args: [{
|
|
887
1284
|
selector: '[rdxComboboxChips]',
|
|
888
1285
|
exportAs: 'rdxComboboxChips',
|
|
889
|
-
hostDirectives: [
|
|
1286
|
+
hostDirectives: [RdxFloatingInsideElement],
|
|
890
1287
|
host: {
|
|
891
|
-
role: '
|
|
1288
|
+
role: 'toolbar'
|
|
892
1289
|
}
|
|
893
1290
|
}]
|
|
894
1291
|
}], ctorParameters: () => [] });
|
|
@@ -916,21 +1313,35 @@ class RdxComboboxChip {
|
|
|
916
1313
|
const list = this.chips?.getChips() ?? [];
|
|
917
1314
|
const index = list.indexOf(this.element);
|
|
918
1315
|
switch (event.key) {
|
|
1316
|
+
case 'ArrowDown':
|
|
1317
|
+
case 'ArrowUp':
|
|
1318
|
+
// Leave the chips and engage the list: focus must return to the input so
|
|
1319
|
+
// `aria-activedescendant` navigation works, then run the same nav as the input.
|
|
1320
|
+
event.preventDefault();
|
|
1321
|
+
this.rootContext.focusInput();
|
|
1322
|
+
this.rootContext.navigateByKeyboard(event.key === 'ArrowDown' ? 1 : -1);
|
|
1323
|
+
break;
|
|
919
1324
|
case 'ArrowLeft':
|
|
920
|
-
|
|
1325
|
+
case 'ArrowRight': {
|
|
1326
|
+
// Direction-aware: in RTL the visual arrows flip. "Forward" steps toward the input
|
|
1327
|
+
// (the next chip, then the input); "backward" steps toward the first chip.
|
|
1328
|
+
const rtl = this.rootContext.dir() === 'rtl';
|
|
1329
|
+
const forward = (event.key === 'ArrowRight') !== rtl;
|
|
1330
|
+
if (forward) {
|
|
921
1331
|
event.preventDefault();
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
list[index + 1].focus();
|
|
1332
|
+
if (index < list.length - 1) {
|
|
1333
|
+
list[index + 1].focus();
|
|
1334
|
+
}
|
|
1335
|
+
else {
|
|
1336
|
+
this.rootContext.focusInput();
|
|
1337
|
+
}
|
|
929
1338
|
}
|
|
930
|
-
else {
|
|
931
|
-
|
|
1339
|
+
else if (index > 0) {
|
|
1340
|
+
event.preventDefault();
|
|
1341
|
+
list[index - 1].focus();
|
|
932
1342
|
}
|
|
933
1343
|
break;
|
|
1344
|
+
}
|
|
934
1345
|
case 'Home':
|
|
935
1346
|
if (list.length) {
|
|
936
1347
|
event.preventDefault();
|
|
@@ -965,7 +1376,7 @@ class RdxComboboxChip {
|
|
|
965
1376
|
}
|
|
966
1377
|
}
|
|
967
1378
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxChip, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
968
|
-
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxComboboxChip, isStandalone: true, selector: "[rdxComboboxChip]", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: true, transformFunction: null } }, host: { attributes: { "
|
|
1379
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxComboboxChip, isStandalone: true, selector: "[rdxComboboxChip]", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: true, transformFunction: null } }, host: { attributes: { "tabindex": "-1" }, listeners: { "keydown": "onKeydown($event)" } }, providers: [provideComboboxChipContext(chipContext)], exportAs: ["rdxComboboxChip"], ngImport: i0 }); }
|
|
969
1380
|
}
|
|
970
1381
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxChip, decorators: [{
|
|
971
1382
|
type: Directive,
|
|
@@ -974,7 +1385,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
|
|
|
974
1385
|
exportAs: 'rdxComboboxChip',
|
|
975
1386
|
providers: [provideComboboxChipContext(chipContext)],
|
|
976
1387
|
host: {
|
|
977
|
-
role:
|
|
1388
|
+
// No explicit role (Base UI): a focusable child of the `toolbar` chips container.
|
|
978
1389
|
tabindex: '-1',
|
|
979
1390
|
'(keydown)': 'onKeydown($event)'
|
|
980
1391
|
}
|
|
@@ -1020,7 +1431,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
|
|
|
1020
1431
|
class RdxComboboxClear {
|
|
1021
1432
|
constructor() {
|
|
1022
1433
|
this.rootContext = injectComboboxRootContext();
|
|
1434
|
+
/** Disables just this clear button (in addition to the combobox's own disabled / read-only state). */
|
|
1435
|
+
this.disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
1436
|
+
this.isDisabled = computed(() => this.disabled() || this.rootContext.disabledState() || this.rootContext.readonly(), ...(ngDevMode ? [{ debugName: "isDisabled" }] : /* istanbul ignore next */ []));
|
|
1437
|
+
/**
|
|
1438
|
+
* Whether there is nothing to clear, mirroring Base UI's visibility rule: in `none` mode the
|
|
1439
|
+
* button tracks the input text (a pure search field has no selection), otherwise it tracks the
|
|
1440
|
+
* selected value(s).
|
|
1441
|
+
*/
|
|
1023
1442
|
this.isEmpty = computed(() => {
|
|
1443
|
+
if (this.rootContext.selectionMode() === 'none') {
|
|
1444
|
+
return (this.rootContext.inputValue() ?? '') === '';
|
|
1445
|
+
}
|
|
1024
1446
|
const value = this.rootContext.value();
|
|
1025
1447
|
if (Array.isArray(value)) {
|
|
1026
1448
|
return value.length === 0;
|
|
@@ -1028,49 +1450,76 @@ class RdxComboboxClear {
|
|
|
1028
1450
|
return value === null || value === undefined;
|
|
1029
1451
|
}, ...(ngDevMode ? [{ debugName: "isEmpty" }] : /* istanbul ignore next */ []));
|
|
1030
1452
|
}
|
|
1453
|
+
// Keep focus on the input — don't let the button take it on pointer/mouse down.
|
|
1454
|
+
onPointerDown(event) {
|
|
1455
|
+
event.preventDefault();
|
|
1456
|
+
}
|
|
1031
1457
|
onClick() {
|
|
1032
1458
|
this.rootContext.clearSelection();
|
|
1033
1459
|
}
|
|
1034
1460
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxClear, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
1035
|
-
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "
|
|
1461
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxComboboxClear, isStandalone: true, selector: "button[rdxComboboxClear]", inputs: { disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "type": "button", "tabindex": "-1", "aria-label": "Clear" }, listeners: { "pointerdown": "onPointerDown($event)", "mousedown": "onPointerDown($event)", "click": "onClick()" }, properties: { "hidden": "isEmpty()", "attr.disabled": "isDisabled() ? \"\" : undefined" } }, exportAs: ["rdxComboboxClear"], hostDirectives: [{ directive: i1$1.RdxFloatingInsideElement }], ngImport: i0 }); }
|
|
1036
1462
|
}
|
|
1037
1463
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxClear, decorators: [{
|
|
1038
1464
|
type: Directive,
|
|
1039
1465
|
args: [{
|
|
1040
1466
|
selector: 'button[rdxComboboxClear]',
|
|
1041
1467
|
exportAs: 'rdxComboboxClear',
|
|
1042
|
-
hostDirectives: [
|
|
1468
|
+
hostDirectives: [RdxFloatingInsideElement],
|
|
1043
1469
|
host: {
|
|
1044
1470
|
type: 'button',
|
|
1045
1471
|
tabindex: '-1',
|
|
1046
1472
|
'aria-label': 'Clear',
|
|
1047
1473
|
'[hidden]': 'isEmpty()',
|
|
1048
|
-
'[attr.disabled]': '
|
|
1474
|
+
'[attr.disabled]': 'isDisabled() ? "" : undefined',
|
|
1475
|
+
'(pointerdown)': 'onPointerDown($event)',
|
|
1476
|
+
'(mousedown)': 'onPointerDown($event)',
|
|
1049
1477
|
'(click)': 'onClick()'
|
|
1050
1478
|
}
|
|
1051
1479
|
}]
|
|
1052
|
-
}] });
|
|
1480
|
+
}], propDecorators: { disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }] } });
|
|
1053
1481
|
|
|
1054
1482
|
/**
|
|
1055
|
-
*
|
|
1483
|
+
* A polite, atomic live region announcing the "no results" message. Mirrors Base UI: the element
|
|
1484
|
+
* stays **mounted and visible at all times** so screen readers reliably announce the transition to
|
|
1485
|
+
* empty — only its *content* is rendered conditionally (projected when nothing matches, removed
|
|
1486
|
+
* otherwise). It must never be hidden/unmounted (`hidden`, `display:none`, `aria-hidden`, `@if`):
|
|
1487
|
+
* pulling the region out of the accessibility tree at the same instant its text appears is exactly
|
|
1488
|
+
* what suppresses the announcement.
|
|
1056
1489
|
*
|
|
1057
1490
|
* @group Components
|
|
1058
1491
|
*/
|
|
1059
1492
|
class RdxComboboxEmpty {
|
|
1060
1493
|
constructor() {
|
|
1061
1494
|
this.rootContext = injectComboboxRootContext();
|
|
1495
|
+
/** Whether no items match the current query (drives projection of the message). */
|
|
1496
|
+
this.isEmpty = computed(() => this.rootContext.visibleCount() === 0, ...(ngDevMode ? [{ debugName: "isEmpty" }] : /* istanbul ignore next */ []));
|
|
1062
1497
|
}
|
|
1063
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxEmpty, deps: [], target: i0.ɵɵFactoryTarget.
|
|
1064
|
-
static { this.ɵ
|
|
1498
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxEmpty, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1499
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: RdxComboboxEmpty, isStandalone: true, selector: "[rdxComboboxEmpty]", host: { attributes: { "role": "status", "aria-live": "polite", "aria-atomic": "true" }, properties: { "attr.data-empty": "isEmpty() ? \"\" : undefined" } }, exportAs: ["rdxComboboxEmpty"], ngImport: i0, template: `
|
|
1500
|
+
@if (isEmpty()) {
|
|
1501
|
+
<ng-content />
|
|
1502
|
+
}
|
|
1503
|
+
`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
1065
1504
|
}
|
|
1066
1505
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxEmpty, decorators: [{
|
|
1067
|
-
type:
|
|
1506
|
+
type: Component,
|
|
1068
1507
|
args: [{
|
|
1069
1508
|
selector: '[rdxComboboxEmpty]',
|
|
1070
1509
|
exportAs: 'rdxComboboxEmpty',
|
|
1510
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1511
|
+
template: `
|
|
1512
|
+
@if (isEmpty()) {
|
|
1513
|
+
<ng-content />
|
|
1514
|
+
}
|
|
1515
|
+
`,
|
|
1071
1516
|
host: {
|
|
1072
|
-
role: '
|
|
1073
|
-
'
|
|
1517
|
+
role: 'status',
|
|
1518
|
+
'aria-live': 'polite',
|
|
1519
|
+
'aria-atomic': 'true',
|
|
1520
|
+
// Present only while the message is shown. Lets consumers collapse the always-mounted region
|
|
1521
|
+
// (e.g. `data-[empty]:py-6`) without `display:none`/`hidden`, which would break the announcement.
|
|
1522
|
+
'[attr.data-empty]': 'isEmpty() ? "" : undefined'
|
|
1074
1523
|
}
|
|
1075
1524
|
}]
|
|
1076
1525
|
}] });
|
|
@@ -1131,6 +1580,12 @@ class RdxComboboxGroupLabel {
|
|
|
1131
1580
|
this.groupContext = injectComboboxGroupContext();
|
|
1132
1581
|
this.id = injectId('rdx-combobox-group-label-');
|
|
1133
1582
|
this.groupContext.labelId.set(this.id);
|
|
1583
|
+
// Clear the registration on unmount so the group doesn't reference a removed label's id.
|
|
1584
|
+
inject(DestroyRef).onDestroy(() => {
|
|
1585
|
+
if (this.groupContext.labelId() === this.id) {
|
|
1586
|
+
this.groupContext.labelId.set(undefined);
|
|
1587
|
+
}
|
|
1588
|
+
});
|
|
1134
1589
|
}
|
|
1135
1590
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxGroupLabel, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
1136
1591
|
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxComboboxGroupLabel, isStandalone: true, selector: "[rdxComboboxGroupLabel]", host: { properties: { "attr.id": "id" } }, exportAs: ["rdxComboboxGroupLabel"], ngImport: i0 }); }
|
|
@@ -1166,7 +1621,50 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
|
|
|
1166
1621
|
}]
|
|
1167
1622
|
}] });
|
|
1168
1623
|
|
|
1169
|
-
|
|
1624
|
+
/**
|
|
1625
|
+
* Positions the combobox popup relative to the input anchor using the popper engine.
|
|
1626
|
+
*
|
|
1627
|
+
* A "thin" positioner (ADR 0012): it inherits the full popper positioning surface — the inputs
|
|
1628
|
+
* (`side`, `sideOffset`, `align`, …), the `placed` output, and the host bindings — from
|
|
1629
|
+
* {@link RdxPopperContentWrapper}, and only declares combobox's Base UI-aligned defaults through the
|
|
1630
|
+
* config provider. `provideRdxPopperContentWrapper` re-wires the `useExisting` alias + context that
|
|
1631
|
+
* the popup and arrow resolve (Angular does not inherit a base directive's `providers`).
|
|
1632
|
+
*
|
|
1633
|
+
* @group Components
|
|
1634
|
+
*/
|
|
1635
|
+
class RdxComboboxPositioner extends RdxPopperContentWrapper {
|
|
1636
|
+
constructor() {
|
|
1637
|
+
super();
|
|
1638
|
+
const rootContext = injectComboboxRootContext();
|
|
1639
|
+
const injector = inject(Injector);
|
|
1640
|
+
const host = inject(ElementRef).nativeElement;
|
|
1641
|
+
// A modal combobox isolates the background with an internal backdrop (Base UI); the input stays
|
|
1642
|
+
// clickable through a cutout. (Combobox is non-modal by default — usually no backdrop.)
|
|
1643
|
+
afterNextRender(() => setupInternalBackdrop(host, injector, {
|
|
1644
|
+
isOpen: () => rootContext.open(),
|
|
1645
|
+
shouldRender: () => rootContext.modal(),
|
|
1646
|
+
cutout: () => rootContext.inputElement() ?? null
|
|
1647
|
+
}));
|
|
1648
|
+
}
|
|
1649
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxPositioner, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
1650
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxComboboxPositioner, isStandalone: true, selector: "[rdxComboboxPositioner]", providers: [
|
|
1651
|
+
...provideRdxPopperContentWrapper(RdxComboboxPositioner),
|
|
1652
|
+
provideRdxPopperContentConfig({ sideOffset: 4, align: 'start' })
|
|
1653
|
+
], exportAs: ["rdxComboboxPositioner"], usesInheritance: true, ngImport: i0 }); }
|
|
1654
|
+
}
|
|
1655
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxPositioner, decorators: [{
|
|
1656
|
+
type: Directive,
|
|
1657
|
+
args: [{
|
|
1658
|
+
selector: '[rdxComboboxPositioner]',
|
|
1659
|
+
exportAs: 'rdxComboboxPositioner',
|
|
1660
|
+
providers: [
|
|
1661
|
+
...provideRdxPopperContentWrapper(RdxComboboxPositioner),
|
|
1662
|
+
provideRdxPopperContentConfig({ sideOffset: 4, align: 'start' })
|
|
1663
|
+
]
|
|
1664
|
+
}]
|
|
1665
|
+
}], ctorParameters: () => [] });
|
|
1666
|
+
|
|
1667
|
+
const attr$1 = (value) => (value ? '' : undefined);
|
|
1170
1668
|
/**
|
|
1171
1669
|
* The combobox text input. Holds DOM focus at all times; the highlighted option is referenced via
|
|
1172
1670
|
* `aria-activedescendant`. Integrates with Field for labeling, description, and validation state.
|
|
@@ -1206,8 +1704,12 @@ class RdxComboboxInput {
|
|
|
1206
1704
|
}, ...(ngDevMode ? [{ debugName: "describedBy" }] : /* istanbul ignore next */ []));
|
|
1207
1705
|
/** Whether an IME composition is in progress (CJK). While composing, don't filter or select. */
|
|
1208
1706
|
this.composing = false;
|
|
1209
|
-
this.dataAttr = attr;
|
|
1707
|
+
this.dataAttr = attr$1;
|
|
1210
1708
|
this.rootContext.setInputElement(this.element);
|
|
1709
|
+
// Report the layout (Base UI's `inputInsidePopup`): a positioner ancestor means the input lives
|
|
1710
|
+
// inside the popup (e.g. a command palette), so the Trigger becomes the focusable
|
|
1711
|
+
// `role="combobox"`; otherwise the input is the tab stop and the Trigger is a `tabindex="-1"` toggle.
|
|
1712
|
+
this.rootContext.setInputLayout(inject(RdxComboboxPositioner, { optional: true }) ? 'inside' : 'outside');
|
|
1211
1713
|
afterNextRender(() => {
|
|
1212
1714
|
this.fieldRootContext?.setControlId(this.id());
|
|
1213
1715
|
});
|
|
@@ -1222,22 +1724,31 @@ class RdxComboboxInput {
|
|
|
1222
1724
|
if (this.composing || event.isComposing) {
|
|
1223
1725
|
return;
|
|
1224
1726
|
}
|
|
1225
|
-
this.commitInput(event.target.value);
|
|
1727
|
+
this.commitInput(event.target.value, event);
|
|
1226
1728
|
}
|
|
1227
1729
|
onCompositionEnd(event) {
|
|
1228
1730
|
this.composing = false;
|
|
1229
|
-
this.commitInput(event.target.value);
|
|
1230
|
-
}
|
|
1231
|
-
commitInput(value) {
|
|
1232
|
-
|
|
1233
|
-
|
|
1731
|
+
this.commitInput(event.target.value, event);
|
|
1732
|
+
}
|
|
1733
|
+
commitInput(value, event) {
|
|
1734
|
+
// Base UI: clearing the field closes the popup only when the input is OUTSIDE it (and doesn't
|
|
1735
|
+
// open on click). When the input lives inside the popup, emptying the search must keep the popup
|
|
1736
|
+
// open (closing it would dismiss the field the user is typing in); otherwise typing (including
|
|
1737
|
+
// down to empty in browse mode) opens it.
|
|
1738
|
+
if (value === '' && !this.rootContext.openOnInputClick() && this.rootContext.inputLayout() !== 'inside') {
|
|
1739
|
+
this.rootContext.closePopup(false, 'input-clear', event);
|
|
1740
|
+
}
|
|
1741
|
+
else if (!this.rootContext.open() && value.trim() !== '') {
|
|
1742
|
+
// Base UI opens on input only for a non-empty trimmed value — whitespace alone won't open it.
|
|
1743
|
+
this.rootContext.openPopup('input-change', event);
|
|
1234
1744
|
}
|
|
1235
|
-
// setInputValue applies any autoHighlight (deferred until items mount)
|
|
1745
|
+
// setInputValue applies any autoHighlight (deferred until items mount) and, in single mode,
|
|
1746
|
+
// deselects when the field is emptied.
|
|
1236
1747
|
this.rootContext.setInputValue(value);
|
|
1237
1748
|
}
|
|
1238
|
-
onClick() {
|
|
1749
|
+
onClick(event) {
|
|
1239
1750
|
if (this.rootContext.openOnInputClick()) {
|
|
1240
|
-
this.rootContext.openForBrowse();
|
|
1751
|
+
this.rootContext.openForBrowse('input-press', event);
|
|
1241
1752
|
}
|
|
1242
1753
|
}
|
|
1243
1754
|
onFocus() {
|
|
@@ -1248,8 +1759,9 @@ class RdxComboboxInput {
|
|
|
1248
1759
|
this.fieldRootContext?.setTouched(true);
|
|
1249
1760
|
}
|
|
1250
1761
|
onKeydown(event) {
|
|
1251
|
-
// Don't interfere with IME composition or text-editing shortcuts / range selection.
|
|
1252
|
-
// and
|
|
1762
|
+
// Don't interfere with IME composition or text-editing shortcuts / range selection. Shift+Arrows
|
|
1763
|
+
// and modified Home/End keep moving/extending the caret; Ctrl/Meta combos stay browser shortcuts.
|
|
1764
|
+
// (Plain Home/End navigate the grid below, but only in `grid` mode.)
|
|
1253
1765
|
if (event.isComposing || this.composing) {
|
|
1254
1766
|
return;
|
|
1255
1767
|
}
|
|
@@ -1260,23 +1772,11 @@ class RdxComboboxInput {
|
|
|
1260
1772
|
switch (event.key) {
|
|
1261
1773
|
case 'ArrowDown':
|
|
1262
1774
|
event.preventDefault();
|
|
1263
|
-
this.rootContext.
|
|
1264
|
-
if (!open) {
|
|
1265
|
-
this.rootContext.openAndHighlight('first');
|
|
1266
|
-
}
|
|
1267
|
-
else {
|
|
1268
|
-
this.rootContext.highlightNext();
|
|
1269
|
-
}
|
|
1775
|
+
this.rootContext.navigateByKeyboard(1, event);
|
|
1270
1776
|
break;
|
|
1271
1777
|
case 'ArrowUp':
|
|
1272
1778
|
event.preventDefault();
|
|
1273
|
-
this.rootContext.
|
|
1274
|
-
if (!open) {
|
|
1275
|
-
this.rootContext.openAndHighlight('last');
|
|
1276
|
-
}
|
|
1277
|
-
else {
|
|
1278
|
-
this.rootContext.highlightPrevious();
|
|
1279
|
-
}
|
|
1779
|
+
this.rootContext.navigateByKeyboard(-1, event);
|
|
1280
1780
|
break;
|
|
1281
1781
|
case 'Enter':
|
|
1282
1782
|
if (open) {
|
|
@@ -1286,33 +1786,69 @@ class RdxComboboxInput {
|
|
|
1286
1786
|
if (hasHighlight) {
|
|
1287
1787
|
// Select the highlighted item (and prevent an accidental form submit).
|
|
1288
1788
|
event.preventDefault();
|
|
1289
|
-
this.rootContext.selectHighlighted();
|
|
1789
|
+
this.rootContext.selectHighlighted(event);
|
|
1290
1790
|
}
|
|
1291
1791
|
else {
|
|
1292
1792
|
// Nothing highlighted: just close, and let the form submit.
|
|
1293
|
-
this.rootContext.closePopup(true);
|
|
1793
|
+
this.rootContext.closePopup(true, 'none', event);
|
|
1294
1794
|
}
|
|
1295
1795
|
}
|
|
1296
1796
|
break;
|
|
1297
1797
|
case 'Escape':
|
|
1298
|
-
// Just close the popup (reverting the in-progress query); never clear the selection.
|
|
1299
1798
|
if (open) {
|
|
1799
|
+
// Close the popup, reverting the in-progress query; keep the selection.
|
|
1300
1800
|
event.preventDefault();
|
|
1301
|
-
this.rootContext.closePopup(true);
|
|
1801
|
+
this.rootContext.closePopup(true, 'escape-key', event);
|
|
1802
|
+
}
|
|
1803
|
+
else if (!this.rootContext.popupMounted()) {
|
|
1804
|
+
// Base UI: Escape on a closed combobox clears the input text and the selection
|
|
1805
|
+
// (`clearSelection` resets both, a no-op while read-only / disabled). Guard on
|
|
1806
|
+
// `popupMounted`: the input's own Escape handler (the `open` branch above) already set
|
|
1807
|
+
// `open()` false when this same Escape just closed an open popup — in that case the
|
|
1808
|
+
// popup is still mounted (exiting) and we must not also clear.
|
|
1809
|
+
this.rootContext.clearSelection();
|
|
1302
1810
|
}
|
|
1303
1811
|
break;
|
|
1304
1812
|
case 'Tab':
|
|
1305
|
-
|
|
1306
|
-
|
|
1813
|
+
// Tab dismisses a real popup and lets focus move on. With no popup mounted (an always-open
|
|
1814
|
+
// inline layout) Tab must NOT close — it just moves focus on. Guard on `popupMounted`.
|
|
1815
|
+
if (open && this.rootContext.popupMounted()) {
|
|
1816
|
+
this.rootContext.closePopup(true, 'none', event);
|
|
1307
1817
|
}
|
|
1308
1818
|
break;
|
|
1819
|
+
case 'ArrowRight':
|
|
1820
|
+
// In a grid, the horizontal arrows move within a row.
|
|
1821
|
+
if (open && this.rootContext.grid()) {
|
|
1822
|
+
event.preventDefault();
|
|
1823
|
+
this.rootContext.setKeyboardActive(true);
|
|
1824
|
+
this.rootContext.highlightNextColumn();
|
|
1825
|
+
break;
|
|
1826
|
+
}
|
|
1827
|
+
this.maybeStepIntoChips('ArrowRight', event);
|
|
1828
|
+
break;
|
|
1309
1829
|
case 'ArrowLeft':
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
this.
|
|
1313
|
-
this.
|
|
1314
|
-
|
|
1830
|
+
if (open && this.rootContext.grid()) {
|
|
1831
|
+
event.preventDefault();
|
|
1832
|
+
this.rootContext.setKeyboardActive(true);
|
|
1833
|
+
this.rootContext.highlightPreviousColumn();
|
|
1834
|
+
break;
|
|
1835
|
+
}
|
|
1836
|
+
this.maybeStepIntoChips('ArrowLeft', event);
|
|
1837
|
+
break;
|
|
1838
|
+
case 'Home':
|
|
1839
|
+
// In a grid the search box is a filter, so Home/End jump to the first/last cell rather
|
|
1840
|
+
// than moving the caret (outside a grid they keep their native text-editing behavior).
|
|
1841
|
+
if (open && this.rootContext.grid()) {
|
|
1842
|
+
event.preventDefault();
|
|
1843
|
+
this.rootContext.setKeyboardActive(true);
|
|
1844
|
+
this.rootContext.highlightFirst();
|
|
1845
|
+
}
|
|
1846
|
+
break;
|
|
1847
|
+
case 'End':
|
|
1848
|
+
if (open && this.rootContext.grid()) {
|
|
1315
1849
|
event.preventDefault();
|
|
1850
|
+
this.rootContext.setKeyboardActive(true);
|
|
1851
|
+
this.rootContext.highlightLast();
|
|
1316
1852
|
}
|
|
1317
1853
|
break;
|
|
1318
1854
|
case 'Backspace':
|
|
@@ -1322,20 +1858,35 @@ class RdxComboboxInput {
|
|
|
1322
1858
|
break;
|
|
1323
1859
|
}
|
|
1324
1860
|
}
|
|
1861
|
+
/**
|
|
1862
|
+
* From the very start of the input in `multiple` mode, step into the chips. The key that points
|
|
1863
|
+
* toward the chips is direction-aware: `ArrowLeft` in LTR, `ArrowRight` in RTL.
|
|
1864
|
+
*/
|
|
1865
|
+
maybeStepIntoChips(key, event) {
|
|
1866
|
+
const towardChips = this.rootContext.dir() === 'rtl' ? 'ArrowRight' : 'ArrowLeft';
|
|
1867
|
+
if (key === towardChips &&
|
|
1868
|
+
this.rootContext.multiple() &&
|
|
1869
|
+
this.element.selectionStart === 0 &&
|
|
1870
|
+
this.element.selectionEnd === 0 &&
|
|
1871
|
+
this.rootContext.focusLastChip()) {
|
|
1872
|
+
event.preventDefault();
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1325
1875
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxInput, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
1326
|
-
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxComboboxInput, isStandalone: true, selector: "input[rdxComboboxInput]", inputs: { id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "combobox", "autocomplete": "off", "aria-autocomplete": "list" }, listeners: { "input": "onInput($event)", "click": "onClick()", "focus": "onFocus()", "blur": "onBlur()", "keydown": "onKeydown($event)", "compositionstart": "composing = true", "compositionend": "onCompositionEnd($event)" }, properties: { "attr.id": "id()", "attr.aria-expanded": "rootContext.open()", "attr.aria-controls": "rootContext.listId", "attr.aria-labelledby": "rootContext.labelId()", "attr.aria-activedescendant": "rootContext.activeId()", "attr.aria-describedby": "describedBy()", "attr.aria-invalid": "invalidState() ? \"true\" : undefined", "attr.aria-required": "requiredState() ? \"true\" : undefined", "attr.aria-disabled": "disabledState() ? \"true\" : undefined", "attr.disabled": "disabledState() ? \"\" : undefined", "attr.readonly": "rootContext.readonly() ? \"\" : undefined", "attr.required": "requiredState() ? \"\" : undefined", "value": "rootContext.inputValue()", "attr.data-popup-open": "dataAttr(rootContext.open())", "attr.data-list-empty": "dataAttr(rootContext.visibleCount() === 0)", "attr.data-placeholder": "dataAttr(isEmptyValue())", "attr.data-invalid": "dataAttr(invalidState())", "attr.data-valid": "dataAttr(!invalidState())", "attr.data-disabled": "dataAttr(disabledState())", "attr.data-required": "dataAttr(requiredState())", "attr.data-filled": "dataAttr(filledState())", "attr.data-focused": "dataAttr(focusedState())" } }, exportAs: ["rdxComboboxInput"], hostDirectives: [{ directive: i1.RdxPopperAnchor }, { directive: i1$1.
|
|
1876
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxComboboxInput, isStandalone: true, selector: "input[rdxComboboxInput]", inputs: { id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "combobox", "autocomplete": "off", "aria-autocomplete": "list" }, listeners: { "input": "onInput($event)", "click": "onClick($event)", "focus": "onFocus()", "blur": "onBlur()", "keydown": "onKeydown($event)", "compositionstart": "composing = true", "compositionend": "onCompositionEnd($event)" }, properties: { "attr.id": "id()", "attr.aria-haspopup": "rootContext.grid() ? \"grid\" : \"listbox\"", "attr.aria-expanded": "rootContext.open()", "attr.aria-controls": "rootContext.listId", "attr.aria-labelledby": "rootContext.labelId()", "attr.aria-activedescendant": "rootContext.activeId()", "attr.aria-describedby": "describedBy()", "attr.aria-invalid": "invalidState() ? \"true\" : undefined", "attr.aria-required": "requiredState() ? \"true\" : undefined", "attr.aria-disabled": "disabledState() ? \"true\" : undefined", "attr.disabled": "disabledState() ? \"\" : undefined", "attr.readonly": "rootContext.readonly() ? \"\" : undefined", "attr.required": "requiredState() ? \"\" : undefined", "value": "rootContext.inputValue()", "attr.data-popup-open": "dataAttr(rootContext.open())", "attr.data-list-empty": "dataAttr(rootContext.visibleCount() === 0)", "attr.data-placeholder": "dataAttr(isEmptyValue())", "attr.data-invalid": "dataAttr(invalidState())", "attr.data-valid": "dataAttr(!invalidState())", "attr.data-disabled": "dataAttr(disabledState())", "attr.data-required": "dataAttr(requiredState())", "attr.data-filled": "dataAttr(filledState())", "attr.data-focused": "dataAttr(focusedState())" } }, exportAs: ["rdxComboboxInput"], hostDirectives: [{ directive: i1.RdxPopperAnchor }, { directive: i1$1.RdxFloatingInsideElement }], ngImport: i0 }); }
|
|
1327
1877
|
}
|
|
1328
1878
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxInput, decorators: [{
|
|
1329
1879
|
type: Directive,
|
|
1330
1880
|
args: [{
|
|
1331
1881
|
selector: 'input[rdxComboboxInput]',
|
|
1332
1882
|
exportAs: 'rdxComboboxInput',
|
|
1333
|
-
hostDirectives: [RdxPopperAnchor,
|
|
1883
|
+
hostDirectives: [RdxPopperAnchor, RdxFloatingInsideElement],
|
|
1334
1884
|
host: {
|
|
1335
1885
|
role: 'combobox',
|
|
1336
1886
|
autocomplete: 'off',
|
|
1337
1887
|
'aria-autocomplete': 'list',
|
|
1338
1888
|
'[attr.id]': 'id()',
|
|
1889
|
+
'[attr.aria-haspopup]': 'rootContext.grid() ? "grid" : "listbox"',
|
|
1339
1890
|
'[attr.aria-expanded]': 'rootContext.open()',
|
|
1340
1891
|
'[attr.aria-controls]': 'rootContext.listId',
|
|
1341
1892
|
'[attr.aria-labelledby]': 'rootContext.labelId()',
|
|
@@ -1358,7 +1909,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
|
|
|
1358
1909
|
'[attr.data-filled]': 'dataAttr(filledState())',
|
|
1359
1910
|
'[attr.data-focused]': 'dataAttr(focusedState())',
|
|
1360
1911
|
'(input)': 'onInput($event)',
|
|
1361
|
-
'(click)': 'onClick()',
|
|
1912
|
+
'(click)': 'onClick($event)',
|
|
1362
1913
|
'(focus)': 'onFocus()',
|
|
1363
1914
|
'(blur)': 'onBlur()',
|
|
1364
1915
|
'(keydown)': 'onKeydown($event)',
|
|
@@ -1368,12 +1919,72 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
|
|
|
1368
1919
|
}]
|
|
1369
1920
|
}], ctorParameters: () => [], propDecorators: { id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }] } });
|
|
1370
1921
|
|
|
1922
|
+
const attr = (value) => (value ? '' : undefined);
|
|
1923
|
+
/**
|
|
1924
|
+
* Optional wrapper around the input and its adornments (icon, clear, trigger). Mirrors the combobox
|
|
1925
|
+
* state via `data-*` so the whole group can be styled together (focus ring, disabled, etc.).
|
|
1926
|
+
*
|
|
1927
|
+
* @group Components
|
|
1928
|
+
*/
|
|
1929
|
+
class RdxComboboxInputGroup {
|
|
1930
|
+
constructor() {
|
|
1931
|
+
this.rootContext = injectComboboxRootContext();
|
|
1932
|
+
/** Whether a value is selected (a non-empty array in multiple mode, or a non-nullish single value). */
|
|
1933
|
+
this.filled = computed(() => {
|
|
1934
|
+
const value = this.rootContext.value();
|
|
1935
|
+
if (Array.isArray(value)) {
|
|
1936
|
+
return value.length > 0;
|
|
1937
|
+
}
|
|
1938
|
+
return value !== null && value !== undefined;
|
|
1939
|
+
}, ...(ngDevMode ? [{ debugName: "filled" }] : /* istanbul ignore next */ []));
|
|
1940
|
+
this.dataAttr = attr;
|
|
1941
|
+
}
|
|
1942
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxInputGroup, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
1943
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxComboboxInputGroup, isStandalone: true, selector: "[rdxComboboxInputGroup]", host: { properties: { "attr.data-popup-open": "dataAttr(rootContext.open())", "attr.data-disabled": "dataAttr(rootContext.disabledState())", "attr.data-required": "dataAttr(rootContext.requiredState())", "attr.data-filled": "dataAttr(filled())" } }, exportAs: ["rdxComboboxInputGroup"], ngImport: i0 }); }
|
|
1944
|
+
}
|
|
1945
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxInputGroup, decorators: [{
|
|
1946
|
+
type: Directive,
|
|
1947
|
+
args: [{
|
|
1948
|
+
selector: '[rdxComboboxInputGroup]',
|
|
1949
|
+
exportAs: 'rdxComboboxInputGroup',
|
|
1950
|
+
host: {
|
|
1951
|
+
'[attr.data-popup-open]': 'dataAttr(rootContext.open())',
|
|
1952
|
+
'[attr.data-disabled]': 'dataAttr(rootContext.disabledState())',
|
|
1953
|
+
'[attr.data-required]': 'dataAttr(rootContext.requiredState())',
|
|
1954
|
+
'[attr.data-filled]': 'dataAttr(filled())'
|
|
1955
|
+
}
|
|
1956
|
+
}]
|
|
1957
|
+
}] });
|
|
1958
|
+
|
|
1959
|
+
/**
|
|
1960
|
+
* A row in a grid-layout combobox list. Groups the items in one row so the root can navigate by row
|
|
1961
|
+
* (`ArrowUp` / `ArrowDown`) and within a row (`ArrowLeft` / `ArrowRight`). Only meaningful when the
|
|
1962
|
+
* root has `grid` enabled; the root resolves an item's row from its nearest `[rdxComboboxRow]` ancestor.
|
|
1963
|
+
*
|
|
1964
|
+
* @group Components
|
|
1965
|
+
*/
|
|
1966
|
+
class RdxComboboxRow {
|
|
1967
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxRow, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
1968
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxComboboxRow, isStandalone: true, selector: "[rdxComboboxRow]", host: { attributes: { "role": "row" } }, exportAs: ["rdxComboboxRow"], ngImport: i0 }); }
|
|
1969
|
+
}
|
|
1970
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxRow, decorators: [{
|
|
1971
|
+
type: Directive,
|
|
1972
|
+
args: [{
|
|
1973
|
+
selector: '[rdxComboboxRow]',
|
|
1974
|
+
exportAs: 'rdxComboboxRow',
|
|
1975
|
+
host: {
|
|
1976
|
+
role: 'row'
|
|
1977
|
+
}
|
|
1978
|
+
}]
|
|
1979
|
+
}] });
|
|
1980
|
+
|
|
1371
1981
|
const itemContext = () => {
|
|
1372
1982
|
const item = inject(RdxComboboxItem);
|
|
1373
1983
|
return {
|
|
1374
1984
|
isSelected: item.isSelected,
|
|
1375
1985
|
isHighlighted: item.isHighlighted,
|
|
1376
1986
|
disabled: item.disabled,
|
|
1987
|
+
// Read-only `Signal` (not `InputSignal`) so autocomplete's computed `value` is assignable too.
|
|
1377
1988
|
value: item.value
|
|
1378
1989
|
};
|
|
1379
1990
|
};
|
|
@@ -1407,6 +2018,16 @@ class RdxComboboxItem {
|
|
|
1407
2018
|
this.elementId = computed(() => this.virtualized() ? this.rootContext.itemId(this.index() ?? -1) : this.id, ...(ngDevMode ? [{ debugName: "elementId" }] : /* istanbul ignore next */ []));
|
|
1408
2019
|
this.ariaSetSize = computed(() => this.virtualized() ? this.rootContext.filteredItems().length : undefined, ...(ngDevMode ? [{ debugName: "ariaSetSize" }] : /* istanbul ignore next */ []));
|
|
1409
2020
|
this.ariaPosInSet = computed(() => (this.virtualized() ? (this.index() ?? -1) + 1 : undefined), ...(ngDevMode ? [{ debugName: "ariaPosInSet" }] : /* istanbul ignore next */ []));
|
|
2021
|
+
/** The nearest enclosing grid row, if any (drives the `gridcell` role). */
|
|
2022
|
+
this.row = inject(RdxComboboxRow, { optional: true });
|
|
2023
|
+
/** `gridcell` only when actually inside a `RdxComboboxRow` of a grid list; otherwise `option`. */
|
|
2024
|
+
this.role = computed(() => (this.rootContext.grid() && this.row ? 'gridcell' : 'option'), ...(ngDevMode ? [{ debugName: "role" }] : /* istanbul ignore next */ []));
|
|
2025
|
+
/**
|
|
2026
|
+
* Whether selection is a meaningful concept here (Base UI's `selectable`). In `selectionMode="none"`
|
|
2027
|
+
* (every autocomplete option, and a pure-search combobox) options carry no selection state, so
|
|
2028
|
+
* `aria-selected` / `data-selected` are omitted entirely rather than rendered as `false`.
|
|
2029
|
+
*/
|
|
2030
|
+
this.selectable = computed(() => this.rootContext.selectionMode() !== 'none', ...(ngDevMode ? [{ debugName: "selectable" }] : /* istanbul ignore next */ []));
|
|
1410
2031
|
// Virtualized items are always rendered (the consumer only mounts the filtered window).
|
|
1411
2032
|
this.isVisible = computed(() => (this.virtualized() ? true : this.rootContext.isVisible(this)), ...(ngDevMode ? [{ debugName: "isVisible" }] : /* istanbul ignore next */ []));
|
|
1412
2033
|
this.isSelected = computed(() => this.rootContext.isSelected(this.value()), ...(ngDevMode ? [{ debugName: "isSelected" }] : /* istanbul ignore next */ []));
|
|
@@ -1414,6 +2035,9 @@ class RdxComboboxItem {
|
|
|
1414
2035
|
? this.rootContext.highlightedIndex() === this.index()
|
|
1415
2036
|
: this.rootContext.highlightedItem() === this, ...(ngDevMode ? [{ debugName: "isHighlighted" }] : /* istanbul ignore next */ []));
|
|
1416
2037
|
this.group = injectComboboxGroupContext(true);
|
|
2038
|
+
// Whether a primary-button pointerdown started on **this** item. A normal press+release here is
|
|
2039
|
+
// committed by `click`; `mouseup` is only the drag-end fallback for a press that began *elsewhere*.
|
|
2040
|
+
this.pointerDownStarted = false;
|
|
1417
2041
|
const destroyRef = inject(DestroyRef);
|
|
1418
2042
|
afterNextRender(() => {
|
|
1419
2043
|
// Virtualized items are not registered: the root navigates over `items` data by index, and
|
|
@@ -1445,18 +2069,51 @@ class RdxComboboxItem {
|
|
|
1445
2069
|
this.element.scrollIntoView({ block: 'nearest' });
|
|
1446
2070
|
}
|
|
1447
2071
|
});
|
|
2072
|
+
// Reset the press flag whenever the popup closes (matches Base UI), so a later drag-end onto
|
|
2073
|
+
// this item isn't blocked by a stale press from an earlier interaction.
|
|
2074
|
+
effect(() => {
|
|
2075
|
+
if (!this.rootContext.open()) {
|
|
2076
|
+
this.pointerDownStarted = false;
|
|
2077
|
+
}
|
|
2078
|
+
});
|
|
1448
2079
|
}
|
|
1449
2080
|
onPointerDown(event) {
|
|
1450
|
-
|
|
2081
|
+
if (event.button !== 0) {
|
|
2082
|
+
return;
|
|
2083
|
+
}
|
|
2084
|
+
// Keep focus on the input; prevent the item from stealing focus.
|
|
1451
2085
|
event.preventDefault();
|
|
1452
2086
|
this.rootContext.setKeyboardActive(false);
|
|
2087
|
+
this.pointerDownStarted = true;
|
|
1453
2088
|
}
|
|
1454
|
-
|
|
2089
|
+
onMouseDown(event) {
|
|
2090
|
+
// Belt-and-suspenders for keeping focus on the input (and iOS Safari blur on tap).
|
|
2091
|
+
if (event.button === 0) {
|
|
2092
|
+
event.preventDefault();
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
onMouseUp(event) {
|
|
2096
|
+
// Read-and-reset the press flag first (matches Base UI), so a press+release here doesn't leave
|
|
2097
|
+
// it set and block a later drag-end onto this same item. Drag-end: commit when the primary
|
|
2098
|
+
// button is released over the highlighted item while the press began on a *different* element
|
|
2099
|
+
// (so `click` won't fire here). A press that began on this item is committed by `click` instead.
|
|
2100
|
+
const startedHere = this.pointerDownStarted;
|
|
2101
|
+
this.pointerDownStarted = false;
|
|
2102
|
+
if (event.button !== 0 || startedHere || !this.isHighlighted()) {
|
|
2103
|
+
return;
|
|
2104
|
+
}
|
|
2105
|
+
this.commitSelection(event);
|
|
2106
|
+
}
|
|
2107
|
+
onClick(event) {
|
|
2108
|
+
// Primary selection trigger; also fires for programmatic `.click()`.
|
|
2109
|
+
this.commitSelection(event);
|
|
2110
|
+
}
|
|
2111
|
+
commitSelection(event) {
|
|
1455
2112
|
if (this.virtualized()) {
|
|
1456
|
-
this.rootContext.selectIndex(this.index() ?? -1);
|
|
2113
|
+
this.rootContext.selectIndex(this.index() ?? -1, event);
|
|
1457
2114
|
}
|
|
1458
2115
|
else {
|
|
1459
|
-
this.rootContext.select(this);
|
|
2116
|
+
this.rootContext.select(this, event);
|
|
1460
2117
|
}
|
|
1461
2118
|
}
|
|
1462
2119
|
onPointerMove() {
|
|
@@ -1497,7 +2154,7 @@ class RdxComboboxItem {
|
|
|
1497
2154
|
this.rootContext.clearHighlight();
|
|
1498
2155
|
}
|
|
1499
2156
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxItem, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
1500
|
-
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxComboboxItem, isStandalone: true, selector: "[rdxComboboxItem]", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: true, transformFunction: null }, textValueInput: { classPropertyName: "textValueInput", publicName: "textValue", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, index: { classPropertyName: "index", publicName: "index", isSignal: true, isRequired: false, transformFunction: null } }, host: {
|
|
2157
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxComboboxItem, isStandalone: true, selector: "[rdxComboboxItem]", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: true, transformFunction: null }, textValueInput: { classPropertyName: "textValueInput", publicName: "textValue", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, index: { classPropertyName: "index", publicName: "index", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "pointerdown": "onPointerDown($event)", "mousedown": "onMouseDown($event)", "mouseup": "onMouseUp($event)", "click": "onClick($event)", "pointermove": "onPointerMove()", "pointerleave": "onPointerLeave($event)" }, properties: { "attr.role": "role()", "attr.id": "elementId()", "attr.aria-selected": "selectable() ? isSelected() : undefined", "attr.aria-disabled": "disabled() ? \"true\" : undefined", "attr.aria-setsize": "ariaSetSize()", "attr.aria-posinset": "ariaPosInSet()", "attr.data-selected": "selectable() && isSelected() ? \"\" : undefined", "attr.data-highlighted": "isHighlighted() ? \"\" : undefined", "attr.data-disabled": "disabled() ? \"\" : undefined", "hidden": "!isVisible()", "attr.data-hidden": "isVisible() ? undefined : \"\"" } }, providers: [provideComboboxItemContext(itemContext)], exportAs: ["rdxComboboxItem"], ngImport: i0 }); }
|
|
1501
2158
|
}
|
|
1502
2159
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxItem, decorators: [{
|
|
1503
2160
|
type: Directive,
|
|
@@ -1506,20 +2163,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
|
|
|
1506
2163
|
exportAs: 'rdxComboboxItem',
|
|
1507
2164
|
providers: [provideComboboxItemContext(itemContext)],
|
|
1508
2165
|
host: {
|
|
1509
|
-
role: '
|
|
2166
|
+
'[attr.role]': 'role()',
|
|
1510
2167
|
'[attr.id]': 'elementId()',
|
|
1511
|
-
'[attr.aria-selected]': 'isSelected()',
|
|
2168
|
+
'[attr.aria-selected]': 'selectable() ? isSelected() : undefined',
|
|
1512
2169
|
'[attr.aria-disabled]': 'disabled() ? "true" : undefined',
|
|
1513
2170
|
'[attr.aria-setsize]': 'ariaSetSize()',
|
|
1514
2171
|
'[attr.aria-posinset]': 'ariaPosInSet()',
|
|
1515
|
-
'[attr.data-selected]': 'isSelected() ? "" : undefined',
|
|
2172
|
+
'[attr.data-selected]': 'selectable() && isSelected() ? "" : undefined',
|
|
1516
2173
|
'[attr.data-highlighted]': 'isHighlighted() ? "" : undefined',
|
|
1517
2174
|
'[attr.data-disabled]': 'disabled() ? "" : undefined',
|
|
1518
2175
|
'[hidden]': '!isVisible()',
|
|
1519
2176
|
'[attr.data-hidden]': 'isVisible() ? undefined : ""',
|
|
1520
2177
|
'(pointerdown)': 'onPointerDown($event)',
|
|
1521
|
-
'(mousedown)': '
|
|
1522
|
-
'(
|
|
2178
|
+
'(mousedown)': 'onMouseDown($event)',
|
|
2179
|
+
'(mouseup)': 'onMouseUp($event)',
|
|
2180
|
+
'(click)': 'onClick($event)',
|
|
1523
2181
|
'(pointermove)': 'onPointerMove()',
|
|
1524
2182
|
'(pointerleave)': 'onPointerLeave($event)'
|
|
1525
2183
|
}
|
|
@@ -1578,7 +2236,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
|
|
|
1578
2236
|
}], ctorParameters: () => [] });
|
|
1579
2237
|
|
|
1580
2238
|
/**
|
|
1581
|
-
* The listbox container for options. Carries the id referenced by the input's `aria-controls
|
|
2239
|
+
* The listbox container for options. Carries the id referenced by the input's `aria-controls`, exposes
|
|
2240
|
+
* `data-empty` while no options match the current query (Base UI's `ComboboxList` empty state), and
|
|
2241
|
+
* switches its `role` to `grid` when the root has `grid` enabled.
|
|
1582
2242
|
*
|
|
1583
2243
|
* @group Components
|
|
1584
2244
|
*/
|
|
@@ -1586,8 +2246,27 @@ class RdxComboboxList {
|
|
|
1586
2246
|
constructor() {
|
|
1587
2247
|
this.rootContext = injectComboboxRootContext();
|
|
1588
2248
|
}
|
|
2249
|
+
onKeydown(event) {
|
|
2250
|
+
if (event.key !== 'Enter') {
|
|
2251
|
+
return;
|
|
2252
|
+
}
|
|
2253
|
+
// Base UI bails early when disabled / read-only — don't swallow Enter (e.g. a form submit).
|
|
2254
|
+
if (this.rootContext.disabledState() || this.rootContext.readonly()) {
|
|
2255
|
+
return;
|
|
2256
|
+
}
|
|
2257
|
+
const hasHighlight = this.rootContext.virtualized()
|
|
2258
|
+
? this.rootContext.highlightedIndex() >= 0
|
|
2259
|
+
: this.rootContext.highlightedItem() !== null;
|
|
2260
|
+
if (hasHighlight) {
|
|
2261
|
+
// Base UI `stopEvent`: also stop propagation so a parent keydown handler doesn't re-handle
|
|
2262
|
+
// Enter after the selection.
|
|
2263
|
+
event.preventDefault();
|
|
2264
|
+
event.stopPropagation();
|
|
2265
|
+
this.rootContext.selectHighlighted();
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
1589
2268
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxList, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
1590
|
-
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxComboboxList, isStandalone: true, selector: "[rdxComboboxList]", host: { attributes: { "
|
|
2269
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxComboboxList, isStandalone: true, selector: "[rdxComboboxList]", host: { attributes: { "tabindex": "-1" }, listeners: { "keydown": "onKeydown($event)" }, properties: { "attr.role": "rootContext.grid() ? \"grid\" : \"listbox\"", "attr.id": "rootContext.listId", "attr.aria-multiselectable": "rootContext.multiple() ? \"true\" : undefined", "attr.data-empty": "rootContext.visibleCount() === 0 ? \"\" : undefined" } }, exportAs: ["rdxComboboxList"], ngImport: i0 }); }
|
|
1591
2270
|
}
|
|
1592
2271
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxList, decorators: [{
|
|
1593
2272
|
type: Directive,
|
|
@@ -1595,9 +2274,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
|
|
|
1595
2274
|
selector: '[rdxComboboxList]',
|
|
1596
2275
|
exportAs: 'rdxComboboxList',
|
|
1597
2276
|
host: {
|
|
1598
|
-
|
|
2277
|
+
// Base UI: the list is a programmatic focus target (`tabindex="-1"`) and selects the highlighted
|
|
2278
|
+
// item on Enter, for custom layouts that move focus onto the list rather than the input.
|
|
2279
|
+
tabindex: '-1',
|
|
2280
|
+
'[attr.role]': 'rootContext.grid() ? "grid" : "listbox"',
|
|
1599
2281
|
'[attr.id]': 'rootContext.listId',
|
|
1600
|
-
'[attr.aria-multiselectable]': 'rootContext.multiple() ? "true" : undefined'
|
|
2282
|
+
'[attr.aria-multiselectable]': 'rootContext.multiple() ? "true" : undefined',
|
|
2283
|
+
'[attr.data-empty]': 'rootContext.visibleCount() === 0 ? "" : undefined',
|
|
2284
|
+
'(keydown)': 'onKeydown($event)'
|
|
1601
2285
|
}
|
|
1602
2286
|
}]
|
|
1603
2287
|
}] });
|
|
@@ -1611,19 +2295,39 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
|
|
|
1611
2295
|
class RdxComboboxPopup {
|
|
1612
2296
|
constructor() {
|
|
1613
2297
|
this.rootContext = injectComboboxRootContext();
|
|
1614
|
-
this.
|
|
2298
|
+
this.floatingContext = inject(RDX_FLOATING_ROOT_CONTEXT);
|
|
2299
|
+
this.registration = inject(RDX_FLOATING_REGISTRATION, { optional: true });
|
|
1615
2300
|
this.popper = injectPopperContentWrapperContext();
|
|
1616
2301
|
this.element = inject(ElementRef).nativeElement;
|
|
1617
|
-
//
|
|
1618
|
-
//
|
|
1619
|
-
|
|
2302
|
+
// Activation policy (ADR 0016 §2 + §3): lock page scroll while a modal popup is OPEN. The gate
|
|
2303
|
+
// keys on `open` (not mounted), so the lock releases at close-start — before the exit animation
|
|
2304
|
+
// finishes — even though the popup stays mounted through it. For a **touch** open the anchored
|
|
2305
|
+
// helper only locks when the popup is effectively viewport-width, so a small dropdown stays
|
|
2306
|
+
// swipe-to-dismissable on mobile (§3).
|
|
2307
|
+
useAnchoredScrollLock(computed(() => this.rootContext.open() && this.rootContext.modal()), {
|
|
2308
|
+
touchOpen: () => this.rootContext.openedByTouch(),
|
|
2309
|
+
element: () => this.element
|
|
2310
|
+
});
|
|
1620
2311
|
// The popup's animation determines when the open/close transition (onOpenChangeComplete) is done.
|
|
1621
2312
|
const unregister = this.rootContext.registerTransitionElement(this.element);
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
2313
|
+
// Track mounted state so Escape can tell "closing this open popup" from "already closed".
|
|
2314
|
+
this.rootContext.setPopupMounted(true);
|
|
2315
|
+
inject(DestroyRef).onDestroy(() => {
|
|
2316
|
+
unregister();
|
|
2317
|
+
this.rootContext.setPopupMounted(false);
|
|
2318
|
+
});
|
|
2319
|
+
// The popup is this layer's floating element (the inside surface for containment checks).
|
|
2320
|
+
this.floatingContext.setFloatingElement(this.element);
|
|
2321
|
+
// Dismissal (ADR 0015): an outside press, or focus leaving everything, closes the combobox. The
|
|
2322
|
+
// input / trigger / chips / clear are registered as "inside" (RdxFloatingInsideElement), so the
|
|
2323
|
+
// input keeping focus — or a press on those parts — never self-dismisses. Escape is owned by the
|
|
2324
|
+
// input (it preventDefaults + closes), so the capability does not handle it (`escapeKey: false`).
|
|
2325
|
+
new RdxDismiss(this.floatingContext, () => this.registration?.node() ?? null, {
|
|
2326
|
+
escapeKey: () => false,
|
|
2327
|
+
outsidePress: () => true,
|
|
2328
|
+
focusOutside: () => true,
|
|
2329
|
+
onDismiss: (reason, event) => this.rootContext.closePopup(true, reason === 'focus-outside' ? 'focus-out' : 'outside-press', event)
|
|
2330
|
+
});
|
|
1627
2331
|
// For the "input inside the popup" pattern, move focus to the input once the popup is
|
|
1628
2332
|
// positioned. Use `afterRenderEffect` (not `effect`): when `isPositioned` flips true the
|
|
1629
2333
|
// popup's final position/visibility is applied in the *following* render, so a synchronous
|
|
@@ -1634,33 +2338,57 @@ class RdxComboboxPopup {
|
|
|
1634
2338
|
}
|
|
1635
2339
|
const input = this.rootContext.inputElement();
|
|
1636
2340
|
if (input && input.closest('[rdxComboboxPopup]')) {
|
|
1637
|
-
|
|
1638
|
-
input.
|
|
2341
|
+
// Base UI: a touch-open focuses the popup itself so Android keeps the virtual keyboard
|
|
2342
|
+
// closed; mouse/keyboard opens focus (and select) the search input as usual.
|
|
2343
|
+
if (this.rootContext.openedByTouch()) {
|
|
2344
|
+
this.element.focus();
|
|
2345
|
+
}
|
|
2346
|
+
else {
|
|
2347
|
+
input.focus();
|
|
2348
|
+
input.select();
|
|
2349
|
+
}
|
|
1639
2350
|
}
|
|
1640
2351
|
});
|
|
1641
2352
|
}
|
|
2353
|
+
/**
|
|
2354
|
+
* Base UI focus handoff: if focus lands on the popup or the list (the `tabindex="-1"` programmatic
|
|
2355
|
+
* focus targets), hand it back to the input so arrow-key navigation (`aria-activedescendant`) keeps
|
|
2356
|
+
* working. Skipped for a touch interaction, where focus is intentionally parked on the popup to keep
|
|
2357
|
+
* the Android virtual keyboard closed.
|
|
2358
|
+
*/
|
|
2359
|
+
onFocusIn(event) {
|
|
2360
|
+
if (this.rootContext.openedByTouch()) {
|
|
2361
|
+
return;
|
|
2362
|
+
}
|
|
2363
|
+
const input = this.rootContext.inputElement();
|
|
2364
|
+
const target = event.target;
|
|
2365
|
+
if (!input || !target || target === input) {
|
|
2366
|
+
return;
|
|
2367
|
+
}
|
|
2368
|
+
if (target === this.element || target.matches('[rdxComboboxList]')) {
|
|
2369
|
+
input.focus();
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
1642
2372
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxPopup, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
1643
|
-
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxComboboxPopup, isStandalone: true, selector: "[rdxComboboxPopup]", host: { properties: { "attr.data-state": "rootContext.open() ? \"open\" : \"closed\"", "attr.data-open": "rootContext.open() ? \"\" : undefined", "attr.data-closed": "rootContext.open() ? undefined : \"\"", "attr.data-starting-style": "rootContext.transitionStatus() === \"starting\" ? \"\" : undefined", "attr.data-ending-style": "rootContext.transitionStatus() === \"ending\" ? \"\" : undefined" } },
|
|
1644
|
-
// In modal mode, make content outside the popup inert (Base UI's `modal`).
|
|
1645
|
-
provideRdxDismissableLayerConfig(() => ({ disableOutsidePointerEvents: injectComboboxRootContext().modal }))
|
|
1646
|
-
], exportAs: ["rdxComboboxPopup"], hostDirectives: [{ directive: i1.RdxPopperContent }, { directive: i1$1.RdxDismissableLayer }], ngImport: i0 }); }
|
|
2373
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxComboboxPopup, isStandalone: true, selector: "[rdxComboboxPopup]", host: { attributes: { "tabindex": "-1" }, listeners: { "focusin": "onFocusIn($event)" }, properties: { "attr.role": "rootContext.inputLayout() === \"inside\" ? \"dialog\" : \"presentation\"", "attr.data-state": "rootContext.open() ? \"open\" : \"closed\"", "attr.data-open": "rootContext.open() ? \"\" : undefined", "attr.data-closed": "rootContext.open() ? undefined : \"\"", "attr.data-starting-style": "rootContext.transitionStatus() === \"starting\" ? \"\" : undefined", "attr.data-ending-style": "rootContext.transitionStatus() === \"ending\" ? \"\" : undefined" } }, exportAs: ["rdxComboboxPopup"], hostDirectives: [{ directive: i1.RdxPopperContent }, { directive: i2.RdxFloatingNodeRegistration }], ngImport: i0 }); }
|
|
1647
2374
|
}
|
|
1648
2375
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxPopup, decorators: [{
|
|
1649
2376
|
type: Directive,
|
|
1650
2377
|
args: [{
|
|
1651
2378
|
selector: '[rdxComboboxPopup]',
|
|
1652
2379
|
exportAs: 'rdxComboboxPopup',
|
|
1653
|
-
hostDirectives: [RdxPopperContent,
|
|
1654
|
-
providers: [
|
|
1655
|
-
// In modal mode, make content outside the popup inert (Base UI's `modal`).
|
|
1656
|
-
provideRdxDismissableLayerConfig(() => ({ disableOutsidePointerEvents: injectComboboxRootContext().modal }))
|
|
1657
|
-
],
|
|
2380
|
+
hostDirectives: [RdxPopperContent, RdxFloatingNodeRegistration],
|
|
1658
2381
|
host: {
|
|
2382
|
+
// Base UI: a `dialog` (focusable, tabindex -1) when the input lives inside the popup, otherwise
|
|
2383
|
+
// a presentational wrapper around the `listbox` (the List part owns the listbox role).
|
|
2384
|
+
tabindex: '-1',
|
|
2385
|
+
'[attr.role]': 'rootContext.inputLayout() === "inside" ? "dialog" : "presentation"',
|
|
1659
2386
|
'[attr.data-state]': 'rootContext.open() ? "open" : "closed"',
|
|
1660
2387
|
'[attr.data-open]': 'rootContext.open() ? "" : undefined',
|
|
1661
2388
|
'[attr.data-closed]': 'rootContext.open() ? undefined : ""',
|
|
1662
2389
|
'[attr.data-starting-style]': 'rootContext.transitionStatus() === "starting" ? "" : undefined',
|
|
1663
|
-
'[attr.data-ending-style]': 'rootContext.transitionStatus() === "ending" ? "" : undefined'
|
|
2390
|
+
'[attr.data-ending-style]': 'rootContext.transitionStatus() === "ending" ? "" : undefined',
|
|
2391
|
+
'(focusin)': 'onFocusIn($event)'
|
|
1664
2392
|
}
|
|
1665
2393
|
}]
|
|
1666
2394
|
}], ctorParameters: () => [] });
|
|
@@ -1685,7 +2413,7 @@ class RdxComboboxPortal {
|
|
|
1685
2413
|
this.container = input(...(ngDevMode ? [undefined, { debugName: "container" }] : /* istanbul ignore next */ []));
|
|
1686
2414
|
}
|
|
1687
2415
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxPortal, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
1688
|
-
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxComboboxPortal, isStandalone: true, selector: "ng-template[rdxComboboxPortal]", inputs: { container: { classPropertyName: "container", publicName: "container", isSignal: true, isRequired: false, transformFunction: null } }, providers: [provideRdxPresenceContext(() => ({ present: injectComboboxRootContext().
|
|
2416
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxComboboxPortal, isStandalone: true, selector: "ng-template[rdxComboboxPortal]", inputs: { container: { classPropertyName: "container", publicName: "container", isSignal: true, isRequired: false, transformFunction: null } }, providers: [provideRdxPresenceContext(() => ({ present: injectComboboxRootContext().present }))], exportAs: ["rdxComboboxPortal"], hostDirectives: [{ directive: i1$2.RdxPortalPresence, inputs: ["container", "container"] }], ngImport: i0 }); }
|
|
1689
2417
|
}
|
|
1690
2418
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxPortal, decorators: [{
|
|
1691
2419
|
type: Directive,
|
|
@@ -1693,7 +2421,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
|
|
|
1693
2421
|
selector: 'ng-template[rdxComboboxPortal]',
|
|
1694
2422
|
exportAs: 'rdxComboboxPortal',
|
|
1695
2423
|
hostDirectives: [{ directive: RdxPortalPresence, inputs: ['container'] }],
|
|
1696
|
-
providers: [provideRdxPresenceContext(() => ({ present: injectComboboxRootContext().
|
|
2424
|
+
providers: [provideRdxPresenceContext(() => ({ present: injectComboboxRootContext().present }))]
|
|
1697
2425
|
}]
|
|
1698
2426
|
}], propDecorators: { container: [{ type: i0.Input, args: [{ isSignal: true, alias: "container", required: false }] }] } });
|
|
1699
2427
|
/**
|
|
@@ -1706,9 +2434,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
|
|
|
1706
2434
|
class RdxComboboxPortalMisuseGuard {
|
|
1707
2435
|
constructor() {
|
|
1708
2436
|
if (isDevMode()) {
|
|
1709
|
-
|
|
2437
|
+
rdxDevError('combobox/portal-on-element', '`rdxComboboxPortal` is now a structural directive. ' +
|
|
1710
2438
|
'Use `*rdxComboboxPortal` on the positioner element or `<ng-template rdxComboboxPortal>`. ' +
|
|
1711
|
-
'rdxComboboxPortalPresence has been removed.
|
|
2439
|
+
'rdxComboboxPortalPresence has been removed.', 'components/combobox');
|
|
1712
2440
|
}
|
|
1713
2441
|
}
|
|
1714
2442
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxPortalMisuseGuard, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
@@ -1722,63 +2450,26 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
|
|
|
1722
2450
|
}], ctorParameters: () => [] });
|
|
1723
2451
|
|
|
1724
2452
|
/**
|
|
1725
|
-
*
|
|
1726
|
-
*
|
|
2453
|
+
* A visual separator between groups of options. Carries `role="separator"` with a horizontal
|
|
2454
|
+
* orientation (it divides rows in a vertical list), so assistive tech announces the grouping break.
|
|
1727
2455
|
*
|
|
1728
2456
|
* @group Components
|
|
1729
2457
|
*/
|
|
1730
|
-
class
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
this.sideOffset = input(4, { ...(ngDevMode ? { debugName: "sideOffset" } : /* istanbul ignore next */ {}), transform: numberAttribute });
|
|
1734
|
-
this.align = input('start', ...(ngDevMode ? [{ debugName: "align" }] : /* istanbul ignore next */ []));
|
|
1735
|
-
this.alignOffset = input(0, { ...(ngDevMode ? { debugName: "alignOffset" } : /* istanbul ignore next */ {}), transform: numberAttribute });
|
|
1736
|
-
this.arrowPadding = input(0, { ...(ngDevMode ? { debugName: "arrowPadding" } : /* istanbul ignore next */ {}), transform: numberAttribute });
|
|
1737
|
-
this.avoidCollisions = input(true, { ...(ngDevMode ? { debugName: "avoidCollisions" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
1738
|
-
this.collisionBoundary = input(...(ngDevMode ? [undefined, { debugName: "collisionBoundary" }] : /* istanbul ignore next */ []));
|
|
1739
|
-
this.collisionPadding = input(0, ...(ngDevMode ? [{ debugName: "collisionPadding" }] : /* istanbul ignore next */ []));
|
|
1740
|
-
this.sticky = input('partial', ...(ngDevMode ? [{ debugName: "sticky" }] : /* istanbul ignore next */ []));
|
|
1741
|
-
this.hideWhenDetached = input(false, { ...(ngDevMode ? { debugName: "hideWhenDetached" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
1742
|
-
this.updatePositionStrategy = input('optimized', ...(ngDevMode ? [{ debugName: "updatePositionStrategy" }] : /* istanbul ignore next */ []));
|
|
1743
|
-
}
|
|
1744
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxPositioner, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
1745
|
-
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxComboboxPositioner, isStandalone: true, selector: "[rdxComboboxPositioner]", inputs: { side: { classPropertyName: "side", publicName: "side", isSignal: true, isRequired: false, transformFunction: null }, sideOffset: { classPropertyName: "sideOffset", publicName: "sideOffset", isSignal: true, isRequired: false, transformFunction: null }, align: { classPropertyName: "align", publicName: "align", isSignal: true, isRequired: false, transformFunction: null }, alignOffset: { classPropertyName: "alignOffset", publicName: "alignOffset", isSignal: true, isRequired: false, transformFunction: null }, arrowPadding: { classPropertyName: "arrowPadding", publicName: "arrowPadding", isSignal: true, isRequired: false, transformFunction: null }, avoidCollisions: { classPropertyName: "avoidCollisions", publicName: "avoidCollisions", isSignal: true, isRequired: false, transformFunction: null }, collisionBoundary: { classPropertyName: "collisionBoundary", publicName: "collisionBoundary", isSignal: true, isRequired: false, transformFunction: null }, collisionPadding: { classPropertyName: "collisionPadding", publicName: "collisionPadding", isSignal: true, isRequired: false, transformFunction: null }, sticky: { classPropertyName: "sticky", publicName: "sticky", isSignal: true, isRequired: false, transformFunction: null }, hideWhenDetached: { classPropertyName: "hideWhenDetached", publicName: "hideWhenDetached", isSignal: true, isRequired: false, transformFunction: null }, updatePositionStrategy: { classPropertyName: "updatePositionStrategy", publicName: "updatePositionStrategy", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "style": "{\n 'boxSizing': 'border-box',\n '--radix-combobox-content-transform-origin': 'var(--radix-popper-transform-origin)',\n '--radix-combobox-content-available-width': 'var(--radix-popper-available-width)',\n '--radix-combobox-content-available-height': 'var(--radix-popper-available-height)',\n '--radix-combobox-trigger-width': 'var(--radix-popper-anchor-width)',\n '--radix-combobox-trigger-height': 'var(--radix-popper-anchor-height)'\n }" } }, exportAs: ["rdxComboboxPositioner"], hostDirectives: [{ directive: i1.RdxPopperContentWrapper, inputs: ["side", "side", "sideOffset", "sideOffset", "align", "align", "alignOffset", "alignOffset", "arrowPadding", "arrowPadding", "avoidCollisions", "avoidCollisions", "collisionBoundary", "collisionBoundary", "collisionPadding", "collisionPadding", "sticky", "sticky", "hideWhenDetached", "hideWhenDetached", "updatePositionStrategy", "updatePositionStrategy"] }], ngImport: i0 }); }
|
|
2458
|
+
class RdxComboboxSeparator {
|
|
2459
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxSeparator, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
2460
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxComboboxSeparator, isStandalone: true, selector: "[rdxComboboxSeparator]", host: { attributes: { "role": "separator", "aria-orientation": "horizontal" } }, exportAs: ["rdxComboboxSeparator"], ngImport: i0 }); }
|
|
1746
2461
|
}
|
|
1747
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type:
|
|
2462
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxSeparator, decorators: [{
|
|
1748
2463
|
type: Directive,
|
|
1749
2464
|
args: [{
|
|
1750
|
-
selector: '[
|
|
1751
|
-
exportAs: '
|
|
1752
|
-
hostDirectives: [
|
|
1753
|
-
{
|
|
1754
|
-
directive: RdxPopperContentWrapper,
|
|
1755
|
-
inputs: [
|
|
1756
|
-
'side',
|
|
1757
|
-
'sideOffset',
|
|
1758
|
-
'align',
|
|
1759
|
-
'alignOffset',
|
|
1760
|
-
'arrowPadding',
|
|
1761
|
-
'avoidCollisions',
|
|
1762
|
-
'collisionBoundary',
|
|
1763
|
-
'collisionPadding',
|
|
1764
|
-
'sticky',
|
|
1765
|
-
'hideWhenDetached',
|
|
1766
|
-
'updatePositionStrategy'
|
|
1767
|
-
]
|
|
1768
|
-
}
|
|
1769
|
-
],
|
|
2465
|
+
selector: '[rdxComboboxSeparator]',
|
|
2466
|
+
exportAs: 'rdxComboboxSeparator',
|
|
1770
2467
|
host: {
|
|
1771
|
-
'
|
|
1772
|
-
|
|
1773
|
-
'--radix-combobox-content-transform-origin': 'var(--radix-popper-transform-origin)',
|
|
1774
|
-
'--radix-combobox-content-available-width': 'var(--radix-popper-available-width)',
|
|
1775
|
-
'--radix-combobox-content-available-height': 'var(--radix-popper-available-height)',
|
|
1776
|
-
'--radix-combobox-trigger-width': 'var(--radix-popper-anchor-width)',
|
|
1777
|
-
'--radix-combobox-trigger-height': 'var(--radix-popper-anchor-height)'
|
|
1778
|
-
}`
|
|
2468
|
+
role: 'separator',
|
|
2469
|
+
'aria-orientation': 'horizontal'
|
|
1779
2470
|
}
|
|
1780
2471
|
}]
|
|
1781
|
-
}]
|
|
2472
|
+
}] });
|
|
1782
2473
|
|
|
1783
2474
|
/**
|
|
1784
2475
|
* A polite live region for async status (loading, result counts) announced without moving focus.
|
|
@@ -1787,7 +2478,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
|
|
|
1787
2478
|
*/
|
|
1788
2479
|
class RdxComboboxStatus {
|
|
1789
2480
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxStatus, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
1790
|
-
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxComboboxStatus, isStandalone: true, selector: "[rdxComboboxStatus]", host: { attributes: { "role": "status", "aria-live": "polite" } }, exportAs: ["rdxComboboxStatus"], ngImport: i0 }); }
|
|
2481
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxComboboxStatus, isStandalone: true, selector: "[rdxComboboxStatus]", host: { attributes: { "role": "status", "aria-live": "polite", "aria-atomic": "true" } }, exportAs: ["rdxComboboxStatus"], ngImport: i0 }); }
|
|
1791
2482
|
}
|
|
1792
2483
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxStatus, decorators: [{
|
|
1793
2484
|
type: Directive,
|
|
@@ -1796,13 +2487,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
|
|
|
1796
2487
|
exportAs: 'rdxComboboxStatus',
|
|
1797
2488
|
host: {
|
|
1798
2489
|
role: 'status',
|
|
1799
|
-
'aria-live': 'polite'
|
|
2490
|
+
'aria-live': 'polite',
|
|
2491
|
+
'aria-atomic': 'true'
|
|
1800
2492
|
}
|
|
1801
2493
|
}]
|
|
1802
2494
|
}] });
|
|
1803
2495
|
|
|
1804
2496
|
/**
|
|
1805
|
-
* Toggles the combobox popup.
|
|
2497
|
+
* Toggles the combobox popup. Its semantics depend on the layout (Base UI's `inputInsidePopup`):
|
|
2498
|
+
*
|
|
2499
|
+
* - **Input outside the popup** (default): a `tabindex="-1"` toggle button (`aria-haspopup="listbox"`)
|
|
2500
|
+
* that never steals focus from the input — `Tab` lands directly on the input.
|
|
2501
|
+
* - **Input inside the popup** (e.g. a command palette / emoji picker): the trigger becomes the primary
|
|
2502
|
+
* `role="combobox"` control — `tabindex="0"` (reachable via `Tab`), `aria-haspopup="dialog"`, and
|
|
2503
|
+
* `ArrowDown`/`ArrowUp` open the popup (which moves focus to the inner input and highlights an item).
|
|
2504
|
+
*
|
|
2505
|
+
* The trigger stays `Tab`-reachable by default and is demoted to `tabindex="-1"` only once an input is
|
|
2506
|
+
* detected *outside* the popup — so a trigger whose input lives in a not-yet-opened popup is focusable
|
|
2507
|
+
* from the first render (`inputLayout` is `unknown` until that input mounts).
|
|
1806
2508
|
*
|
|
1807
2509
|
* @group Components
|
|
1808
2510
|
*/
|
|
@@ -1813,35 +2515,57 @@ class RdxComboboxTrigger {
|
|
|
1813
2515
|
this.rootContext.registerTrigger(this.element);
|
|
1814
2516
|
inject(DestroyRef).onDestroy(() => this.rootContext.registerTrigger(null));
|
|
1815
2517
|
}
|
|
1816
|
-
|
|
2518
|
+
// Record whether the opening interaction is touch, so the popup can keep focus off the inner input
|
|
2519
|
+
// (and Android's virtual keyboard closed) when the input lives inside the popup.
|
|
2520
|
+
onPointerDown(event) {
|
|
2521
|
+
this.rootContext.setOpenedByTouch(event.pointerType === 'touch');
|
|
2522
|
+
}
|
|
2523
|
+
onClick(event) {
|
|
1817
2524
|
if (this.rootContext.open()) {
|
|
1818
|
-
this.rootContext.closePopup(true);
|
|
2525
|
+
this.rootContext.closePopup(true, 'trigger-press', event);
|
|
1819
2526
|
}
|
|
1820
2527
|
else {
|
|
1821
2528
|
this.rootContext.focusInput();
|
|
1822
|
-
this.rootContext.openForBrowse();
|
|
2529
|
+
this.rootContext.openForBrowse('trigger-press', event);
|
|
2530
|
+
}
|
|
2531
|
+
}
|
|
2532
|
+
onKeydown(event) {
|
|
2533
|
+
if (this.rootContext.disabledState() || this.rootContext.readonly()) {
|
|
2534
|
+
return;
|
|
2535
|
+
}
|
|
2536
|
+
// ArrowDown/ArrowUp open the popup and seed the first/last highlight; the popup's own
|
|
2537
|
+
// auto-focus then moves focus to the input (whether it lives inside or outside the popup).
|
|
2538
|
+
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
|
2539
|
+
event.preventDefault();
|
|
2540
|
+
// A keyboard open must focus the input, not the popup — clear any prior touch flag.
|
|
2541
|
+
this.rootContext.setOpenedByTouch(false);
|
|
2542
|
+
this.rootContext.openAndHighlight(event.key === 'ArrowUp' ? 'last' : 'first', 'list-navigation', event);
|
|
1823
2543
|
}
|
|
1824
2544
|
}
|
|
1825
2545
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxTrigger, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
1826
|
-
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxComboboxTrigger, isStandalone: true, selector: "button[rdxComboboxTrigger]", host: { attributes: { "type": "button", "
|
|
2546
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxComboboxTrigger, isStandalone: true, selector: "button[rdxComboboxTrigger]", host: { attributes: { "type": "button" }, listeners: { "pointerdown": "onPointerDown($event)", "click": "onClick($event)", "keydown": "onKeydown($event)" }, properties: { "attr.tabindex": "rootContext.inputLayout() === \"outside\" ? \"-1\" : \"0\"", "attr.role": "rootContext.inputLayout() === \"inside\" ? \"combobox\" : undefined", "attr.aria-haspopup": "rootContext.inputLayout() === \"inside\" ? \"dialog\" : \"listbox\"", "attr.aria-expanded": "rootContext.open()", "attr.aria-controls": "rootContext.listId", "attr.aria-labelledby": "rootContext.labelId()", "attr.aria-required": "rootContext.inputLayout() === \"inside\" && rootContext.requiredState() ? \"true\" : undefined", "attr.disabled": "rootContext.disabledState() ? \"\" : undefined", "attr.data-popup-open": "rootContext.open() ? \"\" : undefined", "attr.data-disabled": "rootContext.disabledState() ? \"\" : undefined" } }, exportAs: ["rdxComboboxTrigger"], hostDirectives: [{ directive: i1$1.RdxFloatingInsideElement }], ngImport: i0 }); }
|
|
1827
2547
|
}
|
|
1828
2548
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxTrigger, decorators: [{
|
|
1829
2549
|
type: Directive,
|
|
1830
2550
|
args: [{
|
|
1831
2551
|
selector: 'button[rdxComboboxTrigger]',
|
|
1832
2552
|
exportAs: 'rdxComboboxTrigger',
|
|
1833
|
-
hostDirectives: [
|
|
2553
|
+
hostDirectives: [RdxFloatingInsideElement],
|
|
1834
2554
|
host: {
|
|
1835
2555
|
type: 'button',
|
|
1836
|
-
tabindex: '-1',
|
|
1837
|
-
'
|
|
2556
|
+
'[attr.tabindex]': 'rootContext.inputLayout() === "outside" ? "-1" : "0"',
|
|
2557
|
+
'[attr.role]': 'rootContext.inputLayout() === "inside" ? "combobox" : undefined',
|
|
2558
|
+
'[attr.aria-haspopup]': 'rootContext.inputLayout() === "inside" ? "dialog" : "listbox"',
|
|
1838
2559
|
'[attr.aria-expanded]': 'rootContext.open()',
|
|
1839
2560
|
'[attr.aria-controls]': 'rootContext.listId',
|
|
1840
2561
|
'[attr.aria-labelledby]': 'rootContext.labelId()',
|
|
2562
|
+
'[attr.aria-required]': 'rootContext.inputLayout() === "inside" && rootContext.requiredState() ? "true" : undefined',
|
|
1841
2563
|
'[attr.disabled]': 'rootContext.disabledState() ? "" : undefined',
|
|
1842
2564
|
'[attr.data-popup-open]': 'rootContext.open() ? "" : undefined',
|
|
1843
2565
|
'[attr.data-disabled]': 'rootContext.disabledState() ? "" : undefined',
|
|
1844
|
-
'(
|
|
2566
|
+
'(pointerdown)': 'onPointerDown($event)',
|
|
2567
|
+
'(click)': 'onClick($event)',
|
|
2568
|
+
'(keydown)': 'onKeydown($event)'
|
|
1845
2569
|
}
|
|
1846
2570
|
}]
|
|
1847
2571
|
}], ctorParameters: () => [] });
|
|
@@ -1895,6 +2619,7 @@ const _importsCombobox = [
|
|
|
1895
2619
|
RdxComboboxAnchor,
|
|
1896
2620
|
RdxComboboxLabel,
|
|
1897
2621
|
RdxComboboxInput,
|
|
2622
|
+
RdxComboboxInputGroup,
|
|
1898
2623
|
RdxComboboxValue,
|
|
1899
2624
|
RdxComboboxTrigger,
|
|
1900
2625
|
RdxComboboxIcon,
|
|
@@ -1906,10 +2631,12 @@ const _importsCombobox = [
|
|
|
1906
2631
|
RdxComboboxPopup,
|
|
1907
2632
|
RdxComboboxArrow,
|
|
1908
2633
|
RdxComboboxList,
|
|
2634
|
+
RdxComboboxRow,
|
|
1909
2635
|
RdxComboboxItem,
|
|
1910
2636
|
RdxComboboxItemIndicator,
|
|
1911
2637
|
RdxComboboxGroup,
|
|
1912
2638
|
RdxComboboxGroupLabel,
|
|
2639
|
+
RdxComboboxSeparator,
|
|
1913
2640
|
RdxComboboxEmpty,
|
|
1914
2641
|
RdxComboboxStatus,
|
|
1915
2642
|
RdxComboboxChips,
|
|
@@ -1922,6 +2649,7 @@ class RdxComboboxModule {
|
|
|
1922
2649
|
RdxComboboxAnchor,
|
|
1923
2650
|
RdxComboboxLabel,
|
|
1924
2651
|
RdxComboboxInput,
|
|
2652
|
+
RdxComboboxInputGroup,
|
|
1925
2653
|
RdxComboboxValue,
|
|
1926
2654
|
RdxComboboxTrigger,
|
|
1927
2655
|
RdxComboboxIcon,
|
|
@@ -1933,10 +2661,12 @@ class RdxComboboxModule {
|
|
|
1933
2661
|
RdxComboboxPopup,
|
|
1934
2662
|
RdxComboboxArrow,
|
|
1935
2663
|
RdxComboboxList,
|
|
2664
|
+
RdxComboboxRow,
|
|
1936
2665
|
RdxComboboxItem,
|
|
1937
2666
|
RdxComboboxItemIndicator,
|
|
1938
2667
|
RdxComboboxGroup,
|
|
1939
2668
|
RdxComboboxGroupLabel,
|
|
2669
|
+
RdxComboboxSeparator,
|
|
1940
2670
|
RdxComboboxEmpty,
|
|
1941
2671
|
RdxComboboxStatus,
|
|
1942
2672
|
RdxComboboxChips,
|
|
@@ -1945,6 +2675,7 @@ class RdxComboboxModule {
|
|
|
1945
2675
|
RdxComboboxAnchor,
|
|
1946
2676
|
RdxComboboxLabel,
|
|
1947
2677
|
RdxComboboxInput,
|
|
2678
|
+
RdxComboboxInputGroup,
|
|
1948
2679
|
RdxComboboxValue,
|
|
1949
2680
|
RdxComboboxTrigger,
|
|
1950
2681
|
RdxComboboxIcon,
|
|
@@ -1956,10 +2687,12 @@ class RdxComboboxModule {
|
|
|
1956
2687
|
RdxComboboxPopup,
|
|
1957
2688
|
RdxComboboxArrow,
|
|
1958
2689
|
RdxComboboxList,
|
|
2690
|
+
RdxComboboxRow,
|
|
1959
2691
|
RdxComboboxItem,
|
|
1960
2692
|
RdxComboboxItemIndicator,
|
|
1961
2693
|
RdxComboboxGroup,
|
|
1962
2694
|
RdxComboboxGroupLabel,
|
|
2695
|
+
RdxComboboxSeparator,
|
|
1963
2696
|
RdxComboboxEmpty,
|
|
1964
2697
|
RdxComboboxStatus,
|
|
1965
2698
|
RdxComboboxChips,
|
|
@@ -1979,5 +2712,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
|
|
|
1979
2712
|
* Generated bundle index. Do not edit.
|
|
1980
2713
|
*/
|
|
1981
2714
|
|
|
1982
|
-
export { RdxComboboxAnchor, RdxComboboxArrow, RdxComboboxBackdrop, RdxComboboxChip, RdxComboboxChipRemove, RdxComboboxChips, RdxComboboxClear, RdxComboboxEmpty, RdxComboboxGroup, RdxComboboxGroupLabel, RdxComboboxIcon, RdxComboboxInput, RdxComboboxItem, RdxComboboxItemIndicator, RdxComboboxLabel, RdxComboboxList, RdxComboboxModule, RdxComboboxPopup, RdxComboboxPortal, RdxComboboxPortalMisuseGuard, RdxComboboxPositioner, RdxComboboxRoot, RdxComboboxStatus, RdxComboboxTrigger, RdxComboboxValue, _importsCombobox, injectComboboxChipContext, injectComboboxGroupContext, injectComboboxItemContext, injectComboboxRootContext, provideComboboxChipContext, provideComboboxGroupContext, provideComboboxItemContext, provideComboboxRootContext };
|
|
2715
|
+
export { RdxComboboxAnchor, RdxComboboxArrow, RdxComboboxBackdrop, RdxComboboxChip, RdxComboboxChipRemove, RdxComboboxChips, RdxComboboxClear, RdxComboboxEmpty, RdxComboboxGroup, RdxComboboxGroupLabel, RdxComboboxIcon, RdxComboboxInput, RdxComboboxInputGroup, RdxComboboxItem, RdxComboboxItemIndicator, RdxComboboxLabel, RdxComboboxList, RdxComboboxModule, RdxComboboxPopup, RdxComboboxPortal, RdxComboboxPortalMisuseGuard, RdxComboboxPositioner, RdxComboboxRoot, RdxComboboxRow, RdxComboboxSeparator, RdxComboboxStatus, RdxComboboxTrigger, RdxComboboxValue, _importsCombobox, injectComboboxChipContext, injectComboboxGroupContext, injectComboboxItemContext, injectComboboxRootContext, provideComboboxChipContext, provideComboboxGroupContext, provideComboboxItemContext, provideComboboxRootContext, useComboboxEngine };
|
|
1983
2716
|
//# sourceMappingURL=radix-ng-primitives-combobox.mjs.map
|