-
Notifications
You must be signed in to change notification settings - Fork 6.8k
fix(material/button): combine MatButton and MatAnchor #30492
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
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,7 +19,6 @@ import { | |
NgZone, | ||
numberAttribute, | ||
OnDestroy, | ||
OnInit, | ||
Renderer2, | ||
} from '@angular/core'; | ||
import {_StructuralStylesLoader, MatRippleLoader, ThemePalette} from '@angular/material/core'; | ||
|
@@ -52,8 +51,13 @@ export const MAT_BUTTON_HOST = { | |
// wants to target all Material buttons. | ||
'[class.mat-mdc-button-base]': 'true', | ||
'[class]': 'color ? "mat-" + color : ""', | ||
'[attr.tabindex]': '_getTabIndex()', | ||
}; | ||
|
||
function transformTabIndex(value: unknown): number | undefined { | ||
return value == null ? undefined : numberAttribute(value); | ||
} | ||
|
||
/** List of classes to add to buttons instances based on host attribute selector. */ | ||
const HOST_SELECTOR_MDC_CLASS_PAIR: {attribute: string; mdcClasses: string[]}[] = [ | ||
{ | ||
|
@@ -94,13 +98,18 @@ export class MatButtonBase implements AfterViewInit, OnDestroy { | |
_animationMode = inject(ANIMATION_MODULE_TYPE, {optional: true}); | ||
|
||
private readonly _focusMonitor = inject(FocusMonitor); | ||
private _cleanupClick: (() => void) | undefined; | ||
private _renderer = inject(Renderer2); | ||
|
||
/** | ||
* Handles the lazy creation of the MatButton ripple. | ||
* Used to improve initial load time of large applications. | ||
*/ | ||
protected _rippleLoader: MatRippleLoader = inject(MatRippleLoader); | ||
|
||
/** Whether the button is set on an anchor node. */ | ||
protected _isAnchor: boolean; | ||
|
||
/** Whether this button is a FAB. Used to apply the correct class on the ripple. */ | ||
protected _isFab = false; | ||
|
||
|
@@ -153,14 +162,28 @@ export class MatButtonBase implements AfterViewInit, OnDestroy { | |
@Input({transform: booleanAttribute}) | ||
disabledInteractive: boolean; | ||
|
||
/** Tab index for the button. */ | ||
@Input({transform: transformTabIndex}) | ||
tabIndex: number; | ||
|
||
/** | ||
* Backwards-compatibility input that handles pre-existing `[tabindex]` bindings. | ||
* @docs-private | ||
*/ | ||
@Input({alias: 'tabindex', transform: transformTabIndex}) | ||
set _tabindex(value: number) { | ||
this.tabIndex = value; | ||
} | ||
|
||
constructor(...args: unknown[]); | ||
|
||
constructor() { | ||
inject(_CdkPrivateStyleLoader).load(_StructuralStylesLoader); | ||
const config = inject(MAT_BUTTON_CONFIG, {optional: true}); | ||
const element = this._elementRef.nativeElement; | ||
const element: HTMLElement = this._elementRef.nativeElement; | ||
const classList = (element as HTMLElement).classList; | ||
|
||
this._isAnchor = element.tagName === 'A'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. probably should There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It should be guaranteed to be uppercase. The only time tag names preserve their case is in SVG. |
||
this.disabledInteractive = config?.disabledInteractive ?? false; | ||
this.color = config?.color ?? null; | ||
this._rippleLoader?.configureRipple(element, {className: 'mat-mdc-button-ripple'}); | ||
|
@@ -176,9 +199,16 @@ export class MatButtonBase implements AfterViewInit, OnDestroy { | |
|
||
ngAfterViewInit() { | ||
this._focusMonitor.monitor(this._elementRef, true); | ||
|
||
// Some internal tests depend on the timing of this, | ||
// otherwise we could bind it in the constructor. | ||
if (this._isAnchor) { | ||
this._setupAsAnchor(); | ||
} | ||
} | ||
|
||
ngOnDestroy() { | ||
this._cleanupClick?.(); | ||
this._focusMonitor.stopMonitoring(this._elementRef); | ||
this._rippleLoader?.destroyRipple(this._elementRef.nativeElement); | ||
} | ||
|
@@ -197,6 +227,10 @@ export class MatButtonBase implements AfterViewInit, OnDestroy { | |
return this.ariaDisabled; | ||
} | ||
|
||
if (this._isAnchor) { | ||
return this.disabled || null; | ||
} | ||
|
||
return this.disabled && this.disabledInteractive ? true : null; | ||
} | ||
|
||
|
@@ -210,74 +244,30 @@ export class MatButtonBase implements AfterViewInit, OnDestroy { | |
this.disableRipple || this.disabled, | ||
); | ||
} | ||
} | ||
|
||
/** Shared host configuration for buttons using the `<a>` tag. */ | ||
export const MAT_ANCHOR_HOST = { | ||
// Note that this is basically a noop on anchors, | ||
// but it appears that some internal apps depend on it. | ||
'[attr.disabled]': '_getDisabledAttribute()', | ||
'[class.mat-mdc-button-disabled]': 'disabled', | ||
'[class.mat-mdc-button-disabled-interactive]': 'disabledInteractive', | ||
'[class._mat-animation-noopable]': '_animationMode === "NoopAnimations"', | ||
protected _getTabIndex() { | ||
if (this._isAnchor) { | ||
return this.disabled && !this.disabledInteractive ? -1 : this.tabIndex; | ||
} | ||
return this.tabIndex; | ||
} | ||
|
||
// Note that we ignore the user-specified tabindex when it's disabled for | ||
// consistency with the `mat-button` applied on native buttons where even | ||
// though they have an index, they're not tabbable. | ||
'[attr.tabindex]': 'disabled && !disabledInteractive ? -1 : tabIndex', | ||
'[attr.aria-disabled]': '_getAriaDisabled()', | ||
// MDC automatically applies the primary theme color to the button, but we want to support | ||
// an unthemed version. If color is undefined, apply a CSS class that makes it easy to | ||
// select and style this "theme". | ||
'[class.mat-unthemed]': '!color', | ||
// Add a class that applies to all buttons. This makes it easier to target if somebody | ||
// wants to target all Material buttons. | ||
'[class.mat-mdc-button-base]': 'true', | ||
'[class]': 'color ? "mat-" + color : ""', | ||
}; | ||
private _setupAsAnchor() { | ||
this._cleanupClick = this._ngZone.runOutsideAngular(() => | ||
this._renderer.listen(this._elementRef.nativeElement, 'click', (event: Event) => { | ||
if (this.disabled) { | ||
event.preventDefault(); | ||
event.stopImmediatePropagation(); | ||
} | ||
}), | ||
); | ||
} | ||
} | ||
|
||
// tslint:disable:variable-name | ||
/** | ||
* Anchor button base. | ||
*/ | ||
@Directive() | ||
export class MatAnchorBase extends MatButtonBase implements OnInit, OnDestroy { | ||
private _renderer = inject(Renderer2); | ||
private _cleanupClick: () => void; | ||
|
||
@Input({ | ||
transform: (value: unknown) => { | ||
return value == null ? undefined : numberAttribute(value); | ||
}, | ||
}) | ||
tabIndex: number; | ||
|
||
ngOnInit(): void { | ||
this._ngZone.runOutsideAngular(() => { | ||
this._cleanupClick = this._renderer.listen( | ||
this._elementRef.nativeElement, | ||
'click', | ||
this._haltDisabledEvents, | ||
); | ||
}); | ||
} | ||
|
||
override ngOnDestroy(): void { | ||
super.ngOnDestroy(); | ||
this._cleanupClick?.(); | ||
} | ||
|
||
_haltDisabledEvents = (event: Event): void => { | ||
// A disabled button shouldn't apply any actions | ||
if (this.disabled) { | ||
event.preventDefault(); | ||
event.stopImmediatePropagation(); | ||
} | ||
}; | ||
|
||
protected override _getAriaDisabled() { | ||
if (this.ariaDisabled != null) { | ||
return this.ariaDisabled; | ||
} | ||
return this.disabled || null; | ||
} | ||
} | ||
export const MatAnchorBase = MatButtonBase; | ||
export type MatAnchorBase = MatButtonBase; | ||
// tslint:enable:variable-name |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should this be the deprecated one instead? I think most of our components that take a tabindex use the all-lowercase spelling
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually we use
tabIndex
across a bunch of other components as well. I think it's because the native property is also calledtabIndex
, see https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/tabIndex