ng-primitives 0.72.0 → 0.73.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,47 +1,49 @@
1
1
  import * as i0 from '@angular/core';
2
- import { input, booleanAttribute, Directive } from '@angular/core';
2
+ import { input, booleanAttribute, Directive, computed, HostListener, inject, ViewContainerRef, TemplateRef, Injector, signal, InjectionToken, output, afterRenderEffect } from '@angular/core';
3
3
  import { setupFormControl } from 'ng-primitives/form-field';
4
- import { setupInteractions } from 'ng-primitives/internal';
4
+ import { setupInteractions, observeResize, injectElementRef } from 'ng-primitives/internal';
5
5
  import { uniqueId } from 'ng-primitives/utils';
6
6
  import { createStateToken, createStateProvider, createStateInjector, createState } from 'ng-primitives/state';
7
+ import { createOverlay } from 'ng-primitives/portal';
8
+ import { activeDescendantManager } from 'ng-primitives/a11y';
7
9
 
8
10
  /**
9
- * The state token for the Select primitive.
11
+ * The state token for the Select primitive.
10
12
  */
11
- const NgpSelectStateToken = createStateToken('Select');
13
+ const NgpNativeSelectStateToken = createStateToken('Select');
12
14
  /**
13
15
  * Provides the Select state.
14
16
  */
15
- const provideSelectState = createStateProvider(NgpSelectStateToken);
17
+ const provideNativeSelectState = createStateProvider(NgpNativeSelectStateToken);
16
18
  /**
17
19
  * Injects the Select state.
18
20
  */
19
- const injectSelectState = createStateInjector(NgpSelectStateToken);
21
+ const injectNativeSelectState = createStateInjector(NgpNativeSelectStateToken);
20
22
  /**
21
23
  * The Select state registration function.
22
24
  */
23
- const selectState = createState(NgpSelectStateToken);
25
+ const selectNativeSelectState = createState(NgpNativeSelectStateToken);
24
26
 
25
27
  /**
26
- * Apply the `ngpSelect` directive to a select element that you want to enhance.
28
+ * Apply the `ngpNativeSelect` directive to a select element that you want to enhance.
27
29
  */
