@radix-ng/primitives 1.0.0-beta.2 → 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/LICENSE +1 -1
- package/README.md +76 -6
- 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 +31 -24
- package/fesm2022/radix-ng-primitives-alert-dialog.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-autocomplete.mjs +1744 -0
- package/fesm2022/radix-ng-primitives-autocomplete.mjs.map +1 -0
- 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 +1399 -606
- 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 +271 -145
- 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 +154 -64
- 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 +894 -299
- 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 +176 -207
- package/fesm2022/radix-ng-primitives-navigation-menu.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-popover.mjs +250 -250
- package/fesm2022/radix-ng-primitives-popover.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-popper.mjs +94 -45
- package/fesm2022/radix-ng-primitives-popper.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-portal.mjs +107 -17
- package/fesm2022/radix-ng-primitives-portal.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-presence.mjs +262 -79
- package/fesm2022/radix-ng-primitives-presence.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-preview-card.mjs +172 -218
- 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 +303 -234
- 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 +105 -145
- package/fesm2022/radix-ng-primitives-tooltip.mjs.map +1 -1
- package/package.json +14 -1
- package/types/radix-ng-primitives-accordion.d.ts +4 -3
- package/types/radix-ng-primitives-alert-dialog.d.ts +17 -11
- package/types/radix-ng-primitives-autocomplete.d.ts +661 -0
- package/types/radix-ng-primitives-calendar.d.ts +5 -3
- package/types/radix-ng-primitives-combobox.d.ts +727 -293
- 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 +107 -55
- 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-drawer.d.ts +49 -22
- 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 +204 -112
- package/types/radix-ng-primitives-navigation-menu.d.ts +61 -101
- package/types/radix-ng-primitives-popover.d.ts +82 -115
- package/types/radix-ng-primitives-popper.d.ts +46 -10
- package/types/radix-ng-primitives-portal.d.ts +53 -8
- package/types/radix-ng-primitives-presence.d.ts +98 -17
- package/types/radix-ng-primitives-preview-card.d.ts +63 -95
- 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 +192 -158
- 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 +48 -84
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { Directive,
|
|
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
|
-
import {
|
|
12
|
-
import
|
|
13
|
-
import { provideRdxPresenceContext, RdxPresenceDirective } from '@radix-ng/primitives/presence';
|
|
13
|
+
import { RdxPortalPresence } from '@radix-ng/primitives/portal';
|
|
14
|
+
import { provideRdxPresenceContext } from '@radix-ng/primitives/presence';
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Optional positioning anchor for the popup. Put it on the element the popup should align to — for
|
|
@@ -53,58 +54,606 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
|
|
|
53
54
|
}]
|
|
54
55
|
}] });
|
|
55
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();
|
|
56
591
|
const context = () => {
|
|
57
592
|
const root = inject(RdxComboboxRoot);
|
|
593
|
+
const engine = engineRegistry.get(root);
|
|
58
594
|
return {
|
|
59
|
-
listId:
|
|
60
|
-
labelId:
|
|
61
|
-
setLabelId: (id) =>
|
|
595
|
+
listId: engine.listId,
|
|
596
|
+
labelId: engine.labelId,
|
|
597
|
+
setLabelId: (id) => engine.setLabelId(id),
|
|
62
598
|
dir: root.dir,
|
|
63
599
|
value: root.value,
|
|
64
600
|
inputValue: root.inputValue,
|
|
65
601
|
open: root.open,
|
|
602
|
+
present: root.present,
|
|
66
603
|
multiple: root.multiple,
|
|
67
604
|
selectionMode: root.mode,
|
|
68
605
|
disabledState: root.disabledState,
|
|
69
|
-
readonly: root.
|
|
606
|
+
readonly: root.readOnly,
|
|
70
607
|
requiredState: root.requiredState,
|
|
71
608
|
openOnInputClick: root.openOnInputClick,
|
|
72
609
|
modal: root.modal,
|
|
73
610
|
virtualized: root.virtualized,
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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),
|
|
85
623
|
isSelected: (value) => root.isSelected(value),
|
|
86
|
-
registerItem: (item) =>
|
|
87
|
-
unregisterItem: (item) =>
|
|
88
|
-
highlight:
|
|
89
|
-
highlightNext: () =>
|
|
90
|
-
highlightPrevious: () =>
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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(),
|
|
636
|
+
highlightItemOnHover: root.highlightItemOnHover,
|
|
637
|
+
keepHighlight: root.keepHighlight,
|
|
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),
|
|
103
651
|
setInputValue: (value) => root.setInputValue(value),
|
|
104
|
-
openAndHighlight: (edge) => root.openAndHighlight(edge),
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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),
|
|
108
657
|
clearSelection: () => root.clearSelection(),
|
|
109
658
|
removeValue: (value) => root.removeValue(value),
|
|
110
659
|
removeLastValue: () => root.removeLastValue(),
|
|
@@ -116,15 +665,56 @@ const context = () => {
|
|
|
116
665
|
};
|
|
117
666
|
const [injectComboboxRootContext, provideComboboxRootContext] = createContext('RdxComboboxRootContext', 'components/combobox');
|
|
118
667
|
/**
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
668
|
+
* `autoHighlight` transform: pass the `'always'` / `'input-change'` string modes through verbatim,
|
|
669
|
+
* coerce everything else as a boolean attribute (so the bare `autoHighlight` attribute reads `true`).
|
|
670
|
+
*
|
|
671
|
+
* Kept as a named module-level function rather than an inline `transform` arrow: compodoc 1.2.1
|
|
672
|
+
* (the metadata source for the API contract and Storybook ArgTypes) hangs parsing an inline arrow
|
|
673
|
+
* combined with explicit generic union type arguments on `input()`. A plain function reference sidesteps it.
|
|
674
|
+
*/
|
|
675
|
+
function coerceAutoHighlight(value) {
|
|
676
|
+
return value === 'always' || value === 'input-change' ? value : booleanAttribute(value);
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
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`.
|
|
122
683
|
*
|
|
123
684
|
* @group Components
|
|
124
685
|
*/
|
|
125
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
|
+
}
|
|
126
711
|
constructor() {
|
|
127
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
|
+
});
|
|
128
718
|
/** Selected value(s). A single value in single mode, an array in `multiple` mode. */
|
|
129
719
|
this.value = model(null, ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
|
|
130
720
|
/** Initial value when uncontrolled. */
|
|
@@ -149,11 +739,12 @@ class RdxComboboxRoot {
|
|
|
149
739
|
/** In `'none'` mode, whether pressing an item fills the input with its label. */
|
|
150
740
|
this.fillInputOnItemPress = input(true, { ...(ngDevMode ? { debugName: "fillInputOnItemPress" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
151
741
|
/** Text direction. */
|
|
152
|
-
this.
|
|
742
|
+
this.dirInput = input(undefined, { ...(ngDevMode ? { debugName: "dirInput" } : /* istanbul ignore next */ {}), alias: 'dir' });
|
|
743
|
+
this.dir = injectDirection(this.dirInput);
|
|
153
744
|
/** Whether the combobox is disabled. */
|
|
154
745
|
this.disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
155
|
-
/** Whether the combobox is read-only. */
|
|
156
|
-
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 });
|
|
157
748
|
/** Whether a value is required (for forms). */
|
|
158
749
|
this.required = input(false, { ...(ngDevMode ? { debugName: "required" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
159
750
|
/** Whether keyboard navigation wraps at the list boundaries. */
|
|
@@ -161,10 +752,10 @@ class RdxComboboxRoot {
|
|
|
161
752
|
/**
|
|
162
753
|
* Auto-highlight behavior:
|
|
163
754
|
* - `false` (default): never auto-highlight;
|
|
164
|
-
* - `true` / `'input-change'`: highlight the first match as the query changes;
|
|
755
|
+
* - `true` (also the bare `autoHighlight` attribute) / `'input-change'`: highlight the first match as the query changes;
|
|
165
756
|
* - `'always'`: keep the first navigable item highlighted whenever the popup is open.
|
|
166
757
|
*/
|
|
167
|
-
this.autoHighlight = input(false, ...(ngDevMode ?
|
|
758
|
+
this.autoHighlight = input(false, { ...(ngDevMode ? { debugName: "autoHighlight" } : /* istanbul ignore next */ {}), transform: coerceAutoHighlight });
|
|
168
759
|
/** Resolved auto-highlight mode. */
|
|
169
760
|
this.autoHighlightMode = computed(() => {
|
|
170
761
|
const value = this.autoHighlight();
|
|
@@ -176,6 +767,18 @@ class RdxComboboxRoot {
|
|
|
176
767
|
}
|
|
177
768
|
return 'off';
|
|
178
769
|
}, ...(ngDevMode ? [{ debugName: "autoHighlightMode" }] : /* istanbul ignore next */ []));
|
|
770
|
+
/**
|
|
771
|
+
* Whether moving the pointer over an item highlights it. `true` (default) paints `data-highlighted`
|
|
772
|
+
* on hover; `false` suppresses hover-driven highlight entirely, letting CSS `:hover` stay distinct
|
|
773
|
+
* from the `data-highlighted` (keyboard) state. Clicking an item still selects it.
|
|
774
|
+
*/
|
|
775
|
+
this.highlightItemOnHover = input(true, { ...(ngDevMode ? { debugName: "highlightItemOnHover" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
776
|
+
/**
|
|
777
|
+
* Whether a pointer-driven highlight is kept when the cursor leaves the list. `false` (default)
|
|
778
|
+
* clears the highlight on pointer-leave; `true` retains the last hovered item highlighted. Keyboard
|
|
779
|
+
* navigation and auto-highlight are unaffected.
|
|
780
|
+
*/
|
|
781
|
+
this.keepHighlight = input(false, { ...(ngDevMode ? { debugName: "keepHighlight" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
179
782
|
/** Whether clicking the input opens the popup. */
|
|
180
783
|
this.openOnInputClick = input(true, { ...(ngDevMode ? { debugName: "openOnInputClick" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
181
784
|
/** Whether the popup is modal: locks page scroll and makes outside content inert while open. */
|
|
@@ -204,8 +807,14 @@ class RdxComboboxRoot {
|
|
|
204
807
|
* items outside the rendered window are not skipped by keyboard navigation.
|
|
205
808
|
*/
|
|
206
809
|
this.virtualized = input(false, { ...(ngDevMode ? { debugName: "virtualized" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
207
|
-
/**
|
|
208
|
-
|
|
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 */ []));
|
|
209
818
|
/** Converts a value to its display label. Defaults to the matching item's text. */
|
|
210
819
|
this.itemToStringLabel = input(...(ngDevMode ? [undefined, { debugName: "itemToStringLabel" }] : /* istanbul ignore next */ []));
|
|
211
820
|
/** Emits when the selection changes. */
|
|
@@ -221,97 +830,51 @@ class RdxComboboxRoot {
|
|
|
221
830
|
this.onItemHighlighted = output();
|
|
222
831
|
/** Emits after the open/close transition (including any exit animation) finishes. */
|
|
223
832
|
this.onOpenChangeComplete = output();
|
|
224
|
-
this.transition = useTransitionStatus((open) => this.onOpenChangeComplete.emit(open));
|
|
225
|
-
/** Open/close transition phase, for `data-starting-style` / `data-ending-style`. */
|
|
226
|
-
this.transitionStatus = this.transition.status;
|
|
227
|
-
/** Registers the popup element whose animation determines transition completion. */
|
|
228
|
-
this.registerTransitionElement = this.transition.registerElement;
|
|
229
|
-
this.listId = injectId('rdx-combobox-list-');
|
|
230
|
-
this.labelId = signal(undefined, ...(ngDevMode ? [{ debugName: "labelId" }] : /* istanbul ignore next */ []));
|
|
231
|
-
this.inputElement = signal(null, ...(ngDevMode ? [{ debugName: "inputElement" }] : /* istanbul ignore next */ []));
|
|
232
833
|
this.cvaDisabled = signal(false, ...(ngDevMode ? [{ debugName: "cvaDisabled" }] : /* istanbul ignore next */ []));
|
|
233
834
|
this.disabledState = computed(() => this.disabled() || this.cvaDisabled(), ...(ngDevMode ? [{ debugName: "disabledState" }] : /* istanbul ignore next */ []));
|
|
234
835
|
this.requiredState = computed(() => this.required(), ...(ngDevMode ? [{ debugName: "requiredState" }] : /* istanbul ignore next */ []));
|
|
235
|
-
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 */ []));
|
|
236
838
|
/**
|
|
237
839
|
* Whether the input text is a fresh user query rather than the current selection's label. While
|
|
238
840
|
* `false` (just opened, or showing a selected label), the list is unfiltered so the user can
|
|
239
841
|
* browse; it flips `true` on the first keystroke.
|
|
240
842
|
*/
|
|
241
843
|
this.typed = signal(false, ...(ngDevMode ? [{ debugName: "typed" }] : /* istanbul ignore next */ []));
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
this.filteredItems = computed(() => {
|
|
270
|
-
const data = this.items();
|
|
271
|
-
if (data === undefined) {
|
|
272
|
-
return this.visibleItems().map((item) => item.value());
|
|
273
|
-
}
|
|
274
|
-
const limit = this.limit();
|
|
275
|
-
const cap = (arr) => (limit >= 0 ? arr.slice(0, limit) : arr);
|
|
276
|
-
const filter = this.filter();
|
|
277
|
-
if (filter === null) {
|
|
278
|
-
return cap(data);
|
|
279
|
-
}
|
|
280
|
-
const query = this.typed() ? (this.inputValue() ?? '') : '';
|
|
281
|
-
if (!query) {
|
|
282
|
-
return cap(data);
|
|
283
|
-
}
|
|
284
|
-
const matcher = filter ?? this.defaultFilter.contains;
|
|
285
|
-
return cap(data.filter((value) => matcher(this.textFor(value), query)));
|
|
286
|
-
}, ...(ngDevMode ? [{ debugName: "filteredItems" }] : /* istanbul ignore next */ []));
|
|
287
|
-
this.visibleCount = computed(() => this.virtualized() ? this.filteredItems().length : this.visibleItems().length, ...(ngDevMode ? [{ debugName: "visibleCount" }] : /* istanbul ignore next */ []));
|
|
288
|
-
this.highlight = useListHighlight({
|
|
289
|
-
items: this.orderedItems,
|
|
290
|
-
isNavigable: (item) => this.isVisible(item) && !item.disabled(),
|
|
291
|
-
getId: (item) => item.id,
|
|
292
|
-
loop: this.loopFocus,
|
|
293
|
-
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)
|
|
294
871
|
});
|
|
295
|
-
this.highlightedItem = this.highlight.highlightedItem;
|
|
296
|
-
/** Highlighted index into {@link filteredItems} in virtualized mode (`-1` when cleared). */
|
|
297
|
-
this.highlightedIndex = signal(-1, ...(ngDevMode ? [{ debugName: "highlightedIndex" }] : /* istanbul ignore next */ []));
|
|
298
|
-
/** Why the highlight last moved; read when emitting {@link onItemHighlighted}. */
|
|
299
|
-
this.highlightReason = signal('none', ...(ngDevMode ? [{ debugName: "highlightReason" }] : /* istanbul ignore next */ []));
|
|
300
|
-
this.activeId = computed(() => {
|
|
301
|
-
if (this.virtualized()) {
|
|
302
|
-
const index = this.highlightedIndex();
|
|
303
|
-
return index >= 0 ? this.itemId(index) : undefined;
|
|
304
|
-
}
|
|
305
|
-
return this.highlight.activeId();
|
|
306
|
-
}, ...(ngDevMode ? [{ debugName: "activeId" }] : /* istanbul ignore next */ []));
|
|
307
|
-
/** Edge to highlight once the list has mounted (items register asynchronously after opening). */
|
|
308
|
-
this.pendingHighlightEdge = signal(null, ...(ngDevMode ? [{ debugName: "pendingHighlightEdge" }] : /* istanbul ignore next */ []));
|
|
309
|
-
// Tracks whether the last interaction was the keyboard, so the highlight doesn't jump to an item
|
|
310
|
-
// the cursor happens to rest on when arrow-key navigation scrolls the list under a still pointer.
|
|
311
|
-
this.keyboardActive = false;
|
|
312
|
-
/** The trigger element, used as a focus fallback when the input lives inside the popup. */
|
|
313
|
-
this.triggerElement = null;
|
|
314
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));
|
|
315
878
|
// Apply uncontrolled defaults once.
|
|
316
879
|
effect(() => {
|
|
317
880
|
const initial = this.defaultValue();
|
|
@@ -324,105 +887,6 @@ class RdxComboboxRoot {
|
|
|
324
887
|
this.open.set(true);
|
|
325
888
|
}
|
|
326
889
|
});
|
|
327
|
-
// Emit open changes and drive the open/close transition (skip the initial run).
|
|
328
|
-
let previousOpen = untracked(this.open);
|
|
329
|
-
effect(() => {
|
|
330
|
-
const open = this.open();
|
|
331
|
-
if (open === previousOpen) {
|
|
332
|
-
return;
|
|
333
|
-
}
|
|
334
|
-
previousOpen = open;
|
|
335
|
-
untracked(() => {
|
|
336
|
-
this.onOpenChange.emit(open);
|
|
337
|
-
this.transition.start(open);
|
|
338
|
-
});
|
|
339
|
-
});
|
|
340
|
-
// Emit highlight changes (skip the initial run). Tracks both the DOM-ref highlight and the
|
|
341
|
-
// virtualized index; only one is active per mode, so the other never fires spuriously.
|
|
342
|
-
let highlightInitialized = false;
|
|
343
|
-
effect(() => {
|
|
344
|
-
const item = this.highlightedItem();
|
|
345
|
-
const index = this.highlightedIndex();
|
|
346
|
-
if (!highlightInitialized) {
|
|
347
|
-
highlightInitialized = true;
|
|
348
|
-
return;
|
|
349
|
-
}
|
|
350
|
-
untracked(() => {
|
|
351
|
-
const reason = this.highlightReason();
|
|
352
|
-
if (this.virtualized()) {
|
|
353
|
-
const value = index >= 0 ? (this.filteredItems()[index] ?? null) : null;
|
|
354
|
-
this.onItemHighlighted.emit({ value, index, reason });
|
|
355
|
-
}
|
|
356
|
-
else {
|
|
357
|
-
const value = item ? item.value() : null;
|
|
358
|
-
const itemIndex = item ? this.visibleItems().indexOf(item) : -1;
|
|
359
|
-
this.onItemHighlighted.emit({ value, index: itemIndex, reason });
|
|
360
|
-
}
|
|
361
|
-
});
|
|
362
|
-
});
|
|
363
|
-
// Apply a deferred open-edge highlight once items (DOM refs) or filtered data have registered.
|
|
364
|
-
effect(() => {
|
|
365
|
-
const edge = this.pendingHighlightEdge();
|
|
366
|
-
const count = this.virtualized() ? this.filteredItems().length : this.orderedItems().length;
|
|
367
|
-
if (!this.open() || edge === null || count === 0) {
|
|
368
|
-
return;
|
|
369
|
-
}
|
|
370
|
-
untracked(() => {
|
|
371
|
-
// Programmatic move — reset the reason in both modes so the emit reports 'none', not a
|
|
372
|
-
// stale 'keyboard'/'pointer' left by the previous user interaction.
|
|
373
|
-
this.highlightReason.set('none');
|
|
374
|
-
if (this.virtualized()) {
|
|
375
|
-
this.highlightedIndex.set(edge === 'first' ? 0 : count - 1);
|
|
376
|
-
}
|
|
377
|
-
else if (edge === 'first') {
|
|
378
|
-
this.highlight.first();
|
|
379
|
-
}
|
|
380
|
-
else {
|
|
381
|
-
this.highlight.last();
|
|
382
|
-
}
|
|
383
|
-
this.pendingHighlightEdge.set(null);
|
|
384
|
-
});
|
|
385
|
-
});
|
|
386
|
-
// autoHighlight 'always': keep the first navigable item highlighted whenever the popup is
|
|
387
|
-
// open. `visibleCount` re-runs this when filtering changes; the current highlight is read
|
|
388
|
-
// untracked to re-establish a highlight only after the self-heal clears it (no loop).
|
|
389
|
-
effect(() => {
|
|
390
|
-
this.orderedItems();
|
|
391
|
-
this.visibleCount();
|
|
392
|
-
if (this.autoHighlightMode() === 'always' && this.open()) {
|
|
393
|
-
untracked(() => {
|
|
394
|
-
if (this.virtualized()) {
|
|
395
|
-
// Re-seed when the index is cleared OR has fallen out of range, so this works
|
|
396
|
-
// regardless of whether the self-heal effect ran first (no ordering dependency).
|
|
397
|
-
const length = this.filteredItems().length;
|
|
398
|
-
const index = this.highlightedIndex();
|
|
399
|
-
if ((index < 0 || index >= length) && length > 0) {
|
|
400
|
-
this.highlightReason.set('none');
|
|
401
|
-
this.highlightedIndex.set(0);
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
else if (this.highlightedItem() === null) {
|
|
405
|
-
this.highlightReason.set('none');
|
|
406
|
-
this.highlight.first();
|
|
407
|
-
}
|
|
408
|
-
});
|
|
409
|
-
}
|
|
410
|
-
});
|
|
411
|
-
// Virtualized self-heal: clear a highlight that filtering has pushed out of range, so
|
|
412
|
-
// `activeId` never references an index past the end of the filtered list.
|
|
413
|
-
effect(() => {
|
|
414
|
-
if (!this.virtualized()) {
|
|
415
|
-
return;
|
|
416
|
-
}
|
|
417
|
-
const length = this.filteredItems().length;
|
|
418
|
-
untracked(() => {
|
|
419
|
-
const index = this.highlightedIndex();
|
|
420
|
-
if (index >= length && index !== -1) {
|
|
421
|
-
this.highlightReason.set('none');
|
|
422
|
-
this.highlightedIndex.set(-1);
|
|
423
|
-
}
|
|
424
|
-
});
|
|
425
|
-
});
|
|
426
890
|
// Virtualized object values can't be labelled from the DOM (items aren't registered) — without
|
|
427
891
|
// `itemToStringLabel`, selection/revert fall back to a generic label. Warn once in dev.
|
|
428
892
|
if (isDevMode()) {
|
|
@@ -440,46 +904,41 @@ class RdxComboboxRoot {
|
|
|
440
904
|
}
|
|
441
905
|
}
|
|
442
906
|
/** Opens the popup for browsing (resets the query to "pristine" and selects the input text). */
|
|
443
|
-
openForBrowse() {
|
|
907
|
+
openForBrowse(reason = 'none', event = new Event('combobox.open-change')) {
|
|
444
908
|
if (!this.open()) {
|
|
445
909
|
this.typed.set(false);
|
|
446
910
|
}
|
|
447
|
-
this.setOpen(true);
|
|
448
|
-
this.selectInputText();
|
|
911
|
+
this.setOpen(true, reason, event);
|
|
912
|
+
this.engine.selectInputText();
|
|
449
913
|
if (this.autoHighlightMode() === 'always') {
|
|
450
|
-
this.
|
|
914
|
+
this.engine.setPendingHighlightEdge('first');
|
|
451
915
|
}
|
|
452
916
|
}
|
|
453
917
|
/** Opens the popup and highlights the given edge once the list mounts. */
|
|
454
|
-
openAndHighlight(edge) {
|
|
918
|
+
openAndHighlight(edge, reason = 'list-navigation', event = new Event('combobox.open-change')) {
|
|
455
919
|
if (!this.open()) {
|
|
456
920
|
this.typed.set(false);
|
|
457
921
|
}
|
|
458
|
-
this.setOpen(true);
|
|
459
|
-
this.selectInputText();
|
|
460
|
-
this.
|
|
922
|
+
this.setOpen(true, reason, event);
|
|
923
|
+
this.engine.selectInputText();
|
|
924
|
+
this.engine.setPendingHighlightEdge(edge);
|
|
461
925
|
}
|
|
462
|
-
/**
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
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();
|
|
467
941
|
}
|
|
468
|
-
// Until the user types a fresh query, show the whole list (the input may still hold the
|
|
469
|
-
// selected item's label, which must not filter everything down to just that item).
|
|
470
|
-
const query = this.typed() ? (this.inputValue() ?? '') : '';
|
|
471
|
-
const matcher = filter ?? this.defaultFilter.contains;
|
|
472
|
-
return matcher(item.textValue(), query);
|
|
473
|
-
}
|
|
474
|
-
/** Whether the item is shown in the list (matches the query and is within `limit`). */
|
|
475
|
-
isVisible(item) {
|
|
476
|
-
return this.visibleSet().has(item);
|
|
477
|
-
}
|
|
478
|
-
isKeyboardActive() {
|
|
479
|
-
return this.keyboardActive;
|
|
480
|
-
}
|
|
481
|
-
setKeyboardActive(value) {
|
|
482
|
-
this.keyboardActive = value;
|
|
483
942
|
}
|
|
484
943
|
isSelected(value) {
|
|
485
944
|
if (this.mode() === 'none') {
|
|
@@ -487,28 +946,34 @@ class RdxComboboxRoot {
|
|
|
487
946
|
}
|
|
488
947
|
const current = this.value();
|
|
489
948
|
if (this.multiple()) {
|
|
490
|
-
return Array.isArray(current) && current.some((v) => isItemEqualToValue(v, value, this.
|
|
949
|
+
return Array.isArray(current) && current.some((v) => isItemEqualToValue(v, value, this.isItemEqualToValue()));
|
|
491
950
|
}
|
|
492
|
-
return !isNullish(current) && isItemEqualToValue(current, value, this.
|
|
493
|
-
}
|
|
494
|
-
registerItem(item) {
|
|
495
|
-
this._items.update((items) => [...items, item]);
|
|
496
|
-
}
|
|
497
|
-
unregisterItem(item) {
|
|
498
|
-
this._items.update((items) => items.filter((i) => i !== item));
|
|
951
|
+
return !isNullish(current) && isItemEqualToValue(current, value, this.isItemEqualToValue());
|
|
499
952
|
}
|
|
500
|
-
setOpen(open) {
|
|
501
|
-
if (
|
|
502
|
-
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;
|
|
503
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());
|
|
504
966
|
this.open.set(open);
|
|
967
|
+
return true;
|
|
505
968
|
}
|
|
506
|
-
closePopup(revert = true) {
|
|
969
|
+
closePopup(revert = true, reason = 'none', event = new Event('combobox.open-change')) {
|
|
507
970
|
if (!this.open()) {
|
|
508
971
|
return;
|
|
509
972
|
}
|
|
510
|
-
this.
|
|
511
|
-
|
|
973
|
+
if (!this.setOpen(false, reason, event)) {
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
this.engine.clearHighlightState();
|
|
512
977
|
if (revert) {
|
|
513
978
|
this.revertInputValue();
|
|
514
979
|
}
|
|
@@ -519,9 +984,19 @@ class RdxComboboxRoot {
|
|
|
519
984
|
this.inputValue.set(value);
|
|
520
985
|
this.typed.set(true);
|
|
521
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
|
+
}
|
|
522
997
|
// Auto-highlight the first match as the query changes (deferred so it lands after items mount).
|
|
523
998
|
if (this.autoHighlightMode() !== 'off') {
|
|
524
|
-
this.
|
|
999
|
+
this.engine.setPendingHighlightEdge('first');
|
|
525
1000
|
}
|
|
526
1001
|
}
|
|
527
1002
|
/** Sets the input text programmatically (a selection label / revert) — not a user query. */
|
|
@@ -530,10 +1005,6 @@ class RdxComboboxRoot {
|
|
|
530
1005
|
this.typed.set(false);
|
|
531
1006
|
this.onInputValueChange.emit(value);
|
|
532
1007
|
}
|
|
533
|
-
/** Selects all input text so the next keystroke replaces a stale selection label. */
|
|
534
|
-
selectInputText() {
|
|
535
|
-
this.inputElement()?.select();
|
|
536
|
-
}
|
|
537
1008
|
/** Resets the input text to the current selection's label (single mode) or empty. */
|
|
538
1009
|
revertInputValue() {
|
|
539
1010
|
if (this.multiple()) {
|
|
@@ -548,37 +1019,31 @@ class RdxComboboxRoot {
|
|
|
548
1019
|
if (custom) {
|
|
549
1020
|
return custom(value);
|
|
550
1021
|
}
|
|
551
|
-
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()));
|
|
552
1023
|
return item ? item.textValue() : itemToStringLabel(value);
|
|
553
1024
|
}
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
const custom = this.itemToStringLabel();
|
|
557
|
-
return custom ? custom(value) : itemToStringLabel(value);
|
|
558
|
-
}
|
|
559
|
-
/** Deterministic id for the item at `index` in virtualized mode (matches `aria-activedescendant`). */
|
|
560
|
-
itemId(index) {
|
|
561
|
-
return `${this.listId}-item-${index}`;
|
|
562
|
-
}
|
|
563
|
-
handleSelect(item) {
|
|
564
|
-
if (this.disabledState() || this.readonly() || item.disabled()) {
|
|
1025
|
+
handleSelect(item, event = new Event('combobox.item-press')) {
|
|
1026
|
+
if (this.disabledState() || this.readOnly() || item.disabled()) {
|
|
565
1027
|
return;
|
|
566
1028
|
}
|
|
567
|
-
this.handleSelectValue(item.value(), item.textValue() || this.labelFor(item.value()));
|
|
1029
|
+
this.handleSelectValue(item.value(), item.textValue() || this.labelFor(item.value()), event);
|
|
568
1030
|
}
|
|
569
1031
|
/** Selects the filtered item at `index` (virtualized mode). The label comes from {@link labelFor}. */
|
|
570
|
-
selectIndex(index) {
|
|
571
|
-
if (this.disabledState() || this.
|
|
1032
|
+
selectIndex(index, event = new Event('combobox.item-press')) {
|
|
1033
|
+
if (this.disabledState() || this.readOnly()) {
|
|
572
1034
|
return;
|
|
573
1035
|
}
|
|
574
|
-
const value = this.filteredItems()[index];
|
|
1036
|
+
const value = this.engine.filteredItems()[index];
|
|
575
1037
|
if (value === undefined) {
|
|
576
1038
|
return;
|
|
577
1039
|
}
|
|
578
|
-
this.handleSelectValue(value, this.labelFor(value));
|
|
1040
|
+
this.handleSelectValue(value, this.labelFor(value), event);
|
|
579
1041
|
}
|
|
580
1042
|
/** Commits a selection from a resolved value/label, independent of whether a DOM item exists. */
|
|
581
|
-
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;
|
|
582
1047
|
if (this.mode() === 'none') {
|
|
583
1048
|
// No value is committed; `onValueChange` fires as a pointer/keyboard activation signal so
|
|
584
1049
|
// command-palette consumers can react. Optionally fill the input, then close.
|
|
@@ -586,15 +1051,14 @@ class RdxComboboxRoot {
|
|
|
586
1051
|
if (this.fillInputOnItemPress()) {
|
|
587
1052
|
this.setLabel(textValue);
|
|
588
1053
|
}
|
|
589
|
-
this.
|
|
590
|
-
this.
|
|
591
|
-
this.restoreFocusAfterSelect();
|
|
1054
|
+
this.closePopup(false, 'item-press', event);
|
|
1055
|
+
this.engine.restoreFocusAfterSelect(activeBefore);
|
|
592
1056
|
this.maybeSubmit();
|
|
593
1057
|
return;
|
|
594
1058
|
}
|
|
595
1059
|
if (this.multiple()) {
|
|
596
1060
|
const current = Array.isArray(this.value()) ? [...this.value()] : [];
|
|
597
|
-
const index = current.findIndex((v) => isItemEqualToValue(v, value, this.
|
|
1061
|
+
const index = current.findIndex((v) => isItemEqualToValue(v, value, this.isItemEqualToValue()));
|
|
598
1062
|
if (index === -1) {
|
|
599
1063
|
current.push(value);
|
|
600
1064
|
}
|
|
@@ -603,124 +1067,57 @@ class RdxComboboxRoot {
|
|
|
603
1067
|
}
|
|
604
1068
|
this.commitValue(current);
|
|
605
1069
|
this.setLabel('');
|
|
606
|
-
|
|
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
|
+
}
|
|
607
1074
|
}
|
|
608
1075
|
else {
|
|
609
1076
|
this.commitValue(value);
|
|
610
1077
|
this.setLabel(textValue);
|
|
611
|
-
this.
|
|
612
|
-
this.
|
|
613
|
-
this.restoreFocusAfterSelect();
|
|
1078
|
+
this.closePopup(false, 'item-press', event);
|
|
1079
|
+
this.engine.restoreFocusAfterSelect(activeBefore);
|
|
614
1080
|
}
|
|
615
1081
|
this.maybeSubmit();
|
|
616
1082
|
}
|
|
617
1083
|
/** Requests submit of the closest form when `submitOnItemClick` is enabled. */
|
|
618
1084
|
maybeSubmit() {
|
|
619
1085
|
if (this.submitOnItemClick()) {
|
|
620
|
-
this.inputElement()?.form?.requestSubmit?.();
|
|
1086
|
+
this.engine.inputElement()?.form?.requestSubmit?.();
|
|
621
1087
|
}
|
|
622
1088
|
}
|
|
623
|
-
selectHighlighted() {
|
|
1089
|
+
selectHighlighted(event = new Event('combobox.item-press')) {
|
|
624
1090
|
if (this.virtualized()) {
|
|
625
|
-
const index = this.highlightedIndex();
|
|
1091
|
+
const index = this.engine.highlightedIndex();
|
|
626
1092
|
if (index >= 0) {
|
|
627
|
-
this.selectIndex(index);
|
|
1093
|
+
this.selectIndex(index, event);
|
|
628
1094
|
}
|
|
629
1095
|
return;
|
|
630
1096
|
}
|
|
631
|
-
const item = this.highlightedItem();
|
|
1097
|
+
const item = this.engine.highlightedItem();
|
|
632
1098
|
if (item) {
|
|
633
|
-
this.handleSelect(item);
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
// --- Highlight navigation facade (mode-aware: index-based when virtualized, else DOM-ref) ---
|
|
637
|
-
highlightNext(reason = 'keyboard') {
|
|
638
|
-
this.highlightReason.set(reason);
|
|
639
|
-
if (this.virtualized()) {
|
|
640
|
-
this.stepIndex(1);
|
|
641
|
-
}
|
|
642
|
-
else {
|
|
643
|
-
this.highlight.next();
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
highlightPrevious(reason = 'keyboard') {
|
|
647
|
-
this.highlightReason.set(reason);
|
|
648
|
-
if (this.virtualized()) {
|
|
649
|
-
this.stepIndex(-1);
|
|
650
|
-
}
|
|
651
|
-
else {
|
|
652
|
-
this.highlight.previous();
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
highlightFirst(reason = 'keyboard') {
|
|
656
|
-
this.highlightReason.set(reason);
|
|
657
|
-
if (this.virtualized()) {
|
|
658
|
-
this.highlightedIndex.set(this.filteredItems().length > 0 ? 0 : -1);
|
|
659
|
-
}
|
|
660
|
-
else {
|
|
661
|
-
this.highlight.first();
|
|
1099
|
+
this.handleSelect(item, event);
|
|
662
1100
|
}
|
|
663
1101
|
}
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
if (this.
|
|
667
|
-
const length = this.filteredItems().length;
|
|
668
|
-
this.highlightedIndex.set(length > 0 ? length - 1 : -1);
|
|
669
|
-
}
|
|
670
|
-
else {
|
|
671
|
-
this.highlight.last();
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
/** Highlights a specific index in virtualized mode (e.g. pointer hover). Ignored if out of range. */
|
|
675
|
-
highlightIndex(index, reason) {
|
|
676
|
-
if (index < 0 || index >= this.filteredItems().length) {
|
|
677
|
-
return;
|
|
678
|
-
}
|
|
679
|
-
this.highlightReason.set(reason);
|
|
680
|
-
this.highlightedIndex.set(index);
|
|
681
|
-
}
|
|
682
|
-
/** Highlights a DOM-ref item (non-virtualized pointer hover). */
|
|
683
|
-
setHighlight(item, reason) {
|
|
684
|
-
this.highlightReason.set(reason);
|
|
685
|
-
this.highlight.set(item);
|
|
686
|
-
}
|
|
687
|
-
/** Clears whichever highlight model is active. */
|
|
688
|
-
clearHighlightState() {
|
|
689
|
-
this.highlight.clear();
|
|
690
|
-
this.highlightedIndex.set(-1);
|
|
691
|
-
}
|
|
692
|
-
/** Steps the virtualized highlight index by `direction`, wrapping when {@link loopFocus}. */
|
|
693
|
-
stepIndex(direction) {
|
|
694
|
-
const length = this.filteredItems().length;
|
|
695
|
-
if (length === 0) {
|
|
696
|
-
this.highlightedIndex.set(-1);
|
|
1102
|
+
clearSelection() {
|
|
1103
|
+
// Read-only / disabled comboboxes are not user-mutable (Base UI blocks Clear here too).
|
|
1104
|
+
if (this.disabledState() || this.readOnly()) {
|
|
697
1105
|
return;
|
|
698
1106
|
}
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
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);
|
|
703
1111
|
}
|
|
704
|
-
let next = current + direction;
|
|
705
|
-
const loop = this.loopFocus();
|
|
706
|
-
if (next < 0) {
|
|
707
|
-
next = loop ? length - 1 : 0;
|
|
708
|
-
}
|
|
709
|
-
else if (next >= length) {
|
|
710
|
-
next = loop ? 0 : length - 1;
|
|
711
|
-
}
|
|
712
|
-
this.highlightedIndex.set(next);
|
|
713
|
-
}
|
|
714
|
-
clearSelection() {
|
|
715
|
-
this.commitValue(this.multiple() ? [] : null);
|
|
716
1112
|
this.setLabel('');
|
|
717
|
-
this.
|
|
1113
|
+
this.engine.clearHighlightState();
|
|
1114
|
+
this.engine.focusInput();
|
|
718
1115
|
}
|
|
719
1116
|
removeValue(value) {
|
|
720
1117
|
if (!this.multiple() || !Array.isArray(this.value())) {
|
|
721
1118
|
return;
|
|
722
1119
|
}
|
|
723
|
-
const next = this.value().filter((v) => !isItemEqualToValue(v, value, this.
|
|
1120
|
+
const next = this.value().filter((v) => !isItemEqualToValue(v, value, this.isItemEqualToValue()));
|
|
724
1121
|
this.commitValue(next);
|
|
725
1122
|
}
|
|
726
1123
|
removeLastValue() {
|
|
@@ -733,21 +1130,7 @@ class RdxComboboxRoot {
|
|
|
733
1130
|
}
|
|
734
1131
|
}
|
|
735
1132
|
focusInput() {
|
|
736
|
-
this.
|
|
737
|
-
}
|
|
738
|
-
/**
|
|
739
|
-
* Restores focus after a selection closes the popup, so the keyboard can reopen it. When the
|
|
740
|
-
* input lives inside the popup it is about to unmount, so focus goes to the trigger instead;
|
|
741
|
-
* otherwise it returns to the input. Done synchronously while the input is still in the DOM.
|
|
742
|
-
*/
|
|
743
|
-
restoreFocusAfterSelect() {
|
|
744
|
-
const input = this.inputElement();
|
|
745
|
-
if (input && !input.closest('[rdxComboboxPopup]')) {
|
|
746
|
-
input.focus();
|
|
747
|
-
}
|
|
748
|
-
else {
|
|
749
|
-
this.triggerElement?.focus();
|
|
750
|
-
}
|
|
1133
|
+
this.engine.focusInput();
|
|
751
1134
|
}
|
|
752
1135
|
/** Registered by `RdxComboboxChips` so the input can hand keyboard focus to the chips. */
|
|
753
1136
|
registerChipsNav(fn) {
|
|
@@ -760,11 +1143,40 @@ class RdxComboboxRoot {
|
|
|
760
1143
|
markAsTouched() {
|
|
761
1144
|
this.onTouched?.();
|
|
762
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
|
+
*/
|
|
763
1151
|
commitValue(value) {
|
|
1152
|
+
if (this.disabledState() || this.readOnly()) {
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
764
1155
|
this.value.set(value);
|
|
765
1156
|
this.onValueChange.emit(value);
|
|
766
1157
|
this.onChange?.(value);
|
|
767
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
|
+
}
|
|
768
1180
|
// ControlValueAccessor
|
|
769
1181
|
writeValue(value) {
|
|
770
1182
|
untracked(() => this.value.set(value));
|
|
@@ -779,9 +1191,12 @@ class RdxComboboxRoot {
|
|
|
779
1191
|
this.cvaDisabled.set(isDisabled);
|
|
780
1192
|
}
|
|
781
1193
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxRoot, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
782
|
-
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: [
|
|
783
1195
|
provideComboboxRootContext(context),
|
|
784
|
-
{ 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)
|
|
785
1200
|
], exportAs: ["rdxComboboxRoot"], hostDirectives: [{ directive: i1.RdxPopper }], ngImport: i0 }); }
|
|
786
1201
|
}
|
|
787
1202
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxRoot, decorators: [{
|
|
@@ -791,14 +1206,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
|
|
|
791
1206
|
exportAs: 'rdxComboboxRoot',
|
|
792
1207
|
providers: [
|
|
793
1208
|
provideComboboxRootContext(context),
|
|
794
|
-
{ 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)
|
|
795
1213
|
],
|
|
796
1214
|
hostDirectives: [RdxPopper],
|
|
797
1215
|
host: {
|
|
798
1216
|
'[attr.data-disabled]': 'disabledState() ? "" : undefined'
|
|
799
1217
|
}
|
|
800
1218
|
}]
|
|
801
|
-
}], 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"] }] } });
|
|
802
1220
|
|
|
803
1221
|
/**
|
|
804
1222
|
* An overlay rendered beneath the popup in `modal` mode. Place it inside the portal/presence; style
|
|
@@ -812,7 +1230,7 @@ class RdxComboboxBackdrop {
|
|
|
812
1230
|
this.rootContext = injectComboboxRootContext();
|
|
813
1231
|
}
|
|
814
1232
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxBackdrop, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
815
|
-
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 }); }
|
|
816
1234
|
}
|
|
817
1235
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxBackdrop, decorators: [{
|
|
818
1236
|
type: Directive,
|
|
@@ -820,7 +1238,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
|
|
|
820
1238
|
selector: '[rdxComboboxBackdrop]',
|
|
821
1239
|
exportAs: 'rdxComboboxBackdrop',
|
|
822
1240
|
host: {
|
|
823
|
-
|
|
1241
|
+
// A decorative overlay — Base UI marks it `role="presentation"` (excluded from the a11y tree).
|
|
1242
|
+
role: 'presentation',
|
|
824
1243
|
'[attr.data-state]': 'rootContext.open() ? "open" : "closed"',
|
|
825
1244
|
'[attr.data-open]': 'rootContext.open() ? "" : undefined',
|
|
826
1245
|
'[attr.data-closed]': 'rootContext.open() ? undefined : ""'
|
|
@@ -830,7 +1249,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
|
|
|
830
1249
|
|
|
831
1250
|
/**
|
|
832
1251
|
* Container for the selected-value chips in `multiple` mode. Sits before the input and coordinates
|
|
833
|
-
* 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.
|
|
834
1255
|
*
|
|
835
1256
|
* @group Components
|
|
836
1257
|
*/
|
|
@@ -855,16 +1276,16 @@ class RdxComboboxChips {
|
|
|
855
1276
|
return true;
|
|
856
1277
|
}
|
|
857
1278
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxChips, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
858
|
-
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 }); }
|
|
859
1280
|
}
|
|
860
1281
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxChips, decorators: [{
|
|
861
1282
|
type: Directive,
|
|
862
1283
|
args: [{
|
|
863
1284
|
selector: '[rdxComboboxChips]',
|
|
864
1285
|
exportAs: 'rdxComboboxChips',
|
|
865
|
-
hostDirectives: [
|
|
1286
|
+
hostDirectives: [RdxFloatingInsideElement],
|
|
866
1287
|
host: {
|
|
867
|
-
role: '
|
|
1288
|
+
role: 'toolbar'
|
|
868
1289
|
}
|
|
869
1290
|
}]
|
|
870
1291
|
}], ctorParameters: () => [] });
|
|
@@ -892,21 +1313,35 @@ class RdxComboboxChip {
|
|
|
892
1313
|
const list = this.chips?.getChips() ?? [];
|
|
893
1314
|
const index = list.indexOf(this.element);
|
|
894
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;
|
|
895
1324
|
case 'ArrowLeft':
|
|
896
|
-
|
|
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) {
|
|
897
1331
|
event.preventDefault();
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
list[index + 1].focus();
|
|
1332
|
+
if (index < list.length - 1) {
|
|
1333
|
+
list[index + 1].focus();
|
|
1334
|
+
}
|
|
1335
|
+
else {
|
|
1336
|
+
this.rootContext.focusInput();
|
|
1337
|
+
}
|
|
905
1338
|
}
|
|
906
|
-
else {
|
|
907
|
-
|
|
1339
|
+
else if (index > 0) {
|
|
1340
|
+
event.preventDefault();
|
|
1341
|
+
list[index - 1].focus();
|
|
908
1342
|
}
|
|
909
1343
|
break;
|
|
1344
|
+
}
|
|
910
1345
|
case 'Home':
|
|
911
1346
|
if (list.length) {
|
|
912
1347
|
event.preventDefault();
|
|
@@ -941,7 +1376,7 @@ class RdxComboboxChip {
|
|
|
941
1376
|
}
|
|
942
1377
|
}
|
|
943
1378
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxChip, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
944
|
-
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 }); }
|
|
945
1380
|
}
|
|
946
1381
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxChip, decorators: [{
|
|
947
1382
|
type: Directive,
|
|
@@ -950,7 +1385,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
|
|
|
950
1385
|
exportAs: 'rdxComboboxChip',
|
|
951
1386
|
providers: [provideComboboxChipContext(chipContext)],
|
|
952
1387
|
host: {
|
|
953
|
-
role:
|
|
1388
|
+
// No explicit role (Base UI): a focusable child of the `toolbar` chips container.
|
|
954
1389
|
tabindex: '-1',
|
|
955
1390
|
'(keydown)': 'onKeydown($event)'
|
|
956
1391
|
}
|
|
@@ -996,7 +1431,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
|
|
|
996
1431
|
class RdxComboboxClear {
|
|
997
1432
|
constructor() {
|
|
998
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
|
+
*/
|
|
999
1442
|
this.isEmpty = computed(() => {
|
|
1443
|
+
if (this.rootContext.selectionMode() === 'none') {
|
|
1444
|
+
return (this.rootContext.inputValue() ?? '') === '';
|
|
1445
|
+
}
|
|
1000
1446
|
const value = this.rootContext.value();
|
|
1001
1447
|
if (Array.isArray(value)) {
|
|
1002
1448
|
return value.length === 0;
|
|
@@ -1004,49 +1450,76 @@ class RdxComboboxClear {
|
|
|
1004
1450
|
return value === null || value === undefined;
|
|
1005
1451
|
}, ...(ngDevMode ? [{ debugName: "isEmpty" }] : /* istanbul ignore next */ []));
|
|
1006
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
|
+
}
|
|
1007
1457
|
onClick() {
|
|
1008
1458
|
this.rootContext.clearSelection();
|
|
1009
1459
|
}
|
|
1010
1460
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxClear, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
1011
|
-
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 }); }
|
|
1012
1462
|
}
|
|
1013
1463
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxClear, decorators: [{
|
|
1014
1464
|
type: Directive,
|
|
1015
1465
|
args: [{
|
|
1016
1466
|
selector: 'button[rdxComboboxClear]',
|
|
1017
1467
|
exportAs: 'rdxComboboxClear',
|
|
1018
|
-
hostDirectives: [
|
|
1468
|
+
hostDirectives: [RdxFloatingInsideElement],
|
|
1019
1469
|
host: {
|
|
1020
1470
|
type: 'button',
|
|
1021
1471
|
tabindex: '-1',
|
|
1022
1472
|
'aria-label': 'Clear',
|
|
1023
1473
|
'[hidden]': 'isEmpty()',
|
|
1024
|
-
'[attr.disabled]': '
|
|
1474
|
+
'[attr.disabled]': 'isDisabled() ? "" : undefined',
|
|
1475
|
+
'(pointerdown)': 'onPointerDown($event)',
|
|
1476
|
+
'(mousedown)': 'onPointerDown($event)',
|
|
1025
1477
|
'(click)': 'onClick()'
|
|
1026
1478
|
}
|
|
1027
1479
|
}]
|
|
1028
|
-
}] });
|
|
1480
|
+
}], propDecorators: { disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }] } });
|
|
1029
1481
|
|
|
1030
1482
|
/**
|
|
1031
|
-
*
|
|
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.
|
|
1032
1489
|
*
|
|
1033
1490
|
* @group Components
|
|
1034
1491
|
*/
|
|
1035
1492
|
class RdxComboboxEmpty {
|
|
1036
1493
|
constructor() {
|
|
1037
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 */ []));
|
|
1038
1497
|
}
|
|
1039
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxEmpty, deps: [], target: i0.ɵɵFactoryTarget.
|
|
1040
|
-
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 }); }
|
|
1041
1504
|
}
|
|
1042
1505
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxEmpty, decorators: [{
|
|
1043
|
-
type:
|
|
1506
|
+
type: Component,
|
|
1044
1507
|
args: [{
|
|
1045
1508
|
selector: '[rdxComboboxEmpty]',
|
|
1046
1509
|
exportAs: 'rdxComboboxEmpty',
|
|
1510
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1511
|
+
template: `
|
|
1512
|
+
@if (isEmpty()) {
|
|
1513
|
+
<ng-content />
|
|
1514
|
+
}
|
|
1515
|
+
`,
|
|
1047
1516
|
host: {
|
|
1048
|
-
role: '
|
|
1049
|
-
'
|
|
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'
|
|
1050
1523
|
}
|
|
1051
1524
|
}]
|
|
1052
1525
|
}] });
|
|
@@ -1107,6 +1580,12 @@ class RdxComboboxGroupLabel {
|
|
|
1107
1580
|
this.groupContext = injectComboboxGroupContext();
|
|
1108
1581
|
this.id = injectId('rdx-combobox-group-label-');
|
|
1109
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
|
+
});
|
|
1110
1589
|
}
|
|
1111
1590
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxGroupLabel, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
1112
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 }); }
|
|
@@ -1142,7 +1621,50 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
|
|
|
1142
1621
|
}]
|
|
1143
1622
|
}] });
|
|
1144
1623
|
|
|
1145
|
-
|
|
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);
|
|
1146
1668
|
/**
|
|
1147
1669
|
* The combobox text input. Holds DOM focus at all times; the highlighted option is referenced via
|
|
1148
1670
|
* `aria-activedescendant`. Integrates with Field for labeling, description, and validation state.
|
|
@@ -1182,8 +1704,12 @@ class RdxComboboxInput {
|
|
|
1182
1704
|
}, ...(ngDevMode ? [{ debugName: "describedBy" }] : /* istanbul ignore next */ []));
|
|
1183
1705
|
/** Whether an IME composition is in progress (CJK). While composing, don't filter or select. */
|
|
1184
1706
|
this.composing = false;
|
|
1185
|
-
this.dataAttr = attr;
|
|
1707
|
+
this.dataAttr = attr$1;
|
|
1186
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');
|
|
1187
1713
|
afterNextRender(() => {
|
|
1188
1714
|
this.fieldRootContext?.setControlId(this.id());
|
|
1189
1715
|
});
|
|
@@ -1198,22 +1724,31 @@ class RdxComboboxInput {
|
|
|
1198
1724
|
if (this.composing || event.isComposing) {
|
|
1199
1725
|
return;
|
|
1200
1726
|
}
|
|
1201
|
-
this.commitInput(event.target.value);
|
|
1727
|
+
this.commitInput(event.target.value, event);
|
|
1202
1728
|
}
|
|
1203
1729
|
onCompositionEnd(event) {
|
|
1204
1730
|
this.composing = false;
|
|
1205
|
-
this.commitInput(event.target.value);
|
|
1206
|
-
}
|
|
1207
|
-
commitInput(value) {
|
|
1208
|
-
|
|
1209
|
-
|
|
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);
|
|
1210
1740
|
}
|
|
1211
|
-
|
|
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);
|
|
1744
|
+
}
|
|
1745
|
+
// setInputValue applies any autoHighlight (deferred until items mount) and, in single mode,
|
|
1746
|
+
// deselects when the field is emptied.
|
|
1212
1747
|
this.rootContext.setInputValue(value);
|
|
1213
1748
|
}
|
|
1214
|
-
onClick() {
|
|
1749
|
+
onClick(event) {
|
|
1215
1750
|
if (this.rootContext.openOnInputClick()) {
|
|
1216
|
-
this.rootContext.openForBrowse();
|
|
1751
|
+
this.rootContext.openForBrowse('input-press', event);
|
|
1217
1752
|
}
|
|
1218
1753
|
}
|
|
1219
1754
|
onFocus() {
|
|
@@ -1224,8 +1759,9 @@ class RdxComboboxInput {
|
|
|
1224
1759
|
this.fieldRootContext?.setTouched(true);
|
|
1225
1760
|
}
|
|
1226
1761
|
onKeydown(event) {
|
|
1227
|
-
// Don't interfere with IME composition or text-editing shortcuts / range selection.
|
|
1228
|
-
// 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.)
|
|
1229
1765
|
if (event.isComposing || this.composing) {
|
|
1230
1766
|
return;
|
|
1231
1767
|
}
|
|
@@ -1236,23 +1772,11 @@ class RdxComboboxInput {
|
|
|
1236
1772
|
switch (event.key) {
|
|
1237
1773
|
case 'ArrowDown':
|
|
1238
1774
|
event.preventDefault();
|
|
1239
|
-
this.rootContext.
|
|
1240
|
-
if (!open) {
|
|
1241
|
-
this.rootContext.openAndHighlight('first');
|
|
1242
|
-
}
|
|
1243
|
-
else {
|
|
1244
|
-
this.rootContext.highlightNext();
|
|
1245
|
-
}
|
|
1775
|
+
this.rootContext.navigateByKeyboard(1, event);
|
|
1246
1776
|
break;
|
|
1247
1777
|
case 'ArrowUp':
|
|
1248
1778
|
event.preventDefault();
|
|
1249
|
-
this.rootContext.
|
|
1250
|
-
if (!open) {
|
|
1251
|
-
this.rootContext.openAndHighlight('last');
|
|
1252
|
-
}
|
|
1253
|
-
else {
|
|
1254
|
-
this.rootContext.highlightPrevious();
|
|
1255
|
-
}
|
|
1779
|
+
this.rootContext.navigateByKeyboard(-1, event);
|
|
1256
1780
|
break;
|
|
1257
1781
|
case 'Enter':
|
|
1258
1782
|
if (open) {
|
|
@@ -1262,33 +1786,69 @@ class RdxComboboxInput {
|
|
|
1262
1786
|
if (hasHighlight) {
|
|
1263
1787
|
// Select the highlighted item (and prevent an accidental form submit).
|
|
1264
1788
|
event.preventDefault();
|
|
1265
|
-
this.rootContext.selectHighlighted();
|
|
1789
|
+
this.rootContext.selectHighlighted(event);
|
|
1266
1790
|
}
|
|
1267
1791
|
else {
|
|
1268
1792
|
// Nothing highlighted: just close, and let the form submit.
|
|
1269
|
-
this.rootContext.closePopup(true);
|
|
1793
|
+
this.rootContext.closePopup(true, 'none', event);
|
|
1270
1794
|
}
|
|
1271
1795
|
}
|
|
1272
1796
|
break;
|
|
1273
1797
|
case 'Escape':
|
|
1274
|
-
// Just close the popup (reverting the in-progress query); never clear the selection.
|
|
1275
1798
|
if (open) {
|
|
1799
|
+
// Close the popup, reverting the in-progress query; keep the selection.
|
|
1276
1800
|
event.preventDefault();
|
|
1277
|
-
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();
|
|
1278
1810
|
}
|
|
1279
1811
|
break;
|
|
1280
1812
|
case 'Tab':
|
|
1281
|
-
|
|
1282
|
-
|
|
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);
|
|
1283
1817
|
}
|
|
1284
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;
|
|
1285
1829
|
case 'ArrowLeft':
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
this.
|
|
1289
|
-
this.
|
|
1290
|
-
|
|
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()) {
|
|
1291
1849
|
event.preventDefault();
|
|
1850
|
+
this.rootContext.setKeyboardActive(true);
|
|
1851
|
+
this.rootContext.highlightLast();
|
|
1292
1852
|
}
|
|
1293
1853
|
break;
|
|
1294
1854
|
case 'Backspace':
|
|
@@ -1298,20 +1858,35 @@ class RdxComboboxInput {
|
|
|
1298
1858
|
break;
|
|
1299
1859
|
}
|
|
1300
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
|
+
}
|
|
1301
1875
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxInput, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
1302
|
-
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 }); }
|
|
1303
1877
|
}
|
|
1304
1878
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxInput, decorators: [{
|
|
1305
1879
|
type: Directive,
|
|
1306
1880
|
args: [{
|
|
1307
1881
|
selector: 'input[rdxComboboxInput]',
|
|
1308
1882
|
exportAs: 'rdxComboboxInput',
|
|
1309
|
-
hostDirectives: [RdxPopperAnchor,
|
|
1883
|
+
hostDirectives: [RdxPopperAnchor, RdxFloatingInsideElement],
|
|
1310
1884
|
host: {
|
|
1311
1885
|
role: 'combobox',
|
|
1312
1886
|
autocomplete: 'off',
|
|
1313
1887
|
'aria-autocomplete': 'list',
|
|
1314
1888
|
'[attr.id]': 'id()',
|
|
1889
|
+
'[attr.aria-haspopup]': 'rootContext.grid() ? "grid" : "listbox"',
|
|
1315
1890
|
'[attr.aria-expanded]': 'rootContext.open()',
|
|
1316
1891
|
'[attr.aria-controls]': 'rootContext.listId',
|
|
1317
1892
|
'[attr.aria-labelledby]': 'rootContext.labelId()',
|
|
@@ -1334,7 +1909,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
|
|
|
1334
1909
|
'[attr.data-filled]': 'dataAttr(filledState())',
|
|
1335
1910
|
'[attr.data-focused]': 'dataAttr(focusedState())',
|
|
1336
1911
|
'(input)': 'onInput($event)',
|
|
1337
|
-
'(click)': 'onClick()',
|
|
1912
|
+
'(click)': 'onClick($event)',
|
|
1338
1913
|
'(focus)': 'onFocus()',
|
|
1339
1914
|
'(blur)': 'onBlur()',
|
|
1340
1915
|
'(keydown)': 'onKeydown($event)',
|
|
@@ -1344,12 +1919,72 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
|
|
|
1344
1919
|
}]
|
|
1345
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 }] }] } });
|
|
1346
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
|
+
|
|
1347
1981
|
const itemContext = () => {
|
|
1348
1982
|
const item = inject(RdxComboboxItem);
|
|
1349
1983
|
return {
|
|
1350
1984
|
isSelected: item.isSelected,
|
|
1351
1985
|
isHighlighted: item.isHighlighted,
|
|
1352
1986
|
disabled: item.disabled,
|
|
1987
|
+
// Read-only `Signal` (not `InputSignal`) so autocomplete's computed `value` is assignable too.
|
|
1353
1988
|
value: item.value
|
|
1354
1989
|
};
|
|
1355
1990
|
};
|
|
@@ -1383,6 +2018,16 @@ class RdxComboboxItem {
|
|
|
1383
2018
|
this.elementId = computed(() => this.virtualized() ? this.rootContext.itemId(this.index() ?? -1) : this.id, ...(ngDevMode ? [{ debugName: "elementId" }] : /* istanbul ignore next */ []));
|
|
1384
2019
|
this.ariaSetSize = computed(() => this.virtualized() ? this.rootContext.filteredItems().length : undefined, ...(ngDevMode ? [{ debugName: "ariaSetSize" }] : /* istanbul ignore next */ []));
|
|
1385
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 */ []));
|
|
1386
2031
|
// Virtualized items are always rendered (the consumer only mounts the filtered window).
|
|
1387
2032
|
this.isVisible = computed(() => (this.virtualized() ? true : this.rootContext.isVisible(this)), ...(ngDevMode ? [{ debugName: "isVisible" }] : /* istanbul ignore next */ []));
|
|
1388
2033
|
this.isSelected = computed(() => this.rootContext.isSelected(this.value()), ...(ngDevMode ? [{ debugName: "isSelected" }] : /* istanbul ignore next */ []));
|
|
@@ -1390,6 +2035,9 @@ class RdxComboboxItem {
|
|
|
1390
2035
|
? this.rootContext.highlightedIndex() === this.index()
|
|
1391
2036
|
: this.rootContext.highlightedItem() === this, ...(ngDevMode ? [{ debugName: "isHighlighted" }] : /* istanbul ignore next */ []));
|
|
1392
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;
|
|
1393
2041
|
const destroyRef = inject(DestroyRef);
|
|
1394
2042
|
afterNextRender(() => {
|
|
1395
2043
|
// Virtualized items are not registered: the root navigates over `items` data by index, and
|
|
@@ -1414,21 +2062,65 @@ class RdxComboboxItem {
|
|
|
1414
2062
|
this.rootContext.unregisterItem(this);
|
|
1415
2063
|
this.group?.unregisterItem(this);
|
|
1416
2064
|
});
|
|
2065
|
+
// Keep the highlighted option in view while navigating a scrollable popup. `block: 'nearest'`
|
|
2066
|
+
// makes hover a no-op (the item is already visible) and only scrolls on keyboard navigation.
|
|
2067
|
+
afterRenderEffect(() => {
|
|
2068
|
+
if (!this.virtualized() && this.isHighlighted()) {
|
|
2069
|
+
this.element.scrollIntoView({ block: 'nearest' });
|
|
2070
|
+
}
|
|
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
|
+
});
|
|
1417
2079
|
}
|
|
1418
2080
|
onPointerDown(event) {
|
|
1419
|
-
|
|
2081
|
+
if (event.button !== 0) {
|
|
2082
|
+
return;
|
|
2083
|
+
}
|
|
2084
|
+
// Keep focus on the input; prevent the item from stealing focus.
|
|
1420
2085
|
event.preventDefault();
|
|
1421
2086
|
this.rootContext.setKeyboardActive(false);
|
|
2087
|
+
this.pointerDownStarted = true;
|
|
1422
2088
|
}
|
|
1423
|
-
|
|
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) {
|
|
1424
2112
|
if (this.virtualized()) {
|
|
1425
|
-
this.rootContext.selectIndex(this.index() ?? -1);
|
|
2113
|
+
this.rootContext.selectIndex(this.index() ?? -1, event);
|
|
1426
2114
|
}
|
|
1427
2115
|
else {
|
|
1428
|
-
this.rootContext.select(this);
|
|
2116
|
+
this.rootContext.select(this, event);
|
|
1429
2117
|
}
|
|
1430
2118
|
}
|
|
1431
2119
|
onPointerMove() {
|
|
2120
|
+
// Hover highlighting disabled: leave `data-highlighted` to keyboard/auto-highlight only.
|
|
2121
|
+
if (!this.rootContext.highlightItemOnHover()) {
|
|
2122
|
+
return;
|
|
2123
|
+
}
|
|
1432
2124
|
// Ignore the first move after keyboard navigation: arrow keys scroll the list under a still
|
|
1433
2125
|
// cursor, and the resulting pointer event must not yank the highlight off the keyboard target.
|
|
1434
2126
|
if (this.rootContext.isKeyboardActive()) {
|
|
@@ -1445,8 +2137,24 @@ class RdxComboboxItem {
|
|
|
1445
2137
|
this.rootContext.setHighlight(this, 'pointer');
|
|
1446
2138
|
}
|
|
1447
2139
|
}
|
|
2140
|
+
// Clear a pointer-driven highlight when the cursor leaves the list (unless `keepHighlight`).
|
|
2141
|
+
// Moving to another element inside the list keeps it (the next item's move re-highlights).
|
|
2142
|
+
onPointerLeave(event) {
|
|
2143
|
+
if (event.pointerType === 'touch' ||
|
|
2144
|
+
!this.rootContext.open() ||
|
|
2145
|
+
!this.rootContext.highlightItemOnHover() ||
|
|
2146
|
+
this.rootContext.keepHighlight()) {
|
|
2147
|
+
return;
|
|
2148
|
+
}
|
|
2149
|
+
const related = event.relatedTarget;
|
|
2150
|
+
const list = related && document.getElementById(this.rootContext.listId);
|
|
2151
|
+
if (list && list.contains(related)) {
|
|
2152
|
+
return;
|
|
2153
|
+
}
|
|
2154
|
+
this.rootContext.clearHighlight();
|
|
2155
|
+
}
|
|
1448
2156
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxItem, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
1449
|
-
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 }); }
|
|
1450
2158
|
}
|
|
1451
2159
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxItem, decorators: [{
|
|
1452
2160
|
type: Directive,
|
|
@@ -1455,21 +2163,23 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
|
|
|
1455
2163
|
exportAs: 'rdxComboboxItem',
|
|
1456
2164
|
providers: [provideComboboxItemContext(itemContext)],
|
|
1457
2165
|
host: {
|
|
1458
|
-
role: '
|
|
2166
|
+
'[attr.role]': 'role()',
|
|
1459
2167
|
'[attr.id]': 'elementId()',
|
|
1460
|
-
'[attr.aria-selected]': 'isSelected()',
|
|
2168
|
+
'[attr.aria-selected]': 'selectable() ? isSelected() : undefined',
|
|
1461
2169
|
'[attr.aria-disabled]': 'disabled() ? "true" : undefined',
|
|
1462
2170
|
'[attr.aria-setsize]': 'ariaSetSize()',
|
|
1463
2171
|
'[attr.aria-posinset]': 'ariaPosInSet()',
|
|
1464
|
-
'[attr.data-selected]': 'isSelected() ? "" : undefined',
|
|
2172
|
+
'[attr.data-selected]': 'selectable() && isSelected() ? "" : undefined',
|
|
1465
2173
|
'[attr.data-highlighted]': 'isHighlighted() ? "" : undefined',
|
|
1466
2174
|
'[attr.data-disabled]': 'disabled() ? "" : undefined',
|
|
1467
2175
|
'[hidden]': '!isVisible()',
|
|
1468
2176
|
'[attr.data-hidden]': 'isVisible() ? undefined : ""',
|
|
1469
2177
|
'(pointerdown)': 'onPointerDown($event)',
|
|
1470
|
-
'(mousedown)': '
|
|
1471
|
-
'(
|
|
1472
|
-
'(
|
|
2178
|
+
'(mousedown)': 'onMouseDown($event)',
|
|
2179
|
+
'(mouseup)': 'onMouseUp($event)',
|
|
2180
|
+
'(click)': 'onClick($event)',
|
|
2181
|
+
'(pointermove)': 'onPointerMove()',
|
|
2182
|
+
'(pointerleave)': 'onPointerLeave($event)'
|
|
1473
2183
|
}
|
|
1474
2184
|
}]
|
|
1475
2185
|
}], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: true }] }], textValueInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "textValue", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], index: [{ type: i0.Input, args: [{ isSignal: true, alias: "index", required: false }] }] } });
|
|
@@ -1526,7 +2236,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
|
|
|
1526
2236
|
}], ctorParameters: () => [] });
|
|
1527
2237
|
|
|
1528
2238
|
/**
|
|
1529
|
-
* 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.
|
|
1530
2242
|
*
|
|
1531
2243
|
* @group Components
|
|
1532
2244
|
*/
|
|
@@ -1534,8 +2246,27 @@ class RdxComboboxList {
|
|
|
1534
2246
|
constructor() {
|
|
1535
2247
|
this.rootContext = injectComboboxRootContext();
|
|
1536
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
|
+
}
|
|
1537
2268
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxList, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
1538
|
-
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 }); }
|
|
1539
2270
|
}
|
|
1540
2271
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxList, decorators: [{
|
|
1541
2272
|
type: Directive,
|
|
@@ -1543,9 +2274,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
|
|
|
1543
2274
|
selector: '[rdxComboboxList]',
|
|
1544
2275
|
exportAs: 'rdxComboboxList',
|
|
1545
2276
|
host: {
|
|
1546
|
-
|
|
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"',
|
|
1547
2281
|
'[attr.id]': 'rootContext.listId',
|
|
1548
|
-
'[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)'
|
|
1549
2285
|
}
|
|
1550
2286
|
}]
|
|
1551
2287
|
}] });
|
|
@@ -1559,166 +2295,181 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
|
|
|
1559
2295
|
class RdxComboboxPopup {
|
|
1560
2296
|
constructor() {
|
|
1561
2297
|
this.rootContext = injectComboboxRootContext();
|
|
1562
|
-
this.
|
|
2298
|
+
this.floatingContext = inject(RDX_FLOATING_ROOT_CONTEXT);
|
|
2299
|
+
this.registration = inject(RDX_FLOATING_REGISTRATION, { optional: true });
|
|
1563
2300
|
this.popper = injectPopperContentWrapperContext();
|
|
1564
2301
|
this.element = inject(ElementRef).nativeElement;
|
|
1565
|
-
//
|
|
1566
|
-
//
|
|
1567
|
-
|
|
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
|
+
});
|
|
1568
2311
|
// The popup's animation determines when the open/close transition (onOpenChangeComplete) is done.
|
|
1569
2312
|
const unregister = this.rootContext.registerTransitionElement(this.element);
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
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
|
+
});
|
|
1575
2331
|
// For the "input inside the popup" pattern, move focus to the input once the popup is
|
|
1576
|
-
// positioned.
|
|
1577
|
-
//
|
|
1578
|
-
effect(
|
|
2332
|
+
// positioned. Use `afterRenderEffect` (not `effect`): when `isPositioned` flips true the
|
|
2333
|
+
// popup's final position/visibility is applied in the *following* render, so a synchronous
|
|
2334
|
+
// `effect` would call `focus()` while the element is still unfocusable and silently no-op.
|
|
2335
|
+
afterRenderEffect(() => {
|
|
1579
2336
|
if (!this.popper.isPositioned() || !this.rootContext.open()) {
|
|
1580
2337
|
return;
|
|
1581
2338
|
}
|
|
1582
2339
|
const input = this.rootContext.inputElement();
|
|
1583
2340
|
if (input && input.closest('[rdxComboboxPopup]')) {
|
|
1584
|
-
|
|
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 {
|
|
1585
2347
|
input.focus();
|
|
1586
2348
|
input.select();
|
|
1587
|
-
}
|
|
2349
|
+
}
|
|
1588
2350
|
}
|
|
1589
2351
|
});
|
|
1590
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
|
+
}
|
|
1591
2372
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxPopup, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
1592
|
-
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" } },
|
|
1593
|
-
// In modal mode, make content outside the popup inert (Base UI's `modal`).
|
|
1594
|
-
provideRdxDismissableLayerConfig(() => ({ disableOutsidePointerEvents: injectComboboxRootContext().modal }))
|
|
1595
|
-
], 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 }); }
|
|
1596
2374
|
}
|
|
1597
2375
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxPopup, decorators: [{
|
|
1598
2376
|
type: Directive,
|
|
1599
2377
|
args: [{
|
|
1600
2378
|
selector: '[rdxComboboxPopup]',
|
|
1601
2379
|
exportAs: 'rdxComboboxPopup',
|
|
1602
|
-
hostDirectives: [RdxPopperContent,
|
|
1603
|
-
providers: [
|
|
1604
|
-
// In modal mode, make content outside the popup inert (Base UI's `modal`).
|
|
1605
|
-
provideRdxDismissableLayerConfig(() => ({ disableOutsidePointerEvents: injectComboboxRootContext().modal }))
|
|
1606
|
-
],
|
|
2380
|
+
hostDirectives: [RdxPopperContent, RdxFloatingNodeRegistration],
|
|
1607
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"',
|
|
1608
2386
|
'[attr.data-state]': 'rootContext.open() ? "open" : "closed"',
|
|
1609
2387
|
'[attr.data-open]': 'rootContext.open() ? "" : undefined',
|
|
1610
2388
|
'[attr.data-closed]': 'rootContext.open() ? undefined : ""',
|
|
1611
2389
|
'[attr.data-starting-style]': 'rootContext.transitionStatus() === "starting" ? "" : undefined',
|
|
1612
|
-
'[attr.data-ending-style]': 'rootContext.transitionStatus() === "ending" ? "" : undefined'
|
|
2390
|
+
'[attr.data-ending-style]': 'rootContext.transitionStatus() === "ending" ? "" : undefined',
|
|
2391
|
+
'(focusin)': 'onFocusIn($event)'
|
|
1613
2392
|
}
|
|
1614
2393
|
}]
|
|
1615
2394
|
}], ctorParameters: () => [] });
|
|
1616
2395
|
|
|
1617
2396
|
/**
|
|
1618
|
-
*
|
|
2397
|
+
* Structural directive that teleports the combobox popup into a container (default `document.body`)
|
|
2398
|
+
* while the combobox is open, and keeps it mounted until any CSS exit `@keyframes` finishes.
|
|
2399
|
+
*
|
|
2400
|
+
* Apply it with the `*` microsyntax on the positioner —
|
|
2401
|
+
* `<div *rdxComboboxPortal rdxComboboxPositioner>` — or as an explicit `<ng-template rdxComboboxPortal>`.
|
|
2402
|
+
* For a custom container use the explicit form with `[container]`.
|
|
1619
2403
|
*
|
|
1620
2404
|
* @group Components
|
|
1621
2405
|
*/
|
|
1622
2406
|
class RdxComboboxPortal {
|
|
2407
|
+
constructor() {
|
|
2408
|
+
/**
|
|
2409
|
+
* Optional container to portal the content into. Defaults to `document.body`. Declared here (and
|
|
2410
|
+
* forwarded to the composed {@link RdxPortalPresence}) so the autocomplete portal can re-expose it
|
|
2411
|
+
* through its own `hostDirectives`.
|
|
2412
|
+
*/
|
|
2413
|
+
this.container = input(...(ngDevMode ? [undefined, { debugName: "container" }] : /* istanbul ignore next */ []));
|
|
2414
|
+
}
|
|
1623
2415
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxPortal, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
1624
|
-
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "
|
|
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 }); }
|
|
1625
2417
|
}
|
|
1626
2418
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxPortal, decorators: [{
|
|
1627
2419
|
type: Directive,
|
|
1628
2420
|
args: [{
|
|
1629
|
-
selector: '[rdxComboboxPortal]',
|
|
2421
|
+
selector: 'ng-template[rdxComboboxPortal]',
|
|
1630
2422
|
exportAs: 'rdxComboboxPortal',
|
|
1631
|
-
hostDirectives: [{ directive:
|
|
2423
|
+
hostDirectives: [{ directive: RdxPortalPresence, inputs: ['container'] }],
|
|
2424
|
+
providers: [provideRdxPresenceContext(() => ({ present: injectComboboxRootContext().present }))]
|
|
1632
2425
|
}]
|
|
1633
|
-
}] });
|
|
1634
|
-
|
|
2426
|
+
}], propDecorators: { container: [{ type: i0.Input, args: [{ isSignal: true, alias: "container", required: false }] }] } });
|
|
1635
2427
|
/**
|
|
1636
|
-
*
|
|
1637
|
-
*
|
|
2428
|
+
* Dev-mode guard: `rdxComboboxPortal` used to be an attribute directive on a `<div>`. It is now
|
|
2429
|
+
* structural, so the old `<div rdxComboboxPortal>` markup would silently stop portaling — fail loudly
|
|
2430
|
+
* instead.
|
|
1638
2431
|
*
|
|
1639
2432
|
* @group Components
|
|
1640
2433
|
*/
|
|
1641
|
-
class
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
2434
|
+
class RdxComboboxPortalMisuseGuard {
|
|
2435
|
+
constructor() {
|
|
2436
|
+
if (isDevMode()) {
|
|
2437
|
+
rdxDevError('combobox/portal-on-element', '`rdxComboboxPortal` is now a structural directive. ' +
|
|
2438
|
+
'Use `*rdxComboboxPortal` on the positioner element or `<ng-template rdxComboboxPortal>`. ' +
|
|
2439
|
+
'rdxComboboxPortalPresence has been removed.', 'components/combobox');
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxPortalMisuseGuard, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
2443
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxComboboxPortalMisuseGuard, isStandalone: true, selector: "[rdxComboboxPortal]:not(ng-template)", ngImport: i0 }); }
|
|
1649
2444
|
}
|
|
1650
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type:
|
|
2445
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxPortalMisuseGuard, decorators: [{
|
|
1651
2446
|
type: Directive,
|
|
1652
2447
|
args: [{
|
|
1653
|
-
selector: 'ng-template
|
|
1654
|
-
hostDirectives: [RdxPresenceDirective],
|
|
1655
|
-
providers: [
|
|
1656
|
-
provideRdxPresenceContext(() => {
|
|
1657
|
-
const context = injectComboboxRootContext();
|
|
1658
|
-
return { present: context.open };
|
|
1659
|
-
})
|
|
1660
|
-
]
|
|
2448
|
+
selector: '[rdxComboboxPortal]:not(ng-template)'
|
|
1661
2449
|
}]
|
|
1662
|
-
}] });
|
|
2450
|
+
}], ctorParameters: () => [] });
|
|
1663
2451
|
|
|
1664
2452
|
/**
|
|
1665
|
-
*
|
|
1666
|
-
*
|
|
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.
|
|
1667
2455
|
*
|
|
1668
2456
|
* @group Components
|
|
1669
2457
|
*/
|
|
1670
|
-
class
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
this.sideOffset = input(4, { ...(ngDevMode ? { debugName: "sideOffset" } : /* istanbul ignore next */ {}), transform: numberAttribute });
|
|
1674
|
-
this.align = input('start', ...(ngDevMode ? [{ debugName: "align" }] : /* istanbul ignore next */ []));
|
|
1675
|
-
this.alignOffset = input(0, { ...(ngDevMode ? { debugName: "alignOffset" } : /* istanbul ignore next */ {}), transform: numberAttribute });
|
|
1676
|
-
this.arrowPadding = input(0, { ...(ngDevMode ? { debugName: "arrowPadding" } : /* istanbul ignore next */ {}), transform: numberAttribute });
|
|
1677
|
-
this.avoidCollisions = input(true, { ...(ngDevMode ? { debugName: "avoidCollisions" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
1678
|
-
this.collisionBoundary = input(...(ngDevMode ? [undefined, { debugName: "collisionBoundary" }] : /* istanbul ignore next */ []));
|
|
1679
|
-
this.collisionPadding = input(0, ...(ngDevMode ? [{ debugName: "collisionPadding" }] : /* istanbul ignore next */ []));
|
|
1680
|
-
this.sticky = input('partial', ...(ngDevMode ? [{ debugName: "sticky" }] : /* istanbul ignore next */ []));
|
|
1681
|
-
this.hideWhenDetached = input(false, { ...(ngDevMode ? { debugName: "hideWhenDetached" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
1682
|
-
this.updatePositionStrategy = input('optimized', ...(ngDevMode ? [{ debugName: "updatePositionStrategy" }] : /* istanbul ignore next */ []));
|
|
1683
|
-
}
|
|
1684
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxPositioner, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
1685
|
-
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 }); }
|
|
1686
2461
|
}
|
|
1687
|
-
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: [{
|
|
1688
2463
|
type: Directive,
|
|
1689
2464
|
args: [{
|
|
1690
|
-
selector: '[
|
|
1691
|
-
exportAs: '
|
|
1692
|
-
hostDirectives: [
|
|
1693
|
-
{
|
|
1694
|
-
directive: RdxPopperContentWrapper,
|
|
1695
|
-
inputs: [
|
|
1696
|
-
'side',
|
|
1697
|
-
'sideOffset',
|
|
1698
|
-
'align',
|
|
1699
|
-
'alignOffset',
|
|
1700
|
-
'arrowPadding',
|
|
1701
|
-
'avoidCollisions',
|
|
1702
|
-
'collisionBoundary',
|
|
1703
|
-
'collisionPadding',
|
|
1704
|
-
'sticky',
|
|
1705
|
-
'hideWhenDetached',
|
|
1706
|
-
'updatePositionStrategy'
|
|
1707
|
-
]
|
|
1708
|
-
}
|
|
1709
|
-
],
|
|
2465
|
+
selector: '[rdxComboboxSeparator]',
|
|
2466
|
+
exportAs: 'rdxComboboxSeparator',
|
|
1710
2467
|
host: {
|
|
1711
|
-
'
|
|
1712
|
-
|
|
1713
|
-
'--radix-combobox-content-transform-origin': 'var(--radix-popper-transform-origin)',
|
|
1714
|
-
'--radix-combobox-content-available-width': 'var(--radix-popper-available-width)',
|
|
1715
|
-
'--radix-combobox-content-available-height': 'var(--radix-popper-available-height)',
|
|
1716
|
-
'--radix-combobox-trigger-width': 'var(--radix-popper-anchor-width)',
|
|
1717
|
-
'--radix-combobox-trigger-height': 'var(--radix-popper-anchor-height)'
|
|
1718
|
-
}`
|
|
2468
|
+
role: 'separator',
|
|
2469
|
+
'aria-orientation': 'horizontal'
|
|
1719
2470
|
}
|
|
1720
2471
|
}]
|
|
1721
|
-
}]
|
|
2472
|
+
}] });
|
|
1722
2473
|
|
|
1723
2474
|
/**
|
|
1724
2475
|
* A polite live region for async status (loading, result counts) announced without moving focus.
|
|
@@ -1727,7 +2478,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
|
|
|
1727
2478
|
*/
|
|
1728
2479
|
class RdxComboboxStatus {
|
|
1729
2480
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxStatus, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
1730
|
-
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 }); }
|
|
1731
2482
|
}
|
|
1732
2483
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxStatus, decorators: [{
|
|
1733
2484
|
type: Directive,
|
|
@@ -1736,13 +2487,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
|
|
|
1736
2487
|
exportAs: 'rdxComboboxStatus',
|
|
1737
2488
|
host: {
|
|
1738
2489
|
role: 'status',
|
|
1739
|
-
'aria-live': 'polite'
|
|
2490
|
+
'aria-live': 'polite',
|
|
2491
|
+
'aria-atomic': 'true'
|
|
1740
2492
|
}
|
|
1741
2493
|
}]
|
|
1742
2494
|
}] });
|
|
1743
2495
|
|
|
1744
2496
|
/**
|
|
1745
|
-
* 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).
|
|
1746
2508
|
*
|
|
1747
2509
|
* @group Components
|
|
1748
2510
|
*/
|
|
@@ -1753,35 +2515,57 @@ class RdxComboboxTrigger {
|
|
|
1753
2515
|
this.rootContext.registerTrigger(this.element);
|
|
1754
2516
|
inject(DestroyRef).onDestroy(() => this.rootContext.registerTrigger(null));
|
|
1755
2517
|
}
|
|
1756
|
-
|
|
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) {
|
|
1757
2524
|
if (this.rootContext.open()) {
|
|
1758
|
-
this.rootContext.closePopup(true);
|
|
2525
|
+
this.rootContext.closePopup(true, 'trigger-press', event);
|
|
1759
2526
|
}
|
|
1760
2527
|
else {
|
|
1761
2528
|
this.rootContext.focusInput();
|
|
1762
|
-
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);
|
|
1763
2543
|
}
|
|
1764
2544
|
}
|
|
1765
2545
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxTrigger, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
1766
|
-
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 }); }
|
|
1767
2547
|
}
|
|
1768
2548
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxComboboxTrigger, decorators: [{
|
|
1769
2549
|
type: Directive,
|
|
1770
2550
|
args: [{
|
|
1771
2551
|
selector: 'button[rdxComboboxTrigger]',
|
|
1772
2552
|
exportAs: 'rdxComboboxTrigger',
|
|
1773
|
-
hostDirectives: [
|
|
2553
|
+
hostDirectives: [RdxFloatingInsideElement],
|
|
1774
2554
|
host: {
|
|
1775
2555
|
type: 'button',
|
|
1776
|
-
tabindex: '-1',
|
|
1777
|
-
'
|
|
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"',
|
|
1778
2559
|
'[attr.aria-expanded]': 'rootContext.open()',
|
|
1779
2560
|
'[attr.aria-controls]': 'rootContext.listId',
|
|
1780
2561
|
'[attr.aria-labelledby]': 'rootContext.labelId()',
|
|
2562
|
+
'[attr.aria-required]': 'rootContext.inputLayout() === "inside" && rootContext.requiredState() ? "true" : undefined',
|
|
1781
2563
|
'[attr.disabled]': 'rootContext.disabledState() ? "" : undefined',
|
|
1782
2564
|
'[attr.data-popup-open]': 'rootContext.open() ? "" : undefined',
|
|
1783
2565
|
'[attr.data-disabled]': 'rootContext.disabledState() ? "" : undefined',
|
|
1784
|
-
'(
|
|
2566
|
+
'(pointerdown)': 'onPointerDown($event)',
|
|
2567
|
+
'(click)': 'onClick($event)',
|
|
2568
|
+
'(keydown)': 'onKeydown($event)'
|
|
1785
2569
|
}
|
|
1786
2570
|
}]
|
|
1787
2571
|
}], ctorParameters: () => [] });
|
|
@@ -1835,21 +2619,24 @@ const _importsCombobox = [
|
|
|
1835
2619
|
RdxComboboxAnchor,
|
|
1836
2620
|
RdxComboboxLabel,
|
|
1837
2621
|
RdxComboboxInput,
|
|
2622
|
+
RdxComboboxInputGroup,
|
|
1838
2623
|
RdxComboboxValue,
|
|
1839
2624
|
RdxComboboxTrigger,
|
|
1840
2625
|
RdxComboboxIcon,
|
|
1841
2626
|
RdxComboboxClear,
|
|
1842
2627
|
RdxComboboxPortal,
|
|
1843
|
-
|
|
2628
|
+
RdxComboboxPortalMisuseGuard,
|
|
1844
2629
|
RdxComboboxBackdrop,
|
|
1845
2630
|
RdxComboboxPositioner,
|
|
1846
2631
|
RdxComboboxPopup,
|
|
1847
2632
|
RdxComboboxArrow,
|
|
1848
2633
|
RdxComboboxList,
|
|
2634
|
+
RdxComboboxRow,
|
|
1849
2635
|
RdxComboboxItem,
|
|
1850
2636
|
RdxComboboxItemIndicator,
|
|
1851
2637
|
RdxComboboxGroup,
|
|
1852
2638
|
RdxComboboxGroupLabel,
|
|
2639
|
+
RdxComboboxSeparator,
|
|
1853
2640
|
RdxComboboxEmpty,
|
|
1854
2641
|
RdxComboboxStatus,
|
|
1855
2642
|
RdxComboboxChips,
|
|
@@ -1862,21 +2649,24 @@ class RdxComboboxModule {
|
|
|
1862
2649
|
RdxComboboxAnchor,
|
|
1863
2650
|
RdxComboboxLabel,
|
|
1864
2651
|
RdxComboboxInput,
|
|
2652
|
+
RdxComboboxInputGroup,
|
|
1865
2653
|
RdxComboboxValue,
|
|
1866
2654
|
RdxComboboxTrigger,
|
|
1867
2655
|
RdxComboboxIcon,
|
|
1868
2656
|
RdxComboboxClear,
|
|
1869
2657
|
RdxComboboxPortal,
|
|
1870
|
-
|
|
2658
|
+
RdxComboboxPortalMisuseGuard,
|
|
1871
2659
|
RdxComboboxBackdrop,
|
|
1872
2660
|
RdxComboboxPositioner,
|
|
1873
2661
|
RdxComboboxPopup,
|
|
1874
2662
|
RdxComboboxArrow,
|
|
1875
2663
|
RdxComboboxList,
|
|
2664
|
+
RdxComboboxRow,
|
|
1876
2665
|
RdxComboboxItem,
|
|
1877
2666
|
RdxComboboxItemIndicator,
|
|
1878
2667
|
RdxComboboxGroup,
|
|
1879
2668
|
RdxComboboxGroupLabel,
|
|
2669
|
+
RdxComboboxSeparator,
|
|
1880
2670
|
RdxComboboxEmpty,
|
|
1881
2671
|
RdxComboboxStatus,
|
|
1882
2672
|
RdxComboboxChips,
|
|
@@ -1885,21 +2675,24 @@ class RdxComboboxModule {
|
|
|
1885
2675
|
RdxComboboxAnchor,
|
|
1886
2676
|
RdxComboboxLabel,
|
|
1887
2677
|
RdxComboboxInput,
|
|
2678
|
+
RdxComboboxInputGroup,
|
|
1888
2679
|
RdxComboboxValue,
|
|
1889
2680
|
RdxComboboxTrigger,
|
|
1890
2681
|
RdxComboboxIcon,
|
|
1891
2682
|
RdxComboboxClear,
|
|
1892
2683
|
RdxComboboxPortal,
|
|
1893
|
-
|
|
2684
|
+
RdxComboboxPortalMisuseGuard,
|
|
1894
2685
|
RdxComboboxBackdrop,
|
|
1895
2686
|
RdxComboboxPositioner,
|
|
1896
2687
|
RdxComboboxPopup,
|
|
1897
2688
|
RdxComboboxArrow,
|
|
1898
2689
|
RdxComboboxList,
|
|
2690
|
+
RdxComboboxRow,
|
|
1899
2691
|
RdxComboboxItem,
|
|
1900
2692
|
RdxComboboxItemIndicator,
|
|
1901
2693
|
RdxComboboxGroup,
|
|
1902
2694
|
RdxComboboxGroupLabel,
|
|
2695
|
+
RdxComboboxSeparator,
|
|
1903
2696
|
RdxComboboxEmpty,
|
|
1904
2697
|
RdxComboboxStatus,
|
|
1905
2698
|
RdxComboboxChips,
|
|
@@ -1919,5 +2712,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
|
|
|
1919
2712
|
* Generated bundle index. Do not edit.
|
|
1920
2713
|
*/
|
|
1921
2714
|
|
|
1922
|
-
export { RdxComboboxAnchor, RdxComboboxArrow, RdxComboboxBackdrop, RdxComboboxChip, RdxComboboxChipRemove, RdxComboboxChips, RdxComboboxClear, RdxComboboxEmpty, RdxComboboxGroup, RdxComboboxGroupLabel, RdxComboboxIcon, RdxComboboxInput, RdxComboboxItem, RdxComboboxItemIndicator, RdxComboboxLabel, RdxComboboxList, RdxComboboxModule, RdxComboboxPopup, RdxComboboxPortal,
|
|
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 };
|
|
1923
2716
|
//# sourceMappingURL=radix-ng-primitives-combobox.mjs.map
|