@angular/cdk-experimental 14.0.3 → 14.1.0-next.2

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,15 +1,14 @@
1
1
  import * as i0 from '@angular/core';
2
- import { forwardRef, EventEmitter, Directive, Inject, Input, Output, Optional, ContentChildren, NgModule } from '@angular/core';
2
+ import { inject, ElementRef, Directive, Input, ChangeDetectorRef, InjectFlags, forwardRef, Output, ContentChildren, NgModule } from '@angular/core';
3
3
  import { ActiveDescendantKeyManager } from '@angular/cdk/a11y';
4
4
  import { SPACE, ENTER, UP_ARROW, DOWN_ARROW, LEFT_ARROW, RIGHT_ARROW } from '@angular/cdk/keycodes';
5
5
  import { coerceBooleanProperty, coerceArray } from '@angular/cdk/coercion';
6
6
  import { SelectionModel } from '@angular/cdk/collections';
7
- import { defer, merge, Subject } from 'rxjs';
8
- import { startWith, switchMap, takeUntil } from 'rxjs/operators';
9
- import { NG_VALUE_ACCESSOR } from '@angular/forms';
10
- import * as i2 from '@angular/cdk-experimental/combobox';
11
- import { CDK_COMBOBOX } from '@angular/cdk-experimental/combobox';
12
- import * as i1 from '@angular/cdk/bidi';
7
+ import { Subject, BehaviorSubject, defer, merge, combineLatest } from 'rxjs';
8
+ import { startWith, switchMap, mapTo, takeUntil, filter, take } from 'rxjs/operators';
9
+ import { Validators, NG_VALUE_ACCESSOR, NG_VALIDATORS } from '@angular/forms';
10
+ import { Directionality } from '@angular/cdk/bidi';
11
+ import { CdkCombobox } from '@angular/cdk-experimental/combobox';
13
12
 
14
13
  /**
15
14
  * @license
@@ -18,116 +17,117 @@ import * as i1 from '@angular/cdk/bidi';
18
17
  * Use of this source code is governed by an MIT-style license that can be
19
18
  * found in the LICENSE file at https://angular.io/license
20
19
  */
20
+ /** The next id to use for creating unique DOM IDs. */
21
21
  let nextId = 0;
