forty-cdk 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/accordion/README.md +122 -0
- package/aspect-ratio/README.md +76 -0
- package/avatar/README.md +100 -0
- package/breadcrumbs/README.md +49 -0
- package/breakpoints/README.md +81 -0
- package/button/README.md +49 -0
- package/calendar/README.md +458 -0
- package/carousel/README.md +358 -0
- package/checkbox/README.md +146 -0
- package/combobox/README.md +535 -0
- package/context-menu/README.md +139 -0
- package/date-field/README.md +184 -0
- package/date-picker/README.md +338 -0
- package/dialog/README.md +388 -0
- package/disclosure/README.md +114 -0
- package/drag-drop/README.md +359 -0
- package/drawer/README.md +560 -0
- package/dropdown-menu/README.md +176 -0
- package/fesm2022/forty-cdk-accordion.mjs +348 -0
- package/fesm2022/forty-cdk-accordion.mjs.map +1 -0
- package/fesm2022/forty-cdk-aspect-ratio.mjs +74 -0
- package/fesm2022/forty-cdk-aspect-ratio.mjs.map +1 -0
- package/fesm2022/forty-cdk-avatar.mjs +308 -0
- package/fesm2022/forty-cdk-avatar.mjs.map +1 -0
- package/fesm2022/forty-cdk-breadcrumbs.mjs +125 -0
- package/fesm2022/forty-cdk-breadcrumbs.mjs.map +1 -0
- package/fesm2022/forty-cdk-breakpoints.mjs +117 -0
- package/fesm2022/forty-cdk-breakpoints.mjs.map +1 -0
- package/fesm2022/forty-cdk-button.mjs +134 -0
- package/fesm2022/forty-cdk-button.mjs.map +1 -0
- package/fesm2022/forty-cdk-calendar.mjs +2034 -0
- package/fesm2022/forty-cdk-calendar.mjs.map +1 -0
- package/fesm2022/forty-cdk-carousel.mjs +968 -0
- package/fesm2022/forty-cdk-carousel.mjs.map +1 -0
- package/fesm2022/forty-cdk-checkbox.mjs +226 -0
- package/fesm2022/forty-cdk-checkbox.mjs.map +1 -0
- package/fesm2022/forty-cdk-combobox.mjs +2596 -0
- package/fesm2022/forty-cdk-combobox.mjs.map +1 -0
- package/fesm2022/forty-cdk-context-menu.mjs +413 -0
- package/fesm2022/forty-cdk-context-menu.mjs.map +1 -0
- package/fesm2022/forty-cdk-core.mjs +9022 -0
- package/fesm2022/forty-cdk-core.mjs.map +1 -0
- package/fesm2022/forty-cdk-date-field.mjs +744 -0
- package/fesm2022/forty-cdk-date-field.mjs.map +1 -0
- package/fesm2022/forty-cdk-date-picker.mjs +1011 -0
- package/fesm2022/forty-cdk-date-picker.mjs.map +1 -0
- package/fesm2022/forty-cdk-dialog.mjs +707 -0
- package/fesm2022/forty-cdk-dialog.mjs.map +1 -0
- package/fesm2022/forty-cdk-disclosure.mjs +190 -0
- package/fesm2022/forty-cdk-disclosure.mjs.map +1 -0
- package/fesm2022/forty-cdk-drag-drop.mjs +1180 -0
- package/fesm2022/forty-cdk-drag-drop.mjs.map +1 -0
- package/fesm2022/forty-cdk-drawer.mjs +1641 -0
- package/fesm2022/forty-cdk-drawer.mjs.map +1 -0
- package/fesm2022/forty-cdk-dropdown-menu.mjs +350 -0
- package/fesm2022/forty-cdk-dropdown-menu.mjs.map +1 -0
- package/fesm2022/forty-cdk-field.mjs +425 -0
- package/fesm2022/forty-cdk-field.mjs.map +1 -0
- package/fesm2022/forty-cdk-fieldset.mjs +164 -0
- package/fesm2022/forty-cdk-fieldset.mjs.map +1 -0
- package/fesm2022/forty-cdk-file-upload.mjs +221 -0
- package/fesm2022/forty-cdk-file-upload.mjs.map +1 -0
- package/fesm2022/forty-cdk-hover-card.mjs +496 -0
- package/fesm2022/forty-cdk-hover-card.mjs.map +1 -0
- package/fesm2022/forty-cdk-input.mjs +274 -0
- package/fesm2022/forty-cdk-input.mjs.map +1 -0
- package/fesm2022/forty-cdk-internationalized-date.mjs +1 -1
- package/fesm2022/forty-cdk-internationalized-date.mjs.map +1 -1
- package/fesm2022/forty-cdk-listbox.mjs +1279 -0
- package/fesm2022/forty-cdk-listbox.mjs.map +1 -0
- package/fesm2022/forty-cdk-menu.mjs +1439 -0
- package/fesm2022/forty-cdk-menu.mjs.map +1 -0
- package/fesm2022/forty-cdk-menubar.mjs +787 -0
- package/fesm2022/forty-cdk-menubar.mjs.map +1 -0
- package/fesm2022/forty-cdk-meter.mjs +211 -0
- package/fesm2022/forty-cdk-meter.mjs.map +1 -0
- package/fesm2022/forty-cdk-navigation-menu.mjs +1145 -0
- package/fesm2022/forty-cdk-navigation-menu.mjs.map +1 -0
- package/fesm2022/forty-cdk-number-input.mjs +559 -0
- package/fesm2022/forty-cdk-number-input.mjs.map +1 -0
- package/fesm2022/forty-cdk-otp-input.mjs +527 -0
- package/fesm2022/forty-cdk-otp-input.mjs.map +1 -0
- package/fesm2022/forty-cdk-pagination.mjs +323 -0
- package/fesm2022/forty-cdk-pagination.mjs.map +1 -0
- package/fesm2022/forty-cdk-pane-resizer.mjs +297 -0
- package/fesm2022/forty-cdk-pane-resizer.mjs.map +1 -0
- package/fesm2022/forty-cdk-popover.mjs +698 -0
- package/fesm2022/forty-cdk-popover.mjs.map +1 -0
- package/fesm2022/forty-cdk-progress.mjs +226 -0
- package/fesm2022/forty-cdk-progress.mjs.map +1 -0
- package/fesm2022/forty-cdk-radio-group.mjs +378 -0
- package/fesm2022/forty-cdk-radio-group.mjs.map +1 -0
- package/fesm2022/forty-cdk-scroll-area.mjs +640 -0
- package/fesm2022/forty-cdk-scroll-area.mjs.map +1 -0
- package/fesm2022/forty-cdk-search.mjs +205 -0
- package/fesm2022/forty-cdk-search.mjs.map +1 -0
- package/fesm2022/forty-cdk-select.mjs +1661 -0
- package/fesm2022/forty-cdk-select.mjs.map +1 -0
- package/fesm2022/forty-cdk-separator.mjs +82 -0
- package/fesm2022/forty-cdk-separator.mjs.map +1 -0
- package/fesm2022/forty-cdk-signal-forms.mjs +97 -0
- package/fesm2022/forty-cdk-signal-forms.mjs.map +1 -0
- package/fesm2022/forty-cdk-slider.mjs +803 -0
- package/fesm2022/forty-cdk-slider.mjs.map +1 -0
- package/fesm2022/forty-cdk-stepper.mjs +886 -0
- package/fesm2022/forty-cdk-stepper.mjs.map +1 -0
- package/fesm2022/forty-cdk-switch.mjs +137 -0
- package/fesm2022/forty-cdk-switch.mjs.map +1 -0
- package/fesm2022/forty-cdk-table.mjs +1518 -0
- package/fesm2022/forty-cdk-table.mjs.map +1 -0
- package/fesm2022/forty-cdk-tabs.mjs +400 -0
- package/fesm2022/forty-cdk-tabs.mjs.map +1 -0
- package/fesm2022/forty-cdk-time-field.mjs +593 -0
- package/fesm2022/forty-cdk-time-field.mjs.map +1 -0
- package/fesm2022/forty-cdk-time-picker.mjs +1013 -0
- package/fesm2022/forty-cdk-time-picker.mjs.map +1 -0
- package/fesm2022/forty-cdk-toast.mjs +1153 -0
- package/fesm2022/forty-cdk-toast.mjs.map +1 -0
- package/fesm2022/forty-cdk-toggle.mjs +516 -0
- package/fesm2022/forty-cdk-toggle.mjs.map +1 -0
- package/fesm2022/forty-cdk-toolbar.mjs +374 -0
- package/fesm2022/forty-cdk-toolbar.mjs.map +1 -0
- package/fesm2022/forty-cdk-tooltip.mjs +672 -0
- package/fesm2022/forty-cdk-tooltip.mjs.map +1 -0
- package/fesm2022/forty-cdk-tree.mjs +2007 -0
- package/fesm2022/forty-cdk-tree.mjs.map +1 -0
- package/fesm2022/forty-cdk-virtualization.mjs +1 -1
- package/fesm2022/forty-cdk-virtualization.mjs.map +1 -1
- package/fesm2022/forty-cdk.mjs +0 -43310
- package/fesm2022/forty-cdk.mjs.map +1 -1
- package/field/README.md +97 -0
- package/fieldset/README.md +86 -0
- package/file-upload/README.md +73 -0
- package/hover-card/README.md +171 -0
- package/input/README.md +156 -0
- package/listbox/README.md +424 -0
- package/menu/README.md +181 -0
- package/menubar/README.md +140 -0
- package/meter/README.md +128 -0
- package/navigation-menu/README.md +253 -0
- package/number-input/README.md +171 -0
- package/otp-input/README.md +198 -0
- package/package.json +213 -1
- package/pagination/README.md +61 -0
- package/pane-resizer/README.md +136 -0
- package/popover/README.md +262 -0
- package/progress/README.md +115 -0
- package/radio-group/README.md +129 -0
- package/scroll-area/README.md +184 -0
- package/search/README.md +42 -0
- package/select/README.md +488 -0
- package/separator/README.md +84 -0
- package/signal-forms/README.md +72 -0
- package/slider/README.md +152 -0
- package/stepper/README.md +292 -0
- package/switch/README.md +116 -0
- package/table/README.md +769 -0
- package/tabs/README.md +130 -0
- package/time-field/README.md +157 -0
- package/time-picker/README.md +172 -0
- package/toast/README.md +398 -0
- package/toggle/README.md +224 -0
- package/toolbar/README.md +109 -0
- package/tooltip/README.md +274 -0
- package/tree/README.md +708 -0
- package/types/forty-cdk-accordion.d.ts +242 -0
- package/types/forty-cdk-aspect-ratio.d.ts +59 -0
- package/types/forty-cdk-avatar.d.ts +133 -0
- package/types/forty-cdk-breadcrumbs.d.ts +92 -0
- package/types/forty-cdk-breakpoints.d.ts +141 -0
- package/types/forty-cdk-button.d.ts +80 -0
- package/types/forty-cdk-calendar.d.ts +914 -0
- package/types/forty-cdk-carousel.d.ts +530 -0
- package/types/forty-cdk-checkbox.d.ts +141 -0
- package/types/forty-cdk-combobox.d.ts +1259 -0
- package/types/forty-cdk-context-menu.d.ts +313 -0
- package/types/forty-cdk-core.d.ts +5774 -0
- package/types/forty-cdk-date-field.d.ts +307 -0
- package/types/forty-cdk-date-picker.d.ts +622 -0
- package/types/forty-cdk-dialog.d.ts +546 -0
- package/types/forty-cdk-disclosure.d.ts +127 -0
- package/types/forty-cdk-drag-drop.d.ts +456 -0
- package/types/forty-cdk-drawer.d.ts +871 -0
- package/types/forty-cdk-dropdown-menu.d.ts +242 -0
- package/types/forty-cdk-field.d.ts +236 -0
- package/types/forty-cdk-fieldset.d.ts +119 -0
- package/types/forty-cdk-file-upload.d.ts +124 -0
- package/types/forty-cdk-hover-card.d.ts +320 -0
- package/types/forty-cdk-input.d.ts +169 -0
- package/types/forty-cdk-internationalized-date.d.ts +1 -1
- package/types/forty-cdk-listbox.d.ts +513 -0
- package/types/forty-cdk-menu.d.ts +629 -0
- package/types/forty-cdk-menubar.d.ts +451 -0
- package/types/forty-cdk-meter.d.ts +122 -0
- package/types/forty-cdk-navigation-menu.d.ts +514 -0
- package/types/forty-cdk-number-input.d.ts +319 -0
- package/types/forty-cdk-otp-input.d.ts +248 -0
- package/types/forty-cdk-pagination.d.ts +214 -0
- package/types/forty-cdk-pane-resizer.d.ts +145 -0
- package/types/forty-cdk-popover.d.ts +509 -0
- package/types/forty-cdk-progress.d.ts +143 -0
- package/types/forty-cdk-radio-group.d.ts +222 -0
- package/types/forty-cdk-scroll-area.d.ts +258 -0
- package/types/forty-cdk-search.d.ts +142 -0
- package/types/forty-cdk-select.d.ts +899 -0
- package/types/forty-cdk-separator.d.ts +59 -0
- package/types/forty-cdk-signal-forms.d.ts +58 -0
- package/types/forty-cdk-slider.d.ts +379 -0
- package/types/forty-cdk-stepper.d.ts +650 -0
- package/types/forty-cdk-switch.d.ts +87 -0
- package/types/forty-cdk-table.d.ts +723 -0
- package/types/forty-cdk-tabs.d.ts +235 -0
- package/types/forty-cdk-time-field.d.ts +307 -0
- package/types/forty-cdk-time-picker.d.ts +578 -0
- package/types/forty-cdk-toast.d.ts +598 -0
- package/types/forty-cdk-toggle.d.ts +310 -0
- package/types/forty-cdk-toolbar.d.ts +217 -0
- package/types/forty-cdk-tooltip.d.ts +436 -0
- package/types/forty-cdk-tree.d.ts +688 -0
- package/types/forty-cdk.d.ts +1 -19743
|
@@ -0,0 +1,1279 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { InjectionToken, inject, ElementRef, model, input, booleanAttribute, numberAttribute, output, computed, signal, effect, Directive, DOCUMENT, PLATFORM_ID, DestroyRef } from '@angular/core';
|
|
3
|
+
import { createDefaults, VirtualizedNavigator, FormUiControlBase, defaultItemToFormValue, singleSelected, injectTextDirection, RovingTabindex, injectTypeahead, Collection, firstEnabledHost, injectHiddenInput, reconcileRovingActive, isInArray, toggleInArray, nextEnabledHandle, findTypeaheadMatch, resolveListNavigation, hostId, registerHandle, registerA11yName, resolveLiftedDragControl, LiveAnnouncer, createKeyboardDragMediator, createPointerDragSession, resolveDropTarget, isDragLiftKey, PreviewController } from 'forty-cdk/core';
|
|
4
|
+
import { isPlatformBrowser } from '@angular/common';
|
|
5
|
+
|
|
6
|
+
const FOR_LISTBOX_CONTEXT = new InjectionToken('FOR_LISTBOX_CONTEXT');
|
|
7
|
+
function injectListboxContext(piece) {
|
|
8
|
+
const ctx = inject(FOR_LISTBOX_CONTEXT, { optional: true });
|
|
9
|
+
if (!ctx) {
|
|
10
|
+
throw new Error(`[forty-cdk/listbox] ${piece} must be used inside a [forListbox] element.`);
|
|
11
|
+
}
|
|
12
|
+
return ctx;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Library fallback for listbox defaults, read at the root injector when no
|
|
17
|
+
* consumer has called `provideForListboxDefaults`. Exported for the shared defaults
|
|
18
|
+
* contract spec; not re-exported from the primitive's public entry.
|
|
19
|
+
*/
|
|
20
|
+
const FOR_LISTBOX_FALLBACK_DEFAULTS = {
|
|
21
|
+
selectionFollowsFocus: false,
|
|
22
|
+
};
|
|
23
|
+
const { token, provideDefaults } = createDefaults('FOR_LISTBOX_DEFAULTS', FOR_LISTBOX_FALLBACK_DEFAULTS);
|
|
24
|
+
/** Token holding the resolved listbox defaults for the current scope. */
|
|
25
|
+
const FOR_LISTBOX_DEFAULTS = token;
|
|
26
|
+
/**
|
|
27
|
+
* Configures forty-cdk listbox defaults for this injector scope. Partial
|
|
28
|
+
* overrides inherit unspecified keys from the parent scope (or library
|
|
29
|
+
* defaults at the root).
|
|
30
|
+
*/
|
|
31
|
+
function provideForListboxDefaults(defaults = {}) {
|
|
32
|
+
return provideDefaults(defaults);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Virtualization navigation engine for `ForListbox`. A thin adapter over the
|
|
37
|
+
* shared `_internal/virtualized-navigator` engine: the listbox snapshot needs
|
|
38
|
+
* only the id + disabled flag (it never reads option values), so no NG0950 read
|
|
39
|
+
* guard is required.
|
|
40
|
+
*
|
|
41
|
+
* Internal — not re-exported from `listbox/index.ts` or `public-api.ts`.
|
|
42
|
+
*/
|
|
43
|
+
class ListboxVirtualizedNavigator {
|
|
44
|
+
#core;
|
|
45
|
+
constructor(deps) {
|
|
46
|
+
this.#core = new VirtualizedNavigator({ ...deps, loop: deps.loop }, {
|
|
47
|
+
posOf: (o) => o.posInSet(),
|
|
48
|
+
idOf: (o) => o.id(),
|
|
49
|
+
hostOf: (o) => o.host,
|
|
50
|
+
readEntry: (o) => ({ id: o.id(), disabled: o.disabled() }),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
/** @see VirtualizedNavigator.prime */
|
|
54
|
+
prime() {
|
|
55
|
+
this.#core.prime();
|
|
56
|
+
}
|
|
57
|
+
/** @see VirtualizedNavigator.tryResolvePending */
|
|
58
|
+
tryResolvePending() {
|
|
59
|
+
return this.#core.tryResolvePending();
|
|
60
|
+
}
|
|
61
|
+
/** @see VirtualizedNavigator.navigate */
|
|
62
|
+
navigate(direction) {
|
|
63
|
+
this.#core.navigate(direction);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Headless implementation of the [WAI-ARIA Listbox pattern](https://www.w3.org/WAI/ARIA/apg/patterns/listbox/).
|
|
69
|
+
* Implements `FormValueControl<readonly T[]>` from
|
|
70
|
+
* `@angular/forms/signals` for `[formField]` auto-wiring.
|
|
71
|
+
*
|
|
72
|
+
* Generic over the option value type `T` (default `string`). When the
|
|
73
|
+
* consumer binds object items the directive infers `T` from `[(value)]` and
|
|
74
|
+
* `[forListboxOption][value]`; object identity is resolved by the
|
|
75
|
+
* consumer-supplied `[isItemEqualToValue]` and the hidden inputs serialize
|
|
76
|
+
* via `[itemToFormValue]`. Option display text is read from the rendered
|
|
77
|
+
* `textContent`, so no separate label function is needed.
|
|
78
|
+
*
|
|
79
|
+
* Selection is always modeled as `readonly T[]`:
|
|
80
|
+
* - In single mode (`multiple=false`, default), the array has 0 or 1 element.
|
|
81
|
+
* - In multi mode, any number of items can be selected.
|
|
82
|
+
*
|
|
83
|
+
* Single-select consumers can read the sole value through the
|
|
84
|
+
* {@link ForListbox.selected} convenience accessor instead of unwrapping the
|
|
85
|
+
* array.
|
|
86
|
+
*
|
|
87
|
+
* Keyboard supports the full APG-recommended model: arrows + Home/End for
|
|
88
|
+
* focus movement, Space/Enter (via native `<button>` activation) to select
|
|
89
|
+
* or toggle, typeahead, plus multi-select range modifiers (Shift+Arrow,
|
|
90
|
+
* Shift+Space, Ctrl/Cmd+A, Ctrl+Shift+Home/End).
|
|
91
|
+
*/
|
|
92
|
+
class ForListbox extends FormUiControlBase {
|
|
93
|
+
#host = inject(ElementRef);
|
|
94
|
+
#defaults = inject(FOR_LISTBOX_DEFAULTS);
|
|
95
|
+
/**
|
|
96
|
+
* Two-way bindable. Selected option values. Single-mode keeps 0 or 1
|
|
97
|
+
* element. The `model()` change emitter (`(valueChange)`) fires only on
|
|
98
|
+
* internal selection changes (option activation or `selectionFollowsFocus`
|
|
99
|
+
* nav), never on consumer writes via `[(value)]` — observe transitions
|
|
100
|
+
* without binding back.
|
|
101
|
+
*/
|
|
102
|
+
value = model([], /* @ts-ignore */
|
|
103
|
+
...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
|
|
104
|
+
/**
|
|
105
|
+
* Compare two items for equality. Defaults to `===`, which is the
|
|
106
|
+
* correct identity for primitive `T` (e.g. strings, numbers). Override
|
|
107
|
+
* when binding object items so the directive can locate selected entries
|
|
108
|
+
* by id (or any other stable key) instead of by reference:
|
|
109
|
+
* `[isItemEqualToValue]="(a, b) => a.id === b.id"`.
|
|
110
|
+
*/
|
|
111
|
+
isItemEqualToValue = input((a, b) => a === b, /* @ts-ignore */
|
|
112
|
+
...(ngDevMode ? [{ debugName: "isItemEqualToValue" }] : /* istanbul ignore next */ []));
|
|
113
|
+
/**
|
|
114
|
+
* Serialize an item for the hidden input that participates in native
|
|
115
|
+
* form submission. Defaults to identity for strings and to
|
|
116
|
+
* `JSON.stringify` for non-string items so the primitive works out of
|
|
117
|
+
* the box round-tripping objects. Override to emit a specific wire
|
|
118
|
+
* format — typically a per-item id — when the backend expects that:
|
|
119
|
+
* `[itemToFormValue]="(it) => it.id"`.
|
|
120
|
+
*/
|
|
121
|
+
itemToFormValue = input(defaultItemToFormValue, /* @ts-ignore */
|
|
122
|
+
...(ngDevMode ? [{ debugName: "itemToFormValue" }] : /* istanbul ignore next */ []));
|
|
123
|
+
/**
|
|
124
|
+
* Read-only single-select convenience view of {@link value}. Returns the
|
|
125
|
+
* sole value when exactly one is selected (regardless of `multiple`),
|
|
126
|
+
* otherwise `null` (zero, or 2+ selected). Lets single-select consumers
|
|
127
|
+
* read `selected()` instead of unwrapping `value()[0]`. The array-backed
|
|
128
|
+
* `value` model remains the source of truth and the `FormValueControl`
|
|
129
|
+
* contract; this is a derived accessor.
|
|
130
|
+
*/
|
|
131
|
+
selected = singleSelected(this.value);
|
|
132
|
+
multiple = input(false, { ...(ngDevMode ? { debugName: "multiple" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
133
|
+
/**
|
|
134
|
+
* Manual `aria-label` for the listbox. Use this when no visible label
|
|
135
|
+
* element exists; otherwise prefer pointing `aria-labelledby` at one. A
|
|
136
|
+
* `null` (default) or empty value emits no attribute.
|
|
137
|
+
*/
|
|
138
|
+
ariaLabel = input(null, /* @ts-ignore */
|
|
139
|
+
...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
|
|
140
|
+
orientation = input('vertical', /* @ts-ignore */
|
|
141
|
+
...(ngDevMode ? [{ debugName: "orientation" }] : /* istanbul ignore next */ []));
|
|
142
|
+
/**
|
|
143
|
+
* When true (default), arrow navigation wraps at the ends — moving past the
|
|
144
|
+
* last option focuses the first and vice versa. Set `false` to stop at the
|
|
145
|
+
* boundaries. Mirrors the `loop` input on `ForSelect`, `ForToggleGroup` and
|
|
146
|
+
* `ForCombobox`. Range extension (Shift+Arrow) never wraps regardless of
|
|
147
|
+
* this input, per the APG.
|
|
148
|
+
*/
|
|
149
|
+
loop = input(true, { ...(ngDevMode ? { debugName: "loop" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
150
|
+
/**
|
|
151
|
+
* Total number of items in the source data. When set, enables the
|
|
152
|
+
* virtualized activedescendant focus model and populates `aria-setsize`
|
|
153
|
+
* on each rendered option. Leave unset (default `undefined`) for the
|
|
154
|
+
* standard roving-tabindex model.
|
|
155
|
+
*/
|
|
156
|
+
totalCount = input(undefined, { ...(ngDevMode ? { debugName: "totalCount" } : /* istanbul ignore next */ {}), transform: (v) => (v == null ? undefined : numberAttribute(v)) });
|
|
157
|
+
/**
|
|
158
|
+
* Inclusive-exclusive `[start, end)` index range of the currently rendered
|
|
159
|
+
* options. The virtualizer provides this; the listbox uses it to decide
|
|
160
|
+
* whether a navigation target is in the visible window.
|
|
161
|
+
*/
|
|
162
|
+
visibleRange = input(undefined, /* @ts-ignore */
|
|
163
|
+
...(ngDevMode ? [{ debugName: "visibleRange" }] : /* istanbul ignore next */ []));
|
|
164
|
+
/**
|
|
165
|
+
* Emitted when keyboard navigation reaches an option outside the rendered
|
|
166
|
+
* window. The consumer passes this index to `injectVirtualizer`'s
|
|
167
|
+
* `scrollToIndex` so the correct option mounts.
|
|
168
|
+
*/
|
|
169
|
+
scrollToIndex = output();
|
|
170
|
+
/**
|
|
171
|
+
* Writing direction. When unset (default `null`), the inherited ambient
|
|
172
|
+
* direction is resolved from the nearest ancestor carrying a `dir` attribute
|
|
173
|
+
* (or `<html dir>`), defaulting to `'ltr'`. An explicit `[dir]` always wins.
|
|
174
|
+
* The resolved value is reflected to the host `dir` attribute and swaps
|
|
175
|
+
* ArrowLeft / ArrowRight semantics in RTL for horizontal listboxes.
|
|
176
|
+
*/
|
|
177
|
+
_dirInput = input(null, { ...(ngDevMode ? { debugName: "_dirInput" } : /* istanbul ignore next */ {}), alias: 'dir' });
|
|
178
|
+
dir = injectTextDirection(this._dirInput);
|
|
179
|
+
/**
|
|
180
|
+
* Single-mode only: when true, arrow nav also selects the focused option.
|
|
181
|
+
* APG calls this optional and recommends caution — leave off unless your
|
|
182
|
+
* UX truly benefits from selection following focus. Default `false`.
|
|
183
|
+
* The default is read from `provideForListboxDefaults` for the surrounding
|
|
184
|
+
* scope.
|
|
185
|
+
*/
|
|
186
|
+
selectionFollowsFocus = input(this.#defaults.selectionFollowsFocus, { ...(ngDevMode ? { debugName: "selectionFollowsFocus" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
187
|
+
roving = new RovingTabindex();
|
|
188
|
+
#typeahead = injectTypeahead();
|
|
189
|
+
#options = new Collection();
|
|
190
|
+
/**
|
|
191
|
+
* All registered options in DOM (rendered) order. Part of the public
|
|
192
|
+
* {@link ForListboxContext} surface so container-level coordinators (e.g.
|
|
193
|
+
* `ForListboxReorder`) can read the ordered hosts without each option owning a
|
|
194
|
+
* `[forDraggable]` that would fight the listbox's roving tabindex.
|
|
195
|
+
*/
|
|
196
|
+
options = this.#options.items;
|
|
197
|
+
#firstEnabledHost = computed(() => firstEnabledHost(this.#options.items()), /* @ts-ignore */
|
|
198
|
+
...(ngDevMode ? [{ debugName: "#firstEnabledHost" }] : /* istanbul ignore next */ []));
|
|
199
|
+
#firstSelectedHost = computed(() => {
|
|
200
|
+
const selected = this.value();
|
|
201
|
+
if (selected.length === 0) {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
const equals = this.isItemEqualToValue();
|
|
205
|
+
for (const option of this.#options.items()) {
|
|
206
|
+
if (!option.disabled() && selected.some((v) => equals(v, option.value()))) {
|
|
207
|
+
return option.host;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return null;
|
|
211
|
+
}, /* @ts-ignore */
|
|
212
|
+
...(ngDevMode ? [{ debugName: "#firstSelectedHost" }] : /* istanbul ignore next */ []));
|
|
213
|
+
#virtualized = computed(() => this.totalCount() !== undefined, /* @ts-ignore */
|
|
214
|
+
...(ngDevMode ? [{ debugName: "#virtualized" }] : /* istanbul ignore next */ []));
|
|
215
|
+
#activeId = signal(null, /* @ts-ignore */
|
|
216
|
+
...(ngDevMode ? [{ debugName: "#activeId" }] : /* istanbul ignore next */ []));
|
|
217
|
+
/**
|
|
218
|
+
* The active option's `id` when using the activedescendant focus model,
|
|
219
|
+
* `null` in the roving-tabindex path. The host reflects this as
|
|
220
|
+
* `aria-activedescendant`; options read it to compute `data-highlighted`.
|
|
221
|
+
*/
|
|
222
|
+
activeDescendantId = computed(() => this.#virtualized() ? this.#activeId() : null, /* @ts-ignore */
|
|
223
|
+
...(ngDevMode ? [{ debugName: "activeDescendantId" }] : /* istanbul ignore next */ []));
|
|
224
|
+
/**
|
|
225
|
+
* Tabindex for the listbox host. In the virtualized path the host is always
|
|
226
|
+
* the single tab stop. In the roving path the host carries `tabindex="0"`
|
|
227
|
+
* only when no option qualifies — an empty listbox or one whose options are
|
|
228
|
+
* all disabled — so the control is still reachable. A disabled listbox is
|
|
229
|
+
* never tabbable.
|
|
230
|
+
*/
|
|
231
|
+
hostTabindex = computed(() => {
|
|
232
|
+
if (this.effectiveDisabled()) {
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
if (this.#virtualized()) {
|
|
236
|
+
return '0';
|
|
237
|
+
}
|
|
238
|
+
const hasRovingEntry = this.#firstSelectedHost() !== null || this.#firstEnabledHost() !== null;
|
|
239
|
+
return hasRovingEntry ? null : '0';
|
|
240
|
+
}, /* @ts-ignore */
|
|
241
|
+
...(ngDevMode ? [{ debugName: "hostTabindex" }] : /* istanbul ignore next */ []));
|
|
242
|
+
/**
|
|
243
|
+
* Anchor value for APG range-selection actions (Shift+Space). Stored as the
|
|
244
|
+
* option's *value* (resolved to its current index at range time via
|
|
245
|
+
* {@link isItemEqualToValue}) rather than a DOM index, so reordering or
|
|
246
|
+
* removing options before the anchor can't silently shift the range to the
|
|
247
|
+
* wrong span. Set on every unmodified activation (click / Space / Enter); not
|
|
248
|
+
* affected by Shift+Arrow, which APG defines as per-option toggle.
|
|
249
|
+
*/
|
|
250
|
+
#anchorValue = signal(null, /* @ts-ignore */
|
|
251
|
+
...(ngDevMode ? [{ debugName: "#anchorValue" }] : /* istanbul ignore next */ []));
|
|
252
|
+
#navigator = null;
|
|
253
|
+
#requireNavigator() {
|
|
254
|
+
return (this.#navigator ??= new ListboxVirtualizedNavigator({
|
|
255
|
+
items: this.#options.items,
|
|
256
|
+
totalCount: this.totalCount,
|
|
257
|
+
visibleRange: this.visibleRange,
|
|
258
|
+
loop: this.loop,
|
|
259
|
+
getActiveId: () => this.#activeId(),
|
|
260
|
+
setActiveId: (id) => this.#activeId.set(id),
|
|
261
|
+
emitScrollToIndex: (idx) => this.scrollToIndex.emit(idx),
|
|
262
|
+
}));
|
|
263
|
+
}
|
|
264
|
+
constructor() {
|
|
265
|
+
super();
|
|
266
|
+
injectHiddenInput({
|
|
267
|
+
name: this.name,
|
|
268
|
+
values: this.value,
|
|
269
|
+
serialize: (item) => this.itemToFormValue()(item),
|
|
270
|
+
disabled: this.effectiveDisabled,
|
|
271
|
+
});
|
|
272
|
+
reconcileRovingActive(this.roving, this.#options.items);
|
|
273
|
+
effect(() => {
|
|
274
|
+
this.#options.items();
|
|
275
|
+
if (!this.#virtualized()) {
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
const navigator = this.#requireNavigator();
|
|
279
|
+
navigator.prime();
|
|
280
|
+
navigator.tryResolvePending();
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
isSelected(v) {
|
|
284
|
+
return isInArray(this.value(), v, this.isItemEqualToValue());
|
|
285
|
+
}
|
|
286
|
+
activate(v) {
|
|
287
|
+
if (this.effectiveDisabled() || this.readonly()) {
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
if (this.multiple()) {
|
|
291
|
+
this.value.set(toggleInArray(this.value(), v, this.isItemEqualToValue()));
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
// Single-mode: idempotent select (no deselect on click of selected).
|
|
295
|
+
this.value.set([v]);
|
|
296
|
+
}
|
|
297
|
+
this.#anchorValue.set(v);
|
|
298
|
+
}
|
|
299
|
+
extendByArrow(currentOption, action) {
|
|
300
|
+
if (this.effectiveDisabled() || !this.multiple()) {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
const target = nextEnabledHandle(this.#options.items(), currentOption, action, { loop: false });
|
|
304
|
+
if (target === null) {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
// Focus moves regardless of readonly — same contract as `navigate()`. Readonly
|
|
308
|
+
// only blocks the selection mutation.
|
|
309
|
+
target.host.focus();
|
|
310
|
+
if (this.readonly()) {
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
this.value.set(toggleInArray(this.value(), target.value(), this.isItemEqualToValue()));
|
|
314
|
+
}
|
|
315
|
+
selectRangeToFocused(currentOption) {
|
|
316
|
+
if (this.effectiveDisabled() || this.readonly() || !this.multiple()) {
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
const options = this.#options.items();
|
|
320
|
+
const currentIndex = options.findIndex((o) => o.host === currentOption);
|
|
321
|
+
if (currentIndex < 0) {
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
const anchorValue = this.#anchorValue();
|
|
325
|
+
const equals = this.isItemEqualToValue();
|
|
326
|
+
const anchorIndex = anchorValue === null ? -1 : options.findIndex((o) => equals(o.value(), anchorValue));
|
|
327
|
+
const start = anchorIndex < 0 ? currentIndex : anchorIndex;
|
|
328
|
+
const [lo, hi] = start <= currentIndex ? [start, currentIndex] : [currentIndex, start];
|
|
329
|
+
const next = [...this.value()];
|
|
330
|
+
for (let i = lo; i <= hi; i++) {
|
|
331
|
+
const opt = options[i];
|
|
332
|
+
if (!opt || opt.disabled()) {
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
const v = opt.value();
|
|
336
|
+
if (!next.some((x) => equals(x, v))) {
|
|
337
|
+
next.push(v);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
this.value.set(next);
|
|
341
|
+
}
|
|
342
|
+
selectAll() {
|
|
343
|
+
if (this.effectiveDisabled() || this.readonly() || !this.multiple()) {
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
const enabled = [];
|
|
347
|
+
for (const opt of this.#options.items()) {
|
|
348
|
+
if (opt.disabled()) {
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
enabled.push(opt.value());
|
|
352
|
+
}
|
|
353
|
+
if (enabled.length === 0) {
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
const equals = this.isItemEqualToValue();
|
|
357
|
+
const current = this.value();
|
|
358
|
+
const allSelected = enabled.every((v) => current.some((x) => equals(x, v)));
|
|
359
|
+
this.value.set(allSelected ? [] : enabled);
|
|
360
|
+
}
|
|
361
|
+
selectFromCurrentToEdge(currentOption, edge) {
|
|
362
|
+
if (this.effectiveDisabled() || !this.multiple()) {
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
const options = this.#options.items();
|
|
366
|
+
const currentIndex = options.findIndex((o) => o.host === currentOption);
|
|
367
|
+
if (currentIndex < 0) {
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
const [lo, hi] = edge === 'first' ? [0, currentIndex] : [currentIndex, options.length - 1];
|
|
371
|
+
const equals = this.isItemEqualToValue();
|
|
372
|
+
const next = [...this.value()];
|
|
373
|
+
let firstEnabled = null;
|
|
374
|
+
let lastEnabled = null;
|
|
375
|
+
// Walk forward so the resulting array preserves DOM order (insertion order).
|
|
376
|
+
for (let i = lo; i <= hi; i++) {
|
|
377
|
+
const opt = options[i];
|
|
378
|
+
if (!opt || opt.disabled()) {
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
const v = opt.value();
|
|
382
|
+
if (!next.some((x) => equals(x, v))) {
|
|
383
|
+
next.push(v);
|
|
384
|
+
}
|
|
385
|
+
if (firstEnabled === null) {
|
|
386
|
+
firstEnabled = opt.host;
|
|
387
|
+
}
|
|
388
|
+
lastEnabled = opt.host;
|
|
389
|
+
}
|
|
390
|
+
const edgeFocusTarget = edge === 'first' ? firstEnabled : lastEnabled;
|
|
391
|
+
// Focus moves regardless of readonly — same contract as `navigate()`. Readonly
|
|
392
|
+
// only blocks the selection mutation.
|
|
393
|
+
edgeFocusTarget?.focus();
|
|
394
|
+
if (this.readonly()) {
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
this.value.set(next);
|
|
398
|
+
}
|
|
399
|
+
navigate(currentOption, action) {
|
|
400
|
+
if (this.effectiveDisabled()) {
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
const target = nextEnabledHandle(this.#options.items(), currentOption, action, {
|
|
404
|
+
loop: this.loop(),
|
|
405
|
+
});
|
|
406
|
+
if (target === null) {
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
target.host.focus();
|
|
410
|
+
target.host.scrollIntoView?.({ block: 'nearest' });
|
|
411
|
+
if (!this.multiple() && this.selectionFollowsFocus() && !this.readonly()) {
|
|
412
|
+
this.value.set([target.value()]);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
handleTypeahead(event) {
|
|
416
|
+
if (!this.#typeahead.handle(event)) {
|
|
417
|
+
return false;
|
|
418
|
+
}
|
|
419
|
+
const options = this.#options.items();
|
|
420
|
+
const currentIndex = options.findIndex((o) => o.host === event.target);
|
|
421
|
+
const match = findTypeaheadMatch(options, {
|
|
422
|
+
buffer: this.#typeahead.buffer(),
|
|
423
|
+
repeated: this.#typeahead.isRepeatedChar(),
|
|
424
|
+
anchorIndex: currentIndex,
|
|
425
|
+
}, (o) => o.host.textContent ?? '', (o) => o.disabled());
|
|
426
|
+
match?.host.focus();
|
|
427
|
+
return true;
|
|
428
|
+
}
|
|
429
|
+
isFirstFocusableOption(el) {
|
|
430
|
+
const firstSelected = this.#firstSelectedHost();
|
|
431
|
+
if (firstSelected) {
|
|
432
|
+
return firstSelected === el;
|
|
433
|
+
}
|
|
434
|
+
return this.#firstEnabledHost() === el;
|
|
435
|
+
}
|
|
436
|
+
isOptionHighlighted(el) {
|
|
437
|
+
return this.roving.active() === el;
|
|
438
|
+
}
|
|
439
|
+
optionTabindex(el) {
|
|
440
|
+
if (this.#virtualized()) {
|
|
441
|
+
return -1;
|
|
442
|
+
}
|
|
443
|
+
return this.roving.hasActive() ? this.roving.tabindexFor(el) : null;
|
|
444
|
+
}
|
|
445
|
+
setActiveOption(el) {
|
|
446
|
+
this.roving.setActive(el);
|
|
447
|
+
}
|
|
448
|
+
registerOption(handle) {
|
|
449
|
+
this.#options.register(handle);
|
|
450
|
+
}
|
|
451
|
+
notifyOptionClick(optionId) {
|
|
452
|
+
if (!this.#virtualized()) {
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
this.#activeId.set(optionId);
|
|
456
|
+
this.#host.nativeElement.focus();
|
|
457
|
+
}
|
|
458
|
+
unregisterOption(handle) {
|
|
459
|
+
this.#options.unregister(handle);
|
|
460
|
+
this.roving.unregister(handle.host);
|
|
461
|
+
if (this.#virtualized() && this.#activeId() === handle.id()) {
|
|
462
|
+
this.#activeId.set(null);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
onHostKeyDown(event) {
|
|
466
|
+
if (!this.#virtualized() || this.effectiveDisabled()) {
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
470
|
+
event.preventDefault();
|
|
471
|
+
this.#activateActiveDescendant();
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
const action = resolveListNavigation(event, {
|
|
475
|
+
orientation: this.orientation(),
|
|
476
|
+
dir: this.dir(),
|
|
477
|
+
pageKeys: true,
|
|
478
|
+
});
|
|
479
|
+
if (action) {
|
|
480
|
+
event.preventDefault();
|
|
481
|
+
this.#requireNavigator().navigate(action);
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
this.#typeaheadVirtualized(event);
|
|
485
|
+
}
|
|
486
|
+
#activateActiveDescendant() {
|
|
487
|
+
const id = this.#activeId();
|
|
488
|
+
if (id === null) {
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
const handle = this.#options.items().find((o) => o.id() === id);
|
|
492
|
+
if (!handle || handle.disabled()) {
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
this.activate(handle.value());
|
|
496
|
+
}
|
|
497
|
+
#typeaheadVirtualized(event) {
|
|
498
|
+
if (!this.#typeahead.handle(event)) {
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
const options = this.#options.items();
|
|
502
|
+
const activeId = this.#activeId();
|
|
503
|
+
const anchor = activeId === null ? -1 : options.findIndex((o) => o.id() === activeId);
|
|
504
|
+
const match = findTypeaheadMatch(options, {
|
|
505
|
+
buffer: this.#typeahead.buffer(),
|
|
506
|
+
repeated: this.#typeahead.isRepeatedChar(),
|
|
507
|
+
anchorIndex: anchor,
|
|
508
|
+
}, (o) => o.host.textContent ?? '', (o) => o.disabled());
|
|
509
|
+
if (match) {
|
|
510
|
+
this.#activeId.set(match.id());
|
|
511
|
+
match.host.scrollIntoView?.({ block: 'nearest' });
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
onHostFocusIn() {
|
|
515
|
+
if (!this.#virtualized() || this.effectiveDisabled()) {
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
if (this.#activeId() !== null) {
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
const items = this.#options.items();
|
|
522
|
+
if (items.length === 0) {
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
const ordered = [...items].sort((a, b) => (a.posInSet() ?? 0) - (b.posInSet() ?? 0));
|
|
526
|
+
const equals = this.isItemEqualToValue();
|
|
527
|
+
const value = this.value();
|
|
528
|
+
const selectedFirst = ordered.find((o) => !o.disabled() && value.some((v) => equals(v, o.value())));
|
|
529
|
+
const target = selectedFirst ?? ordered.find((o) => !o.disabled());
|
|
530
|
+
if (target) {
|
|
531
|
+
this.#activeId.set(target.id());
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
onFocusOut(event) {
|
|
535
|
+
const next = event.relatedTarget;
|
|
536
|
+
if (next && this.#host.nativeElement.contains(next)) {
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
this.markTouched();
|
|
540
|
+
}
|
|
541
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForListbox, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
542
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: ForListbox, isStandalone: true, selector: "[forListbox]", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, isItemEqualToValue: { classPropertyName: "isItemEqualToValue", publicName: "isItemEqualToValue", isSignal: true, isRequired: false, transformFunction: null }, itemToFormValue: { classPropertyName: "itemToFormValue", publicName: "itemToFormValue", isSignal: true, isRequired: false, transformFunction: null }, multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, orientation: { classPropertyName: "orientation", publicName: "orientation", isSignal: true, isRequired: false, transformFunction: null }, loop: { classPropertyName: "loop", publicName: "loop", isSignal: true, isRequired: false, transformFunction: null }, totalCount: { classPropertyName: "totalCount", publicName: "totalCount", isSignal: true, isRequired: false, transformFunction: null }, visibleRange: { classPropertyName: "visibleRange", publicName: "visibleRange", isSignal: true, isRequired: false, transformFunction: null }, _dirInput: { classPropertyName: "_dirInput", publicName: "dir", isSignal: true, isRequired: false, transformFunction: null }, selectionFollowsFocus: { classPropertyName: "selectionFollowsFocus", publicName: "selectionFollowsFocus", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", scrollToIndex: "scrollToIndex" }, host: { attributes: { "role": "listbox" }, listeners: { "keydown": "onHostKeyDown($event)", "focusin": "onHostFocusIn()", "focusout": "onFocusOut($event)" }, properties: { "attr.aria-label": "ariaLabel() || null", "attr.aria-orientation": "orientation()", "attr.aria-multiselectable": "multiple() ? \"true\" : null", "attr.aria-disabled": "effectiveDisabled() ? \"true\" : null", "attr.aria-readonly": "readonly() ? \"true\" : null", "attr.aria-required": "required() ? \"true\" : null", "attr.aria-invalid": "invalid() ? \"true\" : null", "attr.aria-busy": "pending() ? \"true\" : null", "attr.aria-activedescendant": "activeDescendantId()", "attr.tabindex": "hostTabindex()", "attr.data-orientation": "orientation()", "attr.data-disabled": "effectiveDisabled() ? \"\" : null", "attr.dir": "dir()" } }, providers: [{ provide: FOR_LISTBOX_CONTEXT, useExisting: ForListbox }], exportAs: ["forListbox"], usesInheritance: true, ngImport: i0 });
|
|
543
|
+
}
|
|
544
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForListbox, decorators: [{
|
|
545
|
+
type: Directive,
|
|
546
|
+
args: [{
|
|
547
|
+
selector: '[forListbox]',
|
|
548
|
+
exportAs: 'forListbox',
|
|
549
|
+
host: {
|
|
550
|
+
role: 'listbox',
|
|
551
|
+
'[attr.aria-label]': 'ariaLabel() || null',
|
|
552
|
+
'[attr.aria-orientation]': 'orientation()',
|
|
553
|
+
'[attr.aria-multiselectable]': 'multiple() ? "true" : null',
|
|
554
|
+
'[attr.aria-disabled]': 'effectiveDisabled() ? "true" : null',
|
|
555
|
+
'[attr.aria-readonly]': 'readonly() ? "true" : null',
|
|
556
|
+
'[attr.aria-required]': 'required() ? "true" : null',
|
|
557
|
+
'[attr.aria-invalid]': 'invalid() ? "true" : null',
|
|
558
|
+
'[attr.aria-busy]': 'pending() ? "true" : null',
|
|
559
|
+
'[attr.aria-activedescendant]': 'activeDescendantId()',
|
|
560
|
+
'[attr.tabindex]': 'hostTabindex()',
|
|
561
|
+
'[attr.data-orientation]': 'orientation()',
|
|
562
|
+
'[attr.data-disabled]': 'effectiveDisabled() ? "" : null',
|
|
563
|
+
'[attr.dir]': 'dir()',
|
|
564
|
+
'(keydown)': 'onHostKeyDown($event)',
|
|
565
|
+
'(focusin)': 'onHostFocusIn()',
|
|
566
|
+
'(focusout)': 'onFocusOut($event)',
|
|
567
|
+
},
|
|
568
|
+
providers: [{ provide: FOR_LISTBOX_CONTEXT, useExisting: ForListbox }],
|
|
569
|
+
}]
|
|
570
|
+
}], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], isItemEqualToValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "isItemEqualToValue", required: false }] }], itemToFormValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemToFormValue", required: false }] }], multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], orientation: [{ type: i0.Input, args: [{ isSignal: true, alias: "orientation", required: false }] }], loop: [{ type: i0.Input, args: [{ isSignal: true, alias: "loop", required: false }] }], totalCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "totalCount", required: false }] }], visibleRange: [{ type: i0.Input, args: [{ isSignal: true, alias: "visibleRange", required: false }] }], scrollToIndex: [{ type: i0.Output, args: ["scrollToIndex"] }], _dirInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "dir", required: false }] }], selectionFollowsFocus: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectionFollowsFocus", required: false }] }] } });
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Injection key the `[forListboxOptionIndicator]` uses to resolve its parent
|
|
574
|
+
* option, decoupled from the concrete `ForListboxOption` class.
|
|
575
|
+
* `ForListboxOption` provides itself under this token, so a design system
|
|
576
|
+
* wrapping the option by subclassing re-points it at the subclass with a
|
|
577
|
+
* single provider (`{ provide: FOR_LISTBOX_OPTION, useExisting: MtxListboxOption }`)
|
|
578
|
+
* and the indicator keeps resolving — see `docs/wrapping-form-primitives.md`.
|
|
579
|
+
*/
|
|
580
|
+
const FOR_LISTBOX_OPTION = new InjectionToken('FOR_LISTBOX_OPTION');
|
|
581
|
+
/**
|
|
582
|
+
* One option inside a `ForListbox`. Apply on a `<button type="button">` so
|
|
583
|
+
* Space / Enter activation come from native button behavior — printable keys
|
|
584
|
+
* fall through to the parent listbox for typeahead matching.
|
|
585
|
+
*
|
|
586
|
+
* Generic over the option value type `T` (default `string`). Inferred from
|
|
587
|
+
* the `[value]` binding so consumers can pass either primitive ids or full
|
|
588
|
+
* objects (`[value]="lang"` infers `T = Language`); the parent `[forListbox]`
|
|
589
|
+
* must be parameterized over the same `T`. The parent's
|
|
590
|
+
* `[isItemEqualToValue]` decides how options are matched against the
|
|
591
|
+
* committed selection.
|
|
592
|
+
*/
|
|
593
|
+
class ForListboxOption {
|
|
594
|
+
#group = injectListboxContext('ForListboxOption');
|
|
595
|
+
#host = inject(ElementRef);
|
|
596
|
+
/**
|
|
597
|
+
* Stable identifier serialized into `[(value)]` and the hidden input.
|
|
598
|
+
* Defaults to `string` for back-compat; bind an object to specialize the
|
|
599
|
+
* parent `[forListbox]` over a richer `T`. The parent's
|
|
600
|
+
* `[isItemEqualToValue]` decides how options are matched against the
|
|
601
|
+
* committed selection.
|
|
602
|
+
*/
|
|
603
|
+
value = input.required(/* @ts-ignore */
|
|
604
|
+
...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
|
|
605
|
+
disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
606
|
+
/**
|
|
607
|
+
* Zero-based absolute position of this option in the full source data.
|
|
608
|
+
* Required in the virtualized path — the listbox uses it to build the
|
|
609
|
+
* position snapshot and to emit `aria-posinset` (which is `posInSet + 1`).
|
|
610
|
+
* Leave unset (default `null`) outside the virtualized path.
|
|
611
|
+
*/
|
|
612
|
+
posInSet = input(null, /* @ts-ignore */
|
|
613
|
+
...(ngDevMode ? [{ debugName: "posInSet" }] : /* istanbul ignore next */ []));
|
|
614
|
+
id = hostId('for-listbox-option');
|
|
615
|
+
selected = computed(() => this.#group.isSelected(this.value()), /* @ts-ignore */
|
|
616
|
+
...(ngDevMode ? [{ debugName: "selected" }] : /* istanbul ignore next */ []));
|
|
617
|
+
/**
|
|
618
|
+
* True when this option is the keyboard-focused / active candidate.
|
|
619
|
+
* In the roving-tabindex path this tracks the DOM-focused option.
|
|
620
|
+
* In the virtualized activedescendant path it tracks `aria-activedescendant`.
|
|
621
|
+
* Reflected as `data-highlighted` so consumers can style it uniformly
|
|
622
|
+
* with the other primitives.
|
|
623
|
+
*/
|
|
624
|
+
highlighted = computed(() => {
|
|
625
|
+
const activeId = this.#group.activeDescendantId();
|
|
626
|
+
if (activeId !== null) {
|
|
627
|
+
return activeId === this.id();
|
|
628
|
+
}
|
|
629
|
+
return this.#group.isOptionHighlighted(this.#host.nativeElement);
|
|
630
|
+
}, /* @ts-ignore */
|
|
631
|
+
...(ngDevMode ? [{ debugName: "highlighted" }] : /* istanbul ignore next */ []));
|
|
632
|
+
ariaSetSize = computed(() => {
|
|
633
|
+
const total = this.#group.totalCount();
|
|
634
|
+
return total === undefined ? null : String(total);
|
|
635
|
+
}, /* @ts-ignore */
|
|
636
|
+
...(ngDevMode ? [{ debugName: "ariaSetSize" }] : /* istanbul ignore next */ []));
|
|
637
|
+
ariaPosInSet = computed(() => {
|
|
638
|
+
if (this.#group.totalCount() === undefined) {
|
|
639
|
+
return null;
|
|
640
|
+
}
|
|
641
|
+
const pos = this.posInSet();
|
|
642
|
+
return pos === null ? null : String(pos + 1);
|
|
643
|
+
}, /* @ts-ignore */
|
|
644
|
+
...(ngDevMode ? [{ debugName: "ariaPosInSet" }] : /* istanbul ignore next */ []));
|
|
645
|
+
effectiveDisabled = computed(() => this.disabled() || this.#group.effectiveDisabled(), /* @ts-ignore */
|
|
646
|
+
...(ngDevMode ? [{ debugName: "effectiveDisabled" }] : /* istanbul ignore next */ []));
|
|
647
|
+
tabindex = computed(() => {
|
|
648
|
+
if (this.effectiveDisabled()) {
|
|
649
|
+
return -1;
|
|
650
|
+
}
|
|
651
|
+
const rovingTabindex = this.#group.optionTabindex(this.#host.nativeElement);
|
|
652
|
+
if (rovingTabindex !== null) {
|
|
653
|
+
return rovingTabindex;
|
|
654
|
+
}
|
|
655
|
+
return this.#group.isFirstFocusableOption(this.#host.nativeElement) ? 0 : -1;
|
|
656
|
+
}, /* @ts-ignore */
|
|
657
|
+
...(ngDevMode ? [{ debugName: "tabindex" }] : /* istanbul ignore next */ []));
|
|
658
|
+
constructor() {
|
|
659
|
+
const handle = {
|
|
660
|
+
host: this.#host.nativeElement,
|
|
661
|
+
value: this.value,
|
|
662
|
+
disabled: this.effectiveDisabled,
|
|
663
|
+
id: this.id,
|
|
664
|
+
posInSet: this.posInSet,
|
|
665
|
+
};
|
|
666
|
+
registerHandle(handle, (h) => this.#group.registerOption(h), (h) => this.#group.unregisterOption(h));
|
|
667
|
+
}
|
|
668
|
+
onClick() {
|
|
669
|
+
if (this.effectiveDisabled() || this.#group.readonly()) {
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
this.#group.activate(this.value());
|
|
673
|
+
this.#group.notifyOptionClick(this.id());
|
|
674
|
+
}
|
|
675
|
+
onPointerDown(event) {
|
|
676
|
+
if (this.#group.totalCount() === undefined) {
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
event.preventDefault();
|
|
680
|
+
}
|
|
681
|
+
onFocus() {
|
|
682
|
+
if (this.effectiveDisabled()) {
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
this.#group.setActiveOption(this.#host.nativeElement);
|
|
686
|
+
}
|
|
687
|
+
onKeyDown(event) {
|
|
688
|
+
if (this.effectiveDisabled()) {
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
// APG-recommended multi-select range modifiers. Single-mode falls through
|
|
692
|
+
// to plain navigation so Shift+Arrow / Ctrl+A behave like their unmodified
|
|
693
|
+
// counterparts (or no-op).
|
|
694
|
+
if (this.#group.multiple()) {
|
|
695
|
+
// Ctrl/Cmd+A — select all enabled (toggle if all already selected).
|
|
696
|
+
if ((event.ctrlKey || event.metaKey) && !event.shiftKey && !event.altKey) {
|
|
697
|
+
if (event.key === 'a' || event.key === 'A') {
|
|
698
|
+
event.preventDefault();
|
|
699
|
+
this.#group.selectAll();
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
// Ctrl+Shift+Home / Ctrl+Shift+End — extend selection to first/last.
|
|
704
|
+
if ((event.ctrlKey || event.metaKey) && event.shiftKey && !event.altKey) {
|
|
705
|
+
if (event.key === 'Home') {
|
|
706
|
+
event.preventDefault();
|
|
707
|
+
this.#group.selectFromCurrentToEdge(this.#host.nativeElement, 'first');
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
if (event.key === 'End') {
|
|
711
|
+
event.preventDefault();
|
|
712
|
+
this.#group.selectFromCurrentToEdge(this.#host.nativeElement, 'last');
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
// Shift+Space — contiguous range from anchor to current.
|
|
717
|
+
if (event.shiftKey && event.key === ' ') {
|
|
718
|
+
event.preventDefault();
|
|
719
|
+
this.#group.selectRangeToFocused(this.#host.nativeElement);
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
// Shift+Arrow — move focus AND toggle the destination option.
|
|
723
|
+
if (event.shiftKey && !event.ctrlKey && !event.metaKey && !event.altKey) {
|
|
724
|
+
const action = resolveListNavigation(event, {
|
|
725
|
+
orientation: this.#group.orientation(),
|
|
726
|
+
dir: this.#group.dir(),
|
|
727
|
+
});
|
|
728
|
+
if (action === 'next' || action === 'prev') {
|
|
729
|
+
event.preventDefault();
|
|
730
|
+
this.#group.extendByArrow(this.#host.nativeElement, action);
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
const action = resolveListNavigation(event, {
|
|
736
|
+
orientation: this.#group.orientation(),
|
|
737
|
+
dir: this.#group.dir(),
|
|
738
|
+
pageKeys: true,
|
|
739
|
+
});
|
|
740
|
+
if (action) {
|
|
741
|
+
event.preventDefault();
|
|
742
|
+
this.#group.navigate(this.#host.nativeElement, action);
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
this.#group.handleTypeahead(event);
|
|
746
|
+
}
|
|
747
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForListboxOption, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
748
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: ForListboxOption, isStandalone: true, selector: "[forListboxOption]", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: true, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, posInSet: { classPropertyName: "posInSet", publicName: "posInSet", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "option", "type": "button" }, listeners: { "click": "onClick()", "focus": "onFocus()", "keydown": "onKeyDown($event)", "pointerdown": "onPointerDown($event)" }, properties: { "id": "id()", "attr.aria-selected": "selected() ? \"true\" : \"false\"", "attr.aria-disabled": "effectiveDisabled() ? \"true\" : null", "attr.aria-setsize": "ariaSetSize()", "attr.aria-posinset": "ariaPosInSet()", "attr.tabindex": "tabindex()", "attr.data-state": "selected() ? \"checked\" : \"unchecked\"", "attr.data-highlighted": "highlighted() ? \"\" : null", "attr.data-disabled": "effectiveDisabled() ? \"\" : null" } }, providers: [{ provide: FOR_LISTBOX_OPTION, useExisting: ForListboxOption }], exportAs: ["forListboxOption"], ngImport: i0 });
|
|
749
|
+
}
|
|
750
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForListboxOption, decorators: [{
|
|
751
|
+
type: Directive,
|
|
752
|
+
args: [{
|
|
753
|
+
selector: '[forListboxOption]',
|
|
754
|
+
exportAs: 'forListboxOption',
|
|
755
|
+
providers: [{ provide: FOR_LISTBOX_OPTION, useExisting: ForListboxOption }],
|
|
756
|
+
host: {
|
|
757
|
+
role: 'option',
|
|
758
|
+
type: 'button',
|
|
759
|
+
'[id]': 'id()',
|
|
760
|
+
'[attr.aria-selected]': 'selected() ? "true" : "false"',
|
|
761
|
+
'[attr.aria-disabled]': 'effectiveDisabled() ? "true" : null',
|
|
762
|
+
'[attr.aria-setsize]': 'ariaSetSize()',
|
|
763
|
+
'[attr.aria-posinset]': 'ariaPosInSet()',
|
|
764
|
+
'[attr.tabindex]': 'tabindex()',
|
|
765
|
+
'[attr.data-state]': 'selected() ? "checked" : "unchecked"',
|
|
766
|
+
'[attr.data-highlighted]': 'highlighted() ? "" : null',
|
|
767
|
+
'[attr.data-disabled]': 'effectiveDisabled() ? "" : null',
|
|
768
|
+
'(click)': 'onClick()',
|
|
769
|
+
'(focus)': 'onFocus()',
|
|
770
|
+
'(keydown)': 'onKeyDown($event)',
|
|
771
|
+
'(pointerdown)': 'onPointerDown($event)',
|
|
772
|
+
},
|
|
773
|
+
}]
|
|
774
|
+
}], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: true }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], posInSet: [{ type: i0.Input, args: [{ isSignal: true, alias: "posInSet", required: false }] }] } });
|
|
775
|
+
|
|
776
|
+
/**
|
|
777
|
+
* Optional indicator slot inside a `ForListboxOption`. Apply on the
|
|
778
|
+
* element the consumer wants to show only while this option is selected
|
|
779
|
+
* (typically a check icon to the left of the label). Mirrors `data-state`
|
|
780
|
+
* and hides itself while unselected. Visibility is enforced with an inline
|
|
781
|
+
* `display: none` (which beats any author `display` rule a consumer applies
|
|
782
|
+
* via a class) in addition to the `hidden` attribute that removes it from
|
|
783
|
+
* the a11y tree.
|
|
784
|
+
*/
|
|
785
|
+
class ForListboxOptionIndicator {
|
|
786
|
+
parent;
|
|
787
|
+
constructor() {
|
|
788
|
+
const parent = inject(FOR_LISTBOX_OPTION, { optional: true });
|
|
789
|
+
if (!parent) {
|
|
790
|
+
throw new Error('[forty-cdk/listbox] ForListboxOptionIndicator must be used inside a [forListboxOption] element.');
|
|
791
|
+
}
|
|
792
|
+
this.parent = parent;
|
|
793
|
+
}
|
|
794
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForListboxOptionIndicator, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
795
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.2", type: ForListboxOptionIndicator, isStandalone: true, selector: "[forListboxOptionIndicator]", host: { properties: { "attr.data-state": "parent.selected() ? \"checked\" : \"unchecked\"", "attr.hidden": "parent.selected() ? null : \"\"", "style.display": "parent.selected() ? null : \"none\"" } }, exportAs: ["forListboxOptionIndicator"], ngImport: i0 });
|
|
796
|
+
}
|
|
797
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForListboxOptionIndicator, decorators: [{
|
|
798
|
+
type: Directive,
|
|
799
|
+
args: [{
|
|
800
|
+
selector: '[forListboxOptionIndicator]',
|
|
801
|
+
exportAs: 'forListboxOptionIndicator',
|
|
802
|
+
host: {
|
|
803
|
+
'[attr.data-state]': 'parent.selected() ? "checked" : "unchecked"',
|
|
804
|
+
'[attr.hidden]': 'parent.selected() ? null : ""',
|
|
805
|
+
'[style.display]': 'parent.selected() ? null : "none"',
|
|
806
|
+
},
|
|
807
|
+
}]
|
|
808
|
+
}], ctorParameters: () => [] });
|
|
809
|
+
|
|
810
|
+
/**
|
|
811
|
+
* Optional grouping wrapper inside a `ForListbox`. Renders `role="group"`
|
|
812
|
+
* and references the descendant `[forListboxGroupLabel]` (if any) via
|
|
813
|
+
* `aria-labelledby`. Options inside a group still register with the
|
|
814
|
+
* listbox normally, so keyboard navigation traverses across groups in DOM
|
|
815
|
+
* order without special handling.
|
|
816
|
+
*/
|
|
817
|
+
class ForListboxGroup {
|
|
818
|
+
#labelId = signal(null, /* @ts-ignore */
|
|
819
|
+
...(ngDevMode ? [{ debugName: "#labelId" }] : /* istanbul ignore next */ []));
|
|
820
|
+
/** The id of the registered group label (or `null` if none). */
|
|
821
|
+
labelId = this.#labelId.asReadonly();
|
|
822
|
+
/** Called by `ForListboxGroupLabel` on mount. */
|
|
823
|
+
registerLabel(id) {
|
|
824
|
+
this.#labelId.set(id);
|
|
825
|
+
}
|
|
826
|
+
/** Called by `ForListboxGroupLabel` on destroy. Idempotent. */
|
|
827
|
+
unregisterLabel(id) {
|
|
828
|
+
if (this.#labelId() === id) {
|
|
829
|
+
this.#labelId.set(null);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForListboxGroup, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
833
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.2", type: ForListboxGroup, isStandalone: true, selector: "[forListboxGroup]", host: { attributes: { "role": "group" }, properties: { "attr.aria-labelledby": "labelId() || null" } }, exportAs: ["forListboxGroup"], ngImport: i0 });
|
|
834
|
+
}
|
|
835
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForListboxGroup, decorators: [{
|
|
836
|
+
type: Directive,
|
|
837
|
+
args: [{
|
|
838
|
+
selector: '[forListboxGroup]',
|
|
839
|
+
exportAs: 'forListboxGroup',
|
|
840
|
+
host: {
|
|
841
|
+
role: 'group',
|
|
842
|
+
'[attr.aria-labelledby]': 'labelId() || null',
|
|
843
|
+
},
|
|
844
|
+
}]
|
|
845
|
+
}] });
|
|
846
|
+
|
|
847
|
+
/**
|
|
848
|
+
* Visible label for a `ForListboxGroup`. Generates a stable id and
|
|
849
|
+
* registers with its parent group so the group's `aria-labelledby` points
|
|
850
|
+
* at this element. Apply on whatever heading element fits the design
|
|
851
|
+
* (often `<div>` styled as a small caption — `<h3>`/`<h4>` if the
|
|
852
|
+
* surrounding document has a sensible heading hierarchy).
|
|
853
|
+
*/
|
|
854
|
+
class ForListboxGroupLabel {
|
|
855
|
+
/** Stable host id used by the parent group's `aria-labelledby`. */
|
|
856
|
+
id;
|
|
857
|
+
constructor() {
|
|
858
|
+
const group = inject(ForListboxGroup, { optional: true });
|
|
859
|
+
if (!group) {
|
|
860
|
+
throw new Error('[forty-cdk/listbox] ForListboxGroupLabel must be used inside a [forListboxGroup] element.');
|
|
861
|
+
}
|
|
862
|
+
this.id = registerA11yName(group, 'for-listbox-group-label');
|
|
863
|
+
}
|
|
864
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForListboxGroupLabel, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
865
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.2", type: ForListboxGroupLabel, isStandalone: true, selector: "[forListboxGroupLabel]", host: { properties: { "id": "id()" } }, exportAs: ["forListboxGroupLabel"], ngImport: i0 });
|
|
866
|
+
}
|
|
867
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForListboxGroupLabel, decorators: [{
|
|
868
|
+
type: Directive,
|
|
869
|
+
args: [{
|
|
870
|
+
selector: '[forListboxGroupLabel]',
|
|
871
|
+
exportAs: 'forListboxGroupLabel',
|
|
872
|
+
host: {
|
|
873
|
+
'[id]': 'id()',
|
|
874
|
+
},
|
|
875
|
+
}]
|
|
876
|
+
}], ctorParameters: () => [] });
|
|
877
|
+
|
|
878
|
+
/**
|
|
879
|
+
* Live-region phrasing for `ForListboxReorder`. Framework-free and unit-testable in isolation.
|
|
880
|
+
* Not part of the public API. Mirrors the lift / move / drop / cancel cadence the drag-drop and
|
|
881
|
+
* tree coordinators use so a consumer hears consistent reorder announcements across primitives.
|
|
882
|
+
*/
|
|
883
|
+
/** Announcement when an option is lifted. `index` / `total` are 1-based. */
|
|
884
|
+
function announceReorderLift(label, index, total) {
|
|
885
|
+
return `${label}, lifted. ${index} of ${total}.`;
|
|
886
|
+
}
|
|
887
|
+
/** Announcement when the drop position changes during a drag. `index` / `total` are 1-based. */
|
|
888
|
+
function announceReorderMove(label, index, total) {
|
|
889
|
+
return `${label}, moved to position ${index} of ${total}.`;
|
|
890
|
+
}
|
|
891
|
+
/** Announcement on a committed drop. `index` / `total` are 1-based. */
|
|
892
|
+
function announceReorderDrop(label, index, total) {
|
|
893
|
+
return `${label}, dropped at position ${index} of ${total}.`;
|
|
894
|
+
}
|
|
895
|
+
/** Announcement when a drag is cancelled. */
|
|
896
|
+
function announceReorderCancel(label) {
|
|
897
|
+
return `${label}, movement cancelled.`;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
const POINTER_ARM_THRESHOLD_PX = 5;
|
|
901
|
+
/**
|
|
902
|
+
* Resolves a lifted-drag keydown to a reorder action. `Escape` / `Tab` cancel and
|
|
903
|
+
* `Space` / `Enter` commit (shared with the tree coordinator); arrow keys step the target
|
|
904
|
+
* linearly in DOM order (both axes, so a wrapping chip grid reorders by either pair), and
|
|
905
|
+
* `Home` / `End` jump to the ends. Honors writing direction for the horizontal arrows.
|
|
906
|
+
*/
|
|
907
|
+
function resolveReorderAction(event, dir) {
|
|
908
|
+
const control = resolveLiftedDragControl(event);
|
|
909
|
+
if (control) {
|
|
910
|
+
return control;
|
|
911
|
+
}
|
|
912
|
+
const isRtl = dir === 'rtl';
|
|
913
|
+
switch (event.key) {
|
|
914
|
+
case 'ArrowDown':
|
|
915
|
+
return 'next';
|
|
916
|
+
case 'ArrowUp':
|
|
917
|
+
return 'prev';
|
|
918
|
+
case 'ArrowRight':
|
|
919
|
+
return isRtl ? 'prev' : 'next';
|
|
920
|
+
case 'ArrowLeft':
|
|
921
|
+
return isRtl ? 'next' : 'prev';
|
|
922
|
+
case 'Home':
|
|
923
|
+
return 'first';
|
|
924
|
+
case 'End':
|
|
925
|
+
return 'last';
|
|
926
|
+
default:
|
|
927
|
+
return null;
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
/**
|
|
931
|
+
* Opt-in **pointer + keyboard reordering** for `ForListbox`, composed onto the same element as
|
|
932
|
+
* `[forListbox]`.
|
|
933
|
+
*
|
|
934
|
+
* `[forDraggable]` cannot stack on a `[forListboxOption]` — both would manage the option's
|
|
935
|
+
* roving tabindex and keyboard, colliding on `tabindex`, on Space / Enter activation, and on the
|
|
936
|
+
* container `orientation` input. This coordinator is the listbox-side analogue of
|
|
937
|
+
* `ForTreeNodeDrag` / `ForTableRowReorder`: it lives on the container, **never touches the
|
|
938
|
+
* option's roving tabindex**, intercepts keys in the capture phase, and owns its own 2D drop
|
|
939
|
+
* geometry — so the listbox keeps full selection + typeahead behavior while becoming sortable.
|
|
940
|
+
*
|
|
941
|
+
* Keyboard: focus an option, press `Ctrl+Space` (or `Cmd+Space`) to lift. While lifted, the
|
|
942
|
+
* arrow keys step the target position (linearly in DOM order, so a wrapping chip grid works with
|
|
943
|
+
* either axis), `Home` / `End` jump to the ends, `Space` / `Enter` drop, `Escape` / `Tab` cancel.
|
|
944
|
+
*
|
|
945
|
+
* Pointer: drag an option past a small threshold to reorder; a short press without movement still
|
|
946
|
+
* selects (the post-drag click is suppressed). A floating preview follows the pointer; drop
|
|
947
|
+
* geometry is resolved in 2D, so vertical lists, horizontal lists, and wrapping grids all work
|
|
948
|
+
* without configuring `orientation`.
|
|
949
|
+
*
|
|
950
|
+
* It **never reorders the options itself** (BYO-data): `(optionReorder)` emits the previous / new
|
|
951
|
+
* index on every committed drop; apply `moveItemInArray(items, from, to)` to your own array.
|
|
952
|
+
* Intended for the standard roving-tabindex listbox; a virtualized listbox (`[totalCount]` set)
|
|
953
|
+
* is left untouched, since reordering a windowed subset is ill-defined.
|
|
954
|
+
*
|
|
955
|
+
* @example
|
|
956
|
+
* ```html
|
|
957
|
+
* <ul forListbox forListboxReorder multiple [(value)]="selected" (optionReorder)="reorder($event)">
|
|
958
|
+
* @for (tag of tags(); track tag) {
|
|
959
|
+
* <li><button type="button" forListboxOption [value]="tag">{{ tag }}</button></li>
|
|
960
|
+
* }
|
|
961
|
+
* </ul>
|
|
962
|
+
* ```
|
|
963
|
+
*/
|
|
964
|
+
class ForListboxReorder {
|
|
965
|
+
#ctx = injectListboxContext('ForListboxReorder');
|
|
966
|
+
#host = inject(ElementRef).nativeElement;
|
|
967
|
+
#document = inject(DOCUMENT);
|
|
968
|
+
#isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
|
|
969
|
+
#announcer = inject(LiveAnnouncer);
|
|
970
|
+
#destroyRef = inject(DestroyRef);
|
|
971
|
+
/**
|
|
972
|
+
* Disables reorder interactions while leaving selection / typeahead intact. Named distinctly
|
|
973
|
+
* from the listbox's own `disabled` so the two never share a single `[disabled]` binding on the
|
|
974
|
+
* shared host. The listbox being disabled (its own `disabled` or a surrounding `[forFieldset]`)
|
|
975
|
+
* also disables reorder.
|
|
976
|
+
*/
|
|
977
|
+
reorderDisabled = input(false, { ...(ngDevMode ? { debugName: "reorderDisabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
978
|
+
/** Emitted once per committed reorder gesture with the previous / new option index. */
|
|
979
|
+
optionReorder = output();
|
|
980
|
+
_dragging = signal(false, /* @ts-ignore */
|
|
981
|
+
...(ngDevMode ? [{ debugName: "_dragging" }] : /* istanbul ignore next */ []));
|
|
982
|
+
#mode = 'idle';
|
|
983
|
+
#liftedHost = null;
|
|
984
|
+
#fromIndex = 0;
|
|
985
|
+
#targetIndex = 0;
|
|
986
|
+
#label = '';
|
|
987
|
+
#previewController = null;
|
|
988
|
+
#pointerSession = null;
|
|
989
|
+
constructor() {
|
|
990
|
+
createKeyboardDragMediator({
|
|
991
|
+
host: this.#host,
|
|
992
|
+
isBrowser: this.#isBrowser,
|
|
993
|
+
destroyRef: this.#destroyRef,
|
|
994
|
+
isLifted: () => this.#mode === 'keyboard',
|
|
995
|
+
onIdleKeydown: (event) => this.#onIdleKeydown(event),
|
|
996
|
+
onLiftedKeydown: (event) => this.#onLiftedKeydown(event),
|
|
997
|
+
onFocusOut: (event) => this.#onFocusOut(event),
|
|
998
|
+
});
|
|
999
|
+
if (this.#isBrowser) {
|
|
1000
|
+
this.#pointerSession = createPointerDragSession({
|
|
1001
|
+
host: this.#host,
|
|
1002
|
+
document: this.#document,
|
|
1003
|
+
armThreshold: POINTER_ARM_THRESHOLD_PX,
|
|
1004
|
+
cancelOnEscape: true,
|
|
1005
|
+
capturePointer: true,
|
|
1006
|
+
canStart: (event) => this.#canStartPointer(event),
|
|
1007
|
+
onLift: (event) => this.#onPointerLift(event),
|
|
1008
|
+
onMove: (event) => this.#onPointerMove(event),
|
|
1009
|
+
onCommit: (event) => this.#onPointerCommit(event),
|
|
1010
|
+
onCancel: () => this.#onPointerCancel(),
|
|
1011
|
+
});
|
|
1012
|
+
this.#destroyRef.onDestroy(() => {
|
|
1013
|
+
this.#pointerSession?.destroy();
|
|
1014
|
+
if (this.#mode !== 'idle') {
|
|
1015
|
+
this.#clearSession();
|
|
1016
|
+
}
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
#reorderable() {
|
|
1021
|
+
return (!this.reorderDisabled() &&
|
|
1022
|
+
!this.#ctx.effectiveDisabled() &&
|
|
1023
|
+
this.#ctx.totalCount() === undefined);
|
|
1024
|
+
}
|
|
1025
|
+
#resolveOption(target) {
|
|
1026
|
+
if (!(target instanceof Node)) {
|
|
1027
|
+
return null;
|
|
1028
|
+
}
|
|
1029
|
+
const options = this.#ctx.options();
|
|
1030
|
+
for (let i = 0; i < options.length; i++) {
|
|
1031
|
+
const option = options[i];
|
|
1032
|
+
if (option.host === target || option.host.contains(target)) {
|
|
1033
|
+
return option.disabled() ? null : { host: option.host, index: i };
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
return null;
|
|
1037
|
+
}
|
|
1038
|
+
#canStartPointer(event) {
|
|
1039
|
+
if (!this.#reorderable() || (event.pointerType === 'mouse' && event.button !== 0)) {
|
|
1040
|
+
return false;
|
|
1041
|
+
}
|
|
1042
|
+
const resolved = this.#resolveOption(event.target);
|
|
1043
|
+
if (!resolved) {
|
|
1044
|
+
return false;
|
|
1045
|
+
}
|
|
1046
|
+
this.#liftedHost = resolved.host;
|
|
1047
|
+
this.#fromIndex = resolved.index;
|
|
1048
|
+
return true;
|
|
1049
|
+
}
|
|
1050
|
+
#onPointerLift(event) {
|
|
1051
|
+
if (this.#liftedHost === null) {
|
|
1052
|
+
return false;
|
|
1053
|
+
}
|
|
1054
|
+
this.#lift(this.#liftedHost, this.#fromIndex, 'pointer', {
|
|
1055
|
+
x: event.clientX,
|
|
1056
|
+
y: event.clientY,
|
|
1057
|
+
});
|
|
1058
|
+
return true;
|
|
1059
|
+
}
|
|
1060
|
+
#onPointerMove(event) {
|
|
1061
|
+
if (this.#mode !== 'pointer') {
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
const point = { x: event.clientX, y: event.clientY };
|
|
1065
|
+
this.#previewController?.moveTo(point);
|
|
1066
|
+
this.#setTarget(this.#resolvePointerTarget(point), 'polite');
|
|
1067
|
+
}
|
|
1068
|
+
#onPointerCommit(event) {
|
|
1069
|
+
if (this.#mode !== 'pointer') {
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
1072
|
+
this.#targetIndex = this.#resolvePointerTarget({ x: event.clientX, y: event.clientY });
|
|
1073
|
+
this.#commit();
|
|
1074
|
+
}
|
|
1075
|
+
#onPointerCancel() {
|
|
1076
|
+
if (this.#mode !== 'pointer') {
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
this.#cancel();
|
|
1080
|
+
}
|
|
1081
|
+
#resolvePointerTarget(point) {
|
|
1082
|
+
const lifted = this.#liftedHost;
|
|
1083
|
+
const itemRects = this.#ctx
|
|
1084
|
+
.options()
|
|
1085
|
+
.filter((option) => option.host !== lifted)
|
|
1086
|
+
.map((option) => option.host.getBoundingClientRect());
|
|
1087
|
+
const target = resolveDropTarget(point, [{ rect: this.#host.getBoundingClientRect(), itemRects }], 'mixed', this.#ctx.dir());
|
|
1088
|
+
return target ? target.index : this.#targetIndex;
|
|
1089
|
+
}
|
|
1090
|
+
#onIdleKeydown(event) {
|
|
1091
|
+
if (this.#mode !== 'idle' || !isDragLiftKey(event) || !this.#reorderable()) {
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
const resolved = this.#resolveOption(event.target);
|
|
1095
|
+
if (!resolved) {
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
event.preventDefault();
|
|
1099
|
+
event.stopPropagation();
|
|
1100
|
+
this.#lift(resolved.host, resolved.index, 'keyboard');
|
|
1101
|
+
}
|
|
1102
|
+
#onLiftedKeydown(event) {
|
|
1103
|
+
const action = resolveReorderAction(event, this.#ctx.dir());
|
|
1104
|
+
if (action === null) {
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
event.preventDefault();
|
|
1108
|
+
event.stopPropagation();
|
|
1109
|
+
switch (action) {
|
|
1110
|
+
case 'cancel':
|
|
1111
|
+
this.#cancel();
|
|
1112
|
+
return;
|
|
1113
|
+
case 'commit':
|
|
1114
|
+
this.#commit();
|
|
1115
|
+
return;
|
|
1116
|
+
case 'next':
|
|
1117
|
+
this.#setTarget(this.#targetIndex + 1, 'polite');
|
|
1118
|
+
return;
|
|
1119
|
+
case 'prev':
|
|
1120
|
+
this.#setTarget(this.#targetIndex - 1, 'polite');
|
|
1121
|
+
return;
|
|
1122
|
+
case 'first':
|
|
1123
|
+
this.#setTarget(0, 'polite');
|
|
1124
|
+
return;
|
|
1125
|
+
case 'last':
|
|
1126
|
+
this.#setTarget(this.#ctx.options().length - 1, 'polite');
|
|
1127
|
+
return;
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
#onFocusOut(event) {
|
|
1131
|
+
if (this.#mode !== 'keyboard') {
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
const related = event.relatedTarget;
|
|
1135
|
+
if (related && this.#host.contains(related)) {
|
|
1136
|
+
return;
|
|
1137
|
+
}
|
|
1138
|
+
this.#cancel();
|
|
1139
|
+
}
|
|
1140
|
+
#lift(host, index, mode, point) {
|
|
1141
|
+
this.#mode = mode;
|
|
1142
|
+
this.#liftedHost = host;
|
|
1143
|
+
this.#fromIndex = index;
|
|
1144
|
+
this.#targetIndex = index;
|
|
1145
|
+
this.#label = (host.textContent ?? '').trim();
|
|
1146
|
+
host.setAttribute('data-dragging', '');
|
|
1147
|
+
this._dragging.set(true);
|
|
1148
|
+
if (mode === 'pointer' && point) {
|
|
1149
|
+
this.#previewController = new PreviewController({
|
|
1150
|
+
source: host,
|
|
1151
|
+
point,
|
|
1152
|
+
preview: null,
|
|
1153
|
+
doc: this.#document,
|
|
1154
|
+
boundary: null,
|
|
1155
|
+
lockAxis: () => null,
|
|
1156
|
+
});
|
|
1157
|
+
}
|
|
1158
|
+
this.#announcer.announce(announceReorderLift(this.#label, index + 1, this.#ctx.options().length), 'assertive');
|
|
1159
|
+
}
|
|
1160
|
+
#setTarget(to, politeness) {
|
|
1161
|
+
const total = this.#ctx.options().length;
|
|
1162
|
+
const clamped = Math.max(0, Math.min(total - 1, to));
|
|
1163
|
+
if (clamped === this.#targetIndex) {
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
this.#targetIndex = clamped;
|
|
1167
|
+
this.#announcer.announce(announceReorderMove(this.#label, clamped + 1, total), politeness);
|
|
1168
|
+
}
|
|
1169
|
+
#commit() {
|
|
1170
|
+
if (this.#mode === 'idle' || this.#liftedHost === null) {
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1173
|
+
const total = this.#ctx.options().length;
|
|
1174
|
+
const to = Math.max(0, Math.min(total - 1, this.#targetIndex));
|
|
1175
|
+
const from = this.#fromIndex;
|
|
1176
|
+
const label = this.#label;
|
|
1177
|
+
this.#clearSession();
|
|
1178
|
+
this.optionReorder.emit({ from, to });
|
|
1179
|
+
this.#announcer.announce(announceReorderDrop(label, to + 1, total), 'assertive');
|
|
1180
|
+
}
|
|
1181
|
+
#cancel() {
|
|
1182
|
+
if (this.#mode === 'idle') {
|
|
1183
|
+
return;
|
|
1184
|
+
}
|
|
1185
|
+
const label = this.#label;
|
|
1186
|
+
this.#clearSession();
|
|
1187
|
+
this.#announcer.announce(announceReorderCancel(label), 'assertive');
|
|
1188
|
+
}
|
|
1189
|
+
#clearSession() {
|
|
1190
|
+
this.#liftedHost?.removeAttribute('data-dragging');
|
|
1191
|
+
this.#mode = 'idle';
|
|
1192
|
+
this.#liftedHost = null;
|
|
1193
|
+
this.#fromIndex = 0;
|
|
1194
|
+
this.#targetIndex = 0;
|
|
1195
|
+
this.#label = '';
|
|
1196
|
+
this.#previewController?.destroy();
|
|
1197
|
+
this.#previewController = null;
|
|
1198
|
+
this._dragging.set(false);
|
|
1199
|
+
}
|
|
1200
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForListboxReorder, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1201
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: ForListboxReorder, isStandalone: true, selector: "[forListboxReorder]", inputs: { reorderDisabled: { classPropertyName: "reorderDisabled", publicName: "reorderDisabled", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { optionReorder: "optionReorder" }, host: { properties: { "attr.data-dragging": "_dragging() ? \"\" : null" } }, exportAs: ["forListboxReorder"], ngImport: i0 });
|
|
1202
|
+
}
|
|
1203
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForListboxReorder, decorators: [{
|
|
1204
|
+
type: Directive,
|
|
1205
|
+
args: [{
|
|
1206
|
+
selector: '[forListboxReorder]',
|
|
1207
|
+
exportAs: 'forListboxReorder',
|
|
1208
|
+
host: {
|
|
1209
|
+
'[attr.data-dragging]': '_dragging() ? "" : null',
|
|
1210
|
+
},
|
|
1211
|
+
}]
|
|
1212
|
+
}], ctorParameters: () => [], propDecorators: { reorderDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "reorderDisabled", required: false }] }], optionReorder: [{ type: i0.Output, args: ["optionReorder"] }] } });
|
|
1213
|
+
|
|
1214
|
+
/**
|
|
1215
|
+
* Exact public names of every `ForListbox` input, its models included. Spread it into the
|
|
1216
|
+
* `inputs` array of a `hostDirectives` entry so a wrapper component re-exposes the
|
|
1217
|
+
* primitive's full surface — the Signal Forms members `[formField]` binds among them —
|
|
1218
|
+
* without hand-maintaining the list. Always spread into an inline object literal as shown
|
|
1219
|
+
* below: the literal is what keeps the entry statically analyzable for consumers compiling
|
|
1220
|
+
* against the published package. An anti-drift spec fails when this list no longer matches
|
|
1221
|
+
* the directive's actual API. See `docs/wrapping-form-primitives.md` for both supported
|
|
1222
|
+
* wrapping patterns.
|
|
1223
|
+
*
|
|
1224
|
+
* @example
|
|
1225
|
+
* ```ts
|
|
1226
|
+
* @Component({
|
|
1227
|
+
* selector: 'div[myListbox]',
|
|
1228
|
+
* template: '',
|
|
1229
|
+
* hostDirectives: [
|
|
1230
|
+
* {
|
|
1231
|
+
* directive: ForListbox,
|
|
1232
|
+
* inputs: [...FOR_LISTBOX_HOST_DIRECTIVE_INPUTS],
|
|
1233
|
+
* outputs: [...FOR_LISTBOX_HOST_DIRECTIVE_OUTPUTS],
|
|
1234
|
+
* },
|
|
1235
|
+
* ],
|
|
1236
|
+
* })
|
|
1237
|
+
* export class MyListbox {}
|
|
1238
|
+
* ```
|
|
1239
|
+
*/
|
|
1240
|
+
const FOR_LISTBOX_HOST_DIRECTIVE_INPUTS = [
|
|
1241
|
+
'value',
|
|
1242
|
+
'ariaLabel',
|
|
1243
|
+
'dir',
|
|
1244
|
+
'dirty',
|
|
1245
|
+
'disabled',
|
|
1246
|
+
'errors',
|
|
1247
|
+
'invalid',
|
|
1248
|
+
'isItemEqualToValue',
|
|
1249
|
+
'itemToFormValue',
|
|
1250
|
+
'loop',
|
|
1251
|
+
'multiple',
|
|
1252
|
+
'name',
|
|
1253
|
+
'orientation',
|
|
1254
|
+
'pending',
|
|
1255
|
+
'readonly',
|
|
1256
|
+
'required',
|
|
1257
|
+
'selectionFollowsFocus',
|
|
1258
|
+
'totalCount',
|
|
1259
|
+
'touched',
|
|
1260
|
+
'visibleRange',
|
|
1261
|
+
];
|
|
1262
|
+
/**
|
|
1263
|
+
* Exact public names of every `ForListbox` output, the Signal Forms `touch` output
|
|
1264
|
+
* included. Spread it into the `outputs` array of the same `hostDirectives` entry as
|
|
1265
|
+
* {@link FOR_LISTBOX_HOST_DIRECTIVE_INPUTS}.
|
|
1266
|
+
*/
|
|
1267
|
+
const FOR_LISTBOX_HOST_DIRECTIVE_OUTPUTS = [
|
|
1268
|
+
'scrollToIndex',
|
|
1269
|
+
'touch',
|
|
1270
|
+
'touchedChange',
|
|
1271
|
+
'valueChange',
|
|
1272
|
+
];
|
|
1273
|
+
|
|
1274
|
+
/**
|
|
1275
|
+
* Generated bundle index. Do not edit.
|
|
1276
|
+
*/
|
|
1277
|
+
|
|
1278
|
+
export { FOR_LISTBOX_CONTEXT, FOR_LISTBOX_DEFAULTS, FOR_LISTBOX_HOST_DIRECTIVE_INPUTS, FOR_LISTBOX_HOST_DIRECTIVE_OUTPUTS, FOR_LISTBOX_OPTION, ForListbox, ForListboxGroup, ForListboxGroupLabel, ForListboxOption, ForListboxOptionIndicator, ForListboxReorder, provideForListboxDefaults };
|
|
1279
|
+
//# sourceMappingURL=forty-cdk-listbox.mjs.map
|