ng-primitives 0.43.1 → 0.44.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.
Files changed (29) hide show
  1. package/a11y/active-descendant/active-descendant.d.ts +38 -0
  2. package/a11y/index.d.ts +2 -1
  3. package/combobox/README.md +3 -0
  4. package/combobox/combobox/combobox-state.d.ts +59 -0
  5. package/combobox/combobox/combobox.d.ts +176 -0
  6. package/combobox/combobox-button/combobox-button.d.ts +60 -0
  7. package/combobox/combobox-dropdown/combobox-dropdown.d.ts +15 -0
  8. package/combobox/combobox-input/combobox-input.d.ts +78 -0
  9. package/combobox/combobox-option/combobox-option.d.ts +93 -0
  10. package/combobox/combobox-portal/combobox-portal.d.ts +48 -0
  11. package/combobox/index.d.ts +7 -0
  12. package/dialog/config/dialog-config.d.ts +2 -0
  13. package/dialog/dialog/dialog-ref.d.ts +2 -0
  14. package/dialog/dialog-trigger/dialog-trigger.d.ts +8 -1
  15. package/fesm2022/ng-primitives-a11y.mjs +100 -1
  16. package/fesm2022/ng-primitives-a11y.mjs.map +1 -1
  17. package/fesm2022/ng-primitives-combobox.mjs +807 -0
  18. package/fesm2022/ng-primitives-combobox.mjs.map +1 -0
  19. package/fesm2022/ng-primitives-dialog.mjs +19 -3
  20. package/fesm2022/ng-primitives-dialog.mjs.map +1 -1
  21. package/fesm2022/ng-primitives-resize.mjs +21 -2
  22. package/fesm2022/ng-primitives-resize.mjs.map +1 -1
  23. package/package.json +17 -13
  24. package/resize/index.d.ts +1 -1
  25. package/resize/utils/resize.d.ts +5 -0
  26. package/schematics/ng-generate/schema.d.ts +2 -1
  27. package/schematics/ng-generate/schema.json +1 -0
  28. package/schematics/ng-generate/templates/combobox/combobox.__fileSuffix@dasherize__.ts.template +233 -0
  29. package/schematics/ng-generate/templates/listbox/listbox.__fileSuffix@dasherize__.ts.template +2 -0
