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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,14 +1,13 @@
1
1
  import * as i0 from '@angular/core';
2
2
  import { inject, ElementRef, Directive, Input, ChangeDetectorRef, InjectFlags, forwardRef, Output, ContentChildren, NgModule } from '@angular/core';
3
3
  import { ActiveDescendantKeyManager } from '@angular/cdk/a11y';
4
- import { SPACE, ENTER, UP_ARROW, DOWN_ARROW, LEFT_ARROW, RIGHT_ARROW } from '@angular/cdk/keycodes';
4
+ import { A, hasModifierKey, SPACE, ENTER, HOME, END, 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 { Subject, BehaviorSubject, defer, merge, combineLatest } from 'rxjs';
8
- import { startWith, switchMap, mapTo, takeUntil, filter, take } from 'rxjs/operators';
7
+ import { Subject, defer, merge } from 'rxjs';
8
+ import { startWith, switchMap, map, takeUntil, filter } from 'rxjs/operators';
9
9
  import { Validators, NG_VALUE_ACCESSOR, NG_VALIDATORS } from '@angular/forms';
10
10
  import { Directionality } from '@angular/cdk/bidi';
11
- import { CdkCombobox } from '@angular/cdk-experimental/combobox';
12
11
 
13
12
  /**
14
13
  * @license
@@ -19,9 +18,33 @@ import { CdkCombobox } from '@angular/cdk-experimental/combobox';
19
18
  */
20
19
  /** The next id to use for creating unique DOM IDs. */
21
20
  let nextId = 0;
22
- // TODO(mmalerba):
23
- // - should listbox wrap be configurable?
24
- // - should skipping disabled options be configurable?
21
+ /**
22
+ * An implementation of SelectionModel that internally always represents the selection as a
23
+ * multi-selection. This is necessary so that we can recover the full selection if the user
24
+ * switches the listbox from single-selection to multi-selection after initialization.
25
+ *
26
+ * This selection model may report multiple selected values, even if it is in single-selection
27
+ * mode. It is up to the user (CdkListbox) to check for invalid selections.
28
+ */
29
+ class ListboxSelectionModel extends SelectionModel {
30
+ constructor(multiple = false, initiallySelectedValues, emitChanges = true, compareWith) {
31
+ super(true, initiallySelectedValues, emitChanges, compareWith);
32
+ this.multiple = multiple;
33
+ }
34
+ isMultipleSelection() {
35
+ return this.multiple;
36
+ }
37
+ select(...values) {
38
+ // The super class is always in multi-selection mode, so we need to override the behavior if
39
+ // this selection model actually belongs to a single-selection listbox.
40
+ if (this.multiple) {
41
+ return super.select(...values);
42
+ }
43
+ else {
44
+ return super.setSelection(...values);
45
+ }
46
+ }
47
+ }
25
48
  /** A selectable option in a listbox. */
26
49
  class CdkOption {
27
50
  constructor() {
@@ -67,7 +90,7 @@ class CdkOption {
67
90
  }
68
91
  /** Whether this option is selected. */
69
92
  isSelected() {
70
- return this.listbox.isSelected(this.value);
93
+ return this.listbox.isSelected(this);
71
94
  }
72
95
  /** Whether this option is active. */
73
96
  isActive() {
@@ -127,7 +150,7 @@ class CdkOption {
127
150
  }
128
151
  }
129
152
  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 });
153
+ 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($event)", "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
154
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.0.1", ngImport: i0, type: CdkOption, decorators: [{
132
155
  type: Directive,
133
156
  args: [{
@@ -143,7 +166,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.0.1", ngImpor
143
166
  '[class.cdk-option-disabled]': 'disabled',
144
167
  '[class.cdk-option-active]': 'isActive()',
145
168
  '[class.cdk-option-selected]': 'isSelected()',
146
- '(click)': '_clicked.next()',
169
+ '(click)': '_clicked.next($event)',
147
170
  '(focus)': '_handleFocus()',
148
171
  },
149
172
  }]
