sass-references/angular-material/material/core/option/option.ts

347 lines
11 KiB
TypeScript
Raw Permalink Normal View History

2024-12-06 10:42:08 +08:00
/**
* @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.dev/license
*/
import {_IdGenerator, FocusableOption, FocusOrigin} from '@angular/cdk/a11y';
import {ENTER, hasModifierKey, SPACE} from '@angular/cdk/keycodes';
import {
Component,
ViewEncapsulation,
ChangeDetectionStrategy,
ElementRef,
ChangeDetectorRef,
AfterViewChecked,
OnDestroy,
Input,
Output,
EventEmitter,
QueryList,
ViewChild,
booleanAttribute,
inject,
isSignal,
Signal,
} from '@angular/core';
import {Subject} from 'rxjs';
import {MAT_OPTGROUP, MatOptgroup} from './optgroup';
import {MatOptionParentComponent, MAT_OPTION_PARENT_COMPONENT} from './option-parent';
import {MatRipple} from '../ripple/ripple';
import {MatPseudoCheckbox} from '../selection/pseudo-checkbox/pseudo-checkbox';
import {_StructuralStylesLoader} from '../focus-indicators/structural-styles';
import {_CdkPrivateStyleLoader, _VisuallyHiddenLoader} from '@angular/cdk/private';
/** 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: MatOption<T>,
/** Whether the change in the option's value was a result of a user action. */
public isUserInput = false,
) {}
}
/**
* Single option inside of a `<mat-select>` element.
*/
@Component({
selector: 'mat-option',
exportAs: 'matOption',
host: {
'role': 'option',
'[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 mdc-list-item',
},
styleUrl: 'option.css',
templateUrl: 'option.html',
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [MatPseudoCheckbox, MatRipple],
})
export class MatOption<T = any> implements FocusableOption, AfterViewChecked, OnDestroy {
private _element = inject<ElementRef<HTMLElement>>(ElementRef);
_changeDetectorRef = inject(ChangeDetectorRef);
private _parent = inject<MatOptionParentComponent>(MAT_OPTION_PARENT_COMPONENT, {optional: true});
group = inject<MatOptgroup>(MAT_OPTGROUP, {optional: true});
private _signalDisableRipple = false;
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 = inject(_IdGenerator).getId('mat-option-');
/** Whether the option is disabled. */
@Input({transform: booleanAttribute})
get disabled(): boolean {
return (this.group && this.group.disabled) || this._disabled;
}
set disabled(value: boolean) {
this._disabled = value;
}
/** Whether ripples for the option are disabled. */
get disableRipple(): boolean {
return this._signalDisableRipple
? (this._parent!.disableRipple as Signal<boolean>)()
: !!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(...args: unknown[]);
constructor() {
inject(_CdkPrivateStyleLoader).load(_StructuralStylesLoader);
inject(_CdkPrivateStyleLoader).load(_VisuallyHiddenLoader);
this._signalDisableRipple = !!this._parent && isSignal(this._parent.disableRipple);
}
/**
* 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(emitEvent = true): void {
if (!this._selected) {
this._selected = true;
this._changeDetectorRef.markForCheck();
if (emitEvent) {
this._emitSelectionChangeEvent();
}
}
}
/** Deselects the option. */
deselect(emitEvent = true): void {
if (this._selected) {
this._selected = false;
this._changeDetectorRef.markForCheck();
if (emitEvent) {
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. */
// This method is only used by `MatLegacyOption`. Keeping it here to avoid breaking the types.
// That's because `MatLegacyOption` use `MatOption` type in a few places such as
// `MatOptionSelectionChange`. It is safe to delete this when `MatLegacyOption` is deleted.
_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));
}
}
/**
* 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;
}