sass-references/angular-material/material/tabs/ink-bar.ts

213 lines
6.5 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 {
Directive,
ElementRef,
InjectionToken,
Input,
OnDestroy,
OnInit,
QueryList,
booleanAttribute,
inject,
} from '@angular/core';
/**
* Item inside a tab header relative to which the ink bar can be aligned.
* @docs-private
*/
export interface MatInkBarItem extends OnInit, OnDestroy {
elementRef: ElementRef<HTMLElement>;
activateInkBar(previousIndicatorClientRect?: DOMRect): void;
deactivateInkBar(): void;
fitInkBarToContent: boolean;
}
/** Class that is applied when a tab indicator is active. */
const ACTIVE_CLASS = 'mdc-tab-indicator--active';
/** Class that is applied when the tab indicator should not transition. */
const NO_TRANSITION_CLASS = 'mdc-tab-indicator--no-transition';
/**
* Abstraction around the MDC tab indicator that acts as the tab header's ink bar.
* @docs-private
*/
export class MatInkBar {
/** Item to which the ink bar is aligned currently. */
private _currentItem: MatInkBarItem | undefined;
constructor(private _items: QueryList<MatInkBarItem>) {}
/** Hides the ink bar. */
hide() {
this._items.forEach(item => item.deactivateInkBar());
this._currentItem = undefined;
}
/** Aligns the ink bar to a DOM node. */
alignToElement(element: HTMLElement) {
const correspondingItem = this._items.find(item => item.elementRef.nativeElement === element);
const currentItem = this._currentItem;
if (correspondingItem === currentItem) {
return;
}
currentItem?.deactivateInkBar();
if (correspondingItem) {
const domRect = currentItem?.elementRef.nativeElement.getBoundingClientRect?.();
// The ink bar won't animate unless we give it the `DOMRect` of the previous item.
correspondingItem.activateInkBar(domRect);
this._currentItem = correspondingItem;
}
}
}
@Directive()
export abstract class InkBarItem implements OnInit, OnDestroy {
private _elementRef = inject<ElementRef<HTMLElement>>(ElementRef);
private _inkBarElement: HTMLElement | null;
private _inkBarContentElement: HTMLElement | null;
private _fitToContent = false;
/** Whether the ink bar should fit to the entire tab or just its content. */
@Input({transform: booleanAttribute})
get fitInkBarToContent(): boolean {
return this._fitToContent;
}
set fitInkBarToContent(newValue: boolean) {
if (this._fitToContent !== newValue) {
this._fitToContent = newValue;
if (this._inkBarElement) {
this._appendInkBarElement();
}
}
}
/** Aligns the ink bar to the current item. */
activateInkBar(previousIndicatorClientRect?: DOMRect) {
const element = this._elementRef.nativeElement;
// Early exit if no indicator is present to handle cases where an indicator
// may be activated without a prior indicator state
if (
!previousIndicatorClientRect ||
!element.getBoundingClientRect ||
!this._inkBarContentElement
) {
element.classList.add(ACTIVE_CLASS);
return;
}
// This animation uses the FLIP approach. You can read more about it at the link below:
// https://aerotwist.com/blog/flip-your-animations/
// Calculate the dimensions based on the dimensions of the previous indicator
const currentClientRect = element.getBoundingClientRect();
const widthDelta = previousIndicatorClientRect.width / currentClientRect.width;
const xPosition = previousIndicatorClientRect.left - currentClientRect.left;
element.classList.add(NO_TRANSITION_CLASS);
this._inkBarContentElement.style.setProperty(
'transform',
`translateX(${xPosition}px) scaleX(${widthDelta})`,
);
// Force repaint before updating classes and transform to ensure the transform properly takes effect
element.getBoundingClientRect();
element.classList.remove(NO_TRANSITION_CLASS);
element.classList.add(ACTIVE_CLASS);
this._inkBarContentElement.style.setProperty('transform', '');
}
/** Removes the ink bar from the current item. */
deactivateInkBar() {
this._elementRef.nativeElement.classList.remove(ACTIVE_CLASS);
}
/** Initializes the foundation. */
ngOnInit() {
this._createInkBarElement();
}
/** Destroys the foundation. */
ngOnDestroy() {
this._inkBarElement?.remove();
this._inkBarElement = this._inkBarContentElement = null!;
}
/** Creates and appends the ink bar element. */
private _createInkBarElement() {
const documentNode = this._elementRef.nativeElement.ownerDocument || document;
const inkBarElement = (this._inkBarElement = documentNode.createElement('span'));
const inkBarContentElement = (this._inkBarContentElement = documentNode.createElement('span'));
inkBarElement.className = 'mdc-tab-indicator';
inkBarContentElement.className =
'mdc-tab-indicator__content mdc-tab-indicator__content--underline';
inkBarElement.appendChild(this._inkBarContentElement);
this._appendInkBarElement();
}
/**
* Appends the ink bar to the tab host element or content, depending on whether
* the ink bar should fit to content.
*/
private _appendInkBarElement() {
if (!this._inkBarElement && (typeof ngDevMode === 'undefined' || ngDevMode)) {
throw Error('Ink bar element has not been created and cannot be appended');
}
const parentElement = this._fitToContent
? this._elementRef.nativeElement.querySelector('.mdc-tab__content')
: this._elementRef.nativeElement;
if (!parentElement && (typeof ngDevMode === 'undefined' || ngDevMode)) {
throw Error('Missing element to host the ink bar');
}
parentElement!.appendChild(this._inkBarElement!);
}
}
/**
* Interface for a MatInkBar positioner method, defining the positioning and width of the ink
* bar in a set of tabs.
*/
export interface _MatInkBarPositioner {
(element: HTMLElement): {left: string; width: string};
}
/**
* The default positioner function for the MatInkBar.
* @docs-private
*/
export function _MAT_INK_BAR_POSITIONER_FACTORY(): _MatInkBarPositioner {
const method = (element: HTMLElement) => ({
left: element ? (element.offsetLeft || 0) + 'px' : '0',
width: element ? (element.offsetWidth || 0) + 'px' : '0',
});
return method;
}
/** Injection token for the MatInkBar's Positioner. */
export const _MAT_INK_BAR_POSITIONER = new InjectionToken<_MatInkBarPositioner>(
'MatInkBarPositioner',
{
providedIn: 'root',
factory: _MAT_INK_BAR_POSITIONER_FACTORY,
},
);