28
- class NgpSelect {
30
+ class NgpNativeSelect {
29
31
  constructor() {
30
32
  /**
31
33
  * The id of the select. If not provided, a unique id will be generated.
32
34
  */
33
- this.id = input(uniqueId('ngp-select'));
35
+ this.id = input(uniqueId('ngp-native-select'));
34
36
  /**
35
37
  * Whether the select is disabled.
36
38
  */
37
39
  this.disabled = input(false, {
38
- alias: 'ngpSelectDisabled',
40
+ alias: 'ngpNativeSelectDisabled',
39
41
  transform: booleanAttribute,
40
42
  });
41
43
  /**
42
44
  * The select state.
43
45
  */
44
- this.state = selectState(this);
46
+ this.state = selectNativeSelectState(this);
45
47
  setupInteractions({
46
48
  hover: true,
47
49
  press: true,
@@ -51,25 +53,695 @@ class NgpSelect {
51
53
  });
52
54
  setupFormControl({ id: this.state.id, disabled: this.state.disabled });
53
55
  }
56
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.11", ngImport: i0, type: NgpNativeSelect, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
57
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "19.2.11", type: NgpNativeSelect, isStandalone: true, selector: "select[ngpNativeSelect]", inputs: { id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "ngpNativeSelectDisabled", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.disabled": "state.disabled() || null" } }, providers: [provideNativeSelectState()], exportAs: ["ngpNativeSelect"], ngImport: i0 }); }
58
+ }
59
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.11", ngImport: i0, type: NgpNativeSelect, decorators: [{
60
+ type: Directive,
61
+ args: [{
62
+ selector: 'select[ngpNativeSelect]',
63
+ exportAs: 'ngpNativeSelect',
64
+ providers: [provideNativeSelectState()],
65
+ host: {
66
+ '[attr.disabled]': 'state.disabled() || null',
67
+ },
68
+ }]
69
+ }], ctorParameters: () => [] });
70
+
71
+ /**
72
+ * The state token for the Select primitive.
73
+ */
74
+ const NgpSelectStateToken = createStateToken('Select');
75
+ /**
76
+ * Provides the Select state.
77
+ */
78
+ const provideSelectState = createStateProvider(NgpSelectStateToken);
79
+ /**
80
+ * Injects the Select state.
81
+ */
82
+ const injectSelectState = createStateInjector(NgpSelectStateToken);
83
+ /**
84
+ * The Select state registration function.
85
+ */
86
+ const selectState = createState(NgpSelectStateToken);
87
+
88
+ class NgpSelectDropdown {
89
+ constructor() {
90
+ /** Access the select state. */
91
+ this.state = injectSelectState();
92
+ /** The dimensions of the select. */
93
+ this.selectDimensions = observeResize(() => this.state().elementRef.nativeElement);
94
+ /**
95
+ * Access the element reference.
96
+ * @internal
97
+ */
98
+ this.elementRef = injectElementRef();
99
+ /** The id of the dropdown. */
100
+ this.id = input(uniqueId('ngp-select-dropdown'));
101
+ this.state().registerDropdown(this);
102
+ }
103
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.11", ngImport: i0, type: NgpSelectDropdown, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
104
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "19.2.11", type: NgpSelectDropdown, isStandalone: true, selector: "[ngpSelectDropdown]", inputs: { id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "listbox" }, properties: { "id": "id()", "style.left.px": "state().overlay()?.position()?.x", "style.top.px": "state().overlay()?.position()?.y", "style.--ngp-select-transform-origin": "state().overlay()?.transformOrigin()", "style.--ngp-select-width.px": "selectDimensions().width" } }, exportAs: ["ngpSelectDropdown"], ngImport: i0 }); }
105
+ }
106
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.11", ngImport: i0, type: NgpSelectDropdown, decorators: [{
107
+ type: Directive,
108
+ args: [{
109
+ selector: '[ngpSelectDropdown]',
110
+ exportAs: 'ngpSelectDropdown',
111
+ host: {
112
+ role: 'listbox',
113
+ '[id]': 'id()',
114
+ '[style.left.px]': 'state().overlay()?.position()?.x',
115
+ '[style.top.px]': 'state().overlay()?.position()?.y',
116
+ '[style.--ngp-select-transform-origin]': 'state().overlay()?.transformOrigin()',
117
+ '[style.--ngp-select-width.px]': 'selectDimensions().width',
118
+ },
119
+ }]
120
+ }], ctorParameters: () => [] });
121
+
122
+ class NgpSelectOption {
123
+ constructor() {
124
+ /** Access the select state. */
125
+ this.state = injectSelectState();
126
+ /**
127
+ * The element reference of the option.
128
+ * @internal
129
+ */
130
+ this.elementRef = injectElementRef();
131
+ /** The id of the option. */
132
+ this.id = input(uniqueId('ngp-select-option'));
133
+ /** @required The value of the option. */
134
+ this.value = input(undefined, {
135
+ alias: 'ngpSelectOptionValue',
136
+ });
137
+ /** The disabled state of the option. */
138
+ this.disabled = input(false, {
139
+ alias: 'ngpSelectOptionDisabled',
140
+ transform: booleanAttribute,
141
+ });
142
+ /**
143
+ * Whether this option is the active descendant.
144
+ * @internal
145
+ */
146
+ this.active = computed(() => this.state().activeDescendantManager.activeDescendant() === this.id());
147
+ /** Whether this option is selected. */
148
+ this.selected = computed(() => {
149
+ const value = this.value();
150
+ if (!value) {
151
+ return false;
152
+ }
153
+ if (this.state().multiple()) {
154
+ return (Array.isArray(value) && value.some(v => this.state().compareWith()(v, this.state().value())));
155
+ }
156
+ return this.state().compareWith()(value, this.state().value());
157
+ });
158
+ this.state().registerOption(this);
159
+ setupInteractions({
160
+ hover: true,
161
+ press: true,
162
+ disabled: this.disabled,
163
+ });
164
+ }
165
+ ngOnInit() {
166
+ if (this.value() === undefined) {
167
+ throw new Error('ngpSelectOption: The value input is required. Please provide a value for the option.');
168
+ }
169
+ }
170
+ ngOnDestroy() {
171
+ this.state().unregisterOption(this);
172
+ }
173
+ /**
174
+ * Select the option.
175
+ * @internal
176
+ */
177
+ select() {
178
+ if (this.disabled()) {
179
+ return;
180
+ }
181
+ this.state().toggleOption(this);
182
+ }
183
+ /**
184
+ * Scroll the option into view.
185
+ * @internal
186
+ */
187
+ scrollIntoView() {
188
+ this.elementRef.nativeElement.scrollIntoView({ block: 'nearest' });
189
+ }
190
+ /**
191
+ * Whenever the pointer enters the option, activate it.
192
+ * @internal
193
+ */
194
+ onPointerEnter() {
195
+ this.state().activeDescendantManager.activate(this);
196
+ }
197
+ /**
198
+ * Whenever the pointer leaves the option, deactivate it.
199
+ * @internal
200
+ */
201
+ onPointerLeave() {
202
+ this.state().activeDescendantManager.activate(undefined);
203
+ }
204
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.11", ngImport: i0, type: NgpSelectOption, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
205
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "19.2.11", type: NgpSelectOption, isStandalone: true, selector: "[ngpSelectOption]", inputs: { id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "ngpSelectOptionValue", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "ngpSelectOptionDisabled", 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: ["ngpSelectOption"], ngImport: i0 }); }
206
+ }
207
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.11", ngImport: i0, type: NgpSelectOption, decorators: [{
208
+ type: Directive,
209
+ args: [{
210
+ selector: '[ngpSelectOption]',
211
+ exportAs: 'ngpSelectOption',
212
+ host: {
213
+ role: 'option',
214
+ '[id]': 'id()',
215
+ '[attr.tabindex]': '-1',
216
+ '[attr.aria-selected]': 'selected() ? "true" : undefined',
217
+ '[attr.data-selected]': 'selected() ? "" : undefined',
218
+ '[attr.data-active]': 'active() ? "" : undefined',
219
+ '[attr.data-disabled]': 'disabled() ? "" : undefined',
220
+ '(click)': 'select()',
221
+ },
222
+ }]
223
+ }], ctorParameters: () => [], propDecorators: { onPointerEnter: [{
224
+ type: HostListener,
225
+ args: ['pointerenter']
226
+ }], onPointerLeave: [{
227
+ type: HostListener,
228
+ args: ['pointerleave']
229
+ }] } });
230
+
231
+ class NgpSelectPortal {
232
+ constructor() {
233
+ /** Access the select state. */
234
+ this.state = injectSelectState();
235
+ /** Access the view container reference. */
236
+ this.viewContainerRef = inject(ViewContainerRef);
237
+ /** Access the template reference. */
238
+ this.templateRef = inject(TemplateRef);
239
+ /** Access the injector. */
240
+ this.injector = inject(Injector);
241
+ /**
242
+ * The overlay that manages the popover
243
+ * @internal
244
+ */
245
+ this.overlay = signal(null);
246
+ this.state().registerPortal(this);
247
+ }
248
+ /** Cleanup the portal. */
249
+ ngOnDestroy() {
250
+ this.overlay()?.destroy();
251
+ }
252
+ /**
253
+ * Attach the portal.
254
+ * @internal
255
+ */
256
+ show() {
257
+ // Create the overlay if it doesn't exist yet
258
+ if (!this.overlay()) {
259
+ this.createOverlay();
260
+ }
261
+ // Show the overlay
262
+ return this.overlay().show();
263
+ }
264
+ /**
265
+ * Detach the portal.
266
+ * @internal
267
+ */
268
+ async detach() {
269
+ this.overlay()?.hide();
270
+ }
271
+ /**
272
+ * Create the overlay that will contain the dropdown
273
+ */
274
+ createOverlay() {
275
+ // Create config for the overlay
276
+ const config = {
277
+ content: this.templateRef,
278
+ viewContainerRef: this.viewContainerRef,
279
+ triggerElement: this.state().elementRef.nativeElement,
280
+ injector: this.injector,
281
+ placement: this.state().placement(),
282
+ closeOnOutsideClick: true,
283
+ closeOnEscape: true,
284
+ restoreFocus: false,
285
+ scrollBehaviour: 'reposition',
286
+ container: this.state().container(),
287
+ };
288
+ this.overlay.set(createOverlay(config));
289
+ }
290
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.11", ngImport: i0, type: NgpSelectPortal, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
291
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.11", type: NgpSelectPortal, isStandalone: true, selector: "[ngpSelectPortal]", exportAs: ["ngpSelectPortal"], ngImport: i0 }); }
292
+ }
293
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.11", ngImport: i0, type: NgpSelectPortal, decorators: [{
294
+ type: Directive,
295
+ args: [{
296
+ selector: '[ngpSelectPortal]',
297
+ exportAs: 'ngpSelectPortal',
298
+ }]
299
+ }], ctorParameters: () => [] });
300
+
301
+ const defaultSelectConfig = {
302
+ placement: 'bottom',
303
+ container: null,
304
+ };
305
+ const NgpSelectConfigToken = new InjectionToken('NgpSelectConfigToken');
306
+ /**
307
+ * Provide the default Select configuration
308
+ * @param config The Select configuration
309
+ * @returns The provider
310
+ */
311
+ function provideSelectConfig(config) {
312
+ return [
313
+ {
314
+ provide: NgpSelectConfigToken,
315
+ useValue: { ...defaultSelectConfig, ...config },
316
+ },
317
+ ];
318
+ }
319
+ /**
320
+ * Inject the Select configuration
321
+ * @returns The global Select configuration
322
+ */
323
+ function injectSelectConfig() {
324
+ return inject(NgpSelectConfigToken, { optional: true }) ?? defaultSelectConfig;
325
+ }
326
+
327
+ class NgpSelect {
328
+ constructor() {
329
+ /** Access the select configuration. */
330
+ this.config = injectSelectConfig();
331
+ /** @internal Access the select element. */
332
+ this.elementRef = injectElementRef();
333
+ /** Access the injector. */
334
+ this.injector = inject(Injector);
335
+ /** The unique id of the select. */
336
+ this.id = input(uniqueId('ngp-select'));
337
+ /** The value of the select. */
338
+ this.value = input(undefined, {
339
+ alias: 'ngpSelectValue',
340
+ });
341
+ /** Event emitted when the value changes. */
342
+ this.valueChange = output({
343
+ alias: 'ngpSelectValueChange',
344
+ });
345
+ /** Whether the select is multiple selection. */
346
+ this.multiple = input(false, {
347
+ alias: 'ngpSelectMultiple',
348
+ transform: booleanAttribute,
349
+ });
350
+ /** Whether the select is disabled. */
351
+ this.disabled = input(false, {
352
+ alias: 'ngpSelectDisabled',
353
+ transform: booleanAttribute,
354
+ });
355
+ /** Emit when the dropdown open state changes. */
356
+ this.openChange = output({
357
+ alias: 'ngpSelectOpenChange',
358
+ });
359
+ /** The comparator function used to compare options. */
360
+ this.compareWith = input(Object.is, {
361
+ alias: 'ngpSelectCompareWith',
362
+ });
363
+ /** The position of the dropdown. */
364
+ this.placement = input(this.config.placement, {
365
+ alias: 'ngpSelectDropdownPlacement',
366
+ });
367
+ /** The container for the dropdown. */
368
+ this.container = input(this.config.container, {
369
+ alias: 'ngpSelectDropdownContainer',
370
+ });
371
+ /**
372
+ * Store the select portal.
373
+ * @internal
374
+ */
375
+ this.portal = signal(undefined);
376
+ /**
377
+ * Store the select dropdown.
378
+ * @internal
379
+ */
380
+ this.dropdown = signal(undefined);
381
+ /**
382
+ * Store the select options.
383
+ * @internal
384
+ */
385
+ this.options = signal([]);
386
+ /**
387
+ * Access the overlay
388
+ * @internal
389
+ */
390
+ this.overlay = computed(() => this.portal()?.overlay());
391
+ /**
392
+ * The open state of the select.
393
+ * @internal
394
+ */
395
+ this.open = computed(() => this.overlay()?.isOpen() ?? false);
396
+ /**
397
+ * The active key descendant manager.
398
+ * @internal
399
+ */
400
+ this.activeDescendantManager = activeDescendantManager({
401
+ // we must wrap the signal in a computed to ensure it is not used before it is defined
402
+ disabled: computed(() => this.state.disabled()),
403
+ items: this.options,
404
+ });
405
+ /** The state of the select. */
406
+ this.state = selectState(this);
407
+ setupInteractions({
408
+ focus: true,
409
+ focusWithin: true,
410
+ hover: true,
411
+ press: true,
412
+ disabled: this.state.disabled,
413
+ });
414
+ setupFormControl({ id: this.state.id, disabled: this.state.disabled });
415
+ // any time the active descendant changes, ensure we scroll it into view
416
+ // perform after next render to ensure the DOM is updated
417
+ // e.g. the dropdown is open before the option is scrolled into view
418
+ afterRenderEffect({
419
+ write: () => {
420
+ const isPositioned = this.portal()?.overlay()?.isPositioned() ?? false;
421
+ const activeItem = this.activeDescendantManager.activeItem();
422
+ if (!isPositioned || !activeItem) {
423
+ return;
424
+ }
425
+ this.activeDescendantManager.activeItem()?.scrollIntoView?.();
426
+ },
427
+ });
428
+ }
429
+ /**
430
+ * Open the dropdown.
431
+ * @internal
432
+ */
433
+ async openDropdown() {
434
+ if (this.state.disabled() || this.open()) {
435
+ return;
436
+ }
437
+ this.openChange.emit(true);
438
+ await this.portal()?.show();
439
+ // if there is a selected option(s), set the active descendant to the first selected option
440
+ const selectedOption = this.options().find(option => this.isOptionSelected(option));
441
+ // if there is no selected option, set the active descendant to the first option
442
+ const targetOption = selectedOption ?? this.options()[0];
443
+ // if there is no target option, do nothing
444
+ if (!targetOption) {
445
+ return;
446
+ }
447
+ // activate the selected option or the first option
448
+ this.activeDescendantManager.activate(targetOption);
449
+ }
450
+ /**
451
+ * Close the dropdown.
452
+ * @internal
453
+ */
454
+ closeDropdown() {
455
+ if (!this.open()) {
456
+ return;
457
+ }
458
+ this.openChange.emit(false);
459
+ this.portal()?.detach();
460
+ // clear the active descendant
461
+ this.activeDescendantManager.reset();
462
+ }
463
+ /**
464
+ * Toggle the dropdown.
465
+ * @internal
466
+ */
467
+ async toggleDropdown() {
468
+ if (this.open()) {
469
+ this.closeDropdown();
470
+ }
471
+ else {
472
+ await this.openDropdown();
473
+ }
474
+ }
475
+ /**
476
+ * Select an option.
477
+ * @param option The option to select.
478
+ * @internal
479
+ */
480
+ selectOption(option) {
481
+ if (this.state.disabled()) {
482
+ return;
483
+ }
484
+ if (!option) {
485
+ this.state.value.set(undefined);
486
+ this.closeDropdown();
487
+ return;
488
+ }
489
+ if (this.state.multiple()) {
490
+ // if the option is already selected, do nothing
491
+ if (this.isOptionSelected(option)) {
492
+ return;
493
+ }
494
+ const value = [...(this.state.value() ?? []), option.value()];
495
+ // add the option to the value
496
+ this.state.value.set(value);
497
+ this.valueChange.emit(value);
498
+ }
499
+ else {
500
+ this.state.value.set(option.value());
501
+ this.valueChange.emit(option.value());
502
+ // close the dropdown on single selection
503
+ this.closeDropdown();
504
+ }
505
+ }
506
+ /**
507
+ * Deselect an option.
508
+ * @param option The option to deselect.
509
+ * @internal
510
+ */
511
+ deselectOption(option) {
512
+ // if the select is disabled or the option is not selected, do nothing
513
+ // if the select is single selection, we don't allow deselecting
514
+ if (this.state.disabled() || !this.isOptionSelected(option) || !this.state.multiple()) {
515
+ return;
516
+ }
517
+ const values = this.state.value() ?? [];
518
+ const newValue = values.filter(v => !this.state.compareWith()(v, option.value()));
519
+ // remove the option from the value
520
+ this.state.value.set(newValue);
521
+ this.valueChange.emit(newValue);
522
+ }
523
+ /**
524
+ * Toggle the selection of an option.
525
+ * @param option The option to toggle.
526
+ * @internal
527
+ */
528
+ toggleOption(option) {
529
+ if (this.state.disabled()) {
530
+ return;
531
+ }
532
+ // if the state is single selection, we don't allow toggling
533
+ if (!this.state.multiple()) {
534
+ // always select the option in single selection mode even if it is already selected so that we update the input
535
+ this.selectOption(option);
536
+ return;
537
+ }
538
+ // otherwise toggle the option
539
+ if (this.isOptionSelected(option)) {
540
+ this.deselectOption(option);
541
+ }
542
+ else {
543
+ this.selectOption(option);
544
+ }
545
+ }
546
+ /**
547
+ * Determine if an option is selected.
548
+ * @param option The option to check.
549
+ * @internal
550
+ */
551
+ isOptionSelected(option) {
552
+ if (this.state.disabled()) {
553
+ return false;
554
+ }
555
+ const value = this.state.value();
556
+ if (!value) {
557
+ return false;
558
+ }
559
+ if (this.state.multiple()) {
560
+ return value && value.some(v => this.state.compareWith()(option.value(), v));
561
+ }
562
+ return this.state.compareWith()(option.value(), value);
563
+ }
564
+ /**
565
+ * Activate the next option in the list if there is one.
566
+ * If there is no option currently active, activate the selected option or the first option.
567
+ * @internal
568
+ */
569
+ activateNextOption() {
570
+ if (this.state.disabled()) {
571
+ return;
572
+ }
573
+ const options = this.options();
574
+ // if there are no options, do nothing
575
+ if (options.length === 0) {
576
+ return;
577
+ }
578
+ // if there is no active option, activate the first option
579
+ if (!this.activeDescendantManager.activeItem()) {
580
+ const selectedOption = options.find(option => this.isOptionSelected(option));
581
+ // if there is a selected option(s), set the active descendant to the first selected option
582
+ const targetOption = selectedOption ?? options[0];
583
+ this.activeDescendantManager.activate(targetOption);
584
+ return;
585
+ }
586
+ // otherwise activate the next option
587
+ this.activeDescendantManager.next();
588
+ }
589
+ /**
590
+ * Activate the previous option in the list if there is one.
591
+ * @internal
592
+ */
593
+ activatePreviousOption() {
594
+ if (this.state.disabled()) {
595
+ return;
596
+ }
597
+ const options = this.options();
598
+ // if there are no options, do nothing
599
+ if (options.length === 0) {
600
+ return;
601
+ }
602
+ // if there is no active option, activate the last option
603
+ if (!this.activeDescendantManager.activeItem()) {
604
+ const selectedOption = options.find(option => this.isOptionSelected(option));
605
+ // if there is a selected option(s), set the active descendant to the first selected option
606
+ const targetOption = selectedOption ?? options[options.length - 1];
607
+ this.activeDescendantManager.activate(targetOption);
608
+ return;
609
+ }
610
+ // otherwise activate the previous option
611
+ this.activeDescendantManager.previous();
612
+ }
613
+ /**
614
+ * Register the dropdown portal with the select.
615
+ * @param portal The dropdown portal.
616
+ * @internal
617
+ */
618
+ registerPortal(portal) {
619
+ this.portal.set(portal);
620
+ }
621
+ /**
622
+ * Register the dropdown with the select.
623
+ * @param dropdown The dropdown to register.
624
+ * @internal
625
+ */
626
+ registerDropdown(dropdown) {
627
+ this.dropdown.set(dropdown);
628
+ }
629
+ /**
630
+ * Register an option with the select.
631
+ * @param option The option to register.
632
+ * @internal
633
+ */
634
+ registerOption(option) {
635
+ this.options.update(options => [...options, option]);
636
+ }
637
+ /**
638
+ * Unregister an option from the select.
639
+ * @param option The option to unregister.
640
+ * @internal
641
+ */
642
+ unregisterOption(option) {
643
+ this.options.update(options => options.filter(o => o !== option));
644
+ }
645
+ /**
646
+ * Focus the select.
647
+ * @internal
648
+ */
649
+ focus() {
650
+ this.elementRef.nativeElement.focus();
651
+ }
652
+ /** Handle keydown events for accessibility. */
653
+ handleKeydown(event) {
654
+ switch (event.key) {
655
+ case 'ArrowDown':
656
+ if (this.open()) {
657
+ this.activateNextOption();
658
+ }
659
+ else {
660
+ this.openDropdown();
661
+ }
662
+ event.preventDefault();
663
+ break;
664
+ case 'ArrowUp':
665
+ if (this.open()) {
666
+ this.activatePreviousOption();
667
+ }
668
+ else {
669
+ this.openDropdown();
670
+ this.activeDescendantManager.last();
671
+ }
672
+ event.preventDefault();
673
+ break;
674
+ case 'Home':
675
+ if (this.open()) {
676
+ this.activeDescendantManager.first();
677
+ }
678
+ event.preventDefault();
679
+ break;
680
+ case 'End':
681
+ if (this.open()) {
682
+ this.activeDescendantManager.last();
683
+ }
684
+ event.preventDefault();
685
+ break;
686
+ case 'Enter':
687
+ if (this.open()) {
688
+ this.selectOption(this.activeDescendantManager.activeItem());
689
+ }
690
+ else {
691
+ this.openDropdown();
692
+ }
693
+ event.preventDefault();
694
+ break;
695
+ case ' ':
696
+ this.toggleDropdown();
697
+ event.preventDefault();
698
+ break;
699
+ }
700
+ }
701
+ onBlur(event) {
702
+ const relatedTarget = event.relatedTarget;
703
+ // if the blur was caused by focus moving to the dropdown, don't close
704
+ if (relatedTarget && this.dropdown()?.elementRef.nativeElement.contains(relatedTarget)) {
705
+ return;
706
+ }
707
+ this.closeDropdown();
708
+ event.preventDefault();
709
+ }
54
710
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.11", ngImport: i0, type: NgpSelect, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
55
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "19.2.11", type: NgpSelect, isStandalone: true, selector: "select[ngpSelect]", inputs: { id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "ngpSelectDisabled", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "id": "id()", "attr.disabled": "disabled() || null" } }, providers: [provideSelectState()], exportAs: ["ngpSelect"], ngImport: i0 }); }
711
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "19.2.11", type: NgpSelect, isStandalone: true, selector: "[ngpSelect]", inputs: { id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "ngpSelectValue", isSignal: true, isRequired: false, transformFunction: null }, multiple: { classPropertyName: "multiple", publicName: "ngpSelectMultiple", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "ngpSelectDisabled", isSignal: true, isRequired: false, transformFunction: null }, compareWith: { classPropertyName: "compareWith", publicName: "ngpSelectCompareWith", isSignal: true, isRequired: false, transformFunction: null }, placement: { classPropertyName: "placement", publicName: "ngpSelectDropdownPlacement", isSignal: true, isRequired: false, transformFunction: null }, container: { classPropertyName: "container", publicName: "ngpSelectDropdownContainer", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { valueChange: "ngpSelectValueChange", openChange: "ngpSelectOpenChange" }, host: { attributes: { "role": "combobox" }, listeners: { "click": "toggleDropdown()", "keydown": "handleKeydown($event)", "blur": "onBlur($event)" }, properties: { "id": "state.id()", "attr.aria-expanded": "state.open()", "attr.aria-controls": "state.open() ? state.dropdown()?.id() : undefined", "attr.aria-activedescendant": "state.open() ? activeDescendantManager.activeDescendant() : undefined", "attr.tabindex": "state.disabled() ? -1 : 0", "attr.data-open": "state.open() ? \"\" : undefined", "attr.data-disabled": "state.disabled() ? \"\" : undefined", "attr.data-multiple": "state.multiple() ? \"\" : undefined" } }, providers: [provideSelectState()], exportAs: ["ngpSelect"], ngImport: i0 }); }
56
712
  }
