@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.
- package/column-resize/index.d.ts +1 -1
- package/esm2020/column-resize/resize-strategy.mjs +2 -2
- package/esm2020/listbox/listbox.mjs +257 -111
- package/esm2020/popover-edit/edit-event-dispatcher.mjs +2 -2
- package/esm2020/popover-edit/table-directives.mjs +2 -2
- package/esm2020/scrolling/auto-size-virtual-scroll.mjs +2 -2
- package/esm2020/version.mjs +1 -1
- package/fesm2015/cdk-experimental.mjs +1 -1
- package/fesm2015/cdk-experimental.mjs.map +1 -1
- package/fesm2015/column-resize.mjs +1 -1
- package/fesm2015/column-resize.mjs.map +1 -1
- package/fesm2015/listbox.mjs +262 -111
- package/fesm2015/listbox.mjs.map +1 -1
- package/fesm2015/popover-edit.mjs +2 -2
- package/fesm2015/popover-edit.mjs.map +1 -1
- package/fesm2015/scrolling.mjs +1 -1
- package/fesm2015/scrolling.mjs.map +1 -1
- package/fesm2020/cdk-experimental.mjs +1 -1
- package/fesm2020/cdk-experimental.mjs.map +1 -1
- package/fesm2020/column-resize.mjs +1 -1
- package/fesm2020/column-resize.mjs.map +1 -1
- package/fesm2020/listbox.mjs +256 -110
- package/fesm2020/listbox.mjs.map +1 -1
- package/fesm2020/popover-edit.mjs +2 -2
- package/fesm2020/popover-edit.mjs.map +1 -1
- package/fesm2020/scrolling.mjs +1 -1
- package/fesm2020/scrolling.mjs.map +1 -1
- package/listbox/index.d.ts +63 -21
- package/package.json +2 -2
- package/popover-edit/index.d.ts +1 -1
package/fesm2020/listbox.mjs
CHANGED
|
@@ -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,
|
|
8
|
-
import { startWith, switchMap,
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
93
|
+
return this.listbox.isSelected(this);
|
|
71
94
|
}
|
|
72
95
|
/** Whether this option is active. */
|
|
73
96
|
isActive() {
|
|
@@ -126,7 +149,7 @@ class CdkOption {
|
|
|
126
149
|
}
|
|
127
150
|
}
|
|
128
151
|
CdkOption.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.0.1", ngImport: i0, type: CdkOption, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
129
|
-
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 });
|
|
152
|
+
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 });
|
|
130
153
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.0.1", ngImport: i0, type: CdkOption, decorators: [{
|
|
131
154
|
type: Directive,
|
|
132
155
|
args: [{
|
|
@@ -142,7 +165,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.0.1", ngImpor
|
|
|
142
165
|
'[class.cdk-option-disabled]': 'disabled',
|
|
143
166
|
'[class.cdk-option-active]': 'isActive()',
|
|
144
167
|
'[class.cdk-option-selected]': 'isSelected()',
|
|
145
|
-
'(click)': '_clicked.next()',
|
|
168
|
+
'(click)': '_clicked.next($event)',
|
|
146
169
|
'(focus)': '_handleFocus()',
|
|
147
170
|
},
|
|
148
171
|
}]
|
|
@@ -164,22 +187,25 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.0.1", ngImpor
|
|
|
164
187
|
class CdkListbox {
|
|
165
188
|
constructor() {
|
|
166
189
|
this._generatedId = `cdk-listbox-${nextId++}`;
|
|
167
|
-
this._multiple = false;
|
|
168
190
|
this._disabled = false;
|
|
169
191
|
this._useActiveDescendant = false;
|
|
170
|
-
|
|
171
|
-
this.
|
|
192
|
+
this._orientation = 'vertical';
|
|
193
|
+
this._keyboardNavigationWraps = true;
|
|
194
|
+
this._keyboardNavigationSkipsDisabled = true;
|
|
172
195
|
/** Emits when the selected value(s) in the listbox change. */
|
|
173
196
|
this.valueChange = new Subject();
|
|
174
|
-
// TODO(mmalerba): Refactor SelectionModel so that its not necessary to create new instances
|
|
175
197
|
/** The selection model used by the listbox. */
|
|
176
|
-
this.
|
|
198
|
+
this.selectionModel = new ListboxSelectionModel();
|
|
177
199
|
/** Emits when the listbox is destroyed. */
|
|
178
200
|
this.destroyed = new Subject();
|
|
179
201
|
/** The host element of the listbox. */
|
|
180
202
|
this.element = inject(ElementRef).nativeElement;
|
|
181
203
|
/** The change detector for this listbox. */
|
|
182
204
|
this.changeDetectorRef = inject(ChangeDetectorRef);
|
|
205
|
+
/** Whether the currently selected value in the selection model is invalid. */
|
|
206
|
+
this._invalid = false;
|
|
207
|
+
/** The last user-triggered option. */
|
|
208
|
+
this._lastTriggered = null;
|
|
183
209
|
/** Callback called when the listbox has been touched */
|
|
184
210
|
this._onTouched = () => { };
|
|
185
211
|
/** Callback called when the listbox value changes */
|
|
@@ -187,21 +213,23 @@ class CdkListbox {
|
|
|
187
213
|
/** Callback called when the form validator changes. */
|
|
188
214
|
this._onValidatorChange = () => { };
|
|
189
215
|
/** Emits when an option has been clicked. */
|
|
190
|
-
this._optionClicked = defer(() => this.options.changes.pipe(startWith(this.options), switchMap(options => merge(...options.map(option => option._clicked.pipe(
|
|
216
|
+
this._optionClicked = defer(() => this.options.changes.pipe(startWith(this.options), switchMap(options => merge(...options.map(option => option._clicked.pipe(map(event => ({ option, event }))))))));
|
|
191
217
|
/** The directionality of the page. */
|
|
192
218
|
this._dir = inject(Directionality, InjectFlags.Optional);
|
|
193
|
-
|
|
194
|
-
this.
|
|
219
|
+
/** A predicate that skips disabled options. */
|
|
220
|
+
this._skipDisabledPredicate = (option) => option.disabled;
|
|
221
|
+
/** A predicate that does not skip any options. */
|
|
222
|
+
this._skipNonePredicate = () => false;
|
|
195
223
|
/**
|
|
196
224
|
* Validator that produces an error if multiple values are selected in a single selection
|
|
197
225
|
* listbox.
|
|
198
226
|
* @param control The control to validate
|
|
199
227
|
* @return A validation error or null
|
|
200
228
|
*/
|
|
201
|
-
this.
|
|
229
|
+
this._validateUnexpectedMultipleValues = (control) => {
|
|
202
230
|
const controlValue = this._coerceValue(control.value);
|
|
203
231
|
if (!this.multiple && controlValue.length > 1) {
|
|
204
|
-
return { '
|
|
232
|
+
return { 'cdkListboxUnexpectedMultipleValues': true };
|
|
205
233
|
}
|
|
206
234
|
return null;
|
|
207
235
|
};
|
|
@@ -210,22 +238,21 @@ class CdkListbox {
|
|
|
210
238
|
* @param control The control to validate
|
|
211
239
|
* @return A validation error or null
|
|
212
240
|
*/
|
|
213
|
-
this.
|
|
241
|
+
this._validateUnexpectedOptionValues = (control) => {
|
|
214
242
|
const controlValue = this._coerceValue(control.value);
|
|
215
|
-
const invalidValues = this.
|
|
243
|
+
const invalidValues = this._getInvalidOptionValues(controlValue);
|
|
216
244
|
if (invalidValues.length) {
|
|
217
|
-
return { '
|
|
245
|
+
return { 'cdkListboxUnexpectedOptionValues': { 'values': invalidValues } };
|
|
218
246
|
}
|
|
219
247
|
return null;
|
|
220
248
|
};
|
|
221
249
|
/** The combined set of validators for this listbox. */
|
|
222
250
|
this._validators = Validators.compose([
|
|
223
|
-
this.
|
|
224
|
-
this.
|
|
251
|
+
this._validateUnexpectedMultipleValues,
|
|
252
|
+
this._validateUnexpectedOptionValues,
|
|
225
253
|
]);
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
.subscribe(() => {
|
|
254
|
+
// Update the internal value whenever the selection model's selection changes.
|
|
255
|
+
this.selectionModel.changed.pipe(startWith(null), takeUntil(this.destroyed)).subscribe(() => {
|
|
229
256
|
this._updateInternalValue();
|
|
230
257
|
});
|
|
231
258
|
}
|
|
@@ -245,7 +272,7 @@ class CdkListbox {
|
|
|
245
272
|
}
|
|
246
273
|
/** The value selected in the listbox, represented as an array of option values. */
|
|
247
274
|
get value() {
|
|
248
|
-
return this.selectionModel
|
|
275
|
+
return this._invalid ? [] : this.selectionModel.selected;
|
|
249
276
|
}
|
|
250
277
|
set value(value) {
|
|
251
278
|
this._setSelection(value);
|
|
@@ -255,12 +282,11 @@ class CdkListbox {
|
|
|
255
282
|
* to `false`, and more than one option is selected, all options are deselected.
|
|
256
283
|
*/
|
|
257
284
|
get multiple() {
|
|
258
|
-
return this.
|
|
285
|
+
return this.selectionModel.multiple;
|
|
259
286
|
}
|
|
260
287
|
set multiple(value) {
|
|
261
|
-
this.
|
|
262
|
-
this.
|
|
263
|
-
this._onValidatorChange();
|
|
288
|
+
this.selectionModel.multiple = coerceBooleanProperty(value);
|
|
289
|
+
this._updateInternalValue();
|
|
264
290
|
}
|
|
265
291
|
/** Whether the listbox is disabled. */
|
|
266
292
|
get disabled() {
|
|
@@ -276,27 +302,58 @@ class CdkListbox {
|
|
|
276
302
|
set useActiveDescendant(shouldUseActiveDescendant) {
|
|
277
303
|
this._useActiveDescendant = coerceBooleanProperty(shouldUseActiveDescendant);
|
|
278
304
|
}
|
|
305
|
+
/** The orientation of the listbox. Only affects keyboard interaction, not visual layout. */
|
|
306
|
+
get orientation() {
|
|
307
|
+
return this._orientation;
|
|
308
|
+
}
|
|
309
|
+
set orientation(value) {
|
|
310
|
+
this._orientation = value === 'horizontal' ? 'horizontal' : 'vertical';
|
|
311
|
+
if (value === 'horizontal') {
|
|
312
|
+
this.listKeyManager?.withHorizontalOrientation(this._dir?.value || 'ltr');
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
this.listKeyManager?.withVerticalOrientation();
|
|
316
|
+
}
|
|
317
|
+
}
|
|
279
318
|
/** The function used to compare option values. */
|
|
280
319
|
get compareWith() {
|
|
281
|
-
return this.
|
|
320
|
+
return this.selectionModel.compareWith;
|
|
282
321
|
}
|
|
283
322
|
set compareWith(fn) {
|
|
284
|
-
this.
|
|
285
|
-
|
|
323
|
+
this.selectionModel.compareWith = fn;
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Whether the keyboard navigation should wrap when the user presses arrow down on the last item
|
|
327
|
+
* or arrow up on the first item.
|
|
328
|
+
*/
|
|
329
|
+
get keyboardNavigationWraps() {
|
|
330
|
+
return this._keyboardNavigationWraps;
|
|
331
|
+
}
|
|
332
|
+
set keyboardNavigationWraps(wrap) {
|
|
333
|
+
this._keyboardNavigationWraps = coerceBooleanProperty(wrap);
|
|
334
|
+
this.listKeyManager?.withWrap(this._keyboardNavigationWraps);
|
|
335
|
+
}
|
|
336
|
+
/** Whether keyboard navigation should skip over disabled items. */
|
|
337
|
+
get keyboardNavigationSkipsDisabled() {
|
|
338
|
+
return this._keyboardNavigationSkipsDisabled;
|
|
339
|
+
}
|
|
340
|
+
set keyboardNavigationSkipsDisabled(skip) {
|
|
341
|
+
this._keyboardNavigationSkipsDisabled = coerceBooleanProperty(skip);
|
|
342
|
+
this.listKeyManager?.skipPredicate(this._keyboardNavigationSkipsDisabled ? this._skipDisabledPredicate : this._skipNonePredicate);
|
|
286
343
|
}
|
|
287
344
|
ngAfterContentInit() {
|
|
288
345
|
if (typeof ngDevMode === 'undefined' || ngDevMode) {
|
|
289
346
|
this._verifyNoOptionValueCollisions();
|
|
290
347
|
}
|
|
291
348
|
this._initKeyManager();
|
|
292
|
-
|
|
293
|
-
|
|
349
|
+
// Update the internal value whenever the options change, as this may change the validity of
|
|
350
|
+
// the current selection
|
|
351
|
+
this.options.changes.pipe(startWith(this.options), takeUntil(this.destroyed)).subscribe(() => {
|
|
294
352
|
this._updateInternalValue();
|
|
295
|
-
this._onValidatorChange();
|
|
296
353
|
});
|
|
297
354
|
this._optionClicked
|
|
298
|
-
.pipe(filter(option => !option.disabled), takeUntil(this.destroyed))
|
|
299
|
-
.subscribe(option => this._handleOptionClicked(option));
|
|
355
|
+
.pipe(filter(({ option }) => !option.disabled), takeUntil(this.destroyed))
|
|
356
|
+
.subscribe(({ option, event }) => this._handleOptionClicked(option, event));
|
|
300
357
|
}
|
|
301
358
|
ngOnDestroy() {
|
|
302
359
|
this.listKeyManager.change.complete();
|
|
@@ -315,7 +372,10 @@ class CdkListbox {
|
|
|
315
372
|
* @param value The value to toggle
|
|
316
373
|
*/
|
|
317
374
|
toggleValue(value) {
|
|
318
|
-
this.
|
|
375
|
+
if (this._invalid) {
|
|
376
|
+
this.selectionModel.clear(false);
|
|
377
|
+
}
|
|
378
|
+
this.selectionModel.toggle(value);
|
|
319
379
|
}
|
|
320
380
|
/**
|
|
321
381
|
* Select the given option.
|
|
@@ -329,7 +389,10 @@ class CdkListbox {
|
|
|
329
389
|
* @param value The value to select
|
|
330
390
|
*/
|
|
331
391
|
selectValue(value) {
|
|
332
|
-
this.
|
|
392
|
+
if (this._invalid) {
|
|
393
|
+
this.selectionModel.clear(false);
|
|
394
|
+
}
|
|
395
|
+
this.selectionModel.select(value);
|
|
333
396
|
}
|
|
334
397
|
/**
|
|
335
398
|
* Deselect the given option.
|
|
@@ -343,7 +406,10 @@ class CdkListbox {
|
|
|
343
406
|
* @param value The value to deselect
|
|
344
407
|
*/
|
|
345
408
|
deselectValue(value) {
|
|
346
|
-
this.
|
|
409
|
+
if (this._invalid) {
|
|
410
|
+
this.selectionModel.clear(false);
|
|
411
|
+
}
|
|
412
|
+
this.selectionModel.deselect(value);
|
|
347
413
|
}
|
|
348
414
|
/**
|
|
349
415
|
* Set the selected state of all options.
|
|
@@ -351,10 +417,13 @@ class CdkListbox {
|
|
|
351
417
|
*/
|
|
352
418
|
setAllSelected(isSelected) {
|
|
353
419
|
if (!isSelected) {
|
|
354
|
-
this.selectionModel
|
|
420
|
+
this.selectionModel.clear();
|
|
355
421
|
}
|
|
356
422
|
else {
|
|
357
|
-
|
|
423
|
+
if (this._invalid) {
|
|
424
|
+
this.selectionModel.clear(false);
|
|
425
|
+
}
|
|
426
|
+
this.selectionModel.select(...this.options.map(option => option.value));
|
|
358
427
|
}
|
|
359
428
|
}
|
|
360
429
|
/**
|
|
@@ -362,7 +431,17 @@ class CdkListbox {
|
|
|
362
431
|
* @param option The option to get the selected state of
|
|
363
432
|
*/
|
|
364
433
|
isSelected(option) {
|
|
365
|
-
return this.
|
|
434
|
+
return this.isValueSelected(option.value);
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Get whether the given value is selected.
|
|
438
|
+
* @param value The value to get the selected state of
|
|
439
|
+
*/
|
|
440
|
+
isValueSelected(value) {
|
|
441
|
+
if (this._invalid) {
|
|
442
|
+
return false;
|
|
443
|
+
}
|
|
444
|
+
return this.selectionModel.isSelected(value);
|
|
366
445
|
}
|
|
367
446
|
/**
|
|
368
447
|
* Registers a callback to be invoked when the listbox's value changes from user input.
|
|
@@ -415,10 +494,6 @@ class CdkListbox {
|
|
|
415
494
|
focus() {
|
|
416
495
|
this.element.focus();
|
|
417
496
|
}
|
|
418
|
-
/** The selection model used to track the listbox's value. */
|
|
419
|
-
selectionModel() {
|
|
420
|
-
return this.selectionModelSubject.value;
|
|
421
|
-
}
|
|
422
497
|
/**
|
|
423
498
|
* Triggers the given option in response to user interaction.
|
|
424
499
|
* - In single selection mode: selects the option and deselects any other selected option.
|
|
@@ -427,16 +502,10 @@ class CdkListbox {
|
|
|
427
502
|
*/
|
|
428
503
|
triggerOption(option) {
|
|
429
504
|
if (option && !option.disabled) {
|
|
430
|
-
|
|
431
|
-
this.
|
|
432
|
-
.
|
|
433
|
-
.
|
|
434
|
-
if (this.multiple) {
|
|
435
|
-
this.toggle(option);
|
|
436
|
-
}
|
|
437
|
-
else {
|
|
438
|
-
this.select(option);
|
|
439
|
-
}
|
|
505
|
+
this._lastTriggered = option;
|
|
506
|
+
const changed = this.multiple
|
|
507
|
+
? this.selectionModel.toggle(option.value)
|
|
508
|
+
: this.selectionModel.select(option.value);
|
|
440
509
|
if (changed) {
|
|
441
510
|
this._onChange(this.value);
|
|
442
511
|
this.valueChange.next({
|
|
@@ -447,6 +516,44 @@ class CdkListbox {
|
|
|
447
516
|
}
|
|
448
517
|
}
|
|
449
518
|
}
|
|
519
|
+
/**
|
|
520
|
+
* Trigger the given range of options in response to user interaction.
|
|
521
|
+
* Should only be called in multi-selection mode.
|
|
522
|
+
* @param trigger The option that was triggered
|
|
523
|
+
* @param from The start index of the options to toggle
|
|
524
|
+
* @param to The end index of the options to toggle
|
|
525
|
+
* @param on Whether to toggle the option range on
|
|
526
|
+
*/
|
|
527
|
+
triggerRange(trigger, from, to, on) {
|
|
528
|
+
if (this.disabled || (trigger && trigger.disabled)) {
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
this._lastTriggered = trigger;
|
|
532
|
+
const isEqual = this.compareWith ?? Object.is;
|
|
533
|
+
const updateValues = [...this.options]
|
|
534
|
+
.slice(Math.max(0, Math.min(from, to)), Math.min(this.options.length, Math.max(from, to) + 1))
|
|
535
|
+
.filter(option => !option.disabled)
|
|
536
|
+
.map(option => option.value);
|
|
537
|
+
const selected = [...this.value];
|
|
538
|
+
for (const updateValue of updateValues) {
|
|
539
|
+
const selectedIndex = selected.findIndex(selectedValue => isEqual(selectedValue, updateValue));
|
|
540
|
+
if (on && selectedIndex === -1) {
|
|
541
|
+
selected.push(updateValue);
|
|
542
|
+
}
|
|
543
|
+
else if (!on && selectedIndex !== -1) {
|
|
544
|
+
selected.splice(selectedIndex, 1);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
let changed = this.selectionModel.setSelection(...selected);
|
|
548
|
+
if (changed) {
|
|
549
|
+
this._onChange(this.value);
|
|
550
|
+
this.valueChange.next({
|
|
551
|
+
value: this.value,
|
|
552
|
+
listbox: this,
|
|
553
|
+
option: trigger,
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
}
|
|
450
557
|
/**
|
|
451
558
|
* Sets the given option as active.
|
|
452
559
|
* @param option The option to make active
|
|
@@ -468,19 +575,62 @@ class CdkListbox {
|
|
|
468
575
|
}
|
|
469
576
|
const { keyCode } = event;
|
|
470
577
|
const previousActiveIndex = this.listKeyManager.activeItemIndex;
|
|
578
|
+
const ctrlKeys = ['ctrlKey', 'metaKey'];
|
|
579
|
+
if (this.multiple && keyCode === A && hasModifierKey(event, ...ctrlKeys)) {
|
|
580
|
+
// Toggle all options off if they're all selected, otherwise toggle them all on.
|
|
581
|
+
this.triggerRange(null, 0, this.options.length - 1, this.options.length !== this.value.length);
|
|
582
|
+
event.preventDefault();
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
if (this.multiple &&
|
|
586
|
+
(keyCode === SPACE || keyCode === ENTER) &&
|
|
587
|
+
hasModifierKey(event, 'shiftKey')) {
|
|
588
|
+
if (this.listKeyManager.activeItem && this.listKeyManager.activeItemIndex != null) {
|
|
589
|
+
this.triggerRange(this.listKeyManager.activeItem, this._getLastTriggeredIndex() ?? this.listKeyManager.activeItemIndex, this.listKeyManager.activeItemIndex, !this.listKeyManager.activeItem.isSelected());
|
|
590
|
+
}
|
|
591
|
+
event.preventDefault();
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
if (this.multiple &&
|
|
595
|
+
keyCode === HOME &&
|
|
596
|
+
hasModifierKey(event, ...ctrlKeys) &&
|
|
597
|
+
hasModifierKey(event, 'shiftKey')) {
|
|
598
|
+
const trigger = this.listKeyManager.activeItem;
|
|
599
|
+
if (trigger) {
|
|
600
|
+
const from = this.listKeyManager.activeItemIndex;
|
|
601
|
+
this.listKeyManager.setFirstItemActive();
|
|
602
|
+
this.triggerRange(trigger, from, this.listKeyManager.activeItemIndex, !trigger.isSelected());
|
|
603
|
+
}
|
|
604
|
+
event.preventDefault();
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
if (this.multiple &&
|
|
608
|
+
keyCode === END &&
|
|
609
|
+
hasModifierKey(event, ...ctrlKeys) &&
|
|
610
|
+
hasModifierKey(event, 'shiftKey')) {
|
|
611
|
+
const trigger = this.listKeyManager.activeItem;
|
|
612
|
+
if (trigger) {
|
|
613
|
+
const from = this.listKeyManager.activeItemIndex;
|
|
614
|
+
this.listKeyManager.setLastItemActive();
|
|
615
|
+
this.triggerRange(trigger, from, this.listKeyManager.activeItemIndex, !trigger.isSelected());
|
|
616
|
+
}
|
|
617
|
+
event.preventDefault();
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
471
620
|
if (keyCode === SPACE || keyCode === ENTER) {
|
|
472
621
|
this.triggerOption(this.listKeyManager.activeItem);
|
|
473
622
|
event.preventDefault();
|
|
623
|
+
return;
|
|
474
624
|
}
|
|
475
|
-
|
|
476
|
-
this.listKeyManager.onKeydown(event);
|
|
477
|
-
}
|
|
478
|
-
/** Will select an option if shift was pressed while navigating to the option */
|
|
479
|
-
const isArrow = keyCode === UP_ARROW ||
|
|
625
|
+
const isNavKey = keyCode === UP_ARROW ||
|
|
480
626
|
keyCode === DOWN_ARROW ||
|
|
481
627
|
keyCode === LEFT_ARROW ||
|
|
482
|
-
keyCode === RIGHT_ARROW
|
|
483
|
-
|
|
628
|
+
keyCode === RIGHT_ARROW ||
|
|
629
|
+
keyCode === HOME ||
|
|
630
|
+
keyCode === END;
|
|
631
|
+
this.listKeyManager.onKeydown(event);
|
|
632
|
+
// Will select an option if shift was pressed while navigating to the option
|
|
633
|
+
if (isNavKey && event.shiftKey && previousActiveIndex !== this.listKeyManager.activeItemIndex) {
|
|
484
634
|
this.triggerOption(this.listKeyManager.activeItem);
|
|
485
635
|
}
|
|
486
636
|
}
|
|
@@ -508,10 +658,13 @@ class CdkListbox {
|
|
|
508
658
|
/** Initialize the key manager. */
|
|
509
659
|
_initKeyManager() {
|
|
510
660
|
this.listKeyManager = new ActiveDescendantKeyManager(this.options)
|
|
511
|
-
.withWrap()
|
|
661
|
+
.withWrap(this._keyboardNavigationWraps)
|
|
512
662
|
.withTypeAhead()
|
|
513
663
|
.withHomeAndEnd()
|
|
514
|
-
.withAllowedModifierKeys(['shiftKey'])
|
|
664
|
+
.withAllowedModifierKeys(['shiftKey'])
|
|
665
|
+
.skipPredicate(this._keyboardNavigationSkipsDisabled
|
|
666
|
+
? this._skipDisabledPredicate
|
|
667
|
+
: this._skipNonePredicate);
|
|
515
668
|
if (this.orientation === 'vertical') {
|
|
516
669
|
this.listKeyManager.withVerticalOrientation();
|
|
517
670
|
}
|
|
@@ -522,21 +675,6 @@ class CdkListbox {
|
|
|
522
675
|
.pipe(takeUntil(this.destroyed))
|
|
523
676
|
.subscribe(() => this._focusActiveOption());
|
|
524
677
|
}
|
|
525
|
-
// TODO(mmalerba): Should not depend on combobox.
|
|
526
|
-
_updatePanelForSelection(option) {
|
|
527
|
-
if (this._combobox) {
|
|
528
|
-
if (!this.multiple) {
|
|
529
|
-
this._combobox.updateAndClose(option.isSelected() ? option.value : []);
|
|
530
|
-
}
|
|
531
|
-
else {
|
|
532
|
-
this._combobox.updateAndClose(this.value);
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
/** Update the selection mode when the 'multiple' property changes. */
|
|
537
|
-
_updateSelectionModel() {
|
|
538
|
-
this.selectionModelSubject.next(new SelectionModel(this.multiple, !this.multiple && this.value.length > 1 ? [] : this.value.slice(), true, this._compareWith));
|
|
539
|
-
}
|
|
540
678
|
/** Focus the active option. */
|
|
541
679
|
_focusActiveOption() {
|
|
542
680
|
if (!this.useActiveDescendant) {
|
|
@@ -549,26 +687,23 @@ class CdkListbox {
|
|
|
549
687
|
* @param value The list of new selected values.
|
|
550
688
|
*/
|
|
551
689
|
_setSelection(value) {
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
690
|
+
if (this._invalid) {
|
|
691
|
+
this.selectionModel.clear(false);
|
|
692
|
+
}
|
|
693
|
+
this.selectionModel.setSelection(...this._coerceValue(value));
|
|
556
694
|
}
|
|
557
695
|
/** Update the internal value of the listbox based on the selection model. */
|
|
558
696
|
_updateInternalValue() {
|
|
559
697
|
const indexCache = new Map();
|
|
560
|
-
|
|
561
|
-
// (e.g. if the option was removed from the DOM.)
|
|
562
|
-
const selected = this.selectionModel().selected;
|
|
563
|
-
const validSelected = this._getValuesWithValidity(selected, true);
|
|
564
|
-
if (validSelected.length != selected.length) {
|
|
565
|
-
this.selectionModel().setSelection(...validSelected);
|
|
566
|
-
}
|
|
567
|
-
this.selectionModel().sort((a, b) => {
|
|
698
|
+
this.selectionModel.sort((a, b) => {
|
|
568
699
|
const aIndex = this._getIndexForValue(indexCache, a);
|
|
569
700
|
const bIndex = this._getIndexForValue(indexCache, b);
|
|
570
701
|
return aIndex - bIndex;
|
|
571
702
|
});
|
|
703
|
+
const selected = this.selectionModel.selected;
|
|
704
|
+
this._invalid =
|
|
705
|
+
(!this.multiple && selected.length > 1) || !!this._getInvalidOptionValues(selected).length;
|
|
706
|
+
this._onValidatorChange();
|
|
572
707
|
this.changeDetectorRef.markForCheck();
|
|
573
708
|
}
|
|
574
709
|
/**
|
|
@@ -595,17 +730,18 @@ class CdkListbox {
|
|
|
595
730
|
* Handle the user clicking an option.
|
|
596
731
|
* @param option The option that was clicked.
|
|
597
732
|
*/
|
|
598
|
-
_handleOptionClicked(option) {
|
|
733
|
+
_handleOptionClicked(option, event) {
|
|
599
734
|
this.listKeyManager.setActiveItem(option);
|
|
600
|
-
this.
|
|
601
|
-
|
|
735
|
+
if (event.shiftKey && this.multiple) {
|
|
736
|
+
this.triggerRange(option, this._getLastTriggeredIndex() ?? this.listKeyManager.activeItemIndex, this.listKeyManager.activeItemIndex, !option.isSelected());
|
|
737
|
+
}
|
|
738
|
+
else {
|
|
739
|
+
this.triggerOption(option);
|
|
740
|
+
}
|
|
602
741
|
}
|
|
603
742
|
/** Verifies that no two options represent the same value under the compareWith function. */
|
|
604
743
|
_verifyNoOptionValueCollisions() {
|
|
605
|
-
|
|
606
|
-
this.selectionModelSubject,
|
|
607
|
-
this.options.changes.pipe(startWith(this.options)),
|
|
608
|
-
]).subscribe(() => {
|
|
744
|
+
this.options.changes.pipe(startWith(this.options), takeUntil(this.destroyed)).subscribe(() => {
|
|
609
745
|
const isEqual = this.compareWith ?? Object.is;
|
|
610
746
|
for (let i = 0; i < this.options.length; i++) {
|
|
611
747
|
const option = this.options.get(i);
|
|
@@ -646,19 +782,23 @@ class CdkListbox {
|
|
|
646
782
|
return value == null ? [] : coerceArray(value);
|
|
647
783
|
}
|
|
648
784
|
/**
|
|
649
|
-
* Get the sublist of values
|
|
785
|
+
* Get the sublist of values that do not represent valid option values in this listbox.
|
|
650
786
|
* @param values The list of values
|
|
651
|
-
* @
|
|
652
|
-
* @return The sublist of values with the requested validity
|
|
787
|
+
* @return The sublist of values that are not valid option values
|
|
653
788
|
*/
|
|
654
|
-
|
|
789
|
+
_getInvalidOptionValues(values) {
|
|
655
790
|
const isEqual = this.compareWith || Object.is;
|
|
656
791
|
const validValues = (this.options || []).map(option => option.value);
|
|
657
|
-
return values.filter(value =>
|
|
792
|
+
return values.filter(value => !validValues.some(validValue => isEqual(value, validValue)));
|
|
793
|
+
}
|
|
794
|
+
/** Get the index of the last triggered option. */
|
|
795
|
+
_getLastTriggeredIndex() {
|
|
796
|
+
const index = this.options.toArray().indexOf(this._lastTriggered);
|
|
797
|
+
return index === -1 ? null : index;
|
|
658
798
|
}
|
|
659
799
|
}
|
|
660
800
|
CdkListbox.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.0.1", ngImport: i0, type: CdkListbox, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
661
|
-
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: [
|
|
801
|
+
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: [
|
|
662
802
|
{
|
|
663
803
|
provide: NG_VALUE_ACCESSOR,
|
|
664
804
|
useExisting: forwardRef(() => CdkListbox),
|
|
@@ -724,6 +864,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.0.1", ngImpor
|
|
|
724
864
|
}], compareWith: [{
|
|
725
865
|
type: Input,
|
|
726
866
|
args: ['cdkListboxCompareWith']
|
|
867
|
+
}], keyboardNavigationWraps: [{
|
|
868
|
+
type: Input,
|
|
869
|
+
args: ['cdkListboxKeyboardNavigationWraps']
|
|
870
|
+
}], keyboardNavigationSkipsDisabled: [{
|
|
871
|
+
type: Input,
|
|
872
|
+
args: ['cdkListboxKeyboardNavigationSkipsDisabled']
|
|
727
873
|
}], valueChange: [{
|
|
728
874
|
type: Output,
|
|
729
875
|
args: ['cdkListboxValueChange']
|