-
Notifications
You must be signed in to change notification settings - Fork 6.7k
/
option.ts
343 lines (301 loc) · 10.8 KB
/
option.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {FocusableOption, FocusOrigin} from '@angular/cdk/a11y';
import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion';
import {ENTER, hasModifierKey, SPACE} from '@angular/cdk/keycodes';
import {
Component,
ViewEncapsulation,
ChangeDetectionStrategy,
ElementRef,
ChangeDetectorRef,
Optional,
Inject,
Directive,
AfterViewChecked,
OnDestroy,
Input,
Output,
EventEmitter,
QueryList,
ViewChild,
} from '@angular/core';
import {Subject} from 'rxjs';
import {MatOptgroup, MAT_OPTGROUP, _MatOptgroupBase} from './optgroup';
import {MatOptionParentComponent, MAT_OPTION_PARENT_COMPONENT} from './option-parent';
/**
* Option IDs need to be unique across components, so this counter exists outside of
* the component definition.
*/
let _uniqueIdCounter = 0;
/** Event object emitted by MatOption when selected or deselected. */
export class MatOptionSelectionChange<T = any> {
constructor(
/** Reference to the option that emitted the event. */
public source: _MatOptionBase<T>,
/** Whether the change in the option's value was a result of a user action. */
public isUserInput = false,
) {}
}
@Directive()
export class _MatOptionBase<T = any> implements FocusableOption, AfterViewChecked, OnDestroy {
private _selected = false;
private _active = false;
private _disabled = false;
private _mostRecentViewValue = '';
/** Whether the wrapping component is in multiple selection mode. */
get multiple() {
return this._parent && this._parent.multiple;
}
/** Whether or not the option is currently selected. */
get selected(): boolean {
return this._selected;
}
/** The form value of the option. */
@Input() value: T;
/** The unique ID of the option. */
@Input() id: string = `mat-option-${_uniqueIdCounter++}`;
/** Whether the option is disabled. */
@Input()
get disabled(): boolean {
return (this.group && this.group.disabled) || this._disabled;
}
set disabled(value: BooleanInput) {
this._disabled = coerceBooleanProperty(value);
}
/** Whether ripples for the option are disabled. */
get disableRipple(): boolean {
return !!(this._parent && this._parent.disableRipple);
}
/** Whether to display checkmark for single-selection. */
get hideSingleSelectionIndicator(): boolean {
return !!(this._parent && this._parent.hideSingleSelectionIndicator);
}
/** Event emitted when the option is selected or deselected. */
// tslint:disable-next-line:no-output-on-prefix
@Output() readonly onSelectionChange = new EventEmitter<MatOptionSelectionChange<T>>();
/** Element containing the option's text. */
@ViewChild('text', {static: true}) _text: ElementRef<HTMLElement> | undefined;
/** Emits when the state of the option changes and any parents have to be notified. */
readonly _stateChanges = new Subject<void>();
constructor(
private _element: ElementRef<HTMLElement>,
public _changeDetectorRef: ChangeDetectorRef,
private _parent: MatOptionParentComponent,
readonly group: _MatOptgroupBase,
) {}
/**
* Whether or not the option is currently active and ready to be selected.
* An active option displays styles as if it is focused, but the
* focus is actually retained somewhere else. This comes in handy
* for components like autocomplete where focus must remain on the input.
*/
get active(): boolean {
return this._active;
}
/**
* The displayed value of the option. It is necessary to show the selected option in the
* select's trigger.
*/
get viewValue(): string {
// TODO(kara): Add input property alternative for node envs.
return (this._text?.nativeElement.textContent || '').trim();
}
/** Selects the option. */
select(): void {
if (!this._selected) {
this._selected = true;
this._changeDetectorRef.markForCheck();
this._emitSelectionChangeEvent();
}
}
/** Deselects the option. */
deselect(): void {
if (this._selected) {
this._selected = false;
this._changeDetectorRef.markForCheck();
this._emitSelectionChangeEvent();
}
}
/** Sets focus onto this option. */
focus(_origin?: FocusOrigin, options?: FocusOptions): void {
// Note that we aren't using `_origin`, but we need to keep it because some internal consumers
// use `MatOption` in a `FocusKeyManager` and we need it to match `FocusableOption`.
const element = this._getHostElement();
if (typeof element.focus === 'function') {
element.focus(options);
}
}
/**
* This method sets display styles on the option to make it appear
* active. This is used by the ActiveDescendantKeyManager so key
* events will display the proper options as active on arrow key events.
*/
setActiveStyles(): void {
if (!this._active) {
this._active = true;
this._changeDetectorRef.markForCheck();
}
}
/**
* This method removes display styles on the option that made it appear
* active. This is used by the ActiveDescendantKeyManager so key
* events will display the proper options as active on arrow key events.
*/
setInactiveStyles(): void {
if (this._active) {
this._active = false;
this._changeDetectorRef.markForCheck();
}
}
/** Gets the label to be used when determining whether the option should be focused. */
getLabel(): string {
return this.viewValue;
}
/** Ensures the option is selected when activated from the keyboard. */
_handleKeydown(event: KeyboardEvent): void {
if ((event.keyCode === ENTER || event.keyCode === SPACE) && !hasModifierKey(event)) {
this._selectViaInteraction();
// Prevent the page from scrolling down and form submits.
event.preventDefault();
}
}
/**
* `Selects the option while indicating the selection came from the user. Used to
* determine if the select's view -> model callback should be invoked.`
*/
_selectViaInteraction(): void {
if (!this.disabled) {
this._selected = this.multiple ? !this._selected : true;
this._changeDetectorRef.markForCheck();
this._emitSelectionChangeEvent(true);
}
}
/** Returns the correct tabindex for the option depending on disabled state. */
_getTabIndex(): string {
return this.disabled ? '-1' : '0';
}
/** Gets the host DOM element. */
_getHostElement(): HTMLElement {
return this._element.nativeElement;
}
ngAfterViewChecked() {
// Since parent components could be using the option's label to display the selected values
// (e.g. `mat-select`) and they don't have a way of knowing if the option's label has changed
// we have to check for changes in the DOM ourselves and dispatch an event. These checks are
// relatively cheap, however we still limit them only to selected options in order to avoid
// hitting the DOM too often.
if (this._selected) {
const viewValue = this.viewValue;
if (viewValue !== this._mostRecentViewValue) {
if (this._mostRecentViewValue) {
this._stateChanges.next();
}
this._mostRecentViewValue = viewValue;
}
}
}
ngOnDestroy() {
this._stateChanges.complete();
}
/** Emits the selection change event. */
private _emitSelectionChangeEvent(isUserInput = false): void {
this.onSelectionChange.emit(new MatOptionSelectionChange<T>(this, isUserInput));
}
}
/**
* Single option inside of a `<mat-select>` element.
*/
@Component({
selector: 'mat-option',
exportAs: 'matOption',
host: {
'role': 'option',
'[attr.tabindex]': '_getTabIndex()',
'[class.mdc-list-item--selected]': 'selected',
'[class.mat-mdc-option-multiple]': 'multiple',
'[class.mat-mdc-option-active]': 'active',
'[class.mdc-list-item--disabled]': 'disabled',
'[id]': 'id',
// Set aria-selected to false for non-selected items and true for selected items. Conform to
// [WAI ARIA Listbox authoring practices guide](
// https://www.w3.org/WAI/ARIA/apg/patterns/listbox/), "If any options are selected, each
// selected option has either aria-selected or aria-checked set to true. All options that are
// selectable but not selected have either aria-selected or aria-checked set to false." Align
// aria-selected implementation of Chips and List components.
//
// Set `aria-selected="false"` on not-selected listbox options to fix VoiceOver announcing
// every option as "selected" (#21491).
'[attr.aria-selected]': 'selected',
'[attr.aria-disabled]': 'disabled.toString()',
'(click)': '_selectViaInteraction()',
'(keydown)': '_handleKeydown($event)',
'class': 'mat-mdc-option mat-mdc-focus-indicator mdc-list-item',
},
styleUrls: ['option.css'],
templateUrl: 'option.html',
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MatOption<T = any> extends _MatOptionBase<T> {
constructor(
element: ElementRef<HTMLElement>,
changeDetectorRef: ChangeDetectorRef,
@Optional() @Inject(MAT_OPTION_PARENT_COMPONENT) parent: MatOptionParentComponent,
@Optional() @Inject(MAT_OPTGROUP) group: MatOptgroup,
) {
super(element, changeDetectorRef, parent, group);
}
}
/**
* Counts the amount of option group labels that precede the specified option.
* @param optionIndex Index of the option at which to start counting.
* @param options Flat list of all of the options.
* @param optionGroups Flat list of all of the option groups.
* @docs-private
*/
export function _countGroupLabelsBeforeOption(
optionIndex: number,
options: QueryList<MatOption>,
optionGroups: QueryList<MatOptgroup>,
): number {
if (optionGroups.length) {
let optionsArray = options.toArray();
let groups = optionGroups.toArray();
let groupCounter = 0;
for (let i = 0; i < optionIndex + 1; i++) {
if (optionsArray[i].group && optionsArray[i].group === groups[groupCounter]) {
groupCounter++;
}
}
return groupCounter;
}
return 0;
}
/**
* Determines the position to which to scroll a panel in order for an option to be into view.
* @param optionOffset Offset of the option from the top of the panel.
* @param optionHeight Height of the options.
* @param currentScrollPosition Current scroll position of the panel.
* @param panelHeight Height of the panel.
* @docs-private
*/
export function _getOptionScrollPosition(
optionOffset: number,
optionHeight: number,
currentScrollPosition: number,
panelHeight: number,
): number {
if (optionOffset < currentScrollPosition) {
return optionOffset;
}
if (optionOffset + optionHeight > currentScrollPosition + panelHeight) {
return Math.max(0, optionOffset - panelHeight + optionHeight);
}
return currentScrollPosition;
}