57
713
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.11", ngImport: i0, type: NgpSelect, decorators: [{
58
714
  type: Directive,
59
715
  args: [{
60
- selector: 'select[ngpSelect]',
716
+ selector: '[ngpSelect]',
61
717
  exportAs: 'ngpSelect',
62
718
  providers: [provideSelectState()],
63
719
  host: {
64
- '[id]': 'id()',
65
- '[attr.disabled]': 'disabled() || null',
720
+ role: 'combobox',
721
+ '[id]': 'state.id()',
722
+ '[attr.aria-expanded]': 'state.open()',
723
+ '[attr.aria-controls]': 'state.open() ? state.dropdown()?.id() : undefined',
724
+ '[attr.aria-activedescendant]': 'state.open() ? activeDescendantManager.activeDescendant() : undefined',
725
+ '[attr.tabindex]': 'state.disabled() ? -1 : 0',
726
+ '[attr.data-open]': 'state.open() ? "" : undefined',
727
+ '[attr.data-disabled]': 'state.disabled() ? "" : undefined',
728
+ '[attr.data-multiple]': 'state.multiple() ? "" : undefined',
66
729
  },
67
730
  }]
68
- }], ctorParameters: () => [] });
731
+ }], ctorParameters: () => [], propDecorators: { toggleDropdown: [{
732
+ type: HostListener,
733
+ args: ['click']
734
+ }], handleKeydown: [{
735
+ type: HostListener,
736
+ args: ['keydown', ['$event']]
737
+ }], onBlur: [{
738
+ type: HostListener,
739
+ args: ['blur', ['$event']]
740
+ }] } });
69
741
 
70
742
  /**
71
743
  * Generated bundle index. Do not edit.
72
744
  */
73
745
 
74
- export { NgpSelect, injectSelectState, provideSelectState };
746
+ export { NgpNativeSelect, NgpSelect, NgpSelectDropdown, NgpSelectOption, NgpSelectPortal, injectNativeSelectState, injectSelectState, provideNativeSelectState, provideSelectState };
75
747
  //# sourceMappingURL=ng-primitives-select.mjs.map