22
- let listboxId = 0;
23
- const CDK_LISTBOX_VALUE_ACCESSOR = {
24
- provide: NG_VALUE_ACCESSOR,
25
- useExisting: forwardRef(() => CdkListbox),
26
- multi: true,
27
- };
22
+ // TODO(mmalerba):
23
+ // - should listbox wrap be configurable?
24
+ // - should skipping disabled options be configurable?
25
+ /** A selectable option in a listbox. */
28
26
  class CdkOption {
29
- constructor(_elementRef, listbox) {
30
- this._elementRef = _elementRef;
31
- this.listbox = listbox;
32
- this._selected = false;
27
+ constructor() {
28
+ this._generatedId = `cdk-option-${nextId++}`;
33
29
  this._disabled = false;
30
+ /** The option's host element */
31
+ this.element = inject(ElementRef).nativeElement;
32
+ /** The parent listbox this option belongs to. */
33
+ this.listbox = inject(CdkListbox);
34
+ /** Emits when the option is destroyed. */
35
+ this.destroyed = new Subject();
36
+ /** Emits when the option is clicked. */
37
+ this._clicked = new Subject();
38
+ /** Whether the option is currently active. */
34
39
  this._active = false;
35
- /** The id of the option, set to a uniqueid if the user does not provide one. */
36
- this.id = `cdk-option-${nextId++}`;
37
- this.selectionChange = new EventEmitter();
38
40
  }
39
- get selected() {
40
- return this._selected;
41
+ /** The id of the option's host element. */
42
+ get id() {
43
+ return this._id || this._generatedId;
41
44
  }
42
- set selected(value) {
43
- if (!this._disabled) {
44
- this._selected = coerceBooleanProperty(value);
45
- }
45
+ set id(value) {
46
+ this._id = value;
46
47
  }
48
+ /** Whether this option is disabled. */
47
49
  get disabled() {
48
- return this._disabled;
50
+ return this.listbox.disabled || this._disabled;
49
51
  }
50
52
  set disabled(value) {
51
53
  this._disabled = coerceBooleanProperty(value);
52
54
  }
53
- /** The form value of the option. */
54
- get value() {
55
- return this._value;
55
+ /** The tabindex of the option when it is enabled. */
56
+ get enabledTabIndex() {
57
+ return this._enabledTabIndex === undefined
58
+ ? this.listbox.enabledTabIndex
59
+ : this._enabledTabIndex;
56
60
  }
57
- set value(value) {
58
- if (this.selected && value !== this._value) {
59
- this.deselect();
60
- }
61
- this._value = value;
61
+ set enabledTabIndex(value) {
62
+ this._enabledTabIndex = value;
62
63
  }
63
- /** Toggles the selected state, emits a change event through the injected listbox. */
64
- toggle() {
65
- if (!this._isInteractionDisabled()) {
66
- this.selected = !this.selected;
67
- this._emitSelectionChange(true);
68
- }
64
+ ngOnDestroy() {
65
+ this.destroyed.next();
66
+ this.destroyed.complete();
69
67
  }
70
- /** Sets the active property true if the option and listbox aren't disabled. */
71
- activate() {
72
- if (!this._isInteractionDisabled()) {
73
- this._active = true;
74
- }
68
+ /** Whether this option is selected. */
69
+ isSelected() {
70
+ return this.listbox.isSelected(this.value);
75
71
  }
76
- /** Sets the active property false. */
77
- deactivate() {
78
- if (!this._isInteractionDisabled()) {
79
- this._active = false;
80
- }
72
+ /** Whether this option is active. */
73
+ isActive() {
74
+ return this._active;
81
75
  }
82
- /** Sets the selected property true if it was false. */
76
+ /** Toggle the selected state of this option. */
77
+ toggle() {
78
+ this.listbox.toggle(this);
79
+ }
80
+ /** Select this option if it is not selected. */
83
81
  select() {
84
- if (!this.selected) {
85
- this.selected = true;
86
- this._emitSelectionChange();
87
- }
82
+ this.listbox.select(this);
88
83
  }
89
- /** Sets the selected property false if it was true. */
84
+ /** Deselect this option if it is selected. */
90
85
  deselect() {
91
- if (this.selected) {
92
- this.selected = false;
93
- this._emitSelectionChange();
94
- }
86
+ this.listbox.deselect(this);
95
87
  }
96
- /** Applies focus to the option. */
88
+ /** Focus this option. */
97
89
  focus() {
98
- this._elementRef.nativeElement.focus();
99
- }
100
- /** Returns true if the option or listbox are disabled, and false otherwise. */
101
- _isInteractionDisabled() {
102
- return this.listbox.disabled || this._disabled;
103
- }
104
- /** Emits a change event extending the Option Selection Change Event interface. */
105
- _emitSelectionChange(isUserInput = false) {
106
- this.selectionChange.emit({
107
- source: this,
108
- isUserInput: isUserInput,
109
- });
110
- }
111
- /** Returns the tab index which depends on the disabled property. */
112
- _getTabIndex() {
113
- return this._isInteractionDisabled() ? null : '-1';
90
+ this.element.focus();
114
91
  }
115
92
  /** Get the label for this element which is required by the FocusableOption interface. */
116
93
  getLabel() {
117
94
  var _a, _b;
118
- return ((_a = this.typeahead) !== null && _a !== void 0 ? _a : (_b = this._elementRef.nativeElement.textContent) === null || _b === void 0 ? void 0 : _b.trim()) || '';
95
+ return ((_a = this.typeaheadLabel) !== null && _a !== void 0 ? _a : (_b = this.element.textContent) === null || _b === void 0 ? void 0 : _b.trim()) || '';
119
96
  }
120
- /** Sets the active property to true to enable the active css class. */
97
+ /**
98
+ * Set the option as active.
99
+ * @docs-private
100
+ */
121
101
  setActiveStyles() {
122
102
  this._active = true;
123
103
  }
124
- /** Sets the active property to false to disable the active css class. */
104
+ /**
105
+ * Set the option as inactive.
106
+ * @docs-private
107
+ */
125
108
  setInactiveStyles() {
126
109
  this._active = false;
127
110
  }
111
+ /** Handle focus events on the option. */
112
+ _handleFocus() {
113
+ // Options can wind up getting focused in active descendant mode if the user clicks on them.
114
+ // In this case, we push focus back to the parent listbox to prevent an extra tab stop when
115
+ // the user performs a shift+tab.
116
+ if (this.listbox.useActiveDescendant) {
117
+ this.listbox._setActiveOption(this);
118
+ this.listbox.focus();
119
+ }
120
+ }
121
+ /** Get the tabindex for this option. */
122
+ _getTabIndex() {
123
+ if (this.listbox.useActiveDescendant || this.disabled) {
124
+ return -1;
125
+ }
126
+ return this.isActive() ? this.enabledTabIndex : -1;
127
+ }
128
128
  }
129
- CdkOption.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.0.1", ngImport: i0, type: CdkOption, deps: [{ token: i0.ElementRef }, { token: forwardRef(() => CdkListbox) }], target: i0.ɵɵFactoryTarget.Directive });
130
- CdkOption.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "14.0.1", type: CdkOption, selector: "[cdkOption]", inputs: { id: "id", selected: "selected", disabled: "disabled", value: "value", typeahead: "typeahead" }, outputs: { selectionChange: "selectionChange" }, host: { attributes: { "role": "option" }, listeners: { "click": "toggle()", "focus": "activate()", "blur": "deactivate()" }, properties: { "id": "id", "attr.aria-selected": "selected || null", "attr.tabindex": "_getTabIndex()", "attr.aria-disabled": "_isInteractionDisabled()", "class.cdk-option-disabled": "_isInteractionDisabled()", "class.cdk-option-active": "_active", "class.cdk-option-selected": "selected" }, classAttribute: "cdk-option" }, exportAs: ["cdkOption"], ngImport: i0 });
129
+ CdkOption.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.0.1", ngImport: i0, type: CdkOption, deps: [], target: i0.ɵɵFactoryTarget.Directive });
130
+ CdkOption.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "14.0.1", type: CdkOption, 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()", "focus": "_handleFocus()" }, properties: { "id": "id", "attr.aria-selected": "isSelected() || null", "attr.tabindex": "_getTabIndex()", "attr.aria-disabled": "disabled", "class.cdk-option-disabled": "disabled", "class.cdk-option-active": "isActive()", "class.cdk-option-selected": "isSelected()" }, classAttribute: "cdk-option" }, exportAs: ["cdkOption"], ngImport: i0 });
131
131
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.0.1", ngImport: i0, type: CdkOption, decorators: [{
132
132
  type: Directive,
133
133
  args: [{
@@ -136,72 +136,134 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.0.1", ngImpor
136
136
  host: {
137
137
  'role': 'option',
138
138
  'class': 'cdk-option',
139
- '(click)': 'toggle()',
140
- '(focus)': 'activate()',
141
- '(blur)': 'deactivate()',
142
139
  '[id]': 'id',
143
- '[attr.aria-selected]': 'selected || null',
140
+ '[attr.aria-selected]': 'isSelected() || null',
144
141
  '[attr.tabindex]': '_getTabIndex()',
145
- '[attr.aria-disabled]': '_isInteractionDisabled()',
146
- '[class.cdk-option-disabled]': '_isInteractionDisabled()',
147
- '[class.cdk-option-active]': '_active',
148
- '[class.cdk-option-selected]': 'selected',
142
+ '[attr.aria-disabled]': 'disabled',
143
+ '[class.cdk-option-disabled]': 'disabled',
144
+ '[class.cdk-option-active]': 'isActive()',
145
+ '[class.cdk-option-selected]': 'isSelected()',
146
+ '(click)': '_clicked.next()',
147
+ '(focus)': '_handleFocus()',
149
148
  },
150
149
  }]
151
- }], ctorParameters: function () {
152
- return [{ type: i0.ElementRef }, { type: CdkListbox, decorators: [{
153
- type: Inject,
154
- args: [forwardRef(() => CdkListbox)]
155
- }] }];
156
- }, propDecorators: { id: [{
157
- type: Input
158
- }], selected: [{
159
- type: Input
160
- }], disabled: [{
150
+ }], propDecorators: { id: [{
161
151
  type: Input
162
152
  }], value: [{
163
- type: Input
164
- }], typeahead: [{
165
- type: Input
166
- }], selectionChange: [{
167
- type: Output
153
+ type: Input,
154
+ args: ['cdkOption']
155
+ }], typeaheadLabel: [{
156
+ type: Input,
157
+ args: ['cdkOptionTypeaheadLabel']
158
+ }], disabled: [{
159
+ type: Input,
160
+ args: ['cdkOptionDisabled']
161
+ }], enabledTabIndex: [{
162
+ type: Input,
163
+ args: ['tabindex']
168
164
  }] } });
169
165
  class CdkListbox {
170
- constructor(_combobox, _dir) {
171
- this._combobox = _combobox;
172
- this._dir = _dir;
173
- this._tabIndex = 0;
174
- /** `View -> model callback called when select has been touched` */
175
- this._onTouched = () => { };
176
- /** `View -> model callback called when value changes` */
177
- this._onChange = () => { };
178
- this.optionSelectionChanges = defer(() => {
179
- const options = this._options;
180
- return options.changes.pipe(startWith(options), switchMap(() => merge(...options.map(option => option.selectionChange))));
181
- });
182
- this._disabled = false;
166
+ constructor() {
167
+ this._generatedId = `cdk-listbox-${nextId++}`;
183
168
  this._multiple = false;
169
+ this._disabled = false;
184
170
  this._useActiveDescendant = false;
185
- this._autoFocus = true;
186
- this._destroyed = new Subject();
187
- this.selectionChange = new EventEmitter();
188
- this.id = `cdk-listbox-${listboxId++}`;
189
- /** Determines the orientation for the list key manager. Affects keyboard interaction. */
171
+ /** The orientation of the listbox. Only affects keyboard interaction, not visual layout. */
190
172
  this.orientation = 'vertical';
191
- this.compareWith = (a1, a2) => a1 === a2;
173
+ /** Emits when the selected value(s) in the listbox change. */
174
+ this.valueChange = new Subject();
175
+ // TODO(mmalerba): Refactor SelectionModel so that its not necessary to create new instances
176
+ /** The selection model used by the listbox. */
177
+ this.selectionModelSubject = new BehaviorSubject(new SelectionModel(this.multiple, [], true, this._compareWith));
178
+ /** Emits when the listbox is destroyed. */
179
+ this.destroyed = new Subject();
180
+ /** The host element of the listbox. */
181
+ this.element = inject(ElementRef).nativeElement;
182
+ /** The change detector for this listbox. */
183
+ this.changeDetectorRef = inject(ChangeDetectorRef);
184
+ /** Callback called when the listbox has been touched */
185
+ this._onTouched = () => { };
186
+ /** Callback called when the listbox value changes */
187
+ this._onChange = () => { };
188
+ /** Callback called when the form validator changes. */
189
+ this._onValidatorChange = () => { };
190
+ /** Emits when an option has been clicked. */
191
+ this._optionClicked = defer(() => this.options.changes.pipe(startWith(this.options), switchMap(options => merge(...options.map(option => option._clicked.pipe(mapTo(option)))))));
192
+ /** The directionality of the page. */
193
+ this._dir = inject(Directionality, InjectFlags.Optional);
194
+ // TODO(mmalerba): Should not depend on combobox
195
+ this._combobox = inject(CdkCombobox, InjectFlags.Optional);
196
+ /**
197
+ * Validator that produces an error if multiple values are selected in a single selection
198
+ * listbox.
199
+ * @param control The control to validate
200
+ * @return A validation error or null
201
+ */
202
+ this._validateMultipleValues = (control) => {
203
+ const controlValue = this._coerceValue(control.value);
204
+ if (!this.multiple && controlValue.length > 1) {
205
+ return { 'cdkListboxMultipleValues': true };
206
+ }
207
+ return null;
208
+ };
209
+ /**
210
+ * Validator that produces an error if any selected values are not valid options for this listbox.
211
+ * @param control The control to validate
212
+ * @return A validation error or null
213
+ */
214
+ this._validateInvalidValues = (control) => {
215
+ const controlValue = this._coerceValue(control.value);
216
+ const invalidValues = this._getValuesWithValidity(controlValue, false);
217
+ if (invalidValues.length) {
218
+ return { 'cdkListboxInvalidValues': { 'values': invalidValues } };
219
+ }
220
+ return null;
221
+ };
222
+ /** The combined set of validators for this listbox. */
223
+ this._validators = Validators.compose([
224
+ this._validateMultipleValues,
225
+ this._validateInvalidValues,
226
+ ]);
227
+ this.selectionModelSubject
228
+ .pipe(switchMap(selectionModel => selectionModel.changed), takeUntil(this.destroyed))
229
+ .subscribe(() => {
230
+ this._updateInternalValue();
231
+ });
232
+ }
233
+ /** The id of the option's host element. */
234
+ get id() {
235
+ return this._id || this._generatedId;
236
+ }
237
+ set id(value) {
238
+ this._id = value;
239
+ }
240
+ /** The tabindex to use when the listbox is enabled. */
241
+ get enabledTabIndex() {
242
+ return this._enabledTabIndex === undefined ? 0 : this._enabledTabIndex;
243
+ }
244
+ set enabledTabIndex(value) {
245
+ this._enabledTabIndex = value;
246
+ }
247
+ /** The value selected in the listbox, represented as an array of option values. */
248
+ get value() {
249
+ return this.selectionModel().selected;
250
+ }
251
+ set value(value) {
252
+ this._setSelection(value);
192
253
  }
193
254
  /**
194
- * Whether the listbox allows multiple options to be selected.
195
- * If `multiple` switches from `true` to `false`, all options are deselected.
255
+ * Whether the listbox allows multiple options to be selected. If the value switches from `true`
256
+ * to `false`, and more than one option is selected, all options are deselected.
196
257
  */
197
258
  get multiple() {
198
259
  return this._multiple;
199
260
  }
200
261
  set multiple(value) {
201
- const coercedValue = coerceBooleanProperty(value);
202
- this._updateSelectionOnMultiSelectionChange(coercedValue);
203
- this._multiple = coercedValue;
262
+ this._multiple = coerceBooleanProperty(value);
263
+ this._updateSelectionModel();
264
+ this._onValidatorChange();
204
265
  }
266
+ /** Whether the listbox is disabled. */
205
267
  get disabled() {
206
268
  return this._disabled;
207
269
  }
@@ -215,231 +277,405 @@ class CdkListbox {
215
277
  set useActiveDescendant(shouldUseActiveDescendant) {
216
278
  this._useActiveDescendant = coerceBooleanProperty(shouldUseActiveDescendant);
217
279
  }
218
- /** Whether on focus the listbox will focus its active option, default to true. */
219
- get autoFocus() {
220
- return this._autoFocus;
221
- }
222
- set autoFocus(shouldAutoFocus) {
223
- this._autoFocus = coerceBooleanProperty(shouldAutoFocus);
280
+ /** The function used to compare option values. */
281
+ get compareWith() {
282
+ return this._compareWith;
224
283
  }
225
- ngOnInit() {
226
- this._selectionModel = new SelectionModel(this.multiple);
284
+ set compareWith(fn) {
285
+ this._compareWith = fn;
286
+ this._updateSelectionModel();
227
287
  }
228
288
  ngAfterContentInit() {
229
289
  var _a;
290
+ if (typeof ngDevMode === 'undefined' || ngDevMode) {
291
+ this._verifyNoOptionValueCollisions();
292
+ }
230
293
  this._initKeyManager();
231
- this._initSelectionModel();
232
294
  (_a = this._combobox) === null || _a === void 0 ? void 0 : _a._registerContent(this.id, 'listbox');
233
- this.optionSelectionChanges.subscribe(event => {
234
- this._emitChangeEvent(event.source);
235
- this._updateSelectionModel(event.source);
236
- this.setActiveOption(event.source);
237
- this._updatePanelForSelection(event.source);
295
+ this.options.changes.pipe(takeUntil(this.destroyed)).subscribe(() => {
296
+ this._updateInternalValue();
297
+ this._onValidatorChange();
238
298
  });
299
+ this._optionClicked
300
+ .pipe(filter(option => !option.disabled), takeUntil(this.destroyed))
301
+ .subscribe(option => this._handleOptionClicked(option));
239
302
  }
240
303
  ngOnDestroy() {
241
- this._listKeyManager.change.complete();
242
- this._destroyed.next();
243
- this._destroyed.complete();
304
+ this.listKeyManager.change.complete();
305
+ this.destroyed.next();
306
+ this.destroyed.complete();
244
307
  }
245
- _initKeyManager() {
246
- var _a;
247
- this._listKeyManager = new ActiveDescendantKeyManager(this._options)
248
- .withWrap()
249
- .withTypeAhead()
250
- .withHomeAndEnd()
251
- .withAllowedModifierKeys(['shiftKey']);
252
- if (this.orientation === 'vertical') {
253
- this._listKeyManager.withVerticalOrientation();
308
+ /**
309
+ * Toggle the selected state of the given option.
310
+ * @param option The option to toggle
311
+ */
312
+ toggle(option) {
313
+ this.toggleValue(option.value);
314
+ }
315
+ /**
316
+ * Toggle the selected state of the given value.
317
+ * @param value The value to toggle
318
+ */
319
+ toggleValue(value) {
320
+ this.selectionModel().toggle(value);
321
+ }
322
+ /**
323
+ * Select the given option.
324
+ * @param option The option to select
325
+ */
326
+ select(option) {
327
+ this.selectValue(option.value);
328
+ }
329
+ /**
330
+ * Select the given value.
331
+ * @param value The value to select
332
+ */
333
+ selectValue(value) {
334
+ this.selectionModel().select(value);
335
+ }
336
+ /**
337
+ * Deselect the given option.
338
+ * @param option The option to deselect
339
+ */
340
+ deselect(option) {
341
+ this.deselectValue(option.value);
342
+ }
343
+ /**
344
+ * Deselect the given value.
345
+ * @param value The value to deselect
346
+ */
347
+ deselectValue(value) {
348
+ this.selectionModel().deselect(value);
349
+ }
350
+ /**
351
+ * Set the selected state of all options.
352
+ * @param isSelected The new selected state to set
353
+ */
354
+ setAllSelected(isSelected) {
355
+ if (!isSelected) {
356
+ this.selectionModel().clear();
254
357
  }
255
358
  else {
256
- this._listKeyManager.withHorizontalOrientation(((_a = this._dir) === null || _a === void 0 ? void 0 : _a.value) || 'ltr');
359
+ this.selectionModel().select(...this.options.toArray().map(option => option.value));
257
360
  }
258
- this._listKeyManager.change.pipe(takeUntil(this._destroyed)).subscribe(() => {
259
- this._updateActiveOption();
260
- });
261
361
  }
262
- _initSelectionModel() {
263
- this._selectionModel.changed
264
- .pipe(takeUntil(this._destroyed))
265
- .subscribe((event) => {
266
- for (const option of event.added) {
267
- option.selected = true;
362
+ /**
363
+ * Get whether the given option is selected.
364
+ * @param option The option to get the selected state of
365
+ */
366
+ isSelected(option) {
367
+ return this.selectionModel().isSelected(option instanceof CdkOption ? option.value : option);
368
+ }
369
+ /**
370
+ * Registers a callback to be invoked when the listbox's value changes from user input.
371
+ * @param fn The callback to register
372
+ * @docs-private
373
+ */
374
+ registerOnChange(fn) {
375
+ this._onChange = fn;
376
+ }
377
+ /**
378
+ * Registers a callback to be invoked when the listbox is blurred by the user.
379
+ * @param fn The callback to register
380
+ * @docs-private
381
+ */
382
+ registerOnTouched(fn) {
383
+ this._onTouched = fn;
384
+ }
385
+ /**
386
+ * Sets the listbox's value.
387
+ * @param value The new value of the listbox
388
+ * @docs-private
389
+ */
390
+ writeValue(value) {
391
+ this._setSelection(value);
392
+ }
393
+ /**
394
+ * Sets the disabled state of the listbox.
395
+ * @param isDisabled The new disabled state
396
+ * @docs-private
397
+ */
398
+ setDisabledState(isDisabled) {
399
+ this.disabled = isDisabled;
400
+ }
401
+ /**
402
+ * Validate the given control
403
+ * @docs-private
404
+ */
405
+ validate(control) {
406
+ return this._validators(control);
407
+ }
408
+ /**
409
+ * Registers a callback to be called when the form validator changes.
410
+ * @param fn The callback to call
411
+ * @docs-private
412
+ */
413
+ registerOnValidatorChange(fn) {
414
+ this._onValidatorChange = fn;
415
+ }
416
+ /** Focus the listbox's host element. */
417
+ focus() {
418
+ this.element.focus();
419
+ }
420
+ /** The selection model used to track the listbox's value. */
421
+ selectionModel() {
422
+ return this.selectionModelSubject.value;
423
+ }
424
+ /**
425
+ * Triggers the given option in response to user interaction.
426
+ * - In single selection mode: selects the option and deselects any other selected option.
427
+ * - In multi selection mode: toggles the selected state of the option.
428
+ * @param option The option to trigger
429
+ */
430
+ triggerOption(option) {
431
+ if (option && !option.disabled) {
432
+ let changed = false;
433
+ this.selectionModel()
434
+ .changed.pipe(take(1), takeUntil(this.destroyed))
435
+ .subscribe(() => (changed = true));
436
+ if (this.multiple) {
437
+ this.toggle(option);
268
438
  }
269
- for (const option of event.removed) {
270
- option.selected = false;
439
+ else {
440
+ this.select(option);
271
441
  }
272
- });
442
+ if (changed) {
443
+ this._onChange(this.value);
444
+ this.valueChange.next({
445
+ value: this.value,
446
+ listbox: this,
447
+ option: option,
448
+ });
449
+ }
450
+ }
451
+ }
452
+ /**
453
+ * Sets the given option as active.
454
+ * @param option The option to make active
455
+ */
456
+ _setActiveOption(option) {
457
+ this.listKeyManager.setActiveItem(option);
458
+ }
459
+ /** Called when the listbox receives focus. */
460
+ _handleFocus() {
461
+ if (!this.useActiveDescendant) {
462
+ this.listKeyManager.setNextItemActive();
463
+ this._focusActiveOption();
464
+ }
273
465
  }
274
- _keydown(event) {
466
+ /** Called when the user presses keydown on the listbox. */
467
+ _handleKeydown(event) {
275
468
  if (this._disabled) {
276
469
  return;
277
470
  }
278
- const manager = this._listKeyManager;
279
471
  const { keyCode } = event;
280
- const previousActiveIndex = manager.activeItemIndex;
472
+ const previousActiveIndex = this.listKeyManager.activeItemIndex;
281
473
  if (keyCode === SPACE || keyCode === ENTER) {
282
- if (manager.activeItem && !manager.isTyping()) {
283
- this._toggleActiveOption();
284
- }
474
+ this.triggerOption(this.listKeyManager.activeItem);
285
475
  event.preventDefault();
286
476
  }
287
477
  else {
288
- manager.onKeydown(event);
478
+ this.listKeyManager.onKeydown(event);
289
479
  }
290
480
  /** Will select an option if shift was pressed while navigating to the option */
291
481
  const isArrow = keyCode === UP_ARROW ||
292
482
  keyCode === DOWN_ARROW ||
293
483
  keyCode === LEFT_ARROW ||
294
484
  keyCode === RIGHT_ARROW;
295
- if (isArrow && event.shiftKey && previousActiveIndex !== this._listKeyManager.activeItemIndex) {
296
- this._toggleActiveOption();
297
- }
298
- }
299
- /** Emits a selection change event, called when an option has its selected state changed. */
300
- _emitChangeEvent(option) {
301
- this.selectionChange.emit({
302
- source: this,
303
- option: option,
304
- });
305
- }
306
- /** Updates the selection model after a toggle. */
307
- _updateSelectionModel(option) {
308
- if (!this.multiple && this._selectionModel.selected.length !== 0) {
309
- const previouslySelected = this._selectionModel.selected[0];
310
- this.deselect(previouslySelected);
311
- }
312
- option.selected ? this._selectionModel.select(option) : this._selectionModel.deselect(option);
313
- }
314
- _updatePanelForSelection(option) {
315
- if (this._combobox) {
316
- if (!this.multiple) {
317
- this._combobox.updateAndClose(option.selected ? option.value : []);
318
- }
319
- else {
320
- this._combobox.updateAndClose(this.getSelectedValues());
321
- }
485
+ if (isArrow && event.shiftKey && previousActiveIndex !== this.listKeyManager.activeItemIndex) {
486
+ this.triggerOption(this.listKeyManager.activeItem);
322
487
  }
323
488
  }
324
- /** Toggles the selected state of the active option if not disabled. */
325
- _toggleActiveOption() {
326
- const activeOption = this._listKeyManager.activeItem;
327
- if (activeOption && !activeOption.disabled) {
328
- activeOption.toggle();
489
+ /**
490
+ * Called when the focus leaves an element in the listbox.
491
+ * @param event The focusout event
492
+ */
493
+ _handleFocusOut(event) {
494
+ const otherElement = event.relatedTarget;
495
+ if (this.element !== otherElement && !this.element.contains(otherElement)) {
496
+ this._onTouched();
329
497
  }
330
498
  }
331
- /** Returns the id of the active option if active descendant is being used. */
499
+ /** Get the id of the active option if active descendant is being used. */
332
500
  _getAriaActiveDescendant() {
333
501
  var _a, _b;
334
- return this._useActiveDescendant ? (_b = (_a = this._listKeyManager) === null || _a === void 0 ? void 0 : _a.activeItem) === null || _b === void 0 ? void 0 : _b.id : null;
502
+ return this._useActiveDescendant ? (_b = (_a = this.listKeyManager) === null || _a === void 0 ? void 0 : _a.activeItem) === null || _b === void 0 ? void 0 : _b.id : null;
335
503
  }
336
- /** Updates the activeOption and the active and focus properties of the option. */
337
- _updateActiveOption() {
338
- var _a;
339
- if (!this._listKeyManager.activeItem) {
340
- return;
341
- }
342
- (_a = this._activeOption) === null || _a === void 0 ? void 0 : _a.deactivate();
343
- this._activeOption = this._listKeyManager.activeItem;
344
- this._activeOption.activate();
345
- if (!this.useActiveDescendant) {
346
- this._activeOption.focus();
504
+ /** Get the tabindex for the listbox. */
505
+ _getTabIndex() {
506
+ if (this.disabled) {
507
+ return -1;
347
508
  }
509
+ return this.useActiveDescendant || !this.listKeyManager.activeItem ? this.enabledTabIndex : -1;
348
510
  }
349
- /** Updates selection states of options when the 'multiple' property changes. */
350
- _updateSelectionOnMultiSelectionChange(value) {
511
+ /** Initialize the key manager. */
512
+ _initKeyManager() {
351
513
  var _a;
352
- if (this.multiple && !value) {
353
- // Deselect all options instead of arbitrarily keeping one of the selected options.
354
- this.setAllSelected(false);
355
- }
356
- else if (!this.multiple && value) {
357
- this._selectionModel = new SelectionModel(value, (_a = this._selectionModel) === null || _a === void 0 ? void 0 : _a.selected);
358
- }
359
- }
360
- _focusActiveOption() {
361
- if (!this.autoFocus) {
362
- return;
363
- }
364
- if (this._listKeyManager.activeItem) {
365
- this.setActiveOption(this._listKeyManager.activeItem);
514
+ this.listKeyManager = new ActiveDescendantKeyManager(this.options)
515
+ .withWrap()
516
+ .withTypeAhead()
517
+ .withHomeAndEnd()
518
+ .withAllowedModifierKeys(['shiftKey']);
519
+ if (this.orientation === 'vertical') {
520
+ this.listKeyManager.withVerticalOrientation();
366
521
  }
367
- else if (this._options.first) {
368
- this.setActiveOption(this._options.first);
522
+ else {
523
+ this.listKeyManager.withHorizontalOrientation(((_a = this._dir) === null || _a === void 0 ? void 0 : _a.value) || 'ltr');
369
524
  }
525
+ this.listKeyManager.change
526
+ .pipe(takeUntil(this.destroyed))
527
+ .subscribe(() => this._focusActiveOption());
370
528
  }
371
- /** Selects the given option if the option and listbox aren't disabled. */
372
- select(option) {
373
- if (!this.disabled && !option.disabled) {
374
- option.select();
529
+ // TODO(mmalerba): Should not depend on combobox.
530
+ _updatePanelForSelection(option) {
531
+ if (this._combobox) {
532
+ if (!this.multiple) {
533
+ this._combobox.updateAndClose(option.isSelected() ? option.value : []);
534
+ }
535
+ else {
536
+ this._combobox.updateAndClose(this.value);
537
+ }
375
538
  }
376
539
  }
377
- /** Deselects the given option if the option and listbox aren't disabled. */
378
- deselect(option) {
379
- if (!this.disabled && !option.disabled) {
380
- option.deselect();
381
- }
540
+ /** Update the selection mode when the 'multiple' property changes. */
541
+ _updateSelectionModel() {
542
+ this.selectionModelSubject.next(new SelectionModel(this.multiple, !this.multiple && this.value.length > 1 ? [] : this.value.slice(), true, this._compareWith));
382
543
  }
383
- /** Sets the selected state of all options to be the given value. */
384
- setAllSelected(isSelected) {
385
- for (const option of this._options.toArray()) {
386
- isSelected ? this.select(option) : this.deselect(option);
544
+ /** Focus the active option. */
545
+ _focusActiveOption() {
546
+ var _a;
547
+ if (!this.useActiveDescendant) {
548
+ (_a = this.listKeyManager.activeItem) === null || _a === void 0 ? void 0 : _a.focus();
387
549
  }
388
- }
389
- /** Updates the key manager's active item to the given option. */
390
- setActiveOption(option) {
391
- this._listKeyManager.updateActiveItem(option);
392
- this._updateActiveOption();
550
+ this.changeDetectorRef.markForCheck();
393
551
  }
394
552
  /**
395
- * Saves a callback function to be invoked when the select's value
396
- * changes from user input. Required to implement ControlValueAccessor.
553
+ * Set the selected values.
554
+ * @param value The list of new selected values.
397
555
  */
398
- registerOnChange(fn) {
399
- this._onChange = fn;
556
+ _setSelection(value) {
557
+ const coercedValue = this._coerceValue(value);
558
+ this.selectionModel().setSelection(...(!this.multiple && coercedValue.length > 1
559
+ ? []
560
+ : this._getValuesWithValidity(coercedValue, true)));
561
+ }
562
+ /** Update the internal value of the listbox based on the selection model. */
563
+ _updateInternalValue() {
564
+ const indexCache = new Map();
565
+ // Check if we need to remove any values due to them becoming invalid
566
+ // (e.g. if the option was removed from the DOM.)
567
+ const selected = this.selectionModel().selected;
568
+ const validSelected = this._getValuesWithValidity(selected, true);
569
+ if (validSelected.length != selected.length) {
570
+ this.selectionModel().setSelection(...validSelected);
571
+ }
572
+ this.selectionModel().sort((a, b) => {
573
+ const aIndex = this._getIndexForValue(indexCache, a);
574
+ const bIndex = this._getIndexForValue(indexCache, b);
575
+ return aIndex - bIndex;
576
+ });
577
+ this.changeDetectorRef.markForCheck();
400
578
  }
401
579
  /**
402
- * Saves a callback function to be invoked when the select is blurred
403
- * by the user. Required to implement ControlValueAccessor.
580
+ * Gets the index of the given value in the given list of options.
581
+ * @param cache The cache of indices found so far
582
+ * @param value The value to find
583
+ * @return The index of the value in the options list
404
584
  */
405
- registerOnTouched(fn) {
406
- this._onTouched = fn;
407
- }
408
- /** Sets the select's value. Required to implement ControlValueAccessor. */
409
- writeValue(values) {
410
- if (this._options) {
411
- this._setSelectionByValue(values);
585
+ _getIndexForValue(cache, value) {
586
+ const isEqual = this.compareWith || Object.is;
587
+ if (!cache.has(value)) {
588
+ let index = -1;
589
+ for (let i = 0; i < this.options.length; i++) {
590
+ if (isEqual(value, this.options.get(i).value)) {
591
+ index = i;
592
+ break;
593
+ }
594
+ }
595
+ cache.set(value, index);
412
596
  }
597
+ return cache.get(value);
413
598
  }
414
- /** Disables the select. Required to implement ControlValueAccessor. */
415
- setDisabledState(isDisabled) {
416
- this.disabled = isDisabled;
417
- }
418
- /** Returns the values of the currently selected options. */
419
- getSelectedValues() {
420
- return this._options.filter(option => option.selected).map(option => option.value);
421
- }
422
- /** Selects an option that has the corresponding given value. */
423
- _setSelectionByValue(values) {
424
- for (const option of this._options.toArray()) {
425
- this.deselect(option);
426
- }
427
- const valuesArray = coerceArray(values);
428
- for (const value of valuesArray) {
429
- const correspondingOption = this._options.find((option) => {
430
- return option.value != null && this.compareWith(option.value, value);
431
- });
432
- if (correspondingOption) {
433
- this.select(correspondingOption);
434
- if (!this.multiple) {
599
+ /**
600
+ * Handle the user clicking an option.
601
+ * @param option The option that was clicked.
602
+ */
603
+ _handleOptionClicked(option) {
604
+ this.listKeyManager.setActiveItem(option);
605
+ this.triggerOption(option);
606
+ this._updatePanelForSelection(option);
607
+ }
608
+ /** Verifies that no two options represent the same value under the compareWith function. */
609
+ _verifyNoOptionValueCollisions() {
610
+ combineLatest([
611
+ this.selectionModelSubject,
612
+ this.options.changes.pipe(startWith(this.options)),
613
+ ]).subscribe(() => {
614
+ var _a;
615
+ const isEqual = (_a = this.compareWith) !== null && _a !== void 0 ? _a : Object.is;
616
+ for (let i = 0; i < this.options.length; i++) {
617
+ const option = this.options.get(i);
618
+ let duplicate = null;
619
+ for (let j = i + 1; j < this.options.length; j++) {
620
+ const other = this.options.get(j);
621
+ if (isEqual(option.value, other.value)) {
622
+ duplicate = other;
623
+ break;
624
+ }
625
+ }
626
+ if (duplicate) {
627
+ // TODO(mmalerba): Link to docs about this.
628
+ if (this.compareWith) {
629
+ console.warn(`Found multiple CdkOption representing the same value under the given compareWith function`, {
630
+ option1: option.element,
631
+ option2: duplicate.element,
632
+ compareWith: this.compareWith,
633
+ });
634
+ }
635
+ else {
636
+ console.warn(`Found multiple CdkOption with the same value`, {
637
+ option1: option.element,
638
+ option2: duplicate.element,
639
+ });
640
+ }
435
641
  return;
436
642
  }
437
643
  }
438
- }
644
+ });
645
+ }
646
+ /**
647
+ * Coerces a value into an array representing a listbox selection.
648
+ * @param value The value to coerce
649
+ * @return An array
650
+ */
651
+ _coerceValue(value) {
652
+ return value == null ? [] : coerceArray(value);
653
+ }
654
+ /**
655
+ * Get the sublist of values with the given validity.
656
+ * @param values The list of values
657
+ * @param valid Whether to get valid values
658
+ * @return The sublist of values with the requested validity
659
+ */
660
+ _getValuesWithValidity(values, valid) {
661
+ const isEqual = this.compareWith || Object.is;
662
+ const validValues = (this.options || []).map(option => option.value);
663
+ return values.filter(value => valid === validValues.some(validValue => isEqual(value, validValue)));
439
664
  }
440
665
  }
441
- CdkListbox.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.0.1", ngImport: i0, type: CdkListbox, deps: [{ token: CDK_COMBOBOX, optional: true }, { token: i1.Directionality, optional: true }], target: i0.ɵɵFactoryTarget.Directive });
442
- CdkListbox.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "14.0.1", type: CdkListbox, selector: "[cdkListbox]", inputs: { id: "id", multiple: "multiple", disabled: "disabled", useActiveDescendant: "useActiveDescendant", autoFocus: "autoFocus", orientation: ["listboxOrientation", "orientation"], compareWith: "compareWith" }, outputs: { selectionChange: "selectionChange" }, host: { attributes: { "role": "listbox" }, listeners: { "focus": "_focusActiveOption()", "keydown": "_keydown($event)" }, properties: { "id": "id", "attr.tabindex": "_tabIndex", "attr.aria-disabled": "disabled", "attr.aria-multiselectable": "multiple", "attr.aria-activedescendant": "_getAriaActiveDescendant()", "attr.aria-orientation": "orientation" }, classAttribute: "cdk-listbox" }, providers: [CDK_LISTBOX_VALUE_ACCESSOR], queries: [{ propertyName: "_options", predicate: CdkOption, descendants: true }], exportAs: ["cdkListbox"], ngImport: i0 });
666
+ CdkListbox.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.0.1", ngImport: i0, type: CdkListbox, deps: [], target: i0.ɵɵFactoryTarget.Directive });
667
+ CdkListbox.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "14.0.1", type: CdkListbox, 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"] }, outputs: { valueChange: "cdkListboxValueChange" }, host: { attributes: { "role": "listbox" }, listeners: { "focus": "_handleFocus()", "keydown": "_handleKeydown($event)", "focusout": "_handleFocusOut($event)" }, 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: [
668
+ {
669
+ provide: NG_VALUE_ACCESSOR,
670
+ useExisting: forwardRef(() => CdkListbox),
671
+ multi: true,
672
+ },
673
+ {
674
+ provide: NG_VALIDATORS,
675
+ useExisting: forwardRef(() => CdkListbox),
676
+ multi: true,
677
+ },
678
+ ], queries: [{ propertyName: "options", predicate: CdkOption, descendants: true }], exportAs: ["cdkListbox"], ngImport: i0 });
443
679
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.0.1", ngImport: i0, type: CdkListbox, decorators: [{
444
680
  type: Directive,
445
681
  args: [{
@@ -449,45 +685,57 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.0.1", ngImpor
449
685
  'role': 'listbox',
450
686
  'class': 'cdk-listbox',
451
687
  '[id]': 'id',
452
- '(focus)': '_focusActiveOption()',
453
- '(keydown)': '_keydown($event)',
454
- '[attr.tabindex]': '_tabIndex',
688
+ '[attr.tabindex]': '_getTabIndex()',
455
689
  '[attr.aria-disabled]': 'disabled',
456
690
  '[attr.aria-multiselectable]': 'multiple',
457
691
  '[attr.aria-activedescendant]': '_getAriaActiveDescendant()',
458
692
  '[attr.aria-orientation]': 'orientation',
693
+ '(focus)': '_handleFocus()',
694
+ '(keydown)': '_handleKeydown($event)',
695
+ '(focusout)': '_handleFocusOut($event)',
459
696
  },
460
- providers: [CDK_LISTBOX_VALUE_ACCESSOR],
697
+ providers: [
698
+ {
699
+ provide: NG_VALUE_ACCESSOR,
700
+ useExisting: forwardRef(() => CdkListbox),
701
+ multi: true,
702
+ },
703
+ {
704
+ provide: NG_VALIDATORS,
705
+ useExisting: forwardRef(() => CdkListbox),
706
+ multi: true,
707
+ },
708
+ ],
461
709
  }]
462
- }], ctorParameters: function () {
463
- return [{ type: i2.CdkCombobox, decorators: [{
464
- type: Optional
465
- }, {
466
- type: Inject,
467
- args: [CDK_COMBOBOX]
468
- }] }, { type: i1.Directionality, decorators: [{
469
- type: Optional
470
- }] }];
471
- }, propDecorators: { _options: [{
472
- type: ContentChildren,
473
- args: [CdkOption, { descendants: true }]
474
- }], selectionChange: [{
475
- type: Output
476
- }], id: [{
710
+ }], ctorParameters: function () { return []; }, propDecorators: { id: [{
477
711
  type: Input
712
+ }], enabledTabIndex: [{
713
+ type: Input,
714
+ args: ['tabindex']
715
+ }], value: [{
716
+ type: Input,
717
+ args: ['cdkListboxValue']
478
718
  }], multiple: [{
479
- type: Input
719
+ type: Input,
720
+ args: ['cdkListboxMultiple']
480
721
  }], disabled: [{
481
- type: Input
722
+ type: Input,
723
+ args: ['cdkListboxDisabled']
482
724
  }], useActiveDescendant: [{
483
- type: Input
484
- }], autoFocus: [{
485
- type: Input
725
+ type: Input,
726
+ args: ['cdkListboxUseActiveDescendant']
486
727
  }], orientation: [{
487
728
  type: Input,
488
- args: ['listboxOrientation']
729
+ args: ['cdkListboxOrientation']
489
730
  }], compareWith: [{
490
- type: Input
731
+ type: Input,
732
+ args: ['cdkListboxCompareWith']
733
+ }], valueChange: [{
734
+ type: Output,
735
+ args: ['cdkListboxValueChange']
736
+ }], options: [{
737
+ type: ContentChildren,
738
+ args: [CdkOption, { descendants: true }]
491
739
  }] } });
492
740
 
493
741
  /**
@@ -531,5 +779,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.0.1", ngImpor
531
779
  * Generated bundle index. Do not edit.
532
780
  */
533
781
 
534
- export { CDK_LISTBOX_VALUE_ACCESSOR, CdkListbox, CdkListboxModule, CdkOption };
782
+ export { CdkListbox, CdkListboxModule, CdkOption };
535
783
  //# sourceMappingURL=listbox.mjs.map