@@ -165,22 +188,25 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.0.1", ngImpor
165
188
  class CdkListbox {
166
189
  constructor() {
167
190
  this._generatedId = `cdk-listbox-${nextId++}`;
168
- this._multiple = false;
169
191
  this._disabled = false;
170
192
  this._useActiveDescendant = false;
171
- /** The orientation of the listbox. Only affects keyboard interaction, not visual layout. */
172
- this.orientation = 'vertical';
193
+ this._orientation = 'vertical';
194
+ this._keyboardNavigationWraps = true;
195
+ this._keyboardNavigationSkipsDisabled = true;
173
196
  /** Emits when the selected value(s) in the listbox change. */
174
197
  this.valueChange = new Subject();
175
- // TODO(mmalerba): Refactor SelectionModel so that its not necessary to create new instances
176
198
  /** The selection model used by the listbox. */
177
- this.selectionModelSubject = new BehaviorSubject(new SelectionModel(this.multiple, [], true, this._compareWith));
199
+ this.selectionModel = new ListboxSelectionModel();
178
200
  /** Emits when the listbox is destroyed. */
179
201
  this.destroyed = new Subject();
180
202
  /** The host element of the listbox. */
181
203
  this.element = inject(ElementRef).nativeElement;
182
204
  /** The change detector for this listbox. */
183
205
  this.changeDetectorRef = inject(ChangeDetectorRef);
206
+ /** Whether the currently selected value in the selection model is invalid. */
207
+ this._invalid = false;
208
+ /** The last user-triggered option. */
209
+ this._lastTriggered = null;
184
210
  /** Callback called when the listbox has been touched */
185
211
  this._onTouched = () => { };
186
212
  /** Callback called when the listbox value changes */
@@ -188,21 +214,23 @@ class CdkListbox {
188
214
  /** Callback called when the form validator changes. */
189
215
  this._onValidatorChange = () => { };
190
216
  /** 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)))))));
217
+ this._optionClicked = defer(() => this.options.changes.pipe(startWith(this.options), switchMap(options => merge(...options.map(option => option._clicked.pipe(map(event => ({ option, event }))))))));
192
218
  /** The directionality of the page. */
193
219
  this._dir = inject(Directionality, InjectFlags.Optional);
194
- // TODO(mmalerba): Should not depend on combobox
195
- this._combobox = inject(CdkCombobox, InjectFlags.Optional);
220
+ /** A predicate that skips disabled options. */
221
+ this._skipDisabledPredicate = (option) => option.disabled;
222
+ /** A predicate that does not skip any options. */
223
+ this._skipNonePredicate = () => false;
196
224
  /**
197
225
  * Validator that produces an error if multiple values are selected in a single selection
198
226
  * listbox.
199
227
  * @param control The control to validate
200
228
  * @return A validation error or null
201
229
  */
202
- this._validateMultipleValues = (control) => {
230
+ this._validateUnexpectedMultipleValues = (control) => {
203
231
  const controlValue = this._coerceValue(control.value);
204
232
  if (!this.multiple && controlValue.length > 1) {
205
- return { 'cdkListboxMultipleValues': true };
233
+ return { 'cdkListboxUnexpectedMultipleValues': true };
206
234
  }
207
235
  return null;
208
236
  };
@@ -211,22 +239,21 @@ class CdkListbox {
211
239
  * @param control The control to validate
212
240
  * @return A validation error or null
213
241
  */
214
- this._validateInvalidValues = (control) => {
242
+ this._validateUnexpectedOptionValues = (control) => {
215
243
  const controlValue = this._coerceValue(control.value);
216
- const invalidValues = this._getValuesWithValidity(controlValue, false);
244
+ const invalidValues = this._getInvalidOptionValues(controlValue);
217
245
  if (invalidValues.length) {
218
- return { 'cdkListboxInvalidValues': { 'values': invalidValues } };
246
+ return { 'cdkListboxUnexpectedOptionValues': { 'values': invalidValues } };
219
247
  }
220
248
  return null;
221
249
  };
222
250
  /** The combined set of validators for this listbox. */
223
251
  this._validators = Validators.compose([
224
- this._validateMultipleValues,
225
- this._validateInvalidValues,
252
+ this._validateUnexpectedMultipleValues,
253
+ this._validateUnexpectedOptionValues,
226
254
  ]);
227
- this.selectionModelSubject
228
- .pipe(switchMap(selectionModel => selectionModel.changed), takeUntil(this.destroyed))
229
- .subscribe(() => {
255
+ // Update the internal value whenever the selection model's selection changes.
256
+ this.selectionModel.changed.pipe(startWith(null), takeUntil(this.destroyed)).subscribe(() => {
230
257
  this._updateInternalValue();
231
258
  });
232
259
  }
@@ -246,7 +273,7 @@ class CdkListbox {
246
273
  }
247
274
  /** The value selected in the listbox, represented as an array of option values. */
248
275
  get value() {
249
- return this.selectionModel().selected;
276
+ return this._invalid ? [] : this.selectionModel.selected;
250
277
  }
251
278
  set value(value) {
252
279
  this._setSelection(value);
@@ -256,12 +283,11 @@ class CdkListbox {
256
283
  * to `false`, and more than one option is selected, all options are deselected.
257
284
  */
258
285
  get multiple() {
259
- return this._multiple;
286
+ return this.selectionModel.multiple;
260
287
  }
261
288
  set multiple(value) {
262
- this._multiple = coerceBooleanProperty(value);
263
- this._updateSelectionModel();
264
- this._onValidatorChange();
289
+ this.selectionModel.multiple = coerceBooleanProperty(value);
290
+ this._updateInternalValue();
265
291
  }
266
292
  /** Whether the listbox is disabled. */
267
293
  get disabled() {
@@ -277,28 +303,61 @@ class CdkListbox {
277
303
  set useActiveDescendant(shouldUseActiveDescendant) {
278
304
  this._useActiveDescendant = coerceBooleanProperty(shouldUseActiveDescendant);
279
305
  }
306
+ /** The orientation of the listbox. Only affects keyboard interaction, not visual layout. */
307
+ get orientation() {
308
+ return this._orientation;
309
+ }
310
+ set orientation(value) {
311
+ var _a, _b, _c;
312
+ this._orientation = value === 'horizontal' ? 'horizontal' : 'vertical';
313
+ if (value === 'horizontal') {
314
+ (_a = this.listKeyManager) === null || _a === void 0 ? void 0 : _a.withHorizontalOrientation(((_b = this._dir) === null || _b === void 0 ? void 0 : _b.value) || 'ltr');
315
+ }
316
+ else {
317
+ (_c = this.listKeyManager) === null || _c === void 0 ? void 0 : _c.withVerticalOrientation();
318
+ }
319
+ }
280
320
  /** The function used to compare option values. */
281
321
  get compareWith() {
282
- return this._compareWith;
322
+ return this.selectionModel.compareWith;
283
323
  }
284
324
  set compareWith(fn) {
285
- this._compareWith = fn;
286
- this._updateSelectionModel();
325
+ this.selectionModel.compareWith = fn;
287
326
  }
288
- ngAfterContentInit() {
327
+ /**
328
+ * Whether the keyboard navigation should wrap when the user presses arrow down on the last item
329
+ * or arrow up on the first item.
330
+ */
331
+ get keyboardNavigationWraps() {
332
+ return this._keyboardNavigationWraps;
333
+ }
334
+ set keyboardNavigationWraps(wrap) {
289
335
  var _a;
336
+ this._keyboardNavigationWraps = coerceBooleanProperty(wrap);
337
+ (_a = this.listKeyManager) === null || _a === void 0 ? void 0 : _a.withWrap(this._keyboardNavigationWraps);
338
+ }
339
+ /** Whether keyboard navigation should skip over disabled items. */
340
+ get keyboardNavigationSkipsDisabled() {
341
+ return this._keyboardNavigationSkipsDisabled;
342
+ }
343
+ set keyboardNavigationSkipsDisabled(skip) {
344
+ var _a;
345
+ this._keyboardNavigationSkipsDisabled = coerceBooleanProperty(skip);
346
+ (_a = this.listKeyManager) === null || _a === void 0 ? void 0 : _a.skipPredicate(this._keyboardNavigationSkipsDisabled ? this._skipDisabledPredicate : this._skipNonePredicate);
347
+ }
348
+ ngAfterContentInit() {
290
349
  if (typeof ngDevMode === 'undefined' || ngDevMode) {
291
350
  this._verifyNoOptionValueCollisions();
292
351
  }
293
352
  this._initKeyManager();
294
- (_a = this._combobox) === null || _a === void 0 ? void 0 : _a._registerContent(this.id, 'listbox');
295
- this.options.changes.pipe(takeUntil(this.destroyed)).subscribe(() => {
353
+ // Update the internal value whenever the options change, as this may change the validity of
354
+ // the current selection
355
+ this.options.changes.pipe(startWith(this.options), takeUntil(this.destroyed)).subscribe(() => {
296
356
  this._updateInternalValue();
297
- this._onValidatorChange();
298
357
  });
299
358
  this._optionClicked
300
- .pipe(filter(option => !option.disabled), takeUntil(this.destroyed))
301
- .subscribe(option => this._handleOptionClicked(option));
359
+ .pipe(filter(({ option }) => !option.disabled), takeUntil(this.destroyed))
360
+ .subscribe(({ option, event }) => this._handleOptionClicked(option, event));
302
361
  }
303
362
  ngOnDestroy() {
304
363
  this.listKeyManager.change.complete();
@@ -317,7 +376,10 @@ class CdkListbox {
317
376
  * @param value The value to toggle
318
377
  */
319
378
  toggleValue(value) {
320
- this.selectionModel().toggle(value);
379
+ if (this._invalid) {
380
+ this.selectionModel.clear(false);
381
+ }
382
+ this.selectionModel.toggle(value);
321
383
  }
322
384
  /**
323
385
  * Select the given option.
@@ -331,7 +393,10 @@ class CdkListbox {
331
393
  * @param value The value to select
332
394
  */
333
395
  selectValue(value) {
334
- this.selectionModel().select(value);
396
+ if (this._invalid) {
397
+ this.selectionModel.clear(false);
398
+ }
399
+ this.selectionModel.select(value);
335
400
  }
336
401
  /**
337
402
  * Deselect the given option.
@@ -345,7 +410,10 @@ class CdkListbox {
345
410
  * @param value The value to deselect
346
411
  */
347
412
  deselectValue(value) {
348
- this.selectionModel().deselect(value);
413
+ if (this._invalid) {
414
+ this.selectionModel.clear(false);
415
+ }
416
+ this.selectionModel.deselect(value);
349
417
  }
350
418
  /**
351
419
  * Set the selected state of all options.
@@ -353,10 +421,13 @@ class CdkListbox {
353
421
  */
354
422
  setAllSelected(isSelected) {
355
423
  if (!isSelected) {
356
- this.selectionModel().clear();
424
+ this.selectionModel.clear();
357
425
  }
358
426
  else {
359
- this.selectionModel().select(...this.options.toArray().map(option => option.value));
427
+ if (this._invalid) {
428
+ this.selectionModel.clear(false);
429
+ }
430
+ this.selectionModel.select(...this.options.map(option => option.value));
360
431
  }
361
432
  }
362
433
  /**
@@ -364,7 +435,17 @@ class CdkListbox {
364
435
  * @param option The option to get the selected state of
365
436
  */
366
437
  isSelected(option) {
367
- return this.selectionModel().isSelected(option instanceof CdkOption ? option.value : option);
438
+ return this.isValueSelected(option.value);
439
+ }
440
+ /**
441
+ * Get whether the given value is selected.
442
+ * @param value The value to get the selected state of
443
+ */
444
+ isValueSelected(value) {
445
+ if (this._invalid) {
446
+ return false;
447
+ }
448
+ return this.selectionModel.isSelected(value);
368
449
  }
369
450
  /**
370
451
  * Registers a callback to be invoked when the listbox's value changes from user input.
@@ -417,10 +498,6 @@ class CdkListbox {
417
498
  focus() {
418
499
  this.element.focus();
419
500
  }
420
- /** The selection model used to track the listbox's value. */
421
- selectionModel() {
422
- return this.selectionModelSubject.value;
423
- }
424
501
  /**
425
502
  * Triggers the given option in response to user interaction.
426
503
  * - In single selection mode: selects the option and deselects any other selected option.
@@ -429,16 +506,10 @@ class CdkListbox {
429
506
  */
430
507
  triggerOption(option) {
431
508
  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);
438
- }
439
- else {
440
- this.select(option);
441
- }
509
+ this._lastTriggered = option;
510
+ const changed = this.multiple
511
+ ? this.selectionModel.toggle(option.value)
512
+ : this.selectionModel.select(option.value);
442
513
  if (changed) {
443
514
  this._onChange(this.value);
444
515
  this.valueChange.next({
@@ -449,6 +520,45 @@ class CdkListbox {
449
520
  }
450
521
  }
451
522
  }
523
+ /**
524
+ * Trigger the given range of options in response to user interaction.
525
+ * Should only be called in multi-selection mode.
526
+ * @param trigger The option that was triggered
527
+ * @param from The start index of the options to toggle
528
+ * @param to The end index of the options to toggle
529
+ * @param on Whether to toggle the option range on
530
+ */
531
+ triggerRange(trigger, from, to, on) {
532
+ var _a;
533
+ if (this.disabled || (trigger && trigger.disabled)) {
534
+ return;
535
+ }
536
+ this._lastTriggered = trigger;
537
+ const isEqual = (_a = this.compareWith) !== null && _a !== void 0 ? _a : Object.is;
538
+ const updateValues = [...this.options]
539
+ .slice(Math.max(0, Math.min(from, to)), Math.min(this.options.length, Math.max(from, to) + 1))
540
+ .filter(option => !option.disabled)
541
+ .map(option => option.value);
542
+ const selected = [...this.value];
543
+ for (const updateValue of updateValues) {
544
+ const selectedIndex = selected.findIndex(selectedValue => isEqual(selectedValue, updateValue));
545
+ if (on && selectedIndex === -1) {
546
+ selected.push(updateValue);
547
+ }
548
+ else if (!on && selectedIndex !== -1) {
549
+ selected.splice(selectedIndex, 1);
550
+ }
551
+ }
552
+ let changed = this.selectionModel.setSelection(...selected);
553
+ if (changed) {
554
+ this._onChange(this.value);
555
+ this.valueChange.next({
556
+ value: this.value,
557
+ listbox: this,
558
+ option: trigger,
559
+ });
560
+ }
561
+ }
452
562
  /**
453
563
  * Sets the given option as active.
454
564
  * @param option The option to make active
@@ -465,24 +575,68 @@ class CdkListbox {
465
575
  }
466
576
  /** Called when the user presses keydown on the listbox. */
467
577
  _handleKeydown(event) {
578
+ var _a;
468
579
  if (this._disabled) {
469
580
  return;
470
581
  }
471
582
  const { keyCode } = event;
472
583
  const previousActiveIndex = this.listKeyManager.activeItemIndex;
584
+ const ctrlKeys = ['ctrlKey', 'metaKey'];
585
+ if (this.multiple && keyCode === A && hasModifierKey(event, ...ctrlKeys)) {
586
+ // Toggle all options off if they're all selected, otherwise toggle them all on.
587
+ this.triggerRange(null, 0, this.options.length - 1, this.options.length !== this.value.length);
588
+ event.preventDefault();
589
+ return;
590
+ }
591
+ if (this.multiple &&
592
+ (keyCode === SPACE || keyCode === ENTER) &&
593
+ hasModifierKey(event, 'shiftKey')) {
594
+ if (this.listKeyManager.activeItem && this.listKeyManager.activeItemIndex != null) {
595
+ this.triggerRange(this.listKeyManager.activeItem, (_a = this._getLastTriggeredIndex()) !== null && _a !== void 0 ? _a : this.listKeyManager.activeItemIndex, this.listKeyManager.activeItemIndex, !this.listKeyManager.activeItem.isSelected());
596
+ }
597
+ event.preventDefault();
598
+ return;
599
+ }
600
+ if (this.multiple &&
601
+ keyCode === HOME &&
602
+ hasModifierKey(event, ...ctrlKeys) &&
603
+ hasModifierKey(event, 'shiftKey')) {
604
+ const trigger = this.listKeyManager.activeItem;
605
+ if (trigger) {
606
+ const from = this.listKeyManager.activeItemIndex;
607
+ this.listKeyManager.setFirstItemActive();
608
+ this.triggerRange(trigger, from, this.listKeyManager.activeItemIndex, !trigger.isSelected());
609
+ }
610
+ event.preventDefault();
611
+ return;
612
+ }
613
+ if (this.multiple &&
614
+ keyCode === END &&
615
+ hasModifierKey(event, ...ctrlKeys) &&
616
+ hasModifierKey(event, 'shiftKey')) {
617
+ const trigger = this.listKeyManager.activeItem;
618
+ if (trigger) {
619
+ const from = this.listKeyManager.activeItemIndex;
620
+ this.listKeyManager.setLastItemActive();
621
+ this.triggerRange(trigger, from, this.listKeyManager.activeItemIndex, !trigger.isSelected());
622
+ }
623
+ event.preventDefault();
624
+ return;
625
+ }
473
626
  if (keyCode === SPACE || keyCode === ENTER) {
474
627
  this.triggerOption(this.listKeyManager.activeItem);
475
628
  event.preventDefault();
629
+ return;
476
630
  }
477
- else {
478
- this.listKeyManager.onKeydown(event);
479
- }
480
- /** Will select an option if shift was pressed while navigating to the option */
481
- const isArrow = keyCode === UP_ARROW ||
631
+ const isNavKey = keyCode === UP_ARROW ||
482
632
  keyCode === DOWN_ARROW ||
483
633
  keyCode === LEFT_ARROW ||
484
- keyCode === RIGHT_ARROW;
485
- if (isArrow && event.shiftKey && previousActiveIndex !== this.listKeyManager.activeItemIndex) {
634
+ keyCode === RIGHT_ARROW ||
635
+ keyCode === HOME ||
636
+ keyCode === END;
637
+ this.listKeyManager.onKeydown(event);
638
+ // Will select an option if shift was pressed while navigating to the option
639
+ if (isNavKey && event.shiftKey && previousActiveIndex !== this.listKeyManager.activeItemIndex) {
486
640
  this.triggerOption(this.listKeyManager.activeItem);
487
641
  }
488
642
  }
@@ -512,10 +666,13 @@ class CdkListbox {
512
666
  _initKeyManager() {
513
667
  var _a;
514
668
  this.listKeyManager = new ActiveDescendantKeyManager(this.options)
515
- .withWrap()
669
+ .withWrap(this._keyboardNavigationWraps)
516
670
  .withTypeAhead()
517
671
  .withHomeAndEnd()
518
- .withAllowedModifierKeys(['shiftKey']);
672
+ .withAllowedModifierKeys(['shiftKey'])
673
+ .skipPredicate(this._keyboardNavigationSkipsDisabled
674
+ ? this._skipDisabledPredicate
675
+ : this._skipNonePredicate);
519
676
  if (this.orientation === 'vertical') {
520
677
  this.listKeyManager.withVerticalOrientation();
521
678
  }
@@ -526,21 +683,6 @@ class CdkListbox {
526
683
  .pipe(takeUntil(this.destroyed))
527
684
  .subscribe(() => this._focusActiveOption());
528
685
  }
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
- }
538
- }
539
- }
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));
543
- }
544
686
  /** Focus the active option. */
545
687
  _focusActiveOption() {
546
688
  var _a;
@@ -554,26 +696,23 @@ class CdkListbox {
554
696
  * @param value The list of new selected values.
555
697
  */
556
698
  _setSelection(value) {
557
- const coercedValue = this._coerceValue(value);
558
- this.selectionModel().setSelection(...(!this.multiple && coercedValue.length > 1
559
- ? []
560
- : this._getValuesWithValidity(coercedValue, true)));
699
+ if (this._invalid) {
700
+ this.selectionModel.clear(false);
701
+ }
702
+ this.selectionModel.setSelection(...this._coerceValue(value));
561
703
  }
562
704
  /** Update the internal value of the listbox based on the selection model. */
563
705
  _updateInternalValue() {
564
706
  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) => {
707
+ this.selectionModel.sort((a, b) => {
573
708
  const aIndex = this._getIndexForValue(indexCache, a);
574
709
  const bIndex = this._getIndexForValue(indexCache, b);
575
710
  return aIndex - bIndex;
576
711
  });
712
+ const selected = this.selectionModel.selected;
713
+ this._invalid =
714
+ (!this.multiple && selected.length > 1) || !!this._getInvalidOptionValues(selected).length;
715
+ this._onValidatorChange();
577
716
  this.changeDetectorRef.markForCheck();
578
717
  }
579
718
  /**
@@ -600,17 +739,19 @@ class CdkListbox {
600
739
  * Handle the user clicking an option.
601
740
  * @param option The option that was clicked.
602
741
  */
603
- _handleOptionClicked(option) {
742
+ _handleOptionClicked(option, event) {
743
+ var _a;
604
744
  this.listKeyManager.setActiveItem(option);
605
- this.triggerOption(option);
606
- this._updatePanelForSelection(option);
745
+ if (event.shiftKey && this.multiple) {
746
+ this.triggerRange(option, (_a = this._getLastTriggeredIndex()) !== null && _a !== void 0 ? _a : this.listKeyManager.activeItemIndex, this.listKeyManager.activeItemIndex, !option.isSelected());
747
+ }
748
+ else {
749
+ this.triggerOption(option);
750
+ }
607
751
  }
608
752
  /** Verifies that no two options represent the same value under the compareWith function. */
609
753
  _verifyNoOptionValueCollisions() {
610
- combineLatest([
611
- this.selectionModelSubject,
612
- this.options.changes.pipe(startWith(this.options)),
613
- ]).subscribe(() => {
754
+ this.options.changes.pipe(startWith(this.options), takeUntil(this.destroyed)).subscribe(() => {
614
755
  var _a;
615
756
  const isEqual = (_a = this.compareWith) !== null && _a !== void 0 ? _a : Object.is;
616
757
  for (let i = 0; i < this.options.length; i++) {
@@ -652,19 +793,23 @@ class CdkListbox {
652
793
  return value == null ? [] : coerceArray(value);
653
794
  }
654
795
  /**
655
- * Get the sublist of values with the given validity.
796
+ * Get the sublist of values that do not represent valid option values in this listbox.
656
797
  * @param values The list of values
657
- * @param valid Whether to get valid values
658
- * @return The sublist of values with the requested validity
798
+ * @return The sublist of values that are not valid option values
659
799
  */
660
- _getValuesWithValidity(values, valid) {
800
+ _getInvalidOptionValues(values) {
661
801
  const isEqual = this.compareWith || Object.is;
662
802
  const validValues = (this.options || []).map(option => option.value);
663
- return values.filter(value => valid === validValues.some(validValue => isEqual(value, validValue)));
803
+ return values.filter(value => !validValues.some(validValue => isEqual(value, validValue)));
804
+ }
805
+ /** Get the index of the last triggered option. */
806
+ _getLastTriggeredIndex() {
807
+ const index = this.options.toArray().indexOf(this._lastTriggered);
808
+ return index === -1 ? null : index;
664
809
  }
665
810
  }
666
811
  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: [
812
+ 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"], keyboardNavigationWraps: ["cdkListboxKeyboardNavigationWraps", "keyboardNavigationWraps"], keyboardNavigationSkipsDisabled: ["cdkListboxKeyboardNavigationSkipsDisabled", "keyboardNavigationSkipsDisabled"] }, 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
813
  {
669
814
  provide: NG_VALUE_ACCESSOR,
670
815
  useExisting: forwardRef(() => CdkListbox),
@@ -730,6 +875,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.0.1", ngImpor
730
875
  }], compareWith: [{
731
876
  type: Input,
732
877
  args: ['cdkListboxCompareWith']
878
+ }], keyboardNavigationWraps: [{
879
+ type: Input,
880
+ args: ['cdkListboxKeyboardNavigationWraps']
881
+ }], keyboardNavigationSkipsDisabled: [{
882
+ type: Input,
883
+ args: ['cdkListboxKeyboardNavigationSkipsDisabled']
733
884
  }], valueChange: [{
734
885
  type: Output,
735
886
  args: ['cdkListboxValueChange']