UNPKG

36.2 kBJavaScriptView Raw
1import * as i0 from '@angular/core';
2import { inject, ElementRef, Directive, Input, ChangeDetectorRef, forwardRef, Output, ContentChildren, NgModule } from '@angular/core';
3import { ActiveDescendantKeyManager } from '@angular/cdk/a11y';
4import { A, hasModifierKey, SPACE, ENTER, HOME, END, UP_ARROW, DOWN_ARROW, LEFT_ARROW, RIGHT_ARROW } from '@angular/cdk/keycodes';
5import { coerceBooleanProperty, coerceArray } from '@angular/cdk/coercion';
6import { SelectionModel } from '@angular/cdk/collections';
7import { Subject, defer, merge } from 'rxjs';
8import { startWith, switchMap, map, takeUntil, filter } from 'rxjs/operators';
9import { NG_VALUE_ACCESSOR } from '@angular/forms';
10import { Directionality } from '@angular/cdk/bidi';
11
12/** The next id to use for creating unique DOM IDs. */
13let nextId = 0;
14/**
15 * An implementation of SelectionModel that internally always represents the selection as a
16 * multi-selection. This is necessary so that we can recover the full selection if the user
17 * switches the listbox from single-selection to multi-selection after initialization.
18 *
19 * This selection model may report multiple selected values, even if it is in single-selection
20 * mode. It is up to the user (CdkListbox) to check for invalid selections.
21 */
22class ListboxSelectionModel extends SelectionModel {
23 constructor(multiple = false, initiallySelectedValues, emitChanges = true, compareWith) {
24 super(true, initiallySelectedValues, emitChanges, compareWith);
25 this.multiple = multiple;
26 }
27 isMultipleSelection() {
28 return this.multiple;
29 }
30 select(...values) {
31 // The super class is always in multi-selection mode, so we need to override the behavior if
32 // this selection model actually belongs to a single-selection listbox.
33 if (this.multiple) {
34 return super.select(...values);
35 }
36 else {
37 return super.setSelection(...values);
38 }
39 }
40}
41/** A selectable option in a listbox. */
42class CdkOption {
43 constructor() {
44 this._generatedId = `cdk-option-${nextId++}`;
45 this._disabled = false;
46 /** The option's host element */
47 this.element = inject(ElementRef).nativeElement;
48 /** The parent listbox this option belongs to. */
49 this.listbox = inject(CdkListbox);
50 /** Emits when the option is destroyed. */
51 this.destroyed = new Subject();
52 /** Emits when the option is clicked. */
53 this._clicked = new Subject();
54 }
55 /** The id of the option's host element. */
56 get id() {
57 return this._id || this._generatedId;
58 }
59 set id(value) {
60 this._id = value;
61 }
62 /** Whether this option is disabled. */
63 get disabled() {
64 return this.listbox.disabled || this._disabled;
65 }
66 set disabled(value) {
67 this._disabled = coerceBooleanProperty(value);
68 }
69 /** The tabindex of the option when it is enabled. */
70 get enabledTabIndex() {
71 return this._enabledTabIndex === undefined
72 ? this.listbox.enabledTabIndex
73 : this._enabledTabIndex;
74 }
75 set enabledTabIndex(value) {
76 this._enabledTabIndex = value;
77 }
78 ngOnDestroy() {
79 this.destroyed.next();
80 this.destroyed.complete();
81 }
82 /** Whether this option is selected. */
83 isSelected() {
84 return this.listbox.isSelected(this);
85 }
86 /** Whether this option is active. */
87 isActive() {
88 return this.listbox.isActive(this);
89 }
90 /** Toggle the selected state of this option. */
91 toggle() {
92 this.listbox.toggle(this);
93 }
94 /** Select this option if it is not selected. */
95 select() {
96 this.listbox.select(this);
97 }
98 /** Deselect this option if it is selected. */
99 deselect() {
100 this.listbox.deselect(this);
101 }
102 /** Focus this option. */
103 focus() {
104 this.element.focus();
105 }
106 /** Get the label for this element which is required by the FocusableOption interface. */
107 getLabel() {
108 return (this.typeaheadLabel ?? this.element.textContent?.trim()) || '';
109 }
110 /**
111 * No-op implemented as a part of `Highlightable`.
112 * @docs-private
113 */
114 setActiveStyles() { }
115 /**
116 * No-op implemented as a part of `Highlightable`.
117 * @docs-private
118 */
119 setInactiveStyles() { }
120 /** Handle focus events on the option. */
121 _handleFocus() {
122 // Options can wind up getting focused in active descendant mode if the user clicks on them.
123 // In this case, we push focus back to the parent listbox to prevent an extra tab stop when
124 // the user performs a shift+tab.
125 if (this.listbox.useActiveDescendant) {
126 this.listbox._setActiveOption(this);
127 this.listbox.focus();
128 }
129 }
130 /** Get the tabindex for this option. */
131 _getTabIndex() {
132 if (this.listbox.useActiveDescendant || this.disabled) {
133 return -1;
134 }
135 return this.isActive() ? this.enabledTabIndex : -1;
136 }
137 static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkOption, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
138 static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.0.0", type: CdkOption, isStandalone: true, selector: "[cdkOption]", inputs: { id: "id", value: ["cdkOption", "value"], typeaheadLabel: ["cdkOptionTypeaheadLabel", "typeaheadLabel"], disabled: ["cdkOptionDisabled", "disabled"], enabledTabIndex: ["tabindex", "enabledTabIndex"] }, host: { attributes: { "role": "option" }, listeners: { "click": "_clicked.next($event)", "focus": "_handleFocus()" }, properties: { "id": "id", "attr.aria-selected": "isSelected()", "attr.tabindex": "_getTabIndex()", "attr.aria-disabled": "disabled", "class.cdk-option-active": "isActive()" }, classAttribute: "cdk-option" }, exportAs: ["cdkOption"], ngImport: i0 }); }
139}
140i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkOption, decorators: [{
141 type: Directive,
142 args: [{
143 selector: '[cdkOption]',
144 standalone: true,
145 exportAs: 'cdkOption',
146 host: {
147 'role': 'option',
148 'class': 'cdk-option',
149 '[id]': 'id',
150 '[attr.aria-selected]': 'isSelected()',
151 '[attr.tabindex]': '_getTabIndex()',
152 '[attr.aria-disabled]': 'disabled',
153 '[class.cdk-option-active]': 'isActive()',
154 '(click)': '_clicked.next($event)',
155 '(focus)': '_handleFocus()',
156 },
157 }]
158 }], propDecorators: { id: [{
159 type: Input
160 }], value: [{
161 type: Input,
162 args: ['cdkOption']
163 }], typeaheadLabel: [{
164 type: Input,
165 args: ['cdkOptionTypeaheadLabel']
166 }], disabled: [{
167 type: Input,
168 args: ['cdkOptionDisabled']
169 }], enabledTabIndex: [{
170 type: Input,
171 args: ['tabindex']
172 }] } });
173class CdkListbox {
174 constructor() {
175 this._generatedId = `cdk-listbox-${nextId++}`;
176 this._disabled = false;
177 this._useActiveDescendant = false;
178 this._orientation = 'vertical';
179 this._navigationWrapDisabled = false;
180 this._navigateDisabledOptions = false;
181 /** Emits when the selected value(s) in the listbox change. */
182 this.valueChange = new Subject();
183 /** The selection model used by the listbox. */
184 this.selectionModel = new ListboxSelectionModel();
185 /** Emits when the listbox is destroyed. */
186 this.destroyed = new Subject();
187 /** The host element of the listbox. */
188 this.element = inject(ElementRef).nativeElement;
189 /** The change detector for this listbox. */
190 this.changeDetectorRef = inject(ChangeDetectorRef);
191 /** Whether the currently selected value in the selection model is invalid. */
192 this._invalid = false;
193 /** The last user-triggered option. */
194 this._lastTriggered = null;
195 /** Callback called when the listbox has been touched */
196 this._onTouched = () => { };
197 /** Callback called when the listbox value changes */
198 this._onChange = () => { };
199 /** Emits when an option has been clicked. */
200 this._optionClicked = defer(() => this.options.changes.pipe(startWith(this.options), switchMap(options => merge(...options.map(option => option._clicked.pipe(map(event => ({ option, event }))))))));
201 /** The directionality of the page. */
202 this._dir = inject(Directionality, { optional: true });
203 /** A predicate that skips disabled options. */
204 this._skipDisabledPredicate = (option) => option.disabled;
205 /** A predicate that does not skip any options. */
206 this._skipNonePredicate = () => false;
207 /** Whether the listbox currently has focus. */
208 this._hasFocus = false;
209 }
210 /** The id of the option's host element. */
211 get id() {
212 return this._id || this._generatedId;
213 }
214 set id(value) {
215 this._id = value;
216 }
217 /** The tabindex to use when the listbox is enabled. */
218 get enabledTabIndex() {
219 return this._enabledTabIndex === undefined ? 0 : this._enabledTabIndex;
220 }
221 set enabledTabIndex(value) {
222 this._enabledTabIndex = value;
223 }
224 /** The value selected in the listbox, represented as an array of option values. */
225 get value() {
226 return this._invalid ? [] : this.selectionModel.selected;
227 }
228 set value(value) {
229 this._setSelection(value);
230 }
231 /**
232 * Whether the listbox allows multiple options to be selected. If the value switches from `true`
233 * to `false`, and more than one option is selected, all options are deselected.
234 */
235 get multiple() {
236 return this.selectionModel.multiple;
237 }
238 set multiple(value) {
239 this.selectionModel.multiple = coerceBooleanProperty(value);
240 if (this.options) {
241 this._updateInternalValue();
242 }
243 }
244 /** Whether the listbox is disabled. */
245 get disabled() {
246 return this._disabled;
247 }
248 set disabled(value) {
249 this._disabled = coerceBooleanProperty(value);
250 }
251 /** Whether the listbox will use active descendant or will move focus onto the options. */
252 get useActiveDescendant() {
253 return this._useActiveDescendant;
254 }
255 set useActiveDescendant(shouldUseActiveDescendant) {
256 this._useActiveDescendant = coerceBooleanProperty(shouldUseActiveDescendant);
257 }
258 /** The orientation of the listbox. Only affects keyboard interaction, not visual layout. */
259 get orientation() {
260 return this._orientation;
261 }
262 set orientation(value) {
263 this._orientation = value === 'horizontal' ? 'horizontal' : 'vertical';
264 if (value === 'horizontal') {
265 this.listKeyManager?.withHorizontalOrientation(this._dir?.value || 'ltr');
266 }
267 else {
268 this.listKeyManager?.withVerticalOrientation();
269 }
270 }
271 /** The function used to compare option values. */
272 get compareWith() {
273 return this.selectionModel.compareWith;
274 }
275 set compareWith(fn) {
276 this.selectionModel.compareWith = fn;
277 }
278 /**
279 * Whether the keyboard navigation should wrap when the user presses arrow down on the last item
280 * or arrow up on the first item.
281 */
282 get navigationWrapDisabled() {
283 return this._navigationWrapDisabled;
284 }
285 set navigationWrapDisabled(wrap) {
286 this._navigationWrapDisabled = coerceBooleanProperty(wrap);
287 this.listKeyManager?.withWrap(!this._navigationWrapDisabled);
288 }
289 /** Whether keyboard navigation should skip over disabled items. */
290 get navigateDisabledOptions() {
291 return this._navigateDisabledOptions;
292 }
293 set navigateDisabledOptions(skip) {
294 this._navigateDisabledOptions = coerceBooleanProperty(skip);
295 this.listKeyManager?.skipPredicate(this._navigateDisabledOptions ? this._skipNonePredicate : this._skipDisabledPredicate);
296 }
297 ngAfterContentInit() {
298 if (typeof ngDevMode === 'undefined' || ngDevMode) {
299 this._verifyNoOptionValueCollisions();
300 this._verifyOptionValues();
301 }
302 this._initKeyManager();
303 // Update the internal value whenever the options or the model value changes.
304 merge(this.selectionModel.changed, this.options.changes)
305 .pipe(startWith(null), takeUntil(this.destroyed))
306 .subscribe(() => this._updateInternalValue());
307 this._optionClicked
308 .pipe(filter(({ option }) => !option.disabled), takeUntil(this.destroyed))
309 .subscribe(({ option, event }) => this._handleOptionClicked(option, event));
310 }
311 ngOnDestroy() {
312 this.listKeyManager?.destroy();
313 this.destroyed.next();
314 this.destroyed.complete();
315 }
316 /**
317 * Toggle the selected state of the given option.
318 * @param option The option to toggle
319 */
320 toggle(option) {
321 this.toggleValue(option.value);
322 }
323 /**
324 * Toggle the selected state of the given value.
325 * @param value The value to toggle
326 */
327 toggleValue(value) {
328 if (this._invalid) {
329 this.selectionModel.clear(false);
330 }
331 this.selectionModel.toggle(value);
332 }
333 /**
334 * Select the given option.
335 * @param option The option to select
336 */
337 select(option) {
338 this.selectValue(option.value);
339 }
340 /**
341 * Select the given value.
342 * @param value The value to select
343 */
344 selectValue(value) {
345 if (this._invalid) {
346 this.selectionModel.clear(false);
347 }
348 this.selectionModel.select(value);
349 }
350 /**
351 * Deselect the given option.
352 * @param option The option to deselect
353 */
354 deselect(option) {
355 this.deselectValue(option.value);
356 }
357 /**
358 * Deselect the given value.
359 * @param value The value to deselect
360 */
361 deselectValue(value) {
362 if (this._invalid) {
363 this.selectionModel.clear(false);
364 }
365 this.selectionModel.deselect(value);
366 }
367 /**
368 * Set the selected state of all options.
369 * @param isSelected The new selected state to set
370 */
371 setAllSelected(isSelected) {
372 if (!isSelected) {
373 this.selectionModel.clear();
374 }
375 else {
376 if (this._invalid) {
377 this.selectionModel.clear(false);
378 }
379 this.selectionModel.select(...this.options.map(option => option.value));
380 }
381 }
382 /**
383 * Get whether the given option is selected.
384 * @param option The option to get the selected state of
385 */
386 isSelected(option) {
387 return this.isValueSelected(option.value);
388 }
389 /**
390 * Get whether the given option is active.
391 * @param option The option to get the active state of
392 */
393 isActive(option) {
394 return !!(this.listKeyManager?.activeItem === option);
395 }
396 /**
397 * Get whether the given value is selected.
398 * @param value The value to get the selected state of
399 */
400 isValueSelected(value) {
401 if (this._invalid) {
402 return false;
403 }
404 return this.selectionModel.isSelected(value);
405 }
406 /**
407 * Registers a callback to be invoked when the listbox's value changes from user input.
408 * @param fn The callback to register
409 * @docs-private
410 */
411 registerOnChange(fn) {
412 this._onChange = fn;
413 }
414 /**
415 * Registers a callback to be invoked when the listbox is blurred by the user.
416 * @param fn The callback to register
417 * @docs-private
418 */
419 registerOnTouched(fn) {
420 this._onTouched = fn;
421 }
422 /**
423 * Sets the listbox's value.
424 * @param value The new value of the listbox
425 * @docs-private
426 */
427 writeValue(value) {
428 this._setSelection(value);
429 this._verifyOptionValues();
430 }
431 /**
432 * Sets the disabled state of the listbox.
433 * @param isDisabled The new disabled state
434 * @docs-private
435 */
436 setDisabledState(isDisabled) {
437 this.disabled = isDisabled;
438 }
439 /** Focus the listbox's host element. */
440 focus() {
441 this.element.focus();
442 }
443 /**
444 * Triggers the given option in response to user interaction.
445 * - In single selection mode: selects the option and deselects any other selected option.
446 * - In multi selection mode: toggles the selected state of the option.
447 * @param option The option to trigger
448 */
449 triggerOption(option) {
450 if (option && !option.disabled) {
451 this._lastTriggered = option;
452 const changed = this.multiple
453 ? this.selectionModel.toggle(option.value)
454 : this.selectionModel.select(option.value);
455 if (changed) {
456 this._onChange(this.value);
457 this.valueChange.next({
458 value: this.value,
459 listbox: this,
460 option: option,
461 });
462 }
463 }
464 }
465 /**
466 * Trigger the given range of options in response to user interaction.
467 * Should only be called in multi-selection mode.
468 * @param trigger The option that was triggered
469 * @param from The start index of the options to toggle
470 * @param to The end index of the options to toggle
471 * @param on Whether to toggle the option range on
472 */
473 triggerRange(trigger, from, to, on) {
474 if (this.disabled || (trigger && trigger.disabled)) {
475 return;
476 }
477 this._lastTriggered = trigger;
478 const isEqual = this.compareWith ?? Object.is;
479 const updateValues = [...this.options]
480 .slice(Math.max(0, Math.min(from, to)), Math.min(this.options.length, Math.max(from, to) + 1))
481 .filter(option => !option.disabled)
482 .map(option => option.value);
483 const selected = [...this.value];
484 for (const updateValue of updateValues) {
485 const selectedIndex = selected.findIndex(selectedValue => isEqual(selectedValue, updateValue));
486 if (on && selectedIndex === -1) {
487 selected.push(updateValue);
488 }
489 else if (!on && selectedIndex !== -1) {
490 selected.splice(selectedIndex, 1);
491 }
492 }
493 let changed = this.selectionModel.setSelection(...selected);
494 if (changed) {
495 this._onChange(this.value);
496 this.valueChange.next({
497 value: this.value,
498 listbox: this,
499 option: trigger,
500 });
501 }
502 }
503 /**
504 * Sets the given option as active.
505 * @param option The option to make active
506 */
507 _setActiveOption(option) {
508 this.listKeyManager.setActiveItem(option);
509 }
510 /** Called when the listbox receives focus. */
511 _handleFocus() {
512 if (!this.useActiveDescendant) {
513 if (this.selectionModel.selected.length > 0) {
514 this._setNextFocusToSelectedOption();
515 }
516 else {
517 this.listKeyManager.setNextItemActive();
518 }
519 this._focusActiveOption();
520 }
521 }
522 /** Called when the user presses keydown on the listbox. */
523 _handleKeydown(event) {
524 if (this._disabled) {
525 return;
526 }
527 const { keyCode } = event;
528 const previousActiveIndex = this.listKeyManager.activeItemIndex;
529 const ctrlKeys = ['ctrlKey', 'metaKey'];
530 if (this.multiple && keyCode === A && hasModifierKey(event, ...ctrlKeys)) {
531 // Toggle all options off if they're all selected, otherwise toggle them all on.
532 this.triggerRange(null, 0, this.options.length - 1, this.options.length !== this.value.length);
533 event.preventDefault();
534 return;
535 }
536 if (this.multiple &&
537 (keyCode === SPACE || keyCode === ENTER) &&
538 hasModifierKey(event, 'shiftKey')) {
539 if (this.listKeyManager.activeItem && this.listKeyManager.activeItemIndex != null) {
540 this.triggerRange(this.listKeyManager.activeItem, this._getLastTriggeredIndex() ?? this.listKeyManager.activeItemIndex, this.listKeyManager.activeItemIndex, !this.listKeyManager.activeItem.isSelected());
541 }
542 event.preventDefault();
543 return;
544 }
545 if (this.multiple &&
546 keyCode === HOME &&
547 hasModifierKey(event, ...ctrlKeys) &&
548 hasModifierKey(event, 'shiftKey')) {
549 const trigger = this.listKeyManager.activeItem;
550 if (trigger) {
551 const from = this.listKeyManager.activeItemIndex;
552 this.listKeyManager.setFirstItemActive();
553 this.triggerRange(trigger, from, this.listKeyManager.activeItemIndex, !trigger.isSelected());
554 }
555 event.preventDefault();
556 return;
557 }
558 if (this.multiple &&
559 keyCode === END &&
560 hasModifierKey(event, ...ctrlKeys) &&
561 hasModifierKey(event, 'shiftKey')) {
562 const trigger = this.listKeyManager.activeItem;
563 if (trigger) {
564 const from = this.listKeyManager.activeItemIndex;
565 this.listKeyManager.setLastItemActive();
566 this.triggerRange(trigger, from, this.listKeyManager.activeItemIndex, !trigger.isSelected());
567 }
568 event.preventDefault();
569 return;
570 }
571 if (keyCode === SPACE || keyCode === ENTER) {
572 this.triggerOption(this.listKeyManager.activeItem);
573 event.preventDefault();
574 return;
575 }
576 const isNavKey = keyCode === UP_ARROW ||
577 keyCode === DOWN_ARROW ||
578 keyCode === LEFT_ARROW ||
579 keyCode === RIGHT_ARROW ||
580 keyCode === HOME ||
581 keyCode === END;
582 this.listKeyManager.onKeydown(event);
583 // Will select an option if shift was pressed while navigating to the option
584 if (isNavKey && event.shiftKey && previousActiveIndex !== this.listKeyManager.activeItemIndex) {
585 this.triggerOption(this.listKeyManager.activeItem);
586 }
587 }
588 /** Called when a focus moves into the listbox. */
589 _handleFocusIn() {
590 // Note that we use a `focusin` handler for this instead of the existing `focus` handler,
591 // because focus won't land on the listbox if `useActiveDescendant` is enabled.
592 this._hasFocus = true;
593 }
594 /**
595 * Called when the focus leaves an element in the listbox.
596 * @param event The focusout event
597 */
598 _handleFocusOut(event) {
599 const otherElement = event.relatedTarget;
600 if (this.element !== otherElement && !this.element.contains(otherElement)) {
601 this._onTouched();
602 this._hasFocus = false;
603 this._setNextFocusToSelectedOption();
604 }
605 }
606 /** Get the id of the active option if active descendant is being used. */
607 _getAriaActiveDescendant() {
608 return this._useActiveDescendant ? this.listKeyManager?.activeItem?.id : null;
609 }
610 /** Get the tabindex for the listbox. */
611 _getTabIndex() {
612 if (this.disabled) {
613 return -1;
614 }
615 return this.useActiveDescendant || !this.listKeyManager.activeItem ? this.enabledTabIndex : -1;
616 }
617 /** Initialize the key manager. */
618 _initKeyManager() {
619 this.listKeyManager = new ActiveDescendantKeyManager(this.options)
620 .withWrap(!this._navigationWrapDisabled)
621 .withTypeAhead()
622 .withHomeAndEnd()
623 .withAllowedModifierKeys(['shiftKey'])
624 .skipPredicate(this._navigateDisabledOptions ? this._skipNonePredicate : this._skipDisabledPredicate);
625 if (this.orientation === 'vertical') {
626 this.listKeyManager.withVerticalOrientation();
627 }
628 else {
629 this.listKeyManager.withHorizontalOrientation(this._dir?.value || 'ltr');
630 }
631 if (this.selectionModel.selected.length) {
632 Promise.resolve().then(() => this._setNextFocusToSelectedOption());
633 }
634 this.listKeyManager.change.subscribe(() => this._focusActiveOption());
635 }
636 /** Focus the active option. */
637 _focusActiveOption() {
638 if (!this.useActiveDescendant) {
639 this.listKeyManager.activeItem?.focus();
640 }
641 this.changeDetectorRef.markForCheck();
642 }
643 /**
644 * Set the selected values.
645 * @param value The list of new selected values.
646 */
647 _setSelection(value) {
648 if (this._invalid) {
649 this.selectionModel.clear(false);
650 }
651 this.selectionModel.setSelection(...this._coerceValue(value));
652 if (!this._hasFocus) {
653 this._setNextFocusToSelectedOption();
654 }
655 }
656 /** Sets the first selected option as first in the keyboard focus order. */
657 _setNextFocusToSelectedOption() {
658 // Null check the options since they only get defined after `ngAfterContentInit`.
659 const selected = this.options?.find(option => option.isSelected());
660 if (selected) {
661 this.listKeyManager.updateActiveItem(selected);
662 }
663 }
664 /** Update the internal value of the listbox based on the selection model. */
665 _updateInternalValue() {
666 const indexCache = new Map();
667 this.selectionModel.sort((a, b) => {
668 const aIndex = this._getIndexForValue(indexCache, a);
669 const bIndex = this._getIndexForValue(indexCache, b);
670 return aIndex - bIndex;
671 });
672 const selected = this.selectionModel.selected;
673 this._invalid =
674 (!this.multiple && selected.length > 1) || !!this._getInvalidOptionValues(selected).length;
675 this.changeDetectorRef.markForCheck();
676 }
677 /**
678 * Gets the index of the given value in the given list of options.
679 * @param cache The cache of indices found so far
680 * @param value The value to find
681 * @return The index of the value in the options list
682 */
683 _getIndexForValue(cache, value) {
684 const isEqual = this.compareWith || Object.is;
685 if (!cache.has(value)) {
686 let index = -1;
687 for (let i = 0; i < this.options.length; i++) {
688 if (isEqual(value, this.options.get(i).value)) {
689 index = i;
690 break;
691 }
692 }
693 cache.set(value, index);
694 }
695 return cache.get(value);
696 }
697 /**
698 * Handle the user clicking an option.
699 * @param option The option that was clicked.
700 */
701 _handleOptionClicked(option, event) {
702 event.preventDefault();
703 this.listKeyManager.setActiveItem(option);
704 if (event.shiftKey && this.multiple) {
705 this.triggerRange(option, this._getLastTriggeredIndex() ?? this.listKeyManager.activeItemIndex, this.listKeyManager.activeItemIndex, !option.isSelected());
706 }
707 else {
708 this.triggerOption(option);
709 }
710 }
711 /** Verifies that no two options represent the same value under the compareWith function. */
712 _verifyNoOptionValueCollisions() {
713 this.options.changes.pipe(startWith(this.options), takeUntil(this.destroyed)).subscribe(() => {
714 const isEqual = this.compareWith ?? Object.is;
715 for (let i = 0; i < this.options.length; i++) {
716 const option = this.options.get(i);
717 let duplicate = null;
718 for (let j = i + 1; j < this.options.length; j++) {
719 const other = this.options.get(j);
720 if (isEqual(option.value, other.value)) {
721 duplicate = other;
722 break;
723 }
724 }
725 if (duplicate) {
726 // TODO(mmalerba): Link to docs about this.
727 if (this.compareWith) {
728 console.warn(`Found multiple CdkOption representing the same value under the given compareWith function`, {
729 option1: option.element,
730 option2: duplicate.element,
731 compareWith: this.compareWith,
732 });
733 }
734 else {
735 console.warn(`Found multiple CdkOption with the same value`, {
736 option1: option.element,
737 option2: duplicate.element,
738 });
739 }
740 return;
741 }
742 }
743 });
744 }
745 /** Verifies that the option values are valid. */
746 _verifyOptionValues() {
747 if (this.options && (typeof ngDevMode === 'undefined' || ngDevMode)) {
748 const selected = this.selectionModel.selected;
749 const invalidValues = this._getInvalidOptionValues(selected);
750 if (!this.multiple && selected.length > 1) {
751 throw Error('Listbox cannot have more than one selected value in multi-selection mode.');
752 }
753 if (invalidValues.length) {
754 throw Error('Listbox has selected values that do not match any of its options.');
755 }
756 }
757 }
758 /**
759 * Coerces a value into an array representing a listbox selection.
760 * @param value The value to coerce
761 * @return An array
762 */
763 _coerceValue(value) {
764 return value == null ? [] : coerceArray(value);
765 }
766 /**
767 * Get the sublist of values that do not represent valid option values in this listbox.
768 * @param values The list of values
769 * @return The sublist of values that are not valid option values
770 */
771 _getInvalidOptionValues(values) {
772 const isEqual = this.compareWith || Object.is;
773 const validValues = (this.options || []).map(option => option.value);
774 return values.filter(value => !validValues.some(validValue => isEqual(value, validValue)));
775 }
776 /** Get the index of the last triggered option. */
777 _getLastTriggeredIndex() {
778 const index = this.options.toArray().indexOf(this._lastTriggered);
779 return index === -1 ? null : index;
780 }
781 static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkListbox, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
782 static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.0.0", type: CdkListbox, isStandalone: true, selector: "[cdkListbox]", inputs: { id: "id", enabledTabIndex: ["tabindex", "enabledTabIndex"], value: ["cdkListboxValue", "value"], multiple: ["cdkListboxMultiple", "multiple"], disabled: ["cdkListboxDisabled", "disabled"], useActiveDescendant: ["cdkListboxUseActiveDescendant", "useActiveDescendant"], orientation: ["cdkListboxOrientation", "orientation"], compareWith: ["cdkListboxCompareWith", "compareWith"], navigationWrapDisabled: ["cdkListboxNavigationWrapDisabled", "navigationWrapDisabled"], navigateDisabledOptions: ["cdkListboxNavigatesDisabledOptions", "navigateDisabledOptions"] }, outputs: { valueChange: "cdkListboxValueChange" }, host: { attributes: { "role": "listbox" }, listeners: { "focus": "_handleFocus()", "keydown": "_handleKeydown($event)", "focusout": "_handleFocusOut($event)", "focusin": "_handleFocusIn()" }, properties: { "id": "id", "attr.tabindex": "_getTabIndex()", "attr.aria-disabled": "disabled", "attr.aria-multiselectable": "multiple", "attr.aria-activedescendant": "_getAriaActiveDescendant()", "attr.aria-orientation": "orientation" }, classAttribute: "cdk-listbox" }, providers: [
783 {
784 provide: NG_VALUE_ACCESSOR,
785 useExisting: forwardRef(() => CdkListbox),
786 multi: true,
787 },
788 ], queries: [{ propertyName: "options", predicate: CdkOption, descendants: true }], exportAs: ["cdkListbox"], ngImport: i0 }); }
789}
790i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkListbox, decorators: [{
791 type: Directive,
792 args: [{
793 selector: '[cdkListbox]',
794 standalone: true,
795 exportAs: 'cdkListbox',
796 host: {
797 'role': 'listbox',
798 'class': 'cdk-listbox',
799 '[id]': 'id',
800 '[attr.tabindex]': '_getTabIndex()',
801 '[attr.aria-disabled]': 'disabled',
802 '[attr.aria-multiselectable]': 'multiple',
803 '[attr.aria-activedescendant]': '_getAriaActiveDescendant()',
804 '[attr.aria-orientation]': 'orientation',
805 '(focus)': '_handleFocus()',
806 '(keydown)': '_handleKeydown($event)',
807 '(focusout)': '_handleFocusOut($event)',
808 '(focusin)': '_handleFocusIn()',
809 },
810 providers: [
811 {
812 provide: NG_VALUE_ACCESSOR,
813 useExisting: forwardRef(() => CdkListbox),
814 multi: true,
815 },
816 ],
817 }]
818 }], propDecorators: { id: [{
819 type: Input
820 }], enabledTabIndex: [{
821 type: Input,
822 args: ['tabindex']
823 }], value: [{
824 type: Input,
825 args: ['cdkListboxValue']
826 }], multiple: [{
827 type: Input,
828 args: ['cdkListboxMultiple']
829 }], disabled: [{
830 type: Input,
831 args: ['cdkListboxDisabled']
832 }], useActiveDescendant: [{
833 type: Input,
834 args: ['cdkListboxUseActiveDescendant']
835 }], orientation: [{
836 type: Input,
837 args: ['cdkListboxOrientation']
838 }], compareWith: [{
839 type: Input,
840 args: ['cdkListboxCompareWith']
841 }], navigationWrapDisabled: [{
842 type: Input,
843 args: ['cdkListboxNavigationWrapDisabled']
844 }], navigateDisabledOptions: [{
845 type: Input,
846 args: ['cdkListboxNavigatesDisabledOptions']
847 }], valueChange: [{
848 type: Output,
849 args: ['cdkListboxValueChange']
850 }], options: [{
851 type: ContentChildren,
852 args: [CdkOption, { descendants: true }]
853 }] } });
854
855const EXPORTED_DECLARATIONS = [CdkListbox, CdkOption];
856class CdkListboxModule {
857 static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkListboxModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
858 static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "16.0.0", ngImport: i0, type: CdkListboxModule, imports: [CdkListbox, CdkOption], exports: [CdkListbox, CdkOption] }); }
859 static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkListboxModule }); }
860}
861i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkListboxModule, decorators: [{
862 type: NgModule,
863 args: [{
864 imports: [...EXPORTED_DECLARATIONS],
865 exports: [...EXPORTED_DECLARATIONS],
866 }]
867 }] });
868
869/**
870 * Generated bundle index. Do not edit.
871 */
872
873export { CdkListbox, CdkListboxModule, CdkOption };
874//# sourceMappingURL=listbox.mjs.map