@@ -0,0 +1,807 @@
1
+ import * as i0 from '@angular/core';
2
+ import { input, Directive, computed, HostListener, booleanAttribute, inject, TemplateRef, ViewContainerRef, Injector, signal, effect, output, afterNextRender } from '@angular/core';
3
+ import { injectElementRef, setupInteractions, explicitEffect } from 'ng-primitives/internal';
4
+ import { uniqueId } from 'ng-primitives/utils';
5
+ import { createStateToken, createStateProvider, createStateInjector, createState } from 'ng-primitives/state';
6
+ import { DOCUMENT } from '@angular/common';
7
+ import { flip, autoUpdate, computePosition } from '@floating-ui/dom';
8
+ import { createPortal } from 'ng-primitives/portal';
9
+ import { observeResize } from 'ng-primitives/resize';
10
+ import { activeDescendantManager } from 'ng-primitives/a11y';
11
+
12
+ /**
13
+ * The state token for the Combobox primitive.
14
+ */
15
+ const NgpComboboxStateToken = createStateToken('Combobox');
16
+ /**
17
+ * Provides the Combobox state.
18
+ */
19
+ const provideComboboxState = createStateProvider(NgpComboboxStateToken);
20
+ /**
21
+ * Injects the Combobox state.
22
+ */
23
+ const injectComboboxState = createStateInjector(NgpComboboxStateToken);
24
+ /**
25
+ * The Combobox state registration function.
26
+ */
27
+ const comboboxState = createState(NgpComboboxStateToken);
28
+
29
+ class NgpComboboxDropdown {
30
+ constructor() {
31
+ /** Access the combobox state. */
32
+ this.state = injectComboboxState();
33
+ /**
34
+ * Access the element reference.
35
+ * @internal
36
+ */
37
+ this.elementRef = injectElementRef();
38
+ /** The id of the dropdown. */
39
+ this.id = input(uniqueId('ngp-combobox-dropdown'));
40
+ this.state().registerDropdown(this);
41
+ }
42
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.11", ngImport: i0, type: NgpComboboxDropdown, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
43
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "19.2.11", type: NgpComboboxDropdown, isStandalone: true, selector: "[ngpComboboxDropdown]", inputs: { id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "listbox" }, properties: { "id": "id()" } }, exportAs: ["ngpComboboxDropdown"], ngImport: i0 }); }
44
+ }
45
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.11", ngImport: i0, type: NgpComboboxDropdown, decorators: [{
46
+ type: Directive,
47
+ args: [{
48
+ selector: '[ngpComboboxDropdown]',
49
+ exportAs: 'ngpComboboxDropdown',
50
+ host: {
51
+ role: 'listbox',
52
+ '[id]': 'id()',
53
+ },
54
+ }]
55
+ }], ctorParameters: () => [] });
56
+
57
+ class NgpComboboxInput {
58
+ constructor() {
59
+ /** Access the combobox state. */
60
+ this.state = injectComboboxState();
61
+ /**
62
+ * Access the element reference.
63
+ * @internal
64
+ */
65
+ this.elementRef = injectElementRef();
66
+ /** The id of the input. */
67
+ this.id = input(uniqueId('ngp-combobox-input'));
68
+ /**
69
+ * Extract the string representation of the value.
70
+ */
71
+ this.displayWith = input((value) => {
72
+ if (typeof value === 'string') {
73
+ return value;
74
+ }
75
+ throw new Error('You must provide a displayWith function for non-string values');
76
+ });
77
+ /** The id of the dropdown. */
78
+ this.dropdownId = computed(() => this.state().dropdown()?.id());
79
+ /** The id of the active descendant. */
80
+ this.activeDescendant = computed(() => this.state().activeDescendantManager.activeDescendant());
81
+ /** Determine if the pointer was used to focus the input. */
82
+ this.pointerFocused = false;
83
+ setupInteractions({
84
+ focus: true,
85
+ hover: true,
86
+ press: true,
87
+ disabled: this.state().disabled,
88
+ });
89
+ this.state().registerInput(this);
90
+ }
91
+ /** Handle keydown events for accessibility. */
92
+ handleKeydown(event) {
93
+ switch (event.key) {
94
+ case 'ArrowDown':
95
+ if (this.state().open()) {
96
+ this.state().activateNextOption();
97
+ }
98
+ else {
99
+ this.state().openDropdown();
100
+ }
101
+ event.preventDefault();
102
+ break;
103
+ case 'ArrowUp':
104
+ if (this.state().open()) {
105
+ this.state().activatePreviousOption();
106
+ }
107
+ else {
108
+ this.state().openDropdown();
109
+ this.state().activeDescendantManager.last();
110
+ }
111
+ event.preventDefault();
112
+ break;
113
+ case 'Home':
114
+ if (this.state().open()) {
115
+ this.state().activeDescendantManager.first();
116
+ }
117
+ event.preventDefault();
118
+ break;
119
+ case 'End':
120
+ if (this.state().open()) {
121
+ this.state().activeDescendantManager.last();
122
+ }
123
+ event.preventDefault();
124
+ break;
125
+ case 'Enter':
126
+ if (this.state().open()) {
127
+ this.state().selectOption(this.state().activeDescendantManager.activeItem());
128
+ }
129
+ event.preventDefault();
130
+ break;
131
+ case 'Escape':
132
+ this.state().closeDropdown();
133
+ event.preventDefault();
134
+ break;
135
+ default:
136
+ // Ignore keys with length > 1 (e.g., 'Shift', 'ArrowLeft', 'Enter', etc.)
137
+ // Filter out control/meta key combos (e.g., Ctrl+C)
138
+ if (event.key.length > 1 || event.ctrlKey || event.metaKey || event.altKey) {
139
+ return;
140
+ }
141
+ // if this was a character key, we want to open the dropdown
142
+ this.state().openDropdown();
143
+ }
144
+ }
145
+ closeDropdown(event) {
146
+ const relatedTarget = event.relatedTarget;
147
+ // if the blur was caused by focus moving to the dropdown, don't close
148
+ if (relatedTarget &&
149
+ this.state().dropdown()?.elementRef.nativeElement.contains(relatedTarget)) {
150
+ return;
151
+ }
152
+ // if the blur was caused by focus moving to the button, don't close
153
+ if (relatedTarget && this.state().button()?.elementRef.nativeElement.contains(relatedTarget)) {
154
+ return;
155
+ }
156
+ this.state().closeDropdown();
157
+ event.preventDefault();
158
+ }
159
+ /**
160
+ * Focus the input field
161
+ * @internal
162
+ */
163
+ focus() {
164
+ this.elementRef.nativeElement.focus();
165
+ }
166
+ highlightText() {
167
+ if (this.pointerFocused) {
168
+ this.pointerFocused = false;
169
+ return;
170
+ }
171
+ // highlight the text in the input
172
+ this.elementRef.nativeElement.setSelectionRange(0, this.elementRef.nativeElement.value.length);
173
+ }
174
+ handlePointerDown() {
175
+ this.pointerFocused = true;
176
+ }
177
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.11", ngImport: i0, type: NgpComboboxInput, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
178
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "19.2.11", type: NgpComboboxInput, isStandalone: true, selector: "input[ngpComboboxInput]", inputs: { id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, displayWith: { classPropertyName: "displayWith", publicName: "displayWith", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "combobox", "type": "text", "autocomplete": "off", "autocorrect": "off", "spellcheck": "false", "aria-haspopup": "listbox", "aria-autocomplete": "list" }, listeners: { "keydown": "handleKeydown($event)", "blur": "closeDropdown($event)", "focus": "highlightText($event)", "pointerdown": "handlePointerDown($event)" }, properties: { "id": "id()", "attr.aria-controls": "state().open() ? dropdownId() : undefined", "attr.aria-expanded": "state().open()", "attr.data-open": "state().open() ? \"\" : undefined", "attr.data-disabled": "state().disabled() ? \"\" : undefined", "attr.data-multiple": "state().multiple() ? \"\" : undefined", "attr.aria-activedescendant": "activeDescendant()", "disabled": "state().disabled()" } }, exportAs: ["ngpComboboxInput"], ngImport: i0 }); }
179
+ }
180
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.11", ngImport: i0, type: NgpComboboxInput, decorators: [{
181
+ type: Directive,
182
+ args: [{
183
+ selector: 'input[ngpComboboxInput]',
184
+ exportAs: 'ngpComboboxInput',
185
+ host: {
186
+ role: 'combobox',
187
+ type: 'text',
188
+ autocomplete: 'off',
189
+ autocorrect: 'off',
190
+ spellcheck: 'false',
191
+ 'aria-haspopup': 'listbox',
192
+ 'aria-autocomplete': 'list',
193
+ '[id]': 'id()',
194
+ '[attr.aria-controls]': 'state().open() ? dropdownId() : undefined',
195
+ '[attr.aria-expanded]': 'state().open()',
196
+ '[attr.data-open]': 'state().open() ? "" : undefined',
197
+ '[attr.data-disabled]': 'state().disabled() ? "" : undefined',
198
+ '[attr.data-multiple]': 'state().multiple() ? "" : undefined',
199
+ '[attr.aria-activedescendant]': 'activeDescendant()',
200
+ '[disabled]': 'state().disabled()',
201
+ },
202
+ }]
203
+ }], ctorParameters: () => [], propDecorators: { handleKeydown: [{
204
+ type: HostListener,
205
+ args: ['keydown', ['$event']]
206
+ }], closeDropdown: [{
207
+ type: HostListener,
208
+ args: ['blur', ['$event']]
209
+ }], highlightText: [{
210
+ type: HostListener,
211
+ args: ['focus', ['$event']]
212
+ }], handlePointerDown: [{
213
+ type: HostListener,
214
+ args: ['pointerdown', ['$event']]
215
+ }] } });
216
+
217
+ class NgpComboboxOption {
218
+ constructor() {
219
+ /** Access the combobox state. */
220
+ this.state = injectComboboxState();
221
+ /**
222
+ * The element reference of the option.
223
+ * @internal
224
+ */
225
+ this.elementRef = injectElementRef();
226
+ /** The id of the option. */
227
+ this.id = input(uniqueId('ngp-combobox-option'));
228
+ /** The value of the option. */
229
+ this.value = input(undefined, {
230
+ alias: 'ngpComboboxOptionValue',
231
+ });
232
+ /** The disabled state of the option. */
233
+ this.disabled = input(false, {
234
+ alias: 'ngpComboboxOptionDisabled',
235
+ transform: booleanAttribute,
236
+ });
237
+ /**
238
+ * Whether this option is the active descendant.
239
+ * @internal
240
+ */
241
+ this.active = computed(() => this.state().activeDescendantManager.activeDescendant() === this.id());
242
+ /** Whether this option is selected. */
243
+ this.selected = computed(() => {
244
+ const value = this.value();
245
+ if (!value) {
246
+ return false;
247
+ }
248
+ if (this.state().multiple()) {
249
+ return (Array.isArray(value) && value.some(v => this.state().compareWith()(v, this.state().value())));
250
+ }
251
+ return this.state().compareWith()(value, this.state().value());
252
+ });
253
+ this.state().registerOption(this);
254
+ setupInteractions({
255
+ hover: true,
256
+ press: true,
257
+ disabled: this.disabled,
258
+ });
259
+ }
260
+ ngOnInit() {
261
+ if (this.value() === undefined) {
262
+ throw new Error('ngpComboboxOption: The value input is required. Please provide a value for the option.');
263
+ }
264
+ }
265
+ ngOnDestroy() {
266
+ this.state().unregisterOption(this);
267
+ }
268
+ /**
269
+ * Select the option.
270
+ * @internal
271
+ */
272
+ select() {
273
+ if (this.disabled()) {
274
+ return;
275
+ }
276
+ this.state().toggleOption(this);
277
+ }
278
+ /**
279
+ * Scroll the option into view.
280
+ * @internal
281
+ */
282
+ scrollIntoView() {
283
+ this.elementRef.nativeElement.scrollIntoView({ block: 'nearest' });
284
+ }
285
+ /**
286
+ * Whenever the pointer enters the option, activate it.
287
+ * @internal
288
+ */
289
+ onPointerEnter() {
290
+ this.state().activeDescendantManager.activate(this);
291
+ }
292
+ /**
293
+ * Whenever the pointer leaves the option, deactivate it.
294
+ * @internal
295
+ */
296
+ onPointerLeave() {
297
+ this.state().activeDescendantManager.activate(undefined);
298
+ }
299
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.11", ngImport: i0, type: NgpComboboxOption, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
300
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "19.2.11", type: NgpComboboxOption, isStandalone: true, selector: "[ngpComboboxOption]", inputs: { id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "ngpComboboxOptionValue", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "ngpComboboxOptionDisabled", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "option" }, listeners: { "click": "select()", "pointerenter": "onPointerEnter()", "pointerleave": "onPointerLeave()" }, properties: { "id": "id()", "attr.tabindex": "-1", "attr.aria-selected": "selected() ? \"true\" : undefined", "attr.data-selected": "selected() ? \"\" : undefined", "attr.data-active": "active() ? \"\" : undefined", "attr.data-disabled": "disabled() ? \"\" : undefined" } }, exportAs: ["ngpComboboxOption"], ngImport: i0 }); }
301
+ }
302
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.11", ngImport: i0, type: NgpComboboxOption, decorators: [{
303
+ type: Directive,
304
+ args: [{
305
+ selector: '[ngpComboboxOption]',
306
+ exportAs: 'ngpComboboxOption',
307
+ host: {
308
+ role: 'option',
309
+ '[id]': 'id()',
310
+ '[attr.tabindex]': '-1',
311
+ '[attr.aria-selected]': 'selected() ? "true" : undefined',
312
+ '[attr.data-selected]': 'selected() ? "" : undefined',
313
+ '[attr.data-active]': 'active() ? "" : undefined',
314
+ '[attr.data-disabled]': 'disabled() ? "" : undefined',
315
+ '(click)': 'select()',
316
+ },
317
+ }]
318
+ }], ctorParameters: () => [], propDecorators: { onPointerEnter: [{
319
+ type: HostListener,
320
+ args: ['pointerenter']
321
+ }], onPointerLeave: [{
322
+ type: HostListener,
323
+ args: ['pointerleave']
324
+ }] } });
325
+
326
+ class NgpComboboxPortal {
327
+ constructor() {
328
+ /** Access the combobox state. */
329
+ this.state = injectComboboxState();
330
+ /** Access the template reference. */
331
+ this.templateRef = inject(TemplateRef);
332
+ /** Access the view container reference. */
333
+ this.viewContainerRef = inject(ViewContainerRef);
334
+ /** Access the injector. */
335
+ this.injector = inject(Injector);
336
+ /** Access the document. */
337
+ this.document = inject(DOCUMENT);
338
+ /**
339
+ * Store the embedded view reference.
340
+ * @internal
341
+ */
342
+ this.viewRef = signal(null);
343
+ /** Store the dispose function. */
344
+ this.dispose = null;
345
+ /** The position of the dropdown. */
346
+ this.position = signal({ x: 0, y: 0 });
347
+ /** The dimensions of the combobox. */
348
+ this.comboboxDimensions = observeResize(() => this.state().elementRef.nativeElement);
349
+ /** The dimensions of the combobox. */
350
+ this.inputDimensions = observeResize(() => this.state().input()?.elementRef.nativeElement);
351
+ /** Store the combobox button dimensions. */
352
+ this.buttonDimensions = observeResize(() => this.state().button()?.elementRef.nativeElement);
353
+ this.state().registerPortal(this);
354
+ effect(() => {
355
+ const dropdownElement = this.viewRef()?.getElements()[0];
356
+ if (!dropdownElement) {
357
+ return;
358
+ }
359
+ const position = this.position();
360
+ const comboboxWidth = this.comboboxDimensions().width;
361
+ const inputWidth = this.inputDimensions().width;
362
+ const buttonWidth = this.buttonDimensions().width;
363
+ if (!dropdownElement) {
364
+ return;
365
+ }
366
+ const styles = {
367
+ position: 'absolute',
368
+ left: `${position.x}px`,
369
+ top: `${position.y}px`,
370
+ '--ngp-combobox-width': `${comboboxWidth}px`,
371
+ '--ngp-combobox-input-width': `${inputWidth}px`,
372
+ '--ngp-combobox-button-width': `${buttonWidth}px`,
373
+ };
374
+ for (const [key, value] of Object.entries(styles)) {
375
+ dropdownElement.style.setProperty(key, value);
376
+ }
377
+ });
378
+ }
379
+ /** Cleanup the portal. */
380
+ ngOnDestroy() {
381
+ this.detach();
382
+ this.dispose?.();
383
+ }
384
+ /**
385
+ * Attach the portal.
386
+ * @internal
387
+ */
388
+ attach() {
389
+ const viewRef = createPortal(this.templateRef, this.viewContainerRef, this.injector);
390
+ viewRef.attach(this.document.body);
391
+ viewRef.detectChanges();
392
+ this.viewRef.set(viewRef);
393
+ const dropdownElement = this.viewRef()?.getElements()[0];
394
+ if (!dropdownElement) {
395
+ throw new Error('Dropdown element not found');
396
+ }
397
+ let placement;
398
+ const middleware = [];
399
+ switch (this.state().dropdownPosition()) {
400
+ case 'top':
401
+ placement = 'top-start';
402
+ break;
403
+ case 'bottom':
404
+ placement = 'bottom-start';
405
+ break;
406
+ case 'auto':
407
+ placement = 'bottom-start';
408
+ middleware.push(flip({ fallbackPlacements: ['top-start'] }));
409
+ break;
410
+ }
411
+ this.dispose = autoUpdate(this.state().elementRef.nativeElement, dropdownElement, async () => {
412
+ const position = await computePosition(this.state().elementRef.nativeElement, dropdownElement, { placement, middleware, strategy: 'absolute' });
413
+ this.position.set({ x: position.x, y: position.y });
414
+ });
415
+ }
416
+ /**
417
+ * Detach the portal.
418
+ * @internal
419
+ */
420
+ async detach() {
421
+ await this.viewRef()?.detach();
422
+ this.viewRef.set(null);
423
+ this.dispose?.();
424
+ this.dispose = null;
425
+ }
426
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.11", ngImport: i0, type: NgpComboboxPortal, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
427
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.11", type: NgpComboboxPortal, isStandalone: true, selector: "[ngpComboboxPortal]", exportAs: ["ngpComboboxPortal"], ngImport: i0 }); }
428
+ }
429
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.11", ngImport: i0, type: NgpComboboxPortal, decorators: [{
430
+ type: Directive,
431
+ args: [{
432
+ selector: '[ngpComboboxPortal]',
433
+ exportAs: 'ngpComboboxPortal',
434
+ }]
435
+ }], ctorParameters: () => [] });
436
+
437
+ class NgpCombobox {
438
+ constructor() {
439
+ /** @internal Access the combobox element. */
440
+ this.elementRef = injectElementRef();
441
+ /** Access the injector. */
442
+ this.injector = inject(Injector);
443
+ /** The value of the combobox. */
444
+ this.value = input(undefined, {
445
+ alias: 'ngpComboboxValue',
446
+ });
447
+ /** Event emitted when the value changes. */
448
+ this.valueChange = output({
449
+ alias: 'ngpComboboxValueChange',
450
+ });
451
+ /** Whether the combobox is multiple selection. */
452
+ this.multiple = input(false, {
453
+ alias: 'ngpComboboxMultiple',
454
+ transform: booleanAttribute,
455
+ });
456
+ /** Whether the combobox is disabled. */
457
+ this.disabled = input(false, {
458
+ alias: 'ngpComboboxDisabled',
459
+ transform: booleanAttribute,
460
+ });
461
+ /** Emit when the dropdown open state changes. */
462
+ this.openChange = output({
463
+ alias: 'ngpComboboxOpenChange',
464
+ });
465
+ /** The comparator function used to compare options. */
466
+ this.compareWith = input(Object.is, {
467
+ alias: 'ngpComboboxCompareWith',
468
+ });
469
+ /** The position of the dropdown. */
470
+ this.dropdownPosition = input('bottom', {
471
+ alias: 'ngpComboboxDropdownPosition',
472
+ });
473
+ /**
474
+ * Store the combobox input
475
+ * @internal
476
+ */
477
+ this.input = signal(undefined);
478
+ /**
479
+ * Store the combobox button.
480
+ * @internal
481
+ */
482
+ this.button = signal(undefined);
483
+ /**
484
+ * Store the combobox portal.
485
+ * @internal
486
+ */
487
+ this.portal = signal(undefined);
488
+ /**
489
+ * Store the combobox dropdown.
490
+ * @internal
491
+ */
492
+ this.dropdown = signal(undefined);
493
+ /**
494
+ * Store the combobox options.
495
+ * @internal
496
+ */
497
+ this.options = signal([]);
498
+ /**
499
+ * The open state of the combobox.
500
+ * @internal
501
+ */
502
+ this.open = computed(() => this.portal()?.viewRef() !== null);
503
+ /**
504
+ * The active key descendant manager.
505
+ * @internal
506
+ */
507
+ this.activeDescendantManager = activeDescendantManager({
508
+ // we must wrap the signal in a computed to ensure it is not used before it is defined
509
+ disabled: computed(() => this.state.disabled()),
510
+ items: this.options,
511
+ });
512
+ /** The state of the combobox. */
513
+ this.state = comboboxState(this);
514
+ // any time the active descendant changes, ensure we scroll it into view
515
+ explicitEffect([this.activeDescendantManager.activeItem], ([option]) =>
516
+ // perform after next render to ensure the DOM is updated
517
+ // e.g. the dropdown is open before the option is scrolled into view
518
+ afterNextRender({ write: () => option?.scrollIntoView?.() }, { injector: this.injector }));
519
+ }
520
+ /**
521
+ * Open the dropdown.
522
+ * @internal
523
+ */
524
+ openDropdown() {
525
+ if (this.state.disabled() || this.open()) {
526
+ return;
527
+ }
528
+ this.portal()?.attach();
529
+ // if there is a selected option(s), set the active descendant to the first selected option
530
+ const selectedOption = this.options().find(option => this.isOptionSelected(option));
531
+ // if there is no selected option, set the active descendant to the first option
532
+ const targetOption = selectedOption ?? this.options()[0];
533
+ // if there is no target option, do nothing
534
+ if (!targetOption) {
535
+ return;
536
+ }
537
+ // activate the selected option or the first option
538
+ this.activeDescendantManager.activate(targetOption);
539
+ }
540
+ /**
541
+ * Close the dropdown.
542
+ * @internal
543
+ */
544
+ closeDropdown() {
545
+ if (!this.open()) {
546
+ return;
547
+ }
548
+ this.openChange.emit(false);
549
+ this.portal()?.detach();
550
+ // clear the active descendant
551
+ this.activeDescendantManager.reset();
552
+ }
553
+ /**
554
+ * Toggle the dropdown.
555
+ * @internal
556
+ */
557
+ toggleDropdown() {
558
+ if (this.open()) {
559
+ this.closeDropdown();
560
+ }
561
+ else {
562
+ this.openDropdown();
563
+ }
564
+ }
565
+ /**
566
+ * Select an option.
567
+ * @param option The option to select.
568
+ * @internal
569
+ */
570
+ selectOption(option) {
571
+ if (this.state.disabled() || this.isOptionSelected(option)) {
572
+ return;
573
+ }
574
+ if (this.state.multiple()) {
575
+ const value = [...this.state.value(), option.value()];
576
+ // add the option to the value
577
+ this.state.value.set(value);
578
+ this.valueChange.emit(value);
579
+ }
580
+ else {
581
+ this.state.value.set(option.value());
582
+ this.valueChange.emit(option.value());
583
+ // close the dropdown on single selection
584
+ this.closeDropdown();
585
+ }
586
+ }
587
+ /**
588
+ * Deselect an option.
589
+ * @param option The option to deselect.
590
+ * @internal
591
+ */
592
+ deselectOption(option) {
593
+ // if the combobox is disabled or the option is not selected, do nothing
594
+ // if the combobox is single selection, we don't allow deselecting
595
+ if (this.state.disabled() || !this.isOptionSelected(option) || !this.state.multiple()) {
596
+ return;
597
+ }
598
+ const values = this.state.value() ?? [];
599
+ const newValue = values.filter(v => !this.state.compareWith()(v, option.value()));
600
+ // remove the option from the value
601
+ this.state.value.set(newValue);
602
+ this.valueChange.emit(newValue);
603
+ }
604
+ /**
605
+ * Toggle the selection of an option.
606
+ * @param option The option to toggle.
607
+ * @internal
608
+ */
609
+ toggleOption(option) {
610
+ if (this.state.disabled()) {
611
+ return;
612
+ }
613
+ if (this.isOptionSelected(option)) {
614
+ this.deselectOption(option);
615
+ }
616
+ else {
617
+ this.selectOption(option);
618
+ }
619
+ }
620
+ /**
621
+ * Determine if an option is selected.
622
+ * @param option The option to check.
623
+ * @internal
624
+ */
625
+ isOptionSelected(option) {
626
+ if (this.state.disabled()) {
627
+ return false;
628
+ }
629
+ const value = this.state.value();
630
+ if (!value) {
631
+ return false;
632
+ }
633
+ if (this.state.multiple()) {
634
+ return value && value.some(v => this.state.compareWith()(option.value(), v));
635
+ }
636
+ return this.state.compareWith()(option.value(), value);
637
+ }
638
+ /**
639
+ * Activate the next option in the list if there is one.
640
+ * If there is no option currently active, activate the selected option or the first option.
641
+ * @internal
642
+ */
643
+ activateNextOption() {
644
+ if (this.state.disabled()) {
645
+ return;
646
+ }
647
+ const options = this.options();
648
+ // if there are no options, do nothing
649
+ if (options.length === 0) {
650
+ return;
651
+ }
652
+ // if there is no active option, activate the first option
653
+ if (!this.activeDescendantManager.activeItem()) {
654
+ const selectedOption = options.find(option => this.isOptionSelected(option));
655
+ // if there is a selected option(s), set the active descendant to the first selected option
656
+ const targetOption = selectedOption ?? options[0];
657
+ this.activeDescendantManager.activate(targetOption);
658
+ return;
659
+ }
660
+ // otherwise activate the next option
661
+ this.activeDescendantManager.next();
662
+ }
663
+ /**
664
+ * Activate the previous option in the list if there is one.
665
+ * @internal
666
+ */
667
+ activatePreviousOption() {
668
+ if (this.state.disabled()) {
669
+ return;
670
+ }
671
+ const options = this.options();
672
+ // if there are no options, do nothing
673
+ if (options.length === 0) {
674
+ return;
675
+ }
676
+ // if there is no active option, activate the last option
677
+ if (!this.activeDescendantManager.activeItem()) {
678
+ const selectedOption = options.find(option => this.isOptionSelected(option));
679
+ // if there is a selected option(s), set the active descendant to the first selected option
680
+ const targetOption = selectedOption ?? options[options.length - 1];
681
+ this.activeDescendantManager.activate(targetOption);
682
+ return;
683
+ }
684
+ // otherwise activate the previous option
685
+ this.activeDescendantManager.previous();
686
+ }
687
+ /**
688
+ * Register the dropdown portal with the combobox.
689
+ * @param portal The dropdown portal.
690
+ * @internal
691
+ */
692
+ registerPortal(portal) {
693
+ this.portal.set(portal);
694
+ }
695
+ /**
696
+ * Register the combobox input with the combobox.
697
+ * @param input The combobox input.
698
+ * @internal
699
+ */
700
+ registerInput(input) {
701
+ this.input.set(input);
702
+ }
703
+ /**
704
+ * Register the combobox button with the combobox.
705
+ * @param button The combobox button.
706
+ * @internal
707
+ */
708
+ registerButton(button) {
709
+ this.button.set(button);
710
+ }
711
+ /**
712
+ * Register the dropdown with the combobox.
713
+ * @param dropdown The dropdown to register.
714
+ * @internal
715
+ */
716
+ registerDropdown(dropdown) {
717
+ this.dropdown.set(dropdown);
718
+ }
719
+ /**
720
+ * Register an option with the combobox.
721
+ * @param option The option to register.
722
+ * @internal
723
+ */
724
+ registerOption(option) {
725
+ this.options.update(options => [...options, option]);
726
+ }
727
+ /**
728
+ * Unregister an option from the combobox.
729
+ * @param option The option to unregister.
730
+ * @internal
731
+ */
732
+ unregisterOption(option) {
733
+ this.options.update(options => options.filter(o => o !== option));
734
+ }
735
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.11", ngImport: i0, type: NgpCombobox, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
736
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "19.2.11", type: NgpCombobox, isStandalone: true, selector: "[ngpCombobox]", inputs: { value: { classPropertyName: "value", publicName: "ngpComboboxValue", isSignal: true, isRequired: false, transformFunction: null }, multiple: { classPropertyName: "multiple", publicName: "ngpComboboxMultiple", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "ngpComboboxDisabled", isSignal: true, isRequired: false, transformFunction: null }, compareWith: { classPropertyName: "compareWith", publicName: "ngpComboboxCompareWith", isSignal: true, isRequired: false, transformFunction: null }, dropdownPosition: { classPropertyName: "dropdownPosition", publicName: "ngpComboboxDropdownPosition", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { valueChange: "ngpComboboxValueChange", openChange: "ngpComboboxOpenChange" }, host: { properties: { "attr.data-open": "state.open() ? \"\" : undefined", "attr.data-disabled": "state.disabled() ? \"\" : undefined", "attr.data-multiple": "state.multiple() ? \"\" : undefined" } }, providers: [provideComboboxState()], exportAs: ["ngpCombobox"], ngImport: i0 }); }
737
+ }
738
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.11", ngImport: i0, type: NgpCombobox, decorators: [{
739
+ type: Directive,
740
+ args: [{
741
+ selector: '[ngpCombobox]',
742
+ exportAs: 'ngpCombobox',
743
+ providers: [provideComboboxState()],
744
+ host: {
745
+ '[attr.data-open]': 'state.open() ? "" : undefined',
746
+ '[attr.data-disabled]': 'state.disabled() ? "" : undefined',
747
+ '[attr.data-multiple]': 'state.multiple() ? "" : undefined',
748
+ },
749
+ }]
750
+ }], ctorParameters: () => [] });
751
+
752
+ class NgpComboboxButton {
753
+ constructor() {
754
+ /** Access the combobox state. */
755
+ this.state = injectComboboxState();
756
+ /**
757
+ * Access the element reference.
758
+ * @internal
759
+ */
760
+ this.elementRef = injectElementRef();
761
+ /** The id of the button. */
762
+ this.id = input(uniqueId('ngp-combobox-button'));
763
+ /** The id of the dropdown. */
764
+ this.dropdownId = computed(() => this.state().dropdown()?.id());
765
+ setupInteractions({
766
+ hover: true,
767
+ press: true,
768
+ disabled: this.state().disabled,
769
+ });
770
+ this.state().registerButton(this);
771
+ }
772
+ toggleDropdown() {
773
+ this.state().toggleDropdown();
774
+ this.state().input()?.focus();
775
+ }
776
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.11", ngImport: i0, type: NgpComboboxButton, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
777
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "19.2.11", type: NgpComboboxButton, isStandalone: true, selector: "[ngpComboboxButton]", inputs: { id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "type": "button", "tabindex": "-1", "aria-haspopup": "listbox" }, listeners: { "click": "toggleDropdown()" }, properties: { "id": "id()", "attr.aria-controls": "dropdownId()", "attr.aria-expanded": "state().open()", "attr.data-open": "state().open() ? \"\" : undefined", "attr.data-disabled": "state().disabled() ? \"\" : undefined", "attr.data-multiple": "state().multiple() ? \"\" : undefined", "disabled": "state().disabled()" } }, exportAs: ["ngpComboboxButton"], ngImport: i0 }); }
778
+ }
779
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.11", ngImport: i0, type: NgpComboboxButton, decorators: [{
780
+ type: Directive,
781
+ args: [{
782
+ selector: '[ngpComboboxButton]',
783
+ exportAs: 'ngpComboboxButton',
784
+ host: {
785
+ type: 'button',
786
+ tabindex: '-1',
787
+ 'aria-haspopup': 'listbox',
788
+ '[id]': 'id()',
789
+ '[attr.aria-controls]': 'dropdownId()',
790
+ '[attr.aria-expanded]': 'state().open()',
791
+ '[attr.data-open]': 'state().open() ? "" : undefined',
792
+ '[attr.data-disabled]': 'state().disabled() ? "" : undefined',
793
+ '[attr.data-multiple]': 'state().multiple() ? "" : undefined',
794
+ '[disabled]': 'state().disabled()',
795
+ },
796
+ }]
797
+ }], ctorParameters: () => [], propDecorators: { toggleDropdown: [{
798
+ type: HostListener,
799
+ args: ['click']
800
+ }] } });
801
+
802
+ /**
803
+ * Generated bundle index. Do not edit.
804
+ */
805
+
806
+ export { NgpCombobox, NgpComboboxButton, NgpComboboxDropdown, NgpComboboxInput, NgpComboboxOption, NgpComboboxPortal, injectComboboxState, provideComboboxState };
807
+ //# sourceMappingURL=ng-primitives-combobox.mjs.map