Skip to content

refactor(material-experimental/mdc-tabs): remove usage of MDC adapter #24942

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion src/material-experimental/mdc-tabs/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ ng_module(
"@npm//@angular/animations",
"@npm//@angular/common",
"@npm//@angular/core",
"@npm//@material/tab-indicator",
],
)

Expand Down
241 changes: 116 additions & 125 deletions src/material-experimental/mdc-tabs/ink-bar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,26 @@
* found in the LICENSE file at https://angular.io/license
*/

import {ElementRef, QueryList} from '@angular/core';
import {
MDCSlidingTabIndicatorFoundation,
MDCTabIndicatorAdapter,
MDCTabIndicatorFoundation,
} from '@material/tab-indicator';
import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion';
import {ElementRef, OnDestroy, OnInit, QueryList} from '@angular/core';

/**
* Item inside a tab header relative to which the ink bar can be aligned.
* @docs-private
*/
export interface MatInkBarItem {
_foundation: MatInkBarFoundation;
export interface MatInkBarItem extends OnInit, OnDestroy {
elementRef: ElementRef<HTMLElement>;
activateInkBar(previousIndicatorClientRect?: ClientRect): 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
Expand All @@ -34,157 +38,144 @@ export class MatInkBar {

/** Hides the ink bar. */
hide() {
this._items.forEach(item => item._foundation.deactivate());
this._items.forEach(item => item.deactivateInkBar());
}

/** 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 (currentItem) {
currentItem._foundation.deactivate();
}
currentItem?.deactivateInkBar();

if (correspondingItem) {
const clientRect = currentItem
? currentItem._foundation.computeContentClientRect()
: undefined;
const clientRect = currentItem?.elementRef.nativeElement.getBoundingClientRect?.();

// The ink bar won't animate unless we give it the `ClientRect` of the previous item.
correspondingItem._foundation.activate(clientRect);
correspondingItem.activateInkBar(clientRect);
this._currentItem = correspondingItem;
}
}
}

/**
* Implementation of MDC's sliding tab indicator (ink bar) foundation.
* Mixin that can be used to apply the `MatInkBarItem` behavior to a class.
* Base on MDC's `MDCSlidingTabIndicatorFoundation`:
* https://github.com/material-components/material-components-web/blob/c0a11ef0d000a098fd0c372be8f12d6a99302855/packages/mdc-tab-indicator/sliding-foundation.ts
* @docs-private
*/
export class MatInkBarFoundation {
private _destroyed: boolean;
private _foundation: MDCTabIndicatorFoundation;
private _inkBarElement: HTMLElement;
private _inkBarContentElement: HTMLElement;
private _fitToContent = false;
private _adapter: MDCTabIndicatorAdapter = {
addClass: className => {
if (!this._destroyed) {
this._hostElement.classList.add(className);
}
},
removeClass: className => {
if (!this._destroyed) {
this._hostElement.classList.remove(className);
}
},
setContentStyleProperty: (propName, value) => {
if (!this._destroyed) {
this._inkBarContentElement.style.setProperty(propName, value);
}
},
computeContentClientRect: () => {
// `getBoundingClientRect` isn't available on the server.
return this._destroyed || !this._inkBarContentElement.getBoundingClientRect
? ({
width: 0,
height: 0,
top: 0,
left: 0,
right: 0,
bottom: 0,
x: 0,
y: 0,
} as ClientRect)
: this._inkBarContentElement.getBoundingClientRect();
},
};
export function mixinInkBarItem<
T extends new (...args: any[]) => {elementRef: ElementRef<HTMLElement>},
>(base: T): T & (new (...args: any[]) => MatInkBarItem) {
return class extends base {
constructor(...args: any[]) {
super(...args);
}

constructor(private _hostElement: HTMLElement, private _document: Document) {
this._foundation = new MDCSlidingTabIndicatorFoundation(this._adapter);
}
private _inkBarElement: HTMLElement | null;
private _inkBarContentElement: HTMLElement | null;
private _fitToContent = false;

/** Aligns the ink bar to the current item. */
activate(clientRect?: ClientRect) {
this._foundation.activate(clientRect);
}
/** Whether the ink bar should fit to the entire tab or just its content. */
get fitInkBarToContent(): boolean {
return this._fitToContent;
}
set fitInkBarToContent(v: BooleanInput) {
const newValue = coerceBooleanProperty(v);

/** Removes the ink bar from the current item. */
deactivate() {
this._foundation.deactivate();
}
if (this._fitToContent !== newValue) {
this._fitToContent = newValue;

/** Gets the ClientRect of the ink bar. */
computeContentClientRect() {
return this._foundation.computeContentClientRect();
}
if (this._inkBarElement) {
this._appendInkBarElement();
}
}
}

/** Initializes the foundation. */
init() {
this._createInkBarElement();
this._foundation.init();
}
/** Aligns the ink bar to the current item. */
activateInkBar(previousIndicatorClientRect?: ClientRect) {
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;
}

/** Destroys the foundation. */
destroy() {
this._inkBarElement.remove();
this._hostElement = this._inkBarElement = this._inkBarContentElement = null!;
this._foundation.destroy();
this._destroyed = true;
}
// 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', '');
}

/**
* Sets whether the ink bar should be appended to the content, which will cause the ink bar
* to match the width of the content rather than the tab host element.
*/
setFitToContent(fitToContent: boolean) {
if (this._fitToContent !== fitToContent) {
this._fitToContent = fitToContent;
if (this._inkBarElement) {
this._appendInkBarElement();
}
/** Removes the ink bar from the current item. */
deactivateInkBar() {
this.elementRef.nativeElement.classList.remove(ACTIVE_CLASS);
}
}

/**
* Gets whether the ink bar should be appended to the content, which will cause the ink bar
* to match the width of the content rather than the tab host element.
*/
getFitToContent(): boolean {
return this._fitToContent;
}
/** Initializes the foundation. */
ngOnInit() {
this._createInkBarElement();
}

/** Creates and appends the ink bar element. */
private _createInkBarElement() {
this._inkBarElement = this._document.createElement('span');
this._inkBarContentElement = this._document.createElement('span');
/** Destroys the foundation. */
ngOnDestroy() {
this._inkBarElement?.remove();
this._inkBarElement = this._inkBarContentElement = null!;
}

this._inkBarElement.className = 'mdc-tab-indicator';
this._inkBarContentElement.className =
'mdc-tab-indicator__content' + ' mdc-tab-indicator__content--underline';
/** Creates and appends the ink bar element. */
private _createInkBarElement() {
const documentNode = this.elementRef.nativeElement.ownerDocument || document;
this._inkBarElement = documentNode.createElement('span');
this._inkBarContentElement = documentNode.createElement('span');

this._inkBarElement.appendChild(this._inkBarContentElement);
this._appendInkBarElement();
}
this._inkBarElement.className = 'mdc-tab-indicator';
this._inkBarContentElement.className =
'mdc-tab-indicator__content mdc-tab-indicator__content--underline';

/**
* 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');
this._inkBarElement.appendChild(this._inkBarContentElement);
this._appendInkBarElement();
}

const parentElement = this._fitToContent
? this._hostElement.querySelector('.mdc-tab__content')
: this._hostElement;
/**
* 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');
}

if (!parentElement && (typeof ngDevMode === 'undefined' || ngDevMode)) {
throw Error('Missing element to host the ink bar');
}
const parentElement = this._fitToContent
? this.elementRef.nativeElement.querySelector('.mdc-tab__content')
: this.elementRef.nativeElement;

parentElement!.appendChild(this._inkBarElement);
}
if (!parentElement && (typeof ngDevMode === 'undefined' || ngDevMode)) {
throw Error('Missing element to host the ink bar');
}

parentElement!.appendChild(this._inkBarElement!);
}
};
}
42 changes: 6 additions & 36 deletions src/material-experimental/mdc-tabs/tab-label-wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,52 +6,22 @@
* found in the LICENSE file at https://angular.io/license
*/

import {Directive, ElementRef, Inject, Input, OnDestroy, OnInit} from '@angular/core';
import {DOCUMENT} from '@angular/common';
import {Directive} from '@angular/core';
import {MatTabLabelWrapper as BaseMatTabLabelWrapper} from '@angular/material/tabs';
import {MatInkBarFoundation, MatInkBarItem} from './ink-bar';
import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion';
import {MatInkBarItem, mixinInkBarItem} from './ink-bar';

const _MatTabLabelWrapperBase = mixinInkBarItem(BaseMatTabLabelWrapper);

/**
* Used in the `mat-tab-group` view to display tab labels.
* @docs-private
*/
@Directive({
selector: '[matTabLabelWrapper]',
inputs: ['disabled'],
inputs: ['disabled', 'fitInkBarToContent'],
host: {
'[class.mat-mdc-tab-disabled]': 'disabled',
'[attr.aria-disabled]': '!!disabled',
},
})
export class MatTabLabelWrapper
extends BaseMatTabLabelWrapper
implements MatInkBarItem, OnInit, OnDestroy
{
private _document: Document;

_foundation: MatInkBarFoundation;

/** Whether the ink bar should fit its width to the size of the tab label content. */
@Input()
get fitInkBarToContent(): boolean {
return this._foundation.getFitToContent();
}
set fitInkBarToContent(v: BooleanInput) {
this._foundation.setFitToContent(coerceBooleanProperty(v));
}

constructor(elementRef: ElementRef, @Inject(DOCUMENT) _document: any) {
super(elementRef);
this._document = _document;
this._foundation = new MatInkBarFoundation(elementRef.nativeElement, this._document);
}

ngOnInit() {
this._foundation.init();
}

ngOnDestroy() {
this._foundation.destroy();
}
}
export class MatTabLabelWrapper extends _MatTabLabelWrapperBase implements MatInkBarItem {}
Loading