sass-references/angular-material/material/expansion/expansion-panel-header.ts

246 lines
7.0 KiB
TypeScript
Raw 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 {FocusableOption, FocusMonitor, FocusOrigin} from '@angular/cdk/a11y';
import {ENTER, hasModifierKey, SPACE} from '@angular/cdk/keycodes';
import {
AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
Directive,
ElementRef,
Input,
numberAttribute,
OnDestroy,
ViewEncapsulation,
inject,
HostAttributeToken,
} from '@angular/core';
import {EMPTY, merge, Subscription} from 'rxjs';
import {filter} from 'rxjs/operators';
import {MatAccordionTogglePosition} from './accordion-base';
import {
MatExpansionPanel,
MatExpansionPanelDefaultOptions,
MAT_EXPANSION_PANEL_DEFAULT_OPTIONS,
} from './expansion-panel';
import {_CdkPrivateStyleLoader} from '@angular/cdk/private';
import {_StructuralStylesLoader} from '@angular/material/core';
/**
* Header element of a `<mat-expansion-panel>`.
*/
@Component({
selector: 'mat-expansion-panel-header',
styleUrl: 'expansion-panel-header.css',
templateUrl: 'expansion-panel-header.html',
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
host: {
'class': 'mat-expansion-panel-header mat-focus-indicator',
'role': 'button',
'[attr.id]': 'panel._headerId',
'[attr.tabindex]': 'disabled ? -1 : tabIndex',
'[attr.aria-controls]': '_getPanelId()',
'[attr.aria-expanded]': '_isExpanded()',
'[attr.aria-disabled]': 'panel.disabled',
'[class.mat-expanded]': '_isExpanded()',
'[class.mat-expansion-toggle-indicator-after]': `_getTogglePosition() === 'after'`,
'[class.mat-expansion-toggle-indicator-before]': `_getTogglePosition() === 'before'`,
'[style.height]': '_getHeaderHeight()',
'(click)': '_toggle()',
'(keydown)': '_keydown($event)',
},
})
export class MatExpansionPanelHeader implements AfterViewInit, OnDestroy, FocusableOption {
panel = inject(MatExpansionPanel, {host: true});
private _element = inject(ElementRef);
private _focusMonitor = inject(FocusMonitor);
private _changeDetectorRef = inject(ChangeDetectorRef);
private _parentChangeSubscription = Subscription.EMPTY;
constructor(...args: unknown[]);
constructor() {
inject(_CdkPrivateStyleLoader).load(_StructuralStylesLoader);
const panel = this.panel;
const defaultOptions = inject<MatExpansionPanelDefaultOptions>(
MAT_EXPANSION_PANEL_DEFAULT_OPTIONS,
{optional: true},
);
const tabIndex = inject(new HostAttributeToken('tabindex'), {optional: true});
const accordionHideToggleChange = panel.accordion
? panel.accordion._stateChanges.pipe(
filter(changes => !!(changes['hideToggle'] || changes['togglePosition'])),
)
: EMPTY;
this.tabIndex = parseInt(tabIndex || '') || 0;
// Since the toggle state depends on an @Input on the panel, we
// need to subscribe and trigger change detection manually.
this._parentChangeSubscription = merge(
panel.opened,
panel.closed,
accordionHideToggleChange,
panel._inputChanges.pipe(
filter(changes => {
return !!(changes['hideToggle'] || changes['disabled'] || changes['togglePosition']);
}),
),
).subscribe(() => this._changeDetectorRef.markForCheck());
// Avoids focus being lost if the panel contained the focused element and was closed.
panel.closed
.pipe(filter(() => panel._containsFocus()))
.subscribe(() => this._focusMonitor.focusVia(this._element, 'program'));
if (defaultOptions) {
this.expandedHeight = defaultOptions.expandedHeight;
this.collapsedHeight = defaultOptions.collapsedHeight;
}
}
/** Height of the header while the panel is expanded. */
@Input() expandedHeight: string;
/** Height of the header while the panel is collapsed. */
@Input() collapsedHeight: string;
/** Tab index of the header. */
@Input({
transform: (value: unknown) => (value == null ? 0 : numberAttribute(value)),
})
tabIndex: number = 0;
/**
* Whether the associated panel is disabled. Implemented as a part of `FocusableOption`.
* @docs-private
*/
get disabled(): boolean {
return this.panel.disabled;
}
/** Toggles the expanded state of the panel. */
_toggle(): void {
if (!this.disabled) {
this.panel.toggle();
}
}
/** Gets whether the panel is expanded. */
_isExpanded(): boolean {
return this.panel.expanded;
}
/** Gets the expanded state string of the panel. */
_getExpandedState(): string {
return this.panel._getExpandedState();
}
/** Gets the panel id. */
_getPanelId(): string {
return this.panel.id;
}
/** Gets the toggle position for the header. */
_getTogglePosition(): MatAccordionTogglePosition {
return this.panel.togglePosition;
}
/** Gets whether the expand indicator should be shown. */
_showToggle(): boolean {
return !this.panel.hideToggle && !this.panel.disabled;
}
/**
* Gets the current height of the header. Null if no custom height has been
* specified, and if the default height from the stylesheet should be used.
*/
_getHeaderHeight(): string | null {
const isExpanded = this._isExpanded();
if (isExpanded && this.expandedHeight) {
return this.expandedHeight;
} else if (!isExpanded && this.collapsedHeight) {
return this.collapsedHeight;
}
return null;
}
/** Handle keydown event calling to toggle() if appropriate. */
_keydown(event: KeyboardEvent) {
switch (event.keyCode) {
// Toggle for space and enter keys.
case SPACE:
case ENTER:
if (!hasModifierKey(event)) {
event.preventDefault();
this._toggle();
}
break;
default:
if (this.panel.accordion) {
this.panel.accordion._handleHeaderKeydown(event);
}
return;
}
}
/**
* Focuses the panel header. Implemented as a part of `FocusableOption`.
* @param origin Origin of the action that triggered the focus.
* @docs-private
*/
focus(origin?: FocusOrigin, options?: FocusOptions) {
if (origin) {
this._focusMonitor.focusVia(this._element, origin, options);
} else {
this._element.nativeElement.focus(options);
}
}
ngAfterViewInit() {
this._focusMonitor.monitor(this._element).subscribe(origin => {
if (origin && this.panel.accordion) {
this.panel.accordion._handleHeaderFocus(this);
}
});
}
ngOnDestroy() {
this._parentChangeSubscription.unsubscribe();
this._focusMonitor.stopMonitoring(this._element);
}
}
/**
* Description element of a `<mat-expansion-panel-header>`.
*/
@Directive({
selector: 'mat-panel-description',
host: {
class: 'mat-expansion-panel-header-description',
},
})
export class MatExpansionPanelDescription {}
/**
* Title element of a `<mat-expansion-panel-header>`.
*/
@Directive({
selector: 'mat-panel-title',
host: {
class: 'mat-expansion-panel-header-title',
},
})
export class MatExpansionPanelTitle {}