diff --git a/src/material-experimental/mdc-form-field/form-field.scss b/src/material-experimental/mdc-form-field/form-field.scss
index e1b573d8aea3..aea1b18d7b95 100644
--- a/src/material-experimental/mdc-form-field/form-field.scss
+++ b/src/material-experimental/mdc-form-field/form-field.scss
@@ -87,6 +87,9 @@
.mat-mdc-form-field-icon-suffix {
& > .mat-icon {
padding: 12px;
+ // It's common for apps to apply `box-sizing: border-box`
+ // globally which will break the alignment.
+ box-sizing: content-box;
}
}
diff --git a/src/material-experimental/mdc-form-field/form-field.ts b/src/material-experimental/mdc-form-field/form-field.ts
index 2efd38383c73..fa4d7a3ec2eb 100644
--- a/src/material-experimental/mdc-form-field/form-field.ts
+++ b/src/material-experimental/mdc-form-field/form-field.ts
@@ -61,6 +61,9 @@ export type FloatLabelType = 'always' | 'auto';
/** Possible appearance styles for the form field. */
export type MatFormFieldAppearance = 'fill' | 'outline';
+/** Behaviors for how the subscript height is set. */
+export type SubscriptSizing = 'fixed' | 'dynamic';
+
/**
* Represents the default options for the form field that can be configured
* using the `MAT_FORM_FIELD_DEFAULT_OPTIONS` injection token.
@@ -69,6 +72,7 @@ export interface MatFormFieldDefaultOptions {
appearance?: MatFormFieldAppearance;
hideRequiredMarker?: boolean;
floatLabel?: FloatLabelType;
+ subscriptSizing?: SubscriptSizing;
}
/**
@@ -87,6 +91,9 @@ const DEFAULT_APPEARANCE: MatFormFieldAppearance = 'fill';
/** Default appearance used by the form-field. */
const DEFAULT_FLOAT_LABEL: FloatLabelType = 'auto';
+/** Default way that the subscript element height is set. */
+const DEFAULT_SUBSCRIPT_SIZING: SubscriptSizing = 'fixed';
+
/**
* Default transform for docked floating labels in a MDC text-field. This value has been
* extracted from the MDC text-field styles because we programmatically modify the docked
@@ -206,6 +213,20 @@ export class MatFormField
}
private _appearance: MatFormFieldAppearance = DEFAULT_APPEARANCE;
+ /**
+ * Whether the form field should reserve space for one line of hint/error text (default)
+ * or to have the spacing grow from 0px as needed based on the size of the hint/error content.
+ * Note that when using dynamic sizing, layout shifts will occur when hint/error text changes.
+ */
+ @Input()
+ get subscriptSizing(): SubscriptSizing {
+ return this._subscriptSizing || this._defaults?.subscriptSizing || DEFAULT_SUBSCRIPT_SIZING;
+ }
+ set subscriptSizing(value: SubscriptSizing) {
+ this._subscriptSizing = value || this._defaults?.subscriptSizing || DEFAULT_SUBSCRIPT_SIZING;
+ }
+ private _subscriptSizing: SubscriptSizing | null = null;
+
/** Text for the form field hint. */
@Input()
get hintLabel(): string {
diff --git a/src/material-experimental/mdc-helpers/_focus-indicators.scss b/src/material-experimental/mdc-helpers/_focus-indicators.scss
index 4189d3486265..e30dfcc9303b 100644
--- a/src/material-experimental/mdc-helpers/_focus-indicators.scss
+++ b/src/material-experimental/mdc-helpers/_focus-indicators.scss
@@ -28,6 +28,10 @@
pointer-events: none;
border: $border-width $border-style transparent;
border-radius: $border-radius;
+
+ .cdk-high-contrast-active & {
+ display: none;
+ }
}
// By default, all focus indicators are flush with the bounding box of their
@@ -38,7 +42,7 @@
.mat-mdc-unelevated-button .mat-mdc-focus-indicator::before,
.mat-mdc-raised-button .mat-mdc-focus-indicator::before,
.mdc-fab .mat-mdc-focus-indicator::before,
- .mat-mdc-focus-indicator.mdc-chip::before {
+ .mat-mdc-chip-action-label .mat-mdc-focus-indicator::before {
margin: -($border-width + 2px);
}
@@ -46,11 +50,16 @@
margin: -($border-width + 3px);
}
- .mat-mdc-focus-indicator.mat-mdc-chip-remove::before,
- .mat-mdc-focus-indicator.mat-mdc-chip-row-focusable-text-content::before {
+ .mat-mdc-focus-indicator.mat-mdc-chip-remove::before {
margin: -$border-width;
}
+ // MDC sets a padding a on the button which stretches out the focus indicator.
+ .mat-mdc-focus-indicator.mat-mdc-chip-remove::before {
+ left: 8px;
+ right: 8px;
+ }
+
.mat-mdc-focus-indicator.mat-mdc-tab::before,
.mat-mdc-focus-indicator.mat-mdc-tab-link::before {
margin: 5px;
@@ -74,6 +83,9 @@
.mat-mdc-slide-toggle-focused .mat-mdc-focus-indicator::before,
.mat-mdc-radio-button.cdk-focused .mat-mdc-focus-indicator::before,
+ // In the chips the individual actions have focus so we target a different element.
+ .mat-mdc-chip-action:focus .mat-mdc-focus-indicator::before,
+
// For buttons and list items, render the focus indicator when the parent
// button or list item is focused.
.mat-mdc-button-base:focus .mat-mdc-focus-indicator::before,
diff --git a/src/material-experimental/mdc-input/input.spec.ts b/src/material-experimental/mdc-input/input.spec.ts
index db72cfd45405..bb30b65e3eb2 100644
--- a/src/material-experimental/mdc-input/input.spec.ts
+++ b/src/material-experimental/mdc-input/input.spec.ts
@@ -32,6 +32,7 @@ import {
MatFormField,
MatFormFieldAppearance,
MatFormFieldModule,
+ SubscriptSizing,
} from '@angular/material-experimental/mdc-form-field';
import {MatIconModule} from '@angular/material/icon';
import {By} from '@angular/platform-browser';
@@ -1406,6 +1407,66 @@ describe('MatFormField default options', () => {
expect(fixture.componentInstance.formField.hideRequiredMarker).toBe(true);
expect(fixture.componentInstance.formField.appearance).toBe('outline');
});
+
+ it('defaults subscriptSizing to false', () => {
+ const fixture = createComponent(MatInputWithSubscriptSizing);
+ fixture.detectChanges();
+
+ const subscriptElement = fixture.nativeElement.querySelector(
+ '.mat-mdc-form-field-subscript-wrapper',
+ );
+
+ expect(fixture.componentInstance.formField.subscriptSizing).toBe('fixed');
+ expect(subscriptElement.classList.contains('mat-mdc-form-field-subscript-dynamic-size')).toBe(
+ false,
+ );
+
+ fixture.componentInstance.sizing = 'dynamic';
+ fixture.detectChanges();
+
+ expect(fixture.componentInstance.formField.subscriptSizing).toBe('dynamic');
+ expect(subscriptElement.classList.contains('mat-mdc-form-field-subscript-dynamic-size')).toBe(
+ true,
+ );
+ });
+
+ it('changes the default value of subscriptSizing (undefined input)', () => {
+ const fixture = createComponent(MatInputWithSubscriptSizing, [
+ {
+ provide: MAT_FORM_FIELD_DEFAULT_OPTIONS,
+ useValue: {
+ subscriptSizing: 'dynamic',
+ },
+ },
+ ]);
+
+ fixture.detectChanges();
+ expect(fixture.componentInstance.formField.subscriptSizing).toBe('dynamic');
+ expect(
+ fixture.nativeElement
+ .querySelector('.mat-mdc-form-field-subscript-wrapper')
+ .classList.contains('mat-mdc-form-field-subscript-dynamic-size'),
+ ).toBe(true);
+ });
+
+ it('changes the default value of subscriptSizing (no input)', () => {
+ const fixture = createComponent(MatInputWithAppearance, [
+ {
+ provide: MAT_FORM_FIELD_DEFAULT_OPTIONS,
+ useValue: {
+ subscriptSizing: 'dynamic',
+ },
+ },
+ ]);
+
+ fixture.detectChanges();
+ expect(fixture.componentInstance.formField.subscriptSizing).toBe('dynamic');
+ expect(
+ fixture.nativeElement
+ .querySelector('.mat-mdc-form-field-subscript-wrapper')
+ .classList.contains('mat-mdc-form-field-subscript-dynamic-size'),
+ ).toBe(true);
+ });
});
function configureTestingModule(
@@ -1815,6 +1876,19 @@ class MatInputWithAppearance {
appearance: MatFormFieldAppearance;
}
+@Component({
+ template: `
+
+ My Label
+
+
+ `,
+})
+class MatInputWithSubscriptSizing {
+ @ViewChild(MatFormField) formField: MatFormField;
+ sizing: SubscriptSizing;
+}
+
@Component({
template: `
diff --git a/src/material-experimental/mdc-input/input.ts b/src/material-experimental/mdc-input/input.ts
index 6bc4dc5248b7..1fe47f04914f 100644
--- a/src/material-experimental/mdc-input/input.ts
+++ b/src/material-experimental/mdc-input/input.ts
@@ -37,6 +37,7 @@ import {MatInput as BaseMatInput} from '@angular/material/input';
'[id]': 'id',
'[disabled]': 'disabled',
'[required]': 'required',
+ '[attr.name]': 'name',
'[attr.placeholder]': 'placeholder',
'[attr.readonly]': 'readonly && !_isNativeSelect || null',
// Only mark the input as invalid for assistive technology if it has a value since the
diff --git a/src/material-experimental/mdc-list/_list-option-theme.scss b/src/material-experimental/mdc-list/_list-option-theme.scss
index cef9b98d85fc..7a6ccdcee09f 100644
--- a/src/material-experimental/mdc-list/_list-option-theme.scss
+++ b/src/material-experimental/mdc-list/_list-option-theme.scss
@@ -1,14 +1,14 @@
@use '@material/checkbox' as mdc-checkbox;
-@use '../mdc-checkbox/checkbox-theme';
+@use '../mdc-checkbox/checkbox-private';
@use '../mdc-helpers/mdc-helpers';
@use './list-option-trailing-avatar-compat';
// Mixin that overrides the selected item and checkbox colors for list options. By
// default, the MDC list uses the `primary` color for list items. The MDC checkbox
// inside list options by default uses the `primary` color too.
-@mixin private-list-option-color-override($color, $mdcColor) {
+@mixin private-list-option-color-override($color, $mdc-color) {
& .mdc-list-item__start, & .mdc-list-item__end {
- @include checkbox-theme.private-checkbox-styles-with-color($color, $mdcColor);
+ @include checkbox-private.private-checkbox-styles-with-color($color, $mdc-color);
}
}
diff --git a/src/material-experimental/mdc-list/_list-option-trailing-avatar-compat.scss b/src/material-experimental/mdc-list/_list-option-trailing-avatar-compat.scss
index 24c84560b408..1a26a351bb0c 100644
--- a/src/material-experimental/mdc-list/_list-option-trailing-avatar-compat.scss
+++ b/src/material-experimental/mdc-list/_list-option-trailing-avatar-compat.scss
@@ -3,6 +3,7 @@
@use '@material/density/functions' as density-functions;
@use '@material/list/evolution-mixins' as mdc-list;
@use '@material/list/evolution-variables' as mdc-list-variables;
+@use '../mdc-helpers/mdc-helpers';
// For compatibility with the non-MDC selection list, we support avatars that are
// shown at the end of the list option. This is not supported by the MDC list as the
@@ -15,19 +16,21 @@
@mixin core-styles($query) {
$feat-structure: feature-targeting.create-target($query, structure);
- .mat-mdc-list-option-with-trailing-avatar {
- @include mdc-list.item-end-spacing(16px, $query: $query);
- @include mdc-list.item-end-size(40px, $query: $query);
+ @include mdc-helpers.disable-fallback-declarations {
+ .mat-mdc-list-option-with-trailing-avatar {
+ @include mdc-list.item-end-spacing(16px, $query: $query);
+ @include mdc-list.item-end-size(40px, $query: $query);
- &.mdc-list-item--with-two-lines {
- .mdc-list-item__primary-text {
- @include typography.text-baseline($top: 32px, $bottom: 20px, $query: $query);
+ &.mdc-list-item--with-two-lines {
+ .mdc-list-item__primary-text {
+ @include typography.text-baseline($top: 32px, $bottom: 20px, $query: $query);
+ }
}
- }
- .mdc-list-item__end {
- @include feature-targeting.targets($feat-structure) {
- border-radius: 50%;
+ .mdc-list-item__end {
+ @include feature-targeting.targets($feat-structure) {
+ border-radius: 50%;
+ }
}
}
}
@@ -46,8 +49,10 @@
$property-name: height,
);
- .mat-mdc-list-option-with-trailing-avatar {
- @include mdc-list.one-line-item-height($one-line-tall-height);
- @include mdc-list.two-line-item-height($two-line-tall-height);
+ @include mdc-helpers.disable-fallback-declarations {
+ .mat-mdc-list-option-with-trailing-avatar {
+ @include mdc-list.one-line-item-height($one-line-tall-height);
+ @include mdc-list.two-line-item-height($two-line-tall-height);
+ }
}
}
diff --git a/src/material-experimental/mdc-list/_list-theme.scss b/src/material-experimental/mdc-list/_list-theme.scss
index b8d6048d8650..95469ff47000 100644
--- a/src/material-experimental/mdc-list/_list-theme.scss
+++ b/src/material-experimental/mdc-list/_list-theme.scss
@@ -16,11 +16,10 @@
$accent: theming.get-color-from-palette(map.get($config, accent));
$warn: theming.get-color-from-palette(map.get($config, warn));
- // MDC's state styles are tied in with their ripple. Since we don't use the MDC
- // ripple, we need to add the hover, focus and selected states manually.
- @include interactive-list-theme.private-interactive-list-item-state-colors($config);
-
@include mdc-helpers.mat-using-mdc-theme($config) {
+ // MDC's state styles are tied in with their ripple. Since we don't use the MDC
+ // ripple, we need to add the hover, focus and selected states manually.
+ @include interactive-list-theme.private-interactive-list-item-state-colors($config);
@include mdc-list.without-ripple($query: mdc-helpers.$mat-theme-styles-query);
.mat-mdc-list-option {
@@ -38,13 +37,15 @@
@mixin density($config-or-theme) {
$density-scale: theming.get-density-config($config-or-theme);
- .mat-mdc-list-item {
- @include mdc-list.one-line-item-density($density-scale);
- @include mdc-list.two-line-item-density($density-scale);
- @include mdc-list.three-line-item-density($density-scale);
- }
+ @include mdc-helpers.disable-fallback-declarations {
+ .mat-mdc-list-item {
+ @include mdc-list.one-line-item-density($density-scale);
+ @include mdc-list.two-line-item-density($density-scale);
+ @include mdc-list.three-line-item-density($density-scale);
+ }
- @include list-option-theme.private-list-option-density-styles($density-scale);
+ @include list-option-theme.private-list-option-density-styles($density-scale);
+ }
}
@mixin typography($config-or-theme) {
diff --git a/src/material-experimental/mdc-list/list-option.scss b/src/material-experimental/mdc-list/list-option.scss
index 1a3f93841125..351a92dc04fa 100644
--- a/src/material-experimental/mdc-list/list-option.scss
+++ b/src/material-experimental/mdc-list/list-option.scss
@@ -1,9 +1,11 @@
-@use '@material/checkbox' as mdc-checkbox;
+@use 'sass:map';
+@use '@material/checkbox/checkbox' as mdc-checkbox;
@use '@material/list/evolution-variables' as mdc-list-variables;
@use '@material/checkbox/checkbox-theme' as mdc-checkbox-theme;
@use '../mdc-helpers/mdc-helpers';
@use '../../cdk/a11y';
@use './list-option-trailing-avatar-compat';
+@use '../mdc-checkbox/checkbox-private';
// For compatibility with the non-MDC list, we support avatars that are shown at the end
// of the list option. We create a class similar to MDC's `--trailing-icon` one.
@@ -12,19 +14,31 @@
.mat-mdc-list-option {
// The MDC-based list-option uses the MDC checkbox for the selection indicators.
// We need to ensure that the checkbox styles are not included for the list-option.
- @include mdc-checkbox.without-ripple(
- $query: mdc-helpers.$mat-base-styles-without-animation-query);
+ @include mdc-helpers.disable-fallback-declarations {
+ @include mdc-checkbox.static-styles(
+ $query: mdc-helpers.$mat-base-styles-without-animation-query);
- &:not(._mat-animation-noopable) {
- @include mdc-checkbox.without-ripple($query: animation);
+ &:not(._mat-animation-noopable) {
+ @include mdc-checkbox.static-styles($query: animation);
+ }
}
// We can't use the MDC checkbox here directly, because this checkbox is purely
// decorative and including the MDC one will bring in unnecessary JS.
.mdc-checkbox {
+ $config: map.merge(checkbox-private.$private-checkbox-theme-config, (
+ // Since this checkbox isn't interactive, we can exclude the focus/hover/press styles.
+ selected-focus-icon-color: null,
+ selected-hover-icon-color: null,
+ selected-pressed-icon-color: null,
+ unselected-focus-icon-color: null,
+ unselected-hover-icon-color: null,
+ unselected-pressed-icon-color: null,
+ ));
+
// MDC theme styles also include structural styles so we have to include the theme at least
// once here. The values will be overwritten by our own theme file afterwards.
- @include mdc-checkbox-theme.theme-styles(mdc-checkbox-theme.$light-theme);
+ @include mdc-checkbox-theme.theme-styles($config);
}
// The internal checkbox is purely decorative, but because it's an `input`, the user can still
diff --git a/src/material-experimental/mdc-list/list.scss b/src/material-experimental/mdc-list/list.scss
index 039dc1c320a4..c31bf6888ce9 100644
--- a/src/material-experimental/mdc-list/list.scss
+++ b/src/material-experimental/mdc-list/list.scss
@@ -2,7 +2,9 @@
@use '../mdc-helpers/mdc-helpers';
@use '../../material/core/style/layout-common';
-@include mdc-list.without-ripple($query: mdc-helpers.$mat-base-styles-query);
+@include mdc-helpers.disable-fallback-declarations {
+ @include mdc-list.without-ripple($query: mdc-helpers.$mat-base-styles-query);
+}
// MDC expects the list element to be a ``, since we use `` instead we need to
// explicitly set `display: block`
@@ -80,3 +82,24 @@
-webkit-line-clamp: 2;
}
}
+
+// MDC doesn't account for button being used as a list item. We override some of
+// the default button styles here so that they look right when used as a list
+// item.
+mat-action-list button {
+ background: none;
+ color: inherit;
+ border: none;
+ font: inherit;
+ outline: inherit;
+ -webkit-tap-highlight-color: transparent;
+ text-align: left;
+
+ [dir='rtl'] & {
+ text-align: right;
+ }
+
+ &::-moz-focus-inner {
+ border: 0;
+ }
+}
diff --git a/src/material-experimental/mdc-list/selection-list.spec.ts b/src/material-experimental/mdc-list/selection-list.spec.ts
index 9ee28517e1ed..66c82416d2c6 100644
--- a/src/material-experimental/mdc-list/selection-list.spec.ts
+++ b/src/material-experimental/mdc-list/selection-list.spec.ts
@@ -424,14 +424,7 @@ describe('MDC-based MatSelectionList without forms', () => {
expect(listOptions.every(option => option.componentInstance.selected)).toBe(false);
});
- // This is temporarily disabled as the MDC list does not emit a proper event when
- // items are interactively toggled with e.g. `CTRL + A`.
- // TODO(devversion): look more into this. MDC does not expose an `onChange` adapter
- // function. Authors are required to emit a change event on checkbox/radio change, but
- // that is not an viable option for us since we also allow for programmatic selection updates.
- // https://github.com/material-components/material-components-web/blob/a986df922b6b4c1ef5c59925107281d1d40287a8/packages/mdc-list/component.ts#L300-L308.
- // tslint:disable-next-line:ban
- xit('should dispatch the selectionChange event when selecting via ctrl + a', () => {
+ it('should dispatch the selectionChange event when selecting via ctrl + a', () => {
const spy = spyOn(fixture.componentInstance, 'onSelectionChange');
listOptions.forEach(option => (option.componentInstance.disabled = false));
fixture.detectChanges();
diff --git a/src/material-experimental/mdc-list/selection-list.ts b/src/material-experimental/mdc-list/selection-list.ts
index 01af62d55921..a1019934831c 100644
--- a/src/material-experimental/mdc-list/selection-list.ts
+++ b/src/material-experimental/mdc-list/selection-list.ts
@@ -398,8 +398,8 @@ function getSelectionListAdapter(list: MatSelectionList): MDCListAdapter {
baseAdapter.setAttributeForElementIndex(index, attribute, value);
},
- notifyAction(index: number): void {
- list._emitChangeEvent([list._itemsArr[index]]);
+ notifySelectionChange(changedIndices: number[]): void {
+ list._emitChangeEvent(changedIndices.map(index => list._itemsArr[index]));
},
};
}
diff --git a/src/material-experimental/mdc-list/testing/list-harness.spec.ts b/src/material-experimental/mdc-list/testing/list-harness.spec.ts
index 709c395a9fba..f1ae58040970 100644
--- a/src/material-experimental/mdc-list/testing/list-harness.spec.ts
+++ b/src/material-experimental/mdc-list/testing/list-harness.spec.ts
@@ -30,8 +30,9 @@ function runBaseListFunctionalityTests<
BaseListItemHarnessFilters
>,
I extends MatListItemHarnessBase,
+ F extends {disableThirdItem: boolean},
>(
- testComponentFn: () => Type<{}>,
+ testComponentFn: () => Type,
listHarness: ComponentHarnessConstructor & {
with: (config?: BaseHarnessFilters) => HarnessPredicate;
},
@@ -40,7 +41,7 @@ function runBaseListFunctionalityTests<
describe('base list functionality', () => {
let simpleListHarness: L;
let emptyListHarness: L;
- let fixture: ComponentFixture<{}>;
+ let fixture: ComponentFixture;
beforeEach(async () => {
const testComponent = testComponentFn();
@@ -292,6 +293,14 @@ function runBaseListFunctionalityTests<
const loader = await items[1].getChildLoader(MatListItemSection.CONTENT);
await expectAsync(loader.getHarness(TestItemContentHarness)).toBeResolved();
});
+
+ it('should check disabled state of items', async () => {
+ fixture.componentInstance.disableThirdItem = true;
+ const items = await simpleListHarness.getItems();
+ expect(items.length).toBe(5);
+ expect(await items[0].isDisabled()).toBe(false);
+ expect(await items[2].isDisabled()).toBe(true);
+ });
});
}
@@ -488,14 +497,6 @@ describe('MatSelectionListHarness', () => {
await items[0].deselect();
expect(await items[0].isSelected()).toBe(false);
});
-
- it('should check disabled state of options', async () => {
- fixture.componentInstance.disableItem3 = true;
- const items = await harness.getItems();
- expect(items.length).toBe(5);
- expect(await items[0].isDisabled()).toBe(false);
- expect(await items[2].isDisabled()).toBe(true);
- });
});
});
@@ -513,7 +514,7 @@ describe('MatSelectionListHarness', () => {
Item 2
-
+
Section 2
@@ -531,7 +532,9 @@ describe('MatSelectionListHarness', () => {
`,
})
-class ListHarnessTest {}
+class ListHarnessTest {
+ disableThirdItem = false;
+}
@Component({
template: `
@@ -547,7 +550,10 @@ class ListHarnessTest {}
Item 2
-
+
Section 2
-
{
expect(trigger.textContent).not.toContain('Pizza');
}));
+
+ it('should sync up the form control value with the component value', fakeAsync(() => {
+ const fixture = TestBed.createComponent(BasicSelectOnPushPreselected);
+ fixture.detectChanges();
+ flush();
+
+ expect(fixture.componentInstance.control.value).toBe('pizza-1');
+ expect(fixture.componentInstance.select.value).toBe('pizza-1');
+ }));
});
describe('with custom trigger', () => {
@@ -4439,6 +4448,7 @@ class BasicSelectOnPush {
`,
})
class BasicSelectOnPushPreselected {
+ @ViewChild(MatSelect) select: MatSelect;
foods: any[] = [
{value: 'steak-0', viewValue: 'Steak'},
{value: 'pizza-1', viewValue: 'Pizza'},
diff --git a/src/material-experimental/mdc-snack-bar/module.ts b/src/material-experimental/mdc-snack-bar/module.ts
index 8c655c815b3d..d23f44380300 100644
--- a/src/material-experimental/mdc-snack-bar/module.ts
+++ b/src/material-experimental/mdc-snack-bar/module.ts
@@ -13,7 +13,7 @@ import {NgModule} from '@angular/core';
import {MatButtonModule} from '@angular/material-experimental/mdc-button';
import {MatCommonModule} from '@angular/material-experimental/mdc-core';
-import {MatSimpleSnackBar} from './simple-snack-bar';
+import {SimpleSnackBar} from './simple-snack-bar';
import {MatSnackBarContainer} from './snack-bar-container';
import {MatSnackBarAction, MatSnackBarActions, MatSnackBarLabel} from './snack-bar-content';
@@ -27,7 +27,7 @@ import {MatSnackBarAction, MatSnackBarActions, MatSnackBarLabel} from './snack-b
MatSnackBarAction,
],
declarations: [
- MatSimpleSnackBar,
+ SimpleSnackBar,
MatSnackBarContainer,
MatSnackBarLabel,
MatSnackBarActions,
diff --git a/src/material-experimental/mdc-snack-bar/public-api.ts b/src/material-experimental/mdc-snack-bar/public-api.ts
index 923632a13d18..6f0656d33079 100644
--- a/src/material-experimental/mdc-snack-bar/public-api.ts
+++ b/src/material-experimental/mdc-snack-bar/public-api.ts
@@ -16,7 +16,6 @@ export {
MatSnackBarConfig,
MatSnackBarDismiss,
MatSnackBarRef,
- SimpleSnackBar,
MAT_SNACK_BAR_DATA,
MAT_SNACK_BAR_DEFAULT_OPTIONS,
MAT_SNACK_BAR_DEFAULT_OPTIONS_FACTORY,
diff --git a/src/material-experimental/mdc-snack-bar/simple-snack-bar.ts b/src/material-experimental/mdc-snack-bar/simple-snack-bar.ts
index 0c5ff0ed55c4..38235d685090 100644
--- a/src/material-experimental/mdc-snack-bar/simple-snack-bar.ts
+++ b/src/material-experimental/mdc-snack-bar/simple-snack-bar.ts
@@ -7,15 +7,10 @@
*/
import {ChangeDetectionStrategy, Component, Inject, ViewEncapsulation} from '@angular/core';
-import {
- MAT_SNACK_BAR_DATA,
- TextOnlySnackBar,
- MatSnackBarRef,
- SimpleSnackBar,
-} from '@angular/material/snack-bar';
+import {MAT_SNACK_BAR_DATA, TextOnlySnackBar, MatSnackBarRef} from '@angular/material/snack-bar';
@Component({
- selector: 'mat-simple-snack-bar',
+ selector: 'simple-snack-bar',
templateUrl: 'simple-snack-bar.html',
styleUrls: ['simple-snack-bar.css'],
exportAs: 'matSnackBar',
@@ -25,7 +20,7 @@ import {
'class': 'mat-mdc-simple-snack-bar',
},
})
-export class MatSimpleSnackBar implements TextOnlySnackBar {
+export class SimpleSnackBar implements TextOnlySnackBar {
constructor(
public snackBarRef: MatSnackBarRef,
@Inject(MAT_SNACK_BAR_DATA) public data: {message: string; action: string},
diff --git a/src/material-experimental/mdc-snack-bar/snack-bar.spec.ts b/src/material-experimental/mdc-snack-bar/snack-bar.spec.ts
index b8fedddde6c7..d682b03804a8 100644
--- a/src/material-experimental/mdc-snack-bar/snack-bar.spec.ts
+++ b/src/material-experimental/mdc-snack-bar/snack-bar.spec.ts
@@ -14,7 +14,7 @@ import {NoopAnimationsModule} from '@angular/platform-browser/animations';
import {
MAT_SNACK_BAR_DATA,
MAT_SNACK_BAR_DEFAULT_OPTIONS,
- MatSimpleSnackBar,
+ SimpleSnackBar,
MatSnackBar,
MatSnackBarConfig,
MatSnackBarContainer,
@@ -239,7 +239,7 @@ describe('MatSnackBar', () => {
viewContainerFixture.detectChanges();
- expect(snackBarRef.instance instanceof MatSimpleSnackBar)
+ expect(snackBarRef.instance instanceof SimpleSnackBar)
.withContext('Expected the snack bar content component to be SimpleSnackBar')
.toBe(true);
expect(snackBarRef.instance.snackBarRef)
@@ -266,7 +266,7 @@ describe('MatSnackBar', () => {
viewContainerFixture.detectChanges();
- expect(snackBarRef.instance instanceof MatSimpleSnackBar)
+ expect(snackBarRef.instance instanceof SimpleSnackBar)
.withContext('Expected the snack bar content component to be SimpleSnackBar')
.toBe(true);
expect(snackBarRef.instance.snackBarRef)
diff --git a/src/material-experimental/mdc-snack-bar/snack-bar.ts b/src/material-experimental/mdc-snack-bar/snack-bar.ts
index 0dc7d550d101..4ac117696244 100644
--- a/src/material-experimental/mdc-snack-bar/snack-bar.ts
+++ b/src/material-experimental/mdc-snack-bar/snack-bar.ts
@@ -6,18 +6,36 @@
* found in the LICENSE file at https://angular.io/license
*/
-import {Injectable} from '@angular/core';
-import {MatSnackBar as BaseMatSnackBar} from '@angular/material/snack-bar';
+import {LiveAnnouncer} from '@angular/cdk/a11y';
+import {BreakpointObserver} from '@angular/cdk/layout';
+import {Overlay} from '@angular/cdk/overlay';
+import {Inject, Injectable, Injector, Optional, SkipSelf} from '@angular/core';
+import {
+ MatSnackBarConfig,
+ MAT_SNACK_BAR_DEFAULT_OPTIONS,
+ _MatSnackBarBase,
+} from '@angular/material/snack-bar';
import {MatSnackBarModule} from './module';
-import {MatSimpleSnackBar} from './simple-snack-bar';
+import {SimpleSnackBar} from './simple-snack-bar';
import {MatSnackBarContainer} from './snack-bar-container';
/**
* Service to dispatch Material Design snack bar messages.
*/
@Injectable({providedIn: MatSnackBarModule})
-export class MatSnackBar extends BaseMatSnackBar {
- protected override simpleSnackBarComponent = MatSimpleSnackBar;
+export class MatSnackBar extends _MatSnackBarBase {
+ protected override simpleSnackBarComponent = SimpleSnackBar;
protected override snackBarContainerComponent = MatSnackBarContainer;
protected override handsetCssClass = 'mat-mdc-snack-bar-handset';
+
+ constructor(
+ overlay: Overlay,
+ live: LiveAnnouncer,
+ injector: Injector,
+ breakpointObserver: BreakpointObserver,
+ @Optional() @SkipSelf() parentSnackBar: MatSnackBar,
+ @Inject(MAT_SNACK_BAR_DEFAULT_OPTIONS) defaultConfig: MatSnackBarConfig,
+ ) {
+ super(overlay, live, injector, breakpointObserver, parentSnackBar, defaultConfig);
+ }
}
diff --git a/src/material-experimental/mdc-snack-bar/testing/snack-bar-harness.spec.ts b/src/material-experimental/mdc-snack-bar/testing/snack-bar-harness.spec.ts
index c301c9ad05bf..06d5e8ca8ede 100644
--- a/src/material-experimental/mdc-snack-bar/testing/snack-bar-harness.spec.ts
+++ b/src/material-experimental/mdc-snack-bar/testing/snack-bar-harness.spec.ts
@@ -12,7 +12,7 @@ import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed';
import {Component, TemplateRef, ViewChild} from '@angular/core';
describe('MDC-based MatSnackBarHarness', () => {
- runHarnessTests(MatSnackBarModule, MatSnackBar, MatSnackBarHarness as any);
+ runHarnessTests(MatSnackBarModule, MatSnackBar as any, MatSnackBarHarness as any);
});
describe('MDC-based MatSnackBarHarness (MDC only behavior)', () => {
diff --git a/src/material-experimental/mdc-table/table.spec.ts b/src/material-experimental/mdc-table/table.spec.ts
index 08d9f00adcee..471562196177 100644
--- a/src/material-experimental/mdc-table/table.spec.ts
+++ b/src/material-experimental/mdc-table/table.spec.ts
@@ -591,6 +591,45 @@ describe('MDC-based MatTable', () => {
['Footer A', 'Footer B', 'Footer C'],
]);
});
+
+ it('should fall back to empty table if invalid data is passed in', () => {
+ component.underlyingDataSource.addData();
+ fixture.detectChanges();
+ expectTableToMatchContent(tableElement, [
+ ['Column A', 'Column B', 'Column C'],
+ ['a_1', 'b_1', 'c_1'],
+ ['a_2', 'b_2', 'c_2'],
+ ['a_3', 'b_3', 'c_3'],
+ ['a_4', 'b_4', 'c_4'],
+ ['Footer A', 'Footer B', 'Footer C'],
+ ]);
+
+ dataSource.data = null!;
+ fixture.detectChanges();
+ expectTableToMatchContent(tableElement, [
+ ['Column A', 'Column B', 'Column C'],
+ ['Footer A', 'Footer B', 'Footer C'],
+ ]);
+
+ component.underlyingDataSource.addData();
+ fixture.detectChanges();
+ expectTableToMatchContent(tableElement, [
+ ['Column A', 'Column B', 'Column C'],
+ ['a_1', 'b_1', 'c_1'],
+ ['a_2', 'b_2', 'c_2'],
+ ['a_3', 'b_3', 'c_3'],
+ ['a_4', 'b_4', 'c_4'],
+ ['a_5', 'b_5', 'c_5'],
+ ['Footer A', 'Footer B', 'Footer C'],
+ ]);
+
+ dataSource.data = {} as any;
+ fixture.detectChanges();
+ expectTableToMatchContent(tableElement, [
+ ['Column A', 'Column B', 'Column C'],
+ ['Footer A', 'Footer B', 'Footer C'],
+ ]);
+ });
});
});
diff --git a/src/material-experimental/mdc-tabs/_tabs-common.scss b/src/material-experimental/mdc-tabs/_tabs-common.scss
index 20488b19a793..1ae4d6589c3f 100644
--- a/src/material-experimental/mdc-tabs/_tabs-common.scss
+++ b/src/material-experimental/mdc-tabs/_tabs-common.scss
@@ -11,7 +11,7 @@ $mat-tab-animation-duration: 500ms !default;
// Combines the various structural styles we need for the tab group and tab nav bar.
@mixin structural-styles {
@include mdc-helpers.disable-fallback-declarations {
- @include mdc-tab.without-ripple($query: mdc-helpers.$mat-base-styles-query);
+ @include mdc-tab.static-styles($query: mdc-helpers.$mat-base-styles-query);
@include mdc-tab-indicator.core-styles($query: mdc-helpers.$mat-base-styles-query);
}
@@ -51,6 +51,13 @@ $mat-tab-animation-duration: 500ms !default;
pointer-events: none;
}
+ // Required for `fitInkBarToContent` to work. This used to be included with MDC's `without-ripple`
+ // mixin, but that no longer appears to be the case with `static-styles`. Since the latter is
+ // ~10kb smaller, we include this one extra style ourselves.
+ .mdc-tab__content {
+ @include mdc-tab-indicator.surface;
+ }
+
// We need to handle the hover and focus indication ourselves, because we don't use MDC's ripple.
&:hover .mdc-tab__ripple::before {
opacity: map.get(mdc-ripple.$dark-ink-opacities, hover);
diff --git a/src/material-experimental/mdc-tabs/_tabs-theme.scss b/src/material-experimental/mdc-tabs/_tabs-theme.scss
index c9bca6b13355..7e386484329b 100644
--- a/src/material-experimental/mdc-tabs/_tabs-theme.scss
+++ b/src/material-experimental/mdc-tabs/_tabs-theme.scss
@@ -4,7 +4,7 @@
@use '@material/tab-indicator' as mdc-tab-indicator;
@use '@material/tab-indicator/tab-indicator-theme' as mdc-tab-indicator-theme;
@use '@material/tab' as mdc-tab;
-@use '@material/tab/tab-theme' as mdc-tab-theme;
+@use '@material/tab/mixins' as mdc-tab-mixins;
@use '@material/tab-bar' as mdc-tab-bar;
@use '../mdc-helpers/mdc-helpers';
@use '../../material/core/typography/typography';
@@ -18,39 +18,9 @@
@include mdc-helpers.mat-using-mdc-theme($config) {
.mat-mdc-tab, .mat-mdc-tab-link {
- $surface: mdc-theme-color.$surface;
- $on-surface: rgba(mdc-theme-color.$on-surface, 0.6);
-
- // TODO(crisbeto): these styles should actually be set through the `theme` mixin while the
- // `theme-styles` are included in the `tab` mixin inside `_tabs-common.scss`. Currently
- // they are not, because `theme-styles` outputs the token values directly, rather than
- // generating CSS variables.
- @include mdc-tab-theme.primary-navigation-tab-theme-styles(map.merge(
- mdc-tab-theme.$primary-light-theme,
- (
- container-color: $surface,
- inactive-focus-state-layer-color: $on-surface,
- inactive-hover-state-layer-color: $on-surface,
- inactive-pressed-state-layer-color: $on-surface,
- with-icon-inactive-focus-icon-color: $on-surface,
- with-icon-inactive-hover-icon-color: $on-surface,
- with-icon-inactive-icon-color: $on-surface,
- with-icon-inactive-pressed-icon-color: $on-surface,
- with-label-text-inactive-focus-label-text-color: $on-surface,
- with-label-text-inactive-hover-label-text-color: $on-surface,
- with-label-text-inactive-label-text-color: $on-surface,
- with-label-text-inactive-pressed-label-text-color: $on-surface,
-
- // TODO(crisbeto): MDC's styles are set up so that the icon size is set through a
- // `font-size` at the root of the tab while the text size of the tab is set on
- // `.mdc-tab__text-label` which overrides the one from the root. The problem is that
- // the `$light-theme` is looking for a `subhead2` level which doesn't exist in MDC's
- // code which in turn causes no text label styles to be emitted and for the icon size
- // to be applied to the entire tab. Since we don't support icons inside the tab
- // anyway, we can temporarily work around it by preventing MDC from emitting icon
- // styles. The correct label typography will be applied by our theme instead.
- with-icon-icon-size: null
- )));
+ &:not(.mat-mdc-tab-disabled) {
+ @include mdc-tab-mixins.text-label-color(rgba(mdc-theme-color.$on-surface, 0.6));
+ }
// MDC seems to include a background color on tabs which only stands out on dark themes.
// Disable for now for backwards compatibility. These styles are inside the theme in order
@@ -125,27 +95,10 @@
@mixin _palette-styles($color) {
.mat-mdc-tab, .mat-mdc-tab-link {
- // TODO(crisbeto): these styles should actually be set through the `theme` mixin while the
- // `theme-styles` are included in the `tab` mixin inside `_tabs-common.scss`. Currently
- // they are not, because `theme-styles` outputs the token values directly, rather than
- // generating CSS variables.
- @include mdc-tab-theme.primary-navigation-tab-theme-styles((
- active-focus-state-layer-color: $color,
- active-hover-state-layer-color: $color,
- active-pressed-state-layer-color: $color,
- with-icon-active-focus-icon-color: $color,
- with-icon-active-hover-icon-color: $color,
- with-icon-active-icon-color: $color,
- with-icon-active-pressed-icon-color: $color,
- with-label-text-active-focus-label-text-color: $color,
- with-label-text-active-hover-label-text-color: $color,
- with-label-text-active-label-text-color: $color,
- with-label-text-active-pressed-label-text-color: $color,
- ));
-
- @include mdc-tab-indicator-theme.theme-styles((
- active-indicator-color: $color
- ));
+ &:not(.mat-mdc-tab-disabled) {
+ @include mdc-tab-mixins.active-text-label-color($color);
+ @include mdc-tab-indicator-theme.theme-styles((active-indicator-color: $color));
+ }
}
.mdc-tab__ripple::before,
diff --git a/src/material-experimental/mdc-tabs/tab-body.scss b/src/material-experimental/mdc-tabs/tab-body.scss
index 164ed84102fb..fd7ff38c9d55 100644
--- a/src/material-experimental/mdc-tabs/tab-body.scss
+++ b/src/material-experimental/mdc-tabs/tab-body.scss
@@ -22,6 +22,17 @@
.mat-mdc-tab-group.mat-mdc-tab-group-dynamic-height &.mat-mdc-tab-body-active {
overflow-y: hidden;
}
+
+ // Usually the `visibility: hidden` added by the animation is enough to prevent focus from
+ // entering the collapsed content, but children with their own `visibility` can override it.
+ // This is a fallback that completely hides the content when the element becomes hidden.
+ // Note that we can't do this in the animation definition, because the style gets recomputed too
+ // late, breaking the animation because Angular didn't have time to figure out the target height.
+ // This can also be achieved with JS, but it has issues when when starting an animation before
+ // the previous one has finished.
+ &[style*='visibility: hidden'] {
+ display: none;
+ }
}
.mat-mdc-tab-body-content {
diff --git a/src/material-experimental/mdc-tabs/tab-body.ts b/src/material-experimental/mdc-tabs/tab-body.ts
index 3613e72edc5b..75d684f714e7 100644
--- a/src/material-experimental/mdc-tabs/tab-body.ts
+++ b/src/material-experimental/mdc-tabs/tab-body.ts
@@ -56,7 +56,8 @@ export class MatTabBodyPortal extends BaseMatTabBodyPortal {
templateUrl: 'tab-body.html',
styleUrls: ['tab-body.css'],
encapsulation: ViewEncapsulation.None,
- changeDetection: ChangeDetectionStrategy.OnPush,
+ // tslint:disable-next-line:validate-decorators
+ changeDetection: ChangeDetectionStrategy.Default,
animations: [matTabsAnimations.translateTab],
host: {
'class': 'mat-mdc-tab-body',
diff --git a/src/material-experimental/mdc-tabs/tab-group.html b/src/material-experimental/mdc-tabs/tab-group.html
index 8db3d19cd45b..157fe938c340 100644
--- a/src/material-experimental/mdc-tabs/tab-group.html
+++ b/src/material-experimental/mdc-tabs/tab-group.html
@@ -15,10 +15,11 @@
[attr.aria-posinset]="i + 1"
[attr.aria-setsize]="_tabs.length"
[attr.aria-controls]="_getTabContentId(i)"
- [attr.aria-selected]="selectedIndex == i"
+ [attr.aria-selected]="selectedIndex === i"
[attr.aria-label]="tab.ariaLabel || null"
[attr.aria-labelledby]="(!tab.ariaLabel && tab.ariaLabelledby) ? tab.ariaLabelledby : null"
- [class.mdc-tab--active]="selectedIndex == i"
+ [class.mdc-tab--active]="selectedIndex === i"
+ [ngClass]="tab.labelClass"
[disabled]="tab.disabled"
[fitInkBarToContent]="fitInkBarToContent"
(click)="_handleClick(tab, tabHeader, i)"
@@ -36,12 +37,12 @@
-
+
- {{tab.textLabel}}
+ {{tab.textLabel}}
@@ -57,10 +58,12 @@
[attr.tabindex]="(contentTabIndex != null && selectedIndex === i) ? contentTabIndex : null"
[attr.aria-labelledby]="_getTabLabelId(i)"
[class.mat-mdc-tab-body-active]="selectedIndex === i"
+ [ngClass]="tab.bodyClass"
[content]="tab.content!"
[position]="tab.position!"
[origin]="tab.origin"
[animationDuration]="animationDuration"
+ [preserveContent]="preserveContent"
(_onCentered)="_removeTabBodyWrapperHeight()"
(_onCentering)="_setTabBodyWrapperHeight($event)">
diff --git a/src/material-experimental/mdc-tabs/tab-group.spec.ts b/src/material-experimental/mdc-tabs/tab-group.spec.ts
index 1b5930e815dd..dacd95956c01 100644
--- a/src/material-experimental/mdc-tabs/tab-group.spec.ts
+++ b/src/material-experimental/mdc-tabs/tab-group.spec.ts
@@ -1,6 +1,6 @@
import {LEFT_ARROW} from '@angular/cdk/keycodes';
import {dispatchFakeEvent, dispatchKeyboardEvent} from '../../cdk/testing/private';
-import {Component, OnInit, QueryList, ViewChild, ViewChildren} from '@angular/core';
+import {Component, DebugElement, OnInit, QueryList, ViewChild, ViewChildren} from '@angular/core';
import {
waitForAsync,
ComponentFixture,
@@ -40,6 +40,7 @@ describe('MDC-based MatTabGroup', () => {
TabGroupWithIndirectDescendantTabs,
TabGroupWithSpaceAbove,
NestedTabGroupWithLabel,
+ TabsWithClassesTestApp,
],
});
@@ -420,11 +421,16 @@ describe('MDC-based MatTabGroup', () => {
expect(tab.getAttribute('aria-label')).toBe('Fruit');
expect(tab.hasAttribute('aria-labelledby')).toBe(false);
+
+ fixture.componentInstance.ariaLabel = 'Veggie';
+ fixture.detectChanges();
+ expect(tab.getAttribute('aria-label')).toBe('Veggie');
});
});
describe('disable tabs', () => {
let fixture: ComponentFixture
;
+
beforeEach(() => {
fixture = TestBed.createComponent(DisabledTabsTestApp);
});
@@ -660,6 +666,56 @@ describe('MDC-based MatTabGroup', () => {
expect(tabGroupNode.classList).toContain('mat-mdc-tab-group-inverted-header');
});
+
+ it('should be able to opt into keeping the inactive tab content in the DOM', fakeAsync(() => {
+ fixture.componentInstance.preserveContent = true;
+ fixture.detectChanges();
+
+ expect(fixture.nativeElement.textContent).toContain('Pizza, fries');
+ expect(fixture.nativeElement.textContent).not.toContain('Peanuts');
+
+ tabGroup.selectedIndex = 3;
+ fixture.detectChanges();
+ tick();
+
+ expect(fixture.nativeElement.textContent).toContain('Pizza, fries');
+ expect(fixture.nativeElement.textContent).toContain('Peanuts');
+ }));
+
+ it('should visibly hide the content of inactive tabs', fakeAsync(() => {
+ const contentElements: HTMLElement[] = Array.from(
+ fixture.nativeElement.querySelectorAll('.mat-mdc-tab-body-content'),
+ );
+
+ expect(contentElements.map(element => element.style.visibility)).toEqual([
+ '',
+ 'hidden',
+ 'hidden',
+ 'hidden',
+ ]);
+
+ tabGroup.selectedIndex = 2;
+ fixture.detectChanges();
+ tick();
+
+ expect(contentElements.map(element => element.style.visibility)).toEqual([
+ 'hidden',
+ 'hidden',
+ '',
+ 'hidden',
+ ]);
+
+ tabGroup.selectedIndex = 1;
+ fixture.detectChanges();
+ tick();
+
+ expect(contentElements.map(element => element.style.visibility)).toEqual([
+ 'hidden',
+ '',
+ 'hidden',
+ 'hidden',
+ ]);
+ }));
});
describe('lazy loaded tabs', () => {
@@ -780,6 +836,62 @@ describe('MDC-based MatTabGroup', () => {
}));
});
+ describe('tabs with custom css classes', () => {
+ let fixture: ComponentFixture;
+ let labelElements: DebugElement[];
+ let bodyElements: DebugElement[];
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(TabsWithClassesTestApp);
+ fixture.detectChanges();
+ labelElements = fixture.debugElement.queryAll(By.css('.mdc-tab'));
+ bodyElements = fixture.debugElement.queryAll(By.css('mat-tab-body'));
+ });
+
+ it('should apply label/body classes', () => {
+ expect(labelElements[1].nativeElement.classList).toContain('hardcoded-label-class');
+ expect(bodyElements[1].nativeElement.classList).toContain('hardcoded-body-class');
+ });
+
+ it('should set classes as strings dynamically', () => {
+ expect(labelElements[0].nativeElement.classList).not.toContain('custom-label-class');
+ expect(bodyElements[0].nativeElement.classList).not.toContain('custom-body-class');
+
+ fixture.componentInstance.labelClassList = 'custom-label-class';
+ fixture.componentInstance.bodyClassList = 'custom-body-class';
+ fixture.detectChanges();
+
+ expect(labelElements[0].nativeElement.classList).toContain('custom-label-class');
+ expect(bodyElements[0].nativeElement.classList).toContain('custom-body-class');
+
+ delete fixture.componentInstance.labelClassList;
+ delete fixture.componentInstance.bodyClassList;
+ fixture.detectChanges();
+
+ expect(labelElements[0].nativeElement.classList).not.toContain('custom-label-class');
+ expect(bodyElements[0].nativeElement.classList).not.toContain('custom-body-class');
+ });
+
+ it('should set classes as strings array dynamically', () => {
+ expect(labelElements[0].nativeElement.classList).not.toContain('custom-label-class');
+ expect(bodyElements[0].nativeElement.classList).not.toContain('custom-body-class');
+
+ fixture.componentInstance.labelClassList = ['custom-label-class'];
+ fixture.componentInstance.bodyClassList = ['custom-body-class'];
+ fixture.detectChanges();
+
+ expect(labelElements[0].nativeElement.classList).toContain('custom-label-class');
+ expect(bodyElements[0].nativeElement.classList).toContain('custom-body-class');
+
+ delete fixture.componentInstance.labelClassList;
+ delete fixture.componentInstance.bodyClassList;
+ fixture.detectChanges();
+
+ expect(labelElements[0].nativeElement.classList).not.toContain('custom-label-class');
+ expect(bodyElements[0].nativeElement.classList).not.toContain('custom-body-class');
+ });
+ });
+
/**
* Checks that the `selectedIndex` has been updated; checks that the label and body have their
* respective `active` classes
@@ -1014,7 +1126,6 @@ class BindedTabsTestApp {
}
@Component({
- selector: 'test-app',
template: `
@@ -1065,7 +1176,7 @@ class AsyncTabsTestApp implements OnInit {
@Component({
template: `
-
+
Pizza, fries
Broccoli, spinach
{{otherContent}}
@@ -1074,13 +1185,13 @@ class AsyncTabsTestApp implements OnInit {
`,
})
class TabGroupWithSimpleApi {
+ preserveContent = false;
otherLabel = 'Fruit';
otherContent = 'Apples, grapes';
@ViewChild('legumes') legumes: any;
}
@Component({
- selector: 'nested-tabs',
template: `
Tab one content
@@ -1099,7 +1210,6 @@ class NestedTabs {
}
@Component({
- selector: 'template-tabs',
template: `
@@ -1215,3 +1325,21 @@ class TabGroupWithSpaceAbove {
`,
})
class NestedTabGroupWithLabel {}
+
+@Component({
+ template: `
+
+
+ Tab one content
+
+
+ Tab two content
+
+
+ `,
+})
+class TabsWithClassesTestApp {
+ labelClassList?: string | string[];
+ bodyClassList?: string | string[];
+}
diff --git a/src/material-experimental/mdc-tabs/tab-group.ts b/src/material-experimental/mdc-tabs/tab-group.ts
index e268f713057f..2befb5837125 100644
--- a/src/material-experimental/mdc-tabs/tab-group.ts
+++ b/src/material-experimental/mdc-tabs/tab-group.ts
@@ -41,7 +41,8 @@ import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion';
templateUrl: 'tab-group.html',
styleUrls: ['tab-group.css'],
encapsulation: ViewEncapsulation.None,
- changeDetection: ChangeDetectionStrategy.OnPush,
+ // tslint:disable-next-line:validate-decorators
+ changeDetection: ChangeDetectionStrategy.Default,
inputs: ['color', 'disableRipple'],
providers: [
{
diff --git a/src/material-experimental/mdc-tabs/tab-header.ts b/src/material-experimental/mdc-tabs/tab-header.ts
index e733577679fc..a5dce8c6e139 100644
--- a/src/material-experimental/mdc-tabs/tab-header.ts
+++ b/src/material-experimental/mdc-tabs/tab-header.ts
@@ -42,7 +42,8 @@ import {MatInkBar} from './ink-bar';
inputs: ['selectedIndex'],
outputs: ['selectFocusedIndex', 'indexFocused'],
encapsulation: ViewEncapsulation.None,
- changeDetection: ChangeDetectionStrategy.OnPush,
+ // tslint:disable-next-line:validate-decorators
+ changeDetection: ChangeDetectionStrategy.Default,
host: {
'class': 'mat-mdc-tab-header',
'[class.mat-mdc-tab-header-pagination-controls-enabled]': '_showPaginationControls',
diff --git a/src/material-experimental/mdc-tabs/tab-nav-bar/tab-nav-bar.ts b/src/material-experimental/mdc-tabs/tab-nav-bar/tab-nav-bar.ts
index bb88dce83f2c..5b949b82e91e 100644
--- a/src/material-experimental/mdc-tabs/tab-nav-bar/tab-nav-bar.ts
+++ b/src/material-experimental/mdc-tabs/tab-nav-bar/tab-nav-bar.ts
@@ -66,7 +66,8 @@ import {takeUntil} from 'rxjs/operators';
'[class._mat-animation-noopable]': '_animationMode === "NoopAnimations"',
},
encapsulation: ViewEncapsulation.None,
- changeDetection: ChangeDetectionStrategy.OnPush,
+ // tslint:disable-next-line:validate-decorators
+ changeDetection: ChangeDetectionStrategy.Default,
})
export class MatTabNav extends _MatTabNavBase implements AfterContentInit {
/** Whether the ink bar should fit its width to the size of the tab label content. */
diff --git a/src/material-experimental/mdc-tabs/tab.ts b/src/material-experimental/mdc-tabs/tab.ts
index 3d38e03c1a8e..b7384bb6b428 100644
--- a/src/material-experimental/mdc-tabs/tab.ts
+++ b/src/material-experimental/mdc-tabs/tab.ts
@@ -25,7 +25,8 @@ import {MatTabLabel} from './tab-label';
// that creating the extra class will generate more code than just duplicating the template.
templateUrl: 'tab.html',
inputs: ['disabled'],
- changeDetection: ChangeDetectionStrategy.OnPush,
+ // tslint:disable-next-line:validate-decorators
+ changeDetection: ChangeDetectionStrategy.Default,
encapsulation: ViewEncapsulation.None,
exportAs: 'matTab',
providers: [{provide: MAT_TAB, useExisting: MatTab}],
diff --git a/src/material-experimental/mdc-tooltip/tooltip.spec.ts b/src/material-experimental/mdc-tooltip/tooltip.spec.ts
index 4c2462d62bd0..272e70e165ef 100644
--- a/src/material-experimental/mdc-tooltip/tooltip.spec.ts
+++ b/src/material-experimental/mdc-tooltip/tooltip.spec.ts
@@ -697,9 +697,28 @@ describe('MDC-based MatTooltip', () => {
expect(overlayContainerElement.textContent).toContain(initialTooltipMessage);
}));
+ it('should hide when pressing escape', fakeAsync(() => {
+ tooltipDirective.show();
+ tick(0);
+ fixture.detectChanges();
+ tick(500);
+
+ expect(tooltipDirective._isTooltipVisible()).toBe(true);
+ expect(overlayContainerElement.textContent).toContain(initialTooltipMessage);
+
+ dispatchKeyboardEvent(document.body, 'keydown', ESCAPE);
+ tick(0);
+ fixture.detectChanges();
+ tick(500);
+ fixture.detectChanges();
+
+ expect(tooltipDirective._isTooltipVisible()).toBe(false);
+ expect(overlayContainerElement.textContent).toBe('');
+ }));
+
it('should not throw when pressing ESCAPE', fakeAsync(() => {
expect(() => {
- dispatchKeyboardEvent(buttonElement, 'keydown', ESCAPE);
+ dispatchKeyboardEvent(document.body, 'keydown', ESCAPE);
fixture.detectChanges();
}).not.toThrow();
@@ -712,7 +731,7 @@ describe('MDC-based MatTooltip', () => {
tick(0);
fixture.detectChanges();
- const event = dispatchKeyboardEvent(buttonElement, 'keydown', ESCAPE);
+ const event = dispatchKeyboardEvent(document.body, 'keydown', ESCAPE);
fixture.detectChanges();
flush();
@@ -725,7 +744,7 @@ describe('MDC-based MatTooltip', () => {
fixture.detectChanges();
const event = createKeyboardEvent('keydown', ESCAPE, undefined, {alt: true});
- dispatchEvent(buttonElement, event);
+ dispatchEvent(document.body, event);
fixture.detectChanges();
flush();
diff --git a/src/material/autocomplete/autocomplete-trigger.ts b/src/material/autocomplete/autocomplete-trigger.ts
index ccb6078b32cb..fa6f0b15d39e 100644
--- a/src/material/autocomplete/autocomplete-trigger.ts
+++ b/src/material/autocomplete/autocomplete-trigger.ts
@@ -47,7 +47,7 @@ import {
} from '@angular/material/core';
import {MAT_FORM_FIELD, MatFormField} from '@angular/material/form-field';
import {defer, fromEvent, merge, Observable, of as observableOf, Subject, Subscription} from 'rxjs';
-import {delay, filter, map, switchMap, take, tap} from 'rxjs/operators';
+import {delay, filter, map, switchMap, take, tap, startWith} from 'rxjs/operators';
import {
_MatAutocompleteBase,
@@ -309,10 +309,15 @@ export abstract class _MatAutocompleteTriggerBase
);
}
- /** Stream of autocomplete option selections. */
+ /** Stream of changes to the selection state of the autocomplete options. */
readonly optionSelections: Observable = defer(() => {
- if (this.autocomplete && this.autocomplete.options) {
- return merge(...this.autocomplete.options.map(option => option.onSelectionChange));
+ const options = this.autocomplete ? this.autocomplete.options : null;
+
+ if (options) {
+ return options.changes.pipe(
+ startWith(options),
+ switchMap(() => merge(...options.map(option => option.onSelectionChange))),
+ );
}
// If there are any subscribers before `ngAfterViewInit`, the `autocomplete` will be undefined.
@@ -360,7 +365,7 @@ export abstract class _MatAutocompleteTriggerBase
// Implemented as part of ControlValueAccessor.
writeValue(value: any): void {
- Promise.resolve(null).then(() => this._setTriggerValue(value));
+ Promise.resolve().then(() => this._setTriggerValue(value));
}
// Implemented as part of ControlValueAccessor.
@@ -389,7 +394,7 @@ export abstract class _MatAutocompleteTriggerBase
event.preventDefault();
}
- if (this.activeOption && keyCode === ENTER && this.panelOpen) {
+ if (this.activeOption && keyCode === ENTER && this.panelOpen && !hasModifierKey(event)) {
this.activeOption._selectViaInteraction();
this._resetActiveItem();
event.preventDefault();
@@ -551,12 +556,14 @@ export abstract class _MatAutocompleteTriggerBase
* stemmed from the user.
*/
private _setValueAndClose(event: MatOptionSelectionChange | null): void {
- if (event && event.source) {
- this._clearPreviousSelectedOption(event.source);
- this._setTriggerValue(event.source.value);
- this._onChange(event.source.value);
+ const source = event && event.source;
+
+ if (source) {
+ this._clearPreviousSelectedOption(source);
+ this._setTriggerValue(source.value);
+ this._onChange(source.value);
+ this.autocomplete._emitSelectEvent(source);
this._element.nativeElement.focus();
- this.autocomplete._emitSelectEvent(event.source);
}
this.closePanel();
diff --git a/src/material/autocomplete/autocomplete.spec.ts b/src/material/autocomplete/autocomplete.spec.ts
index 5ea9ab5bb257..b905b193b4a3 100644
--- a/src/material/autocomplete/autocomplete.spec.ts
+++ b/src/material/autocomplete/autocomplete.spec.ts
@@ -1081,6 +1081,27 @@ describe('MatAutocomplete', () => {
.toBe(false);
});
+ it('should not interfere with the ENTER key when pressing a modifier', fakeAsync(() => {
+ const trigger = fixture.componentInstance.trigger;
+
+ expect(input.value).withContext('Expected input to start off blank.').toBeFalsy();
+ expect(trigger.panelOpen).withContext('Expected panel to start off open.').toBe(true);
+
+ fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
+ flush();
+ fixture.detectChanges();
+
+ Object.defineProperty(ENTER_EVENT, 'altKey', {get: () => true});
+ fixture.componentInstance.trigger._handleKeydown(ENTER_EVENT);
+ fixture.detectChanges();
+
+ expect(trigger.panelOpen).withContext('Expected panel to remain open.').toBe(true);
+ expect(input.value).withContext('Expected input to remain blank.').toBeFalsy();
+ expect(ENTER_EVENT.defaultPrevented)
+ .withContext('Expected the default ENTER action not to have been prevented.')
+ .toBe(false);
+ }));
+
it('should fill the text field, not select an option, when SPACE is entered', () => {
typeInElement(input, 'New');
fixture.detectChanges();
@@ -2265,6 +2286,34 @@ describe('MatAutocomplete', () => {
subscription!.unsubscribe();
}));
+ it('should emit to `optionSelections` if the list of options changes', fakeAsync(() => {
+ const spy = jasmine.createSpy('option selection spy');
+ const subscription = fixture.componentInstance.trigger.optionSelections.subscribe(spy);
+ const openAndSelectFirstOption = () => {
+ fixture.detectChanges();
+ fixture.componentInstance.trigger.openPanel();
+ fixture.detectChanges();
+ zone.simulateZoneExit();
+ (overlayContainerElement.querySelector('mat-option') as HTMLElement).click();
+ fixture.detectChanges();
+ zone.simulateZoneExit();
+ };
+
+ fixture.componentInstance.states = [{code: 'OR', name: 'Oregon'}];
+ fixture.detectChanges();
+
+ openAndSelectFirstOption();
+ expect(spy).toHaveBeenCalledTimes(1);
+
+ fixture.componentInstance.states = [{code: 'WV', name: 'West Virginia'}];
+ fixture.detectChanges();
+
+ openAndSelectFirstOption();
+ expect(spy).toHaveBeenCalledTimes(2);
+
+ subscription!.unsubscribe();
+ }));
+
it('should reposition the panel when the amount of options changes', fakeAsync(() => {
const formField = fixture.debugElement.query(By.css('.mat-form-field'))!.nativeElement;
const inputReference = formField.querySelector('.mat-form-field-flex');
@@ -2824,6 +2873,29 @@ describe('MatAutocomplete', () => {
expect(event.option.value).toBe('Washington');
}));
+ it('should refocus the input after the selection event is emitted', fakeAsync(() => {
+ const events: string[] = [];
+ const fixture = createComponent(AutocompleteWithSelectEvent);
+ fixture.detectChanges();
+ const input = fixture.nativeElement.querySelector('input');
+
+ fixture.componentInstance.trigger.openPanel();
+ zone.simulateZoneExit();
+ fixture.detectChanges();
+
+ const options = overlayContainerElement.querySelectorAll(
+ 'mat-option',
+ ) as NodeListOf;
+ spyOn(input, 'focus').and.callFake(() => events.push('focus'));
+ fixture.componentInstance.optionSelected.and.callFake(() => events.push('select'));
+
+ options[1].click();
+ tick();
+ fixture.detectChanges();
+
+ expect(events).toEqual(['select', 'focus']);
+ }));
+
it('should emit an event when a newly-added option is selected', fakeAsync(() => {
const fixture = createComponent(AutocompleteWithSelectEvent);
diff --git a/src/material/button/BUILD.bazel b/src/material/button/BUILD.bazel
index 1302f96a0374..f411fdcf24de 100644
--- a/src/material/button/BUILD.bazel
+++ b/src/material/button/BUILD.bazel
@@ -52,6 +52,7 @@ ng_test_library(
),
deps = [
":button",
+ "//src/cdk/testing/private",
"//src/material/core",
"@npm//@angular/platform-browser",
],
diff --git a/src/material/button/button.spec.ts b/src/material/button/button.spec.ts
index a0eced1ece92..e4c5bcd179a9 100644
--- a/src/material/button/button.spec.ts
+++ b/src/material/button/button.spec.ts
@@ -1,8 +1,9 @@
import {waitForAsync, ComponentFixture, TestBed} from '@angular/core/testing';
-import {Component, DebugElement} from '@angular/core';
+import {ApplicationRef, Component, DebugElement} from '@angular/core';
import {By} from '@angular/platform-browser';
import {MatButtonModule, MatButton} from './index';
import {MatRipple, ThemePalette} from '@angular/material/core';
+import {createMouseEvent, dispatchEvent} from '@angular/cdk/testing/private';
describe('MatButton', () => {
beforeEach(
@@ -243,6 +244,28 @@ describe('MatButton', () => {
.withContext('Expected custom tabindex to be overwritten when disabled.')
.toBe('-1');
});
+
+ describe('change detection behavior', () => {
+ it('should not run change detection for disabled anchor but should prevent the default behavior and stop event propagation', () => {
+ const appRef = TestBed.inject(ApplicationRef);
+ const fixture = TestBed.createComponent(TestApp);
+ fixture.componentInstance.isDisabled = true;
+ fixture.detectChanges();
+ const anchorElement = fixture.debugElement.query(By.css('a'))!.nativeElement;
+
+ spyOn(appRef, 'tick');
+
+ const event = createMouseEvent('click');
+ spyOn(event, 'preventDefault').and.callThrough();
+ spyOn(event, 'stopImmediatePropagation').and.callThrough();
+
+ dispatchEvent(anchorElement, event);
+
+ expect(appRef.tick).not.toHaveBeenCalled();
+ expect(event.preventDefault).toHaveBeenCalled();
+ expect(event.stopImmediatePropagation).toHaveBeenCalled();
+ });
+ });
});
// Ripple tests.
diff --git a/src/material/button/button.ts b/src/material/button/button.ts
index 96b24e98f7e7..ca1100b324eb 100644
--- a/src/material/button/button.ts
+++ b/src/material/button/button.ts
@@ -18,6 +18,7 @@ import {
Inject,
Input,
AfterViewInit,
+ NgZone,
} from '@angular/core';
import {
CanColor,
@@ -164,7 +165,6 @@ export class MatButton
'[attr.tabindex]': 'disabled ? -1 : (tabIndex || 0)',
'[attr.disabled]': 'disabled || null',
'[attr.aria-disabled]': 'disabled.toString()',
- '(click)': '_haltDisabledEvents($event)',
'[class._mat-animation-noopable]': '_animationMode === "NoopAnimations"',
'[class.mat-button-disabled]': 'disabled',
'class': 'mat-focus-indicator',
@@ -175,7 +175,7 @@ export class MatButton
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
})
-export class MatAnchor extends MatButton {
+export class MatAnchor extends MatButton implements AfterViewInit, OnDestroy {
/** Tabindex of the button. */
@Input() tabIndex: number;
@@ -183,15 +183,35 @@ export class MatAnchor extends MatButton {
focusMonitor: FocusMonitor,
elementRef: ElementRef,
@Optional() @Inject(ANIMATION_MODULE_TYPE) animationMode: string,
+ /** @breaking-change 14.0.0 _ngZone will be required. */
+ @Optional() private _ngZone?: NgZone,
) {
super(elementRef, focusMonitor, animationMode);
}
- _haltDisabledEvents(event: Event) {
+ override ngAfterViewInit(): void {
+ super.ngAfterViewInit();
+
+ /** @breaking-change 14.0.0 _ngZone will be required. */
+ if (this._ngZone) {
+ this._ngZone.runOutsideAngular(() => {
+ this._elementRef.nativeElement.addEventListener('click', this._haltDisabledEvents);
+ });
+ } else {
+ this._elementRef.nativeElement.addEventListener('click', this._haltDisabledEvents);
+ }
+ }
+
+ override ngOnDestroy(): void {
+ super.ngOnDestroy();
+ this._elementRef.nativeElement.removeEventListener('click', this._haltDisabledEvents);
+ }
+
+ _haltDisabledEvents = (event: Event): void => {
// A disabled button shouldn't apply any actions
if (this.disabled) {
event.preventDefault();
event.stopImmediatePropagation();
}
- }
+ };
}
diff --git a/src/material/card/card.scss b/src/material/card/card.scss
index e208e4a6d098..20fb0ce20f79 100644
--- a/src/material/card/card.scss
+++ b/src/material/card/card.scss
@@ -73,6 +73,14 @@ $header-size: 40px !default;
.mat-card-image {
width: calc(100% + #{$padding * 2});
margin: 0 (-$padding) 16px (-$padding);
+
+ // The following properties are to handle `mat-card-image` on a `picture` element.
+ display: block;
+ overflow: hidden;
+
+ img {
+ width: 100%;
+ }
}
.mat-card-footer {
diff --git a/src/material/chips/chip.ts b/src/material/chips/chip.ts
index d0a9114af50e..1307926cba3b 100644
--- a/src/material/chips/chip.ts
+++ b/src/material/chips/chip.ts
@@ -404,8 +404,6 @@ export class MatChip
_handleClick(event: Event) {
if (this.disabled) {
event.preventDefault();
- } else {
- event.stopPropagation();
}
}
diff --git a/src/material/core/common-behaviors/common-module.ts b/src/material/core/common-behaviors/common-module.ts
index d864c099535b..85c07a50e282 100644
--- a/src/material/core/common-behaviors/common-module.ts
+++ b/src/material/core/common-behaviors/common-module.ts
@@ -8,16 +8,11 @@
import {HighContrastModeDetector} from '@angular/cdk/a11y';
import {BidiModule} from '@angular/cdk/bidi';
-import {Inject, InjectionToken, NgModule, Optional, Version} from '@angular/core';
+import {Inject, InjectionToken, NgModule, Optional} from '@angular/core';
import {VERSION as CDK_VERSION} from '@angular/cdk';
import {DOCUMENT} from '@angular/common';
import {_isTestEnvironment} from '@angular/cdk/platform';
-
-// Private version constant to circumvent test/build issues,
-// i.e. avoid core to depend on the @angular/material primary entry-point
-// Can be removed once the Material primary entry-point no longer
-// re-exports all secondary entry-points
-const VERSION = new Version('0.0.0-PLACEHOLDER');
+import {VERSION} from '../version';
/** @docs-private */
export function MATERIAL_SANITY_CHECKS_FACTORY(): SanityChecks {
diff --git a/src/material/core/focus-indicators/_focus-indicators.scss b/src/material/core/focus-indicators/_focus-indicators.scss
index ac68751be83a..0062400610b0 100644
--- a/src/material/core/focus-indicators/_focus-indicators.scss
+++ b/src/material/core/focus-indicators/_focus-indicators.scss
@@ -28,6 +28,10 @@
pointer-events: none;
border: $border-width $border-style transparent;
border-radius: $border-radius;
+
+ .cdk-high-contrast-active & {
+ display: none;
+ }
}
// By default, all focus indicators are flush with the bounding box of their
diff --git a/src/material/core/option/option.ts b/src/material/core/option/option.ts
index cdab4794355d..445982250fc8 100644
--- a/src/material/core/option/option.ts
+++ b/src/material/core/option/option.ts
@@ -36,17 +36,17 @@ import {MatOptionParentComponent, MAT_OPTION_PARENT_COMPONENT} from './option-pa
let _uniqueIdCounter = 0;
/** Event object emitted by MatOption when selected or deselected. */
-export class MatOptionSelectionChange {
+export class MatOptionSelectionChange {
constructor(
/** Reference to the option that emitted the event. */
- public source: _MatOptionBase,
+ public source: _MatOptionBase,
/** Whether the change in the option's value was a result of a user action. */
public isUserInput = false,
) {}
}
@Directive()
-export class _MatOptionBase implements FocusableOption, AfterViewChecked, OnDestroy {
+export class _MatOptionBase implements FocusableOption, AfterViewChecked, OnDestroy {
private _selected = false;
private _active = false;
private _disabled = false;
@@ -63,7 +63,7 @@ export class _MatOptionBase implements FocusableOption, AfterViewChecked, OnDest
}
/** The form value of the option. */
- @Input() value: any;
+ @Input() value: T;
/** The unique ID of the option. */
@Input() id: string = `mat-option-${_uniqueIdCounter++}`;
@@ -84,7 +84,7 @@ export class _MatOptionBase implements FocusableOption, AfterViewChecked, OnDest
/** Event emitted when the option is selected or deselected. */
// tslint:disable-next-line:no-output-on-prefix
- @Output() readonly onSelectionChange = new EventEmitter();
+ @Output() readonly onSelectionChange = new EventEmitter>();
/** Emits when the state of the option changes and any parents have to be notified. */
readonly _stateChanges = new Subject();
@@ -237,7 +237,7 @@ export class _MatOptionBase implements FocusableOption, AfterViewChecked, OnDest
/** Emits the selection change event. */
private _emitSelectionChangeEvent(isUserInput = false): void {
- this.onSelectionChange.emit(new MatOptionSelectionChange(this, isUserInput));
+ this.onSelectionChange.emit(new MatOptionSelectionChange(this, isUserInput));
}
}
@@ -266,7 +266,7 @@ export class _MatOptionBase implements FocusableOption, AfterViewChecked, OnDest
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
})
-export class MatOption extends _MatOptionBase {
+export class MatOption extends _MatOptionBase {
constructor(
element: ElementRef,
changeDetectorRef: ChangeDetectorRef,
diff --git a/src/material/core/ripple/_ripple.scss b/src/material/core/ripple/_ripple.scss
index 536f15c826ef..f4485547fbc8 100644
--- a/src/material/core/ripple/_ripple.scss
+++ b/src/material/core/ripple/_ripple.scss
@@ -30,7 +30,10 @@
pointer-events: none;
transition: opacity, transform 0ms cubic-bezier(0, 0, 0.2, 1);
- transform: scale(0);
+
+ // We use a 3d transform here in order to avoid an issue in Safari where
+ // the ripples aren't clipped when inside the shadow DOM (see #24028).
+ transform: scale3d(0, 0, 0);
// In high contrast mode the ripple is opaque, causing it to obstruct the content.
@include a11y.high-contrast(active, off) {
diff --git a/src/material/core/ripple/ripple-renderer.ts b/src/material/core/ripple/ripple-renderer.ts
index 1941bb970d3b..8887d83b4d96 100644
--- a/src/material/core/ripple/ripple-renderer.ts
+++ b/src/material/core/ripple/ripple-renderer.ts
@@ -138,7 +138,9 @@ export class RippleRenderer implements EventListenerObject {
// ripple elements. This is critical because then the `scale` would not animate properly.
enforceStyleRecalculation(ripple);
- ripple.style.transform = 'scale(1)';
+ // We use a 3d transform here in order to avoid an issue in Safari where
+ // the ripples aren't clipped when inside the shadow DOM (see #24028).
+ ripple.style.transform = 'scale3d(1, 1, 1)';
// Exposed reference to the ripple that will be returned.
const rippleRef = new RippleRef(this, ripple, config);
diff --git a/src/material/core/selection/index.ts b/src/material/core/selection/index.ts
index 31f98de88d59..102eaefd2bab 100644
--- a/src/material/core/selection/index.ts
+++ b/src/material/core/selection/index.ts
@@ -6,15 +6,5 @@
* found in the LICENSE file at https://angular.io/license
*/
-import {NgModule} from '@angular/core';
-import {MatPseudoCheckbox} from './pseudo-checkbox/pseudo-checkbox';
-import {MatCommonModule} from '../common-behaviors/common-module';
-
-@NgModule({
- imports: [MatCommonModule],
- exports: [MatPseudoCheckbox],
- declarations: [MatPseudoCheckbox],
-})
-export class MatPseudoCheckboxModule {}
-
export * from './pseudo-checkbox/pseudo-checkbox';
+export * from './pseudo-checkbox/pseudo-checkbox-module';
diff --git a/src/material/core/selection/pseudo-checkbox/pseudo-checkbox-module.ts b/src/material/core/selection/pseudo-checkbox/pseudo-checkbox-module.ts
new file mode 100644
index 000000000000..4a35ebc701dc
--- /dev/null
+++ b/src/material/core/selection/pseudo-checkbox/pseudo-checkbox-module.ts
@@ -0,0 +1,18 @@
+/**
+ * @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.io/license
+ */
+
+import {NgModule} from '@angular/core';
+import {MatPseudoCheckbox} from './pseudo-checkbox';
+import {MatCommonModule} from '../../common-behaviors/common-module';
+
+@NgModule({
+ imports: [MatCommonModule],
+ exports: [MatPseudoCheckbox],
+ declarations: [MatPseudoCheckbox],
+})
+export class MatPseudoCheckboxModule {}
diff --git a/src/material/core/theming/_palette.scss b/src/material/core/theming/_palette.scss
index 10fa42bf4553..e2b5faada05c 100644
--- a/src/material/core/theming/_palette.scss
+++ b/src/material/core/theming/_palette.scss
@@ -705,7 +705,7 @@ $dark-theme-background-palette: (
selected-disabled-button: map.get($grey-palette, 800),
disabled-button-toggle: black,
unselected-chip: map.get($grey-palette, 700),
- disabled-list-option: black,
+ disabled-list-option: rgba(white, 0.12),
tooltip: map.get($grey-palette, 700),
);
diff --git a/src/material/core/theming/_theming.scss b/src/material/core/theming/_theming.scss
index 0773c7f966fc..eca5a6819091 100644
--- a/src/material/core/theming/_theming.scss
+++ b/src/material/core/theming/_theming.scss
@@ -20,6 +20,17 @@ $_emitted-color: () !default;
$_emitted-typography: () !default;
$_emitted-density: () !default;
+/// Extracts a color from a palette or throws an error if it doesn't exist.
+/// @param {Map} $palette The palette from which to extract a color.
+/// @param {String | Number} $hue The hue for which to get the color.
+@function _get-color-from-palette($palette, $hue) {
+ @if map.has-key($palette, $hue) {
+ @return map.get($palette, $hue);
+ }
+
+ @error 'Hue "' + $hue + '" does not exist in palette. Available hues are: ' + map.keys($palette);
+}
+
/// For a given hue in a palette, return the contrast color from the map of contrast palettes.
/// @param {Map} $palette The palette from which to extract a color.
/// @param {String | Number} $hue The hue for which to get a contrast color.
@@ -40,10 +51,10 @@ $_emitted-density: () !default;
@function define-palette($base-palette, $default: 500, $lighter: 100, $darker: 700,
$text: $default) {
$result: map.merge($base-palette, (
- default: map.get($base-palette, $default),
- lighter: map.get($base-palette, $lighter),
- darker: map.get($base-palette, $darker),
- text: map.get($base-palette, $text),
+ default: _get-color-from-palette($base-palette, $default),
+ lighter: _get-color-from-palette($base-palette, $lighter),
+ darker: _get-color-from-palette($base-palette, $darker),
+ text: _get-color-from-palette($base-palette, $text),
default-contrast: get-contrast-color-from-palette($base-palette, $default),
lighter-contrast: get-contrast-color-from-palette($base-palette, $lighter),
diff --git a/src/material/datepicker/calendar-body.html b/src/material/datepicker/calendar-body.html
index ffdfef27c1cd..1b0f5f0b1a4a 100644
--- a/src/material/datepicker/calendar-body.html
+++ b/src/material/datepicker/calendar-body.html
@@ -26,40 +26,51 @@
[style.paddingBottom]="_cellPadding">
{{_firstRowOffset >= labelMinRequiredCells ? label : ''}}
-
-
- {{item.displayValue}}
-
-
+
+ |
+
|
diff --git a/src/material/datepicker/calendar-body.scss b/src/material/datepicker/calendar-body.scss
index c2d2f78df048..334585fcd42b 100644
--- a/src/material/datepicker/calendar-body.scss
+++ b/src/material/datepicker/calendar-body.scss
@@ -1,4 +1,5 @@
@use 'sass:math';
+@use '../core/style/button-common';
@use '../../cdk/a11y';
$calendar-body-label-padding-start: 5% !default;
@@ -31,13 +32,24 @@ $calendar-range-end-body-cell-size:
padding-right: $calendar-body-label-side-padding;
}
-.mat-calendar-body-cell {
+.mat-calendar-body-cell-container {
position: relative;
height: 0;
line-height: 0;
+}
+
+.mat-calendar-body-cell {
+ @include button-common.reset();
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: none;
text-align: center;
outline: none;
- cursor: pointer;
+ font-family: inherit;
+ margin: 0;
}
// We use ::before to apply a background to the body cell, because we need to apply a border
@@ -208,8 +220,12 @@ $calendar-range-end-body-cell-size:
.cdk-keyboard-focused .mat-calendar-body-active,
.cdk-program-focused .mat-calendar-body-active {
- & > .mat-calendar-body-cell-content:not(.mat-calendar-body-selected) {
+ & > .mat-calendar-body-cell-content {
outline: dotted 2px;
+
+ &.mat-calendar-body-selected {
+ outline: solid 3px;
+ }
}
}
diff --git a/src/material/datepicker/calendar-body.spec.ts b/src/material/datepicker/calendar-body.spec.ts
index df10b757c081..52ff40e20359 100644
--- a/src/material/datepicker/calendar-body.spec.ts
+++ b/src/material/datepicker/calendar-body.spec.ts
@@ -92,15 +92,13 @@ describe('MatCalendarBody', () => {
expect(selectedCell.innerHTML.trim()).toBe('4');
});
- it('should set aria-selected correctly', () => {
- const selectedCells = cellEls.filter(c => c.getAttribute('aria-selected') === 'true');
- const deselectedCells = cellEls.filter(c => c.getAttribute('aria-selected') === 'false');
-
- expect(selectedCells.length)
- .withContext('Expected one cell to be marked as selected.')
- .toBe(1);
- expect(deselectedCells.length)
- .withContext('Expected remaining cells to be marked as deselected.')
+ it('should set aria-pressed correctly', () => {
+ const pressedCells = cellEls.filter(c => c.getAttribute('aria-pressed') === 'true');
+ const depressedCells = cellEls.filter(c => c.getAttribute('aria-pressed') === 'false');
+
+ expect(pressedCells.length).withContext('Expected one cell to be marked as pressed.').toBe(1);
+ expect(depressedCells.length)
+ .withContext('Expected remaining cells to be marked as not pressed.')
.toBe(cellEls.length - 1);
});
diff --git a/src/material/datepicker/calendar-body.ts b/src/material/datepicker/calendar-body.ts
index 895db4b41472..ef34dc45fbff 100644
--- a/src/material/datepicker/calendar-body.ts
+++ b/src/material/datepicker/calendar-body.ts
@@ -337,7 +337,7 @@ export class MatCalendarBody implements OnChanges, OnDestroy {
// Only reset the preview end value when leaving cells. This looks better, because
// we have a gap between the cells and the rows and we don't want to remove the
// range just for it to show up again when the user moves a few pixels to the side.
- if (event.target && isTableCell(event.target as HTMLElement)) {
+ if (event.target && this._getCellFromElement(event.target as HTMLElement)) {
this._ngZone.run(() => this.previewChange.emit({value: null, event}));
}
}
diff --git a/src/material/datepicker/datepicker-content.html b/src/material/datepicker/datepicker-content.html
index 97109f1f59df..182377885cf7 100644
--- a/src/material/datepicker/datepicker-content.html
+++ b/src/material/datepicker/datepicker-content.html
@@ -1,6 +1,7 @@
-
- Date type |
- Date |
-
-
- Supported locales |
- en-US |
-
-
- Dependencies |
- None |
-
-
- Import from |
- @angular/material/core |
-
+
+ Date type |
+ Date |
+
+
+ Supported locales |
+ en-US |
+
+
+ Dependencies |
+ None |
+
+
+ Import from |
+ @angular/material/core |
+
@@ -329,22 +329,22 @@ The easiest way to ensure this is to import one of the provided date modules:
-
- Date type |
- Date |
-
-
- Supported locales |
- See project for details |
-
-
- Dependencies |
- date-fns |
-
-
- Import from |
- @angular/material-date-fns-adapter |
-
+
+ Date type |
+ Date |
+
+
+ Supported locales |
+ See project for details |
+
+
+ Dependencies |
+ date-fns |
+
+
+ Import from |
+ @angular/material-date-fns-adapter |
+
@@ -352,22 +352,22 @@ The easiest way to ensure this is to import one of the provided date modules:
-
- Date type |
- DateTime |
-
-
- Supported locales |
- See project for details |
-
-
- Dependencies |
- Luxon |
-
-
- Import from |
- @angular/material-luxon-adapter |
-
+
+ Date type |
+ DateTime |
+
+
+ Supported locales |
+ See project for details |
+
+
+ Dependencies |
+ Luxon |
+
+
+ Import from |
+ @angular/material-luxon-adapter |
+
@@ -375,22 +375,22 @@ The easiest way to ensure this is to import one of the provided date modules:
diff --git a/src/material/datepicker/testing/calendar-cell-harness.ts b/src/material/datepicker/testing/calendar-cell-harness.ts
index 4ebee4c8ccb4..c0274c0d281d 100644
--- a/src/material/datepicker/testing/calendar-cell-harness.ts
+++ b/src/material/datepicker/testing/calendar-cell-harness.ts
@@ -69,7 +69,7 @@ export class MatCalendarCellHarness extends ComponentHarness {
/** Whether the cell is selected. */
async isSelected(): Promise {
const host = await this.host();
- return (await host.getAttribute('aria-selected')) === 'true';
+ return (await host.getAttribute('aria-pressed')) === 'true';
}
/** Whether the cell is disabled. */
diff --git a/src/material/expansion/expansion-panel-header.scss b/src/material/expansion/expansion-panel-header.scss
index b0984e25fee6..7c45e216d13c 100644
--- a/src/material/expansion/expansion-panel-header.scss
+++ b/src/material/expansion/expansion-panel-header.scss
@@ -55,6 +55,7 @@
display: flex;
flex-grow: 1;
margin-right: 16px;
+ align-items: center;
[dir='rtl'] & {
margin-right: 0;
diff --git a/src/material/form-field/form-field.ts b/src/material/form-field/form-field.ts
index 21975e33ea37..7bb7f497d5f5 100644
--- a/src/material/form-field/form-field.ts
+++ b/src/material/form-field/form-field.ts
@@ -549,20 +549,26 @@ export class MatFormField
*/
updateOutlineGap() {
const labelEl = this._label ? this._label.nativeElement : null;
+ const container = this._connectionContainerRef.nativeElement;
+ const outlineStartSelector = '.mat-form-field-outline-start';
+ const outlineGapSelector = '.mat-form-field-outline-gap';
- if (
- this.appearance !== 'outline' ||
- !labelEl ||
- !labelEl.children.length ||
- !labelEl.textContent!.trim()
- ) {
+ // getBoundingClientRect isn't available on the server.
+ if (this.appearance !== 'outline' || !this._platform.isBrowser) {
return;
}
- if (!this._platform.isBrowser) {
- // getBoundingClientRect isn't available on the server.
+ // If there is no content, set the gap elements to zero.
+ if (!labelEl || !labelEl.children.length || !labelEl.textContent!.trim()) {
+ const gapElements = container.querySelectorAll(
+ `${outlineStartSelector}, ${outlineGapSelector}`,
+ );
+ for (let i = 0; i < gapElements.length; i++) {
+ gapElements[i].style.width = '0';
+ }
return;
}
+
// If the element is not present in the DOM, the outline gap will need to be calculated
// the next time it is checked and in the DOM.
if (!this._isAttachedToDOM()) {
@@ -573,9 +579,8 @@ export class MatFormField
let startWidth = 0;
let gapWidth = 0;
- const container = this._connectionContainerRef.nativeElement;
- const startEls = container.querySelectorAll('.mat-form-field-outline-start');
- const gapEls = container.querySelectorAll('.mat-form-field-outline-gap');
+ const startEls = container.querySelectorAll(outlineStartSelector);
+ const gapEls = container.querySelectorAll(outlineGapSelector);
if (this._label && this._label.nativeElement.children.length) {
const containerRect = container.getBoundingClientRect();
diff --git a/src/material/input/input.spec.ts b/src/material/input/input.spec.ts
index c81888a72f33..f3fd311ba2ad 100644
--- a/src/material/input/input.spec.ts
+++ b/src/material/input/input.spec.ts
@@ -1761,6 +1761,34 @@ describe('MatInput with appearance', () => {
expect(parseInt(outlineStart.style.width || '0')).toBeGreaterThan(0);
expect(parseInt(outlineGap.style.width || '0')).toBeGreaterThan(0);
}));
+
+ it('should recalculate the outline gap when the label changes to empty after init', fakeAsync(() => {
+ fixture.destroy();
+ TestBed.resetTestingModule();
+
+ const outlineFixture = createComponent(MatInputWithAppearanceAndLabel);
+
+ outlineFixture.componentInstance.appearance = 'outline';
+ outlineFixture.detectChanges();
+ flush();
+ outlineFixture.detectChanges();
+
+ const wrapperElement = outlineFixture.nativeElement;
+ const outlineStart = wrapperElement.querySelector('.mat-form-field-outline-start');
+ const outlineGap = wrapperElement.querySelector('.mat-form-field-outline-gap');
+
+ expect(parseInt(outlineStart.style.width)).toBeGreaterThan(0);
+ expect(parseInt(outlineGap.style.width)).toBeGreaterThan(0);
+
+ outlineFixture.componentInstance.labelContent = '';
+ outlineFixture.detectChanges();
+
+ outlineFixture.componentInstance.formField.updateOutlineGap();
+ outlineFixture.detectChanges();
+
+ expect(parseInt(outlineStart.style.width)).toBe(0);
+ expect(parseInt(outlineGap.style.width)).toBe(0);
+ }));
});
describe('MatFormField default options', () => {
diff --git a/src/material/input/input.ts b/src/material/input/input.ts
index 7c4abffd45a1..cf67fd0bd6ff 100644
--- a/src/material/input/input.ts
+++ b/src/material/input/input.ts
@@ -78,6 +78,7 @@ const _MatInputBase = mixinErrorState(
'[attr.data-placeholder]': 'placeholder',
'[disabled]': 'disabled',
'[required]': 'required',
+ '[attr.name]': 'name || null',
'[attr.readonly]': 'readonly && !_isNativeSelect || null',
'[class.mat-native-select-inline]': '_isInlineSelect()',
// Only mark the input as invalid for assistive technology if it has a value since the
@@ -183,6 +184,12 @@ export class MatInput
*/
@Input() placeholder: string;
+ /**
+ * Name of the input.
+ * @docs-private
+ */
+ @Input() name: string;
+
/**
* Implemented as part of MatFormFieldControl.
* @docs-private
diff --git a/src/material/input/testing/shared-input.spec.ts b/src/material/input/testing/shared-input.spec.ts
index 8eb4268975e5..e51383ed0ffa 100644
--- a/src/material/input/testing/shared-input.spec.ts
+++ b/src/material/input/testing/shared-input.spec.ts
@@ -2,7 +2,7 @@ import {HarnessLoader} from '@angular/cdk/testing';
import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed';
import {Component} from '@angular/core';
import {ComponentFixture, TestBed} from '@angular/core/testing';
-import {ReactiveFormsModule} from '@angular/forms';
+import {FormsModule} from '@angular/forms';
import {MatInputModule} from '@angular/material/input';
import {getSupportedInputTypes} from '@angular/cdk/platform';
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
@@ -18,7 +18,7 @@ export function runInputHarnessTests(
beforeEach(async () => {
await TestBed.configureTestingModule({
- imports: [NoopAnimationsModule, inputModule, ReactiveFormsModule],
+ imports: [NoopAnimationsModule, inputModule, FormsModule],
declarations: [InputHarnessTest],
}).compileComponents();
@@ -29,7 +29,7 @@ export function runInputHarnessTests(
it('should load all input harnesses', async () => {
const inputs = await loader.getAllHarnesses(inputHarness);
- expect(inputs.length).toBe(6);
+ expect(inputs.length).toBe(7);
});
it('should load input with specific id', async () => {
@@ -68,37 +68,40 @@ export function runInputHarnessTests(
it('should be able to get id of input', async () => {
const inputs = await loader.getAllHarnesses(inputHarness);
- expect(inputs.length).toBe(6);
+ expect(inputs.length).toBe(7);
expect(await inputs[0].getId()).toMatch(/mat-input-\d+/);
expect(await inputs[1].getId()).toMatch(/mat-input-\d+/);
expect(await inputs[2].getId()).toBe('myTextarea');
expect(await inputs[3].getId()).toBe('nativeControl');
expect(await inputs[4].getId()).toMatch(/mat-input-\d+/);
+ expect(await inputs[5].getId()).toBe('has-ng-model');
});
it('should be able to get name of input', async () => {
const inputs = await loader.getAllHarnesses(inputHarness);
- expect(inputs.length).toBe(6);
+ expect(inputs.length).toBe(7);
expect(await inputs[0].getName()).toBe('favorite-food');
expect(await inputs[1].getName()).toBe('');
expect(await inputs[2].getName()).toBe('');
expect(await inputs[3].getName()).toBe('');
expect(await inputs[4].getName()).toBe('');
+ expect(await inputs[5].getName()).toBe('has-ng-model');
});
it('should be able to get value of input', async () => {
const inputs = await loader.getAllHarnesses(inputHarness);
- expect(inputs.length).toBe(6);
+ expect(inputs.length).toBe(7);
expect(await inputs[0].getValue()).toBe('Sushi');
expect(await inputs[1].getValue()).toBe('');
expect(await inputs[2].getValue()).toBe('');
expect(await inputs[3].getValue()).toBe('');
expect(await inputs[4].getValue()).toBe('');
+ expect(await inputs[5].getValue()).toBe('');
});
it('should be able to set value of input', async () => {
const inputs = await loader.getAllHarnesses(inputHarness);
- expect(inputs.length).toBe(6);
+ expect(inputs.length).toBe(7);
expect(await inputs[0].getValue()).toBe('Sushi');
expect(await inputs[1].getValue()).toBe('');
expect(await inputs[3].getValue()).toBe('');
@@ -117,13 +120,14 @@ export function runInputHarnessTests(
it('should be able to get disabled state', async () => {
const inputs = await loader.getAllHarnesses(inputHarness);
- expect(inputs.length).toBe(6);
+ expect(inputs.length).toBe(7);
expect(await inputs[0].isDisabled()).toBe(false);
expect(await inputs[1].isDisabled()).toBe(false);
expect(await inputs[2].isDisabled()).toBe(false);
expect(await inputs[3].isDisabled()).toBe(false);
expect(await inputs[4].isDisabled()).toBe(false);
+ expect(await inputs[5].isDisabled()).toBe(false);
fixture.componentInstance.disabled = true;
@@ -132,13 +136,14 @@ export function runInputHarnessTests(
it('should be able to get readonly state', async () => {
const inputs = await loader.getAllHarnesses(inputHarness);
- expect(inputs.length).toBe(6);
+ expect(inputs.length).toBe(7);
expect(await inputs[0].isReadonly()).toBe(false);
expect(await inputs[1].isReadonly()).toBe(false);
expect(await inputs[2].isReadonly()).toBe(false);
expect(await inputs[3].isReadonly()).toBe(false);
expect(await inputs[4].isReadonly()).toBe(false);
+ expect(await inputs[5].isReadonly()).toBe(false);
fixture.componentInstance.readonly = true;
@@ -147,13 +152,14 @@ export function runInputHarnessTests(
it('should be able to get required state', async () => {
const inputs = await loader.getAllHarnesses(inputHarness);
- expect(inputs.length).toBe(6);
+ expect(inputs.length).toBe(7);
expect(await inputs[0].isRequired()).toBe(false);
expect(await inputs[1].isRequired()).toBe(false);
expect(await inputs[2].isRequired()).toBe(false);
expect(await inputs[3].isRequired()).toBe(false);
expect(await inputs[4].isRequired()).toBe(false);
+ expect(await inputs[5].isRequired()).toBe(false);
fixture.componentInstance.required = true;
@@ -162,22 +168,24 @@ export function runInputHarnessTests(
it('should be able to get placeholder of input', async () => {
const inputs = await loader.getAllHarnesses(inputHarness);
- expect(inputs.length).toBe(6);
+ expect(inputs.length).toBe(7);
expect(await inputs[0].getPlaceholder()).toBe('Favorite food');
expect(await inputs[1].getPlaceholder()).toBe('');
expect(await inputs[2].getPlaceholder()).toBe('Leave a comment');
expect(await inputs[3].getPlaceholder()).toBe('Native control');
expect(await inputs[4].getPlaceholder()).toBe('');
+ expect(await inputs[5].getPlaceholder()).toBe('');
});
it('should be able to get type of input', async () => {
const inputs = await loader.getAllHarnesses(inputHarness);
- expect(inputs.length).toBe(6);
+ expect(inputs.length).toBe(7);
expect(await inputs[0].getType()).toBe('text');
expect(await inputs[1].getType()).toBe('number');
expect(await inputs[2].getType()).toBe('textarea');
expect(await inputs[3].getType()).toBe('text');
expect(await inputs[4].getType()).toBe('textarea');
+ expect(await inputs[5].getType()).toBe('text');
fixture.componentInstance.inputType = 'text';
@@ -248,6 +256,10 @@ export function runInputHarnessTests(
+
+
+
+
@@ -258,4 +270,6 @@ class InputHarnessTest {
readonly = false;
disabled = false;
required = false;
+ ngModelValue = '';
+ ngModelName = 'has-ng-model';
}
diff --git a/src/material/list/_list-theme.scss b/src/material/list/_list-theme.scss
index 6cbe89e7fe5a..c3721145362b 100644
--- a/src/material/list/_list-theme.scss
+++ b/src/material/list/_list-theme.scss
@@ -22,10 +22,11 @@
.mat-subheader {
color: theming.get-color-from-palette($foreground, secondary-text);
}
- }
- .mat-list-item-disabled {
- background-color: theming.get-color-from-palette($background, disabled-list-option);
+ .mat-list-item-disabled {
+ background-color: theming.get-color-from-palette($background, disabled-list-option);
+ color: theming.get-color-from-palette($foreground, disabled-text);
+ }
}
.mat-list-option,
diff --git a/src/material/list/list-item.html b/src/material/list/list-item.html
index 1d49a0c95949..03e57de4ac13 100644
--- a/src/material/list/list-item.html
+++ b/src/material/list/list-item.html
@@ -1,13 +1,13 @@
-
+
diff --git a/src/material/list/list.scss b/src/material/list/list.scss
index 7321cd89ad35..649e7726f824 100644
--- a/src/material/list/list.scss
+++ b/src/material/list/list.scss
@@ -66,6 +66,7 @@ $item-inset-divider-offset: 72px;
}
.mat-list-item-ripple {
+ display: block;
@include layout-common.fill;
// Disable pointer events for the ripple container because the container will overlay the
diff --git a/src/material/list/testing/list-item-harness-base.ts b/src/material/list/testing/list-item-harness-base.ts
index b600a2c96c01..fc3f71cbbba1 100644
--- a/src/material/list/testing/list-item-harness-base.ts
+++ b/src/material/list/testing/list-item-harness-base.ts
@@ -92,6 +92,11 @@ export abstract class MatListItemHarnessBase extends ContentContainerComponentHa
return !!(await this._icon());
}
+ /** Whether this list option is disabled. */
+ async isDisabled(): Promise {
+ return (await this.host()).hasClass('mat-list-item-disabled');
+ }
+
/**
* Gets a `HarnessLoader` used to get harnesses within the list item's content.
* @deprecated Use `getChildLoader(MatListItemSection.CONTENT)` or `getHarness` instead.
diff --git a/src/material/list/testing/selection-list-harness.ts b/src/material/list/testing/selection-list-harness.ts
index e60e66e98c4b..a768553af1f9 100644
--- a/src/material/list/testing/selection-list-harness.ts
+++ b/src/material/list/testing/selection-list-harness.ts
@@ -107,11 +107,6 @@ export class MatListOptionHarness extends MatListItemHarnessBase {
return (await (await this.host()).getAttribute('aria-selected')) === 'true';
}
- /** Whether the list option is disabled. */
- async isDisabled(): Promise {
- return (await (await this.host()).getAttribute('aria-disabled')) === 'true';
- }
-
/** Focuses the list option. */
async focus(): Promise {
return (await this.host()).focus();
diff --git a/src/material/list/testing/shared.spec.ts b/src/material/list/testing/shared.spec.ts
index 73a9ed4478d9..e366d07fe040 100644
--- a/src/material/list/testing/shared.spec.ts
+++ b/src/material/list/testing/shared.spec.ts
@@ -30,8 +30,9 @@ function runBaseListFunctionalityTests<
BaseListItemHarnessFilters
>,
I extends MatListItemHarnessBase,
+ F extends {disableThirdItem: boolean},
>(
- testComponent: Type<{}>,
+ testComponent: Type,
listModule: typeof MatListModule,
listHarness: ComponentHarnessConstructor & {
with: (config?: BaseHarnessFilters) => HarnessPredicate;
@@ -44,7 +45,7 @@ function runBaseListFunctionalityTests<
describe('base list functionality', () => {
let simpleListHarness: L;
let emptyListHarness: L;
- let fixture: ComponentFixture<{}>;
+ let fixture: ComponentFixture;
beforeEach(async () => {
await TestBed.configureTestingModule({
@@ -223,6 +224,14 @@ function runBaseListFunctionalityTests<
const loader = await items[1].getChildLoader(selectors.content as MatListItemSection);
await expectAsync(loader.getHarness(TestItemContentHarness)).toBeResolved();
});
+
+ it('should check disabled state of items', async () => {
+ fixture.componentInstance.disableThirdItem = true;
+ const items = await simpleListHarness.getItems();
+ expect(items.length).toBe(3);
+ expect(await items[0].isDisabled()).toBe(false);
+ expect(await items[2].isDisabled()).toBe(true);
+ });
});
}
@@ -242,7 +251,7 @@ export function runHarnessTests(
selectors: {content: string},
) {
describe('MatListHarness', () => {
- runBaseListFunctionalityTests(
+ runBaseListFunctionalityTests(
ListHarnessTest,
listModule,
listHarness,
@@ -254,7 +263,11 @@ export function runHarnessTests(
});
describe('MatActionListHarness', () => {
- runBaseListFunctionalityTests(
+ runBaseListFunctionalityTests<
+ MatActionListHarness,
+ MatActionListItemHarness,
+ ActionListHarnessTest
+ >(
ActionListHarnessTest,
listModule,
actionListHarness,
@@ -296,7 +309,7 @@ export function runHarnessTests(
});
describe('MatNavListHarness', () => {
- runBaseListFunctionalityTests(
+ runBaseListFunctionalityTests(
NavListHarnessTest,
listModule,
navListHarness,
@@ -349,7 +362,11 @@ export function runHarnessTests(
});
describe('MatSelectionListHarness', () => {
- runBaseListFunctionalityTests(
+ runBaseListFunctionalityTests<
+ MatSelectionListHarness,
+ MatListOptionHarness,
+ SelectionListHarnessTest
+ >(
SelectionListHarnessTest,
listModule,
selectionListHarness,
@@ -448,14 +465,6 @@ export function runHarnessTests(
await items[0].deselect();
expect(await items[0].isSelected()).toBe(false);
});
-
- it('should check disabled state of options', async () => {
- fixture.componentInstance.disableItem3 = true;
- const items = await harness.getItems();
- expect(items.length).toBe(3);
- expect(await items[0].isDisabled()).toBe(false);
- expect(await items[2].isDisabled()).toBe(true);
- });
});
});
}
@@ -474,7 +483,7 @@ export function runHarnessTests(
Item 2
-
+
Section 2
@@ -482,7 +491,9 @@ export function runHarnessTests(
`,
})
-class ListHarnessTest {}
+class ListHarnessTest {
+ disableThirdItem = false;
+}
@Component({
template: `
@@ -498,7 +509,10 @@ class ListHarnessTest {}
Item 2
-
+
Section 2
@@ -508,6 +522,7 @@ class ListHarnessTest {}
})
class ActionListHarnessTest {
lastClicked: string;
+ disableThirdItem = false;
}
@Component({
@@ -524,7 +539,11 @@ class ActionListHarnessTest {
Item 2
- Item 3
+ Item 3
Section 2
@@ -534,6 +553,7 @@ class ActionListHarnessTest {
})
class NavListHarnessTest {
lastClicked: string;
+ disableThirdItem = false;
onClick(event: Event, value: string) {
event.preventDefault();
@@ -555,7 +575,7 @@ class NavListHarnessTest {
Item 2
- Item 3
+ Item 3
Section 2
@@ -564,7 +584,7 @@ class NavListHarnessTest {
`,
})
class SelectionListHarnessTest {
- disableItem3 = false;
+ disableThirdItem = false;
}
class TestItemContentHarness extends ComponentHarness {
diff --git a/src/material/menu/menu-content.ts b/src/material/menu/menu-content.ts
index 1510ec0e9c55..d3f2f9753002 100644
--- a/src/material/menu/menu-content.ts
+++ b/src/material/menu/menu-content.ts
@@ -37,6 +37,20 @@ export abstract class _MatMenuContentBase implements OnDestroy {
/** Emits when the menu content has been attached. */
readonly _attached = new Subject();
+ /**
+ * @deprecated `changeDetectorRef` is now a required parameter.
+ * @breaking-change 9.0.0
+ */
+ constructor(
+ template: TemplateRef,
+ componentFactoryResolver: ComponentFactoryResolver,
+ appRef: ApplicationRef,
+ injector: Injector,
+ viewContainerRef: ViewContainerRef,
+ document: any,
+ changeDetectorRef?: ChangeDetectorRef,
+ );
+
constructor(
private _template: TemplateRef,
private _componentFactoryResolver: ComponentFactoryResolver,
@@ -80,10 +94,7 @@ export abstract class _MatMenuContentBase implements OnDestroy {
// not be updated by Angular. By explicitly marking for check here, we tell Angular that
// it needs to check for new menu items and update the `@ContentChild` in `MatMenu`.
// @breaking-change 9.0.0 Make change detector ref required
- if (this._changeDetectorRef) {
- this._changeDetectorRef.markForCheck();
- }
-
+ this._changeDetectorRef?.markForCheck();
this._portal.attach(this._outlet, context);
this._attached.next();
}
diff --git a/src/material/menu/menu-item.ts b/src/material/menu/menu-item.ts
index 63bb1ce25852..1756ffd5e38a 100644
--- a/src/material/menu/menu-item.ts
+++ b/src/material/menu/menu-item.ts
@@ -75,27 +75,28 @@ export class MatMenuItem
/** Whether the menu item acts as a trigger for a sub-menu. */
_triggersSubmenu: boolean = false;
+ /**
+ * @deprecated `document` parameter to be removed, `changeDetectorRef` and
+ * `focusMonitor` to become required.
+ * @breaking-change 12.0.0
+ */
+ constructor(
+ elementRef: ElementRef,
+ document?: any,
+ focusMonitor?: FocusMonitor,
+ parentMenu?: MatMenuPanel,
+ changeDetectorRef?: ChangeDetectorRef,
+ );
+
constructor(
private _elementRef: ElementRef,
- /**
- * @deprecated `_document` parameter is no longer being used and will be removed.
- * @breaking-change 12.0.0
- */
@Inject(DOCUMENT) _document?: any,
private _focusMonitor?: FocusMonitor,
@Inject(MAT_MENU_PANEL) @Optional() public _parentMenu?: MatMenuPanel,
- /**
- * @deprecated `_changeDetectorRef` to become a required parameter.
- * @breaking-change 14.0.0
- */
private _changeDetectorRef?: ChangeDetectorRef,
) {
- // @breaking-change 8.0.0 make `_focusMonitor` and `document` required params.
super();
-
- if (_parentMenu && _parentMenu.addItem) {
- _parentMenu.addItem(this);
- }
+ _parentMenu?.addItem?.(this);
}
/** Focuses the menu item. */
@@ -171,7 +172,7 @@ export class MatMenuItem
// We need to mark this for check for the case where the content is coming from a
// `matMenuContent` whose change detection tree is at the declaration position,
// not the insertion position. See #23175.
- // @breaking-change 14.0.0 Remove null check for `_changeDetectorRef`.
+ // @breaking-change 12.0.0 Remove null check for `_changeDetectorRef`.
this._highlighted = isHighlighted;
this._changeDetectorRef?.markForCheck();
}
diff --git a/src/material/menu/menu-trigger.ts b/src/material/menu/menu-trigger.ts
index 0456a2e79ac5..d51c42715890 100644
--- a/src/material/menu/menu-trigger.ts
+++ b/src/material/menu/menu-trigger.ts
@@ -185,6 +185,21 @@ export abstract class _MatMenuTriggerBase implements AfterContentInit, OnDestroy
// tslint:disable-next-line:no-output-on-prefix
@Output() readonly onMenuClose: EventEmitter = this.menuClosed;
+ /**
+ * @deprecated `focusMonitor` will become a required parameter.
+ * @breaking-change 8.0.0
+ */
+ constructor(
+ overlay: Overlay,
+ element: ElementRef,
+ viewContainerRef: ViewContainerRef,
+ scrollStrategy: any,
+ parentMenu: MatMenuPanel,
+ menuItemInstance: MatMenuItem,
+ dir: Directionality,
+ focusMonitor?: FocusMonitor | null,
+ );
+
constructor(
private _overlay: Overlay,
private _element: ElementRef,
@@ -195,9 +210,7 @@ export abstract class _MatMenuTriggerBase implements AfterContentInit, OnDestroy
// tslint:disable-next-line: lightweight-tokens
@Optional() @Self() private _menuItemInstance: MatMenuItem,
@Optional() private _dir: Directionality,
- // TODO(crisbeto): make the _focusMonitor required when doing breaking changes.
- // @breaking-change 8.0.0
- private _focusMonitor?: FocusMonitor,
+ private _focusMonitor: FocusMonitor | null,
) {
this._scrollStrategy = scrollStrategy;
this._parentMaterialMenu = parentMenu instanceof _MatMenuBase ? parentMenu : undefined;
diff --git a/src/material/menu/menu.spec.ts b/src/material/menu/menu.spec.ts
index 0848e63c9a29..1adb1fd42934 100644
--- a/src/material/menu/menu.spec.ts
+++ b/src/material/menu/menu.spec.ts
@@ -428,6 +428,7 @@ describe('MatMenu', () => {
const panel = overlayContainerElement.querySelector('.mat-menu-panel')!;
const event = createKeyboardEvent('keydown', ESCAPE);
+ spyOn(event, 'stopPropagation').and.callThrough();
dispatchEvent(panel, event);
fixture.detectChanges();
@@ -435,6 +436,7 @@ describe('MatMenu', () => {
expect(overlayContainerElement.textContent).toBe('');
expect(event.defaultPrevented).toBe(true);
+ expect(event.stopPropagation).toHaveBeenCalled();
}));
it('should not close the menu when pressing ESCAPE with a modifier', fakeAsync(() => {
@@ -2051,7 +2053,7 @@ describe('MatMenu', () => {
.toBe(true);
}));
- it('should restore focus to a nested trigger when navgating via the keyboard', fakeAsync(() => {
+ it('should restore focus to a nested trigger when navigating via the keyboard', fakeAsync(() => {
compileTestComponent();
instance.rootTriggerEl.nativeElement.click();
fixture.detectChanges();
diff --git a/src/material/menu/menu.ts b/src/material/menu/menu.ts
index c7718eddfa1d..91fb0eff4c01 100644
--- a/src/material/menu/menu.ts
+++ b/src/material/menu/menu.ts
@@ -357,7 +357,12 @@ export class _MatMenuBase
}
manager.onKeydown(event);
+ return;
}
+
+ // Don't allow the event to propagate if we've already handled it, or it may
+ // end up reaching other overlays that were opened earlier (see #22694).
+ event.stopPropagation();
}
/**
diff --git a/src/material/paginator/paginator.spec.ts b/src/material/paginator/paginator.spec.ts
index 84bdb4e64b68..540c38187cd7 100644
--- a/src/material/paginator/paginator.spec.ts
+++ b/src/material/paginator/paginator.spec.ts
@@ -506,6 +506,13 @@ describe('MatPaginator', () => {
const hostElement = fixture.nativeElement.querySelector('mat-paginator');
expect(hostElement.getAttribute('role')).toBe('group');
});
+
+ it('should handle the page size options input being passed in as readonly array', () => {
+ const fixture = createComponent(MatPaginatorWithReadonlyOptions);
+ fixture.detectChanges();
+
+ expect(fixture.componentInstance.paginator._displayedPageSizeOptions).toEqual([5, 10, 25, 100]);
+ });
});
function getPreviousButton(fixture: ComponentFixture) {
@@ -595,3 +602,14 @@ class MatPaginatorWithoutOptionsApp {
class MatPaginatorWithStringValues {
@ViewChild(MatPaginator) paginator: MatPaginator;
}
+
+@Component({
+ template: `
+
+
+ `,
+})
+class MatPaginatorWithReadonlyOptions {
+ @ViewChild(MatPaginator) paginator: MatPaginator;
+ pageSizeOptions: readonly number[] = [5, 10, 25, 100];
+}
diff --git a/src/material/paginator/paginator.ts b/src/material/paginator/paginator.ts
index 894fcbb6ee37..a676f8660519 100644
--- a/src/material/paginator/paginator.ts
+++ b/src/material/paginator/paginator.ts
@@ -149,7 +149,7 @@ export abstract class _MatPaginatorBase<
get pageSizeOptions(): number[] {
return this._pageSizeOptions;
}
- set pageSizeOptions(value: number[]) {
+ set pageSizeOptions(value: number[] | readonly number[]) {
this._pageSizeOptions = (value || []).map(p => coerceNumberProperty(p));
this._updateDisplayedPageSizeOptions();
}
diff --git a/src/material/progress-bar/progress-bar.md b/src/material/progress-bar/progress-bar.md
index 3a909d265521..0516a1192add 100644
--- a/src/material/progress-bar/progress-bar.md
+++ b/src/material/progress-bar/progress-bar.md
@@ -45,6 +45,6 @@ use the theme's primary color. This can be changed to `'accent'` or `'warn'`.
`MatProgressBar` implements the ARIA `role="progressbar"` pattern. By default, the progress bar
sets `aria-valuemin` to `0` and `aria-valuemax` to `100`. Avoid changing these values, as this may
-cause incompatiblity with some assitive technology.
+cause incompatiblity with some assistive technology.
Always provide an accessible label via `aria-label` or `aria-labelledby` for each progress bar.
diff --git a/src/material/progress-bar/progress-bar.spec.ts b/src/material/progress-bar/progress-bar.spec.ts
index c80cb1120613..a2a0b0844d65 100644
--- a/src/material/progress-bar/progress-bar.spec.ts
+++ b/src/material/progress-bar/progress-bar.spec.ts
@@ -218,6 +218,41 @@ describe('MatProgressBar', () => {
expect(progressElement.componentInstance.mode).toBe('buffer');
expect(progressElement.componentInstance.color).toBe('warn');
});
+
+ it('should update the DOM transform when the value has changed', () => {
+ const fixture = createComponent(BasicProgressBar);
+ fixture.detectChanges();
+
+ const progressElement = fixture.debugElement.query(By.css('mat-progress-bar'))!;
+ const progressComponent = progressElement.componentInstance;
+ const primaryBar = progressElement.nativeElement.querySelector('.mat-progress-bar-primary');
+
+ expect(primaryBar.style.transform).toBe('scale3d(0, 1, 1)');
+
+ progressComponent.value = 40;
+ fixture.detectChanges();
+
+ expect(primaryBar.style.transform).toBe('scale3d(0.4, 1, 1)');
+ });
+
+ it('should update the DOM transform when the bufferValue has changed', () => {
+ const fixture = createComponent(BasicProgressBar);
+ fixture.detectChanges();
+
+ const progressElement = fixture.debugElement.query(By.css('mat-progress-bar'))!;
+ const progressComponent = progressElement.componentInstance;
+ const bufferBar = progressElement.nativeElement.querySelector('.mat-progress-bar-buffer');
+
+ progressComponent.mode = 'buffer';
+ fixture.detectChanges();
+
+ expect(bufferBar.style.transform).toBeFalsy();
+
+ progressComponent.bufferValue = 40;
+ fixture.detectChanges();
+
+ expect(bufferBar.style.transform).toBe('scale3d(0.4, 1, 1)');
+ });
});
describe('animation trigger on determinate setting', () => {
diff --git a/src/material/progress-bar/progress-bar.ts b/src/material/progress-bar/progress-bar.ts
index 3614ac0576a9..c35b625ab09f 100644
--- a/src/material/progress-bar/progress-bar.ts
+++ b/src/material/progress-bar/progress-bar.ts
@@ -23,6 +23,7 @@ import {
Output,
ViewChild,
ViewEncapsulation,
+ ChangeDetectorRef,
} from '@angular/core';
import {CanColor, mixinColor, ThemePalette} from '@angular/material/core';
import {ANIMATION_MODULE_TYPE} from '@angular/platform-browser/animations';
@@ -135,16 +136,20 @@ export class MatProgressBar
@Optional()
@Inject(MAT_PROGRESS_BAR_DEFAULT_OPTIONS)
defaults?: MatProgressBarDefaultOptions,
+ /**
+ * @deprecated `_changeDetectorRef` parameter to be made required.
+ * @breaking-change 11.0.0
+ */
+ private _changeDetectorRef?: ChangeDetectorRef,
) {
super(elementRef);
// We need to prefix the SVG reference with the current path, otherwise they won't work
// in Safari if the page has a `` tag. Note that we need quotes inside the `url()`,
-
- // because named route URLs can contain parentheses (see #12338). Also we don't use since
- // we can't tell the difference between whether
- // the consumer is using the hash location strategy or not, because `Location` normalizes
- // both `/#/foo/bar` and `/foo/bar` to the same thing.
+ // because named route URLs can contain parentheses (see #12338). Also we don't use `Location`
+ // since we can't tell the difference between whether the consumer is using the hash location
+ // strategy or not, because `Location` normalizes both `/#/foo/bar` and `/foo/bar` to
+ // the same thing.
const path = location ? location.getPathname().split('#')[0] : '';
this._rectangleFillValue = `url('${path}#${this.progressbarId}')`;
this._isNoopAnimation = _animationMode === 'NoopAnimations';
@@ -168,6 +173,9 @@ export class MatProgressBar
}
set value(v: NumberInput) {
this._value = clamp(coerceNumberProperty(v) || 0);
+
+ // @breaking-change 11.0.0 Remove null check for _changeDetectorRef.
+ this._changeDetectorRef?.markForCheck();
}
private _value: number = 0;
@@ -178,6 +186,9 @@ export class MatProgressBar
}
set bufferValue(v: number) {
this._bufferValue = clamp(v || 0);
+
+ // @breaking-change 11.0.0 Remove null check for _changeDetectorRef.
+ this._changeDetectorRef?.markForCheck();
}
private _bufferValue: number = 0;
diff --git a/src/material/progress-spinner/BUILD.bazel b/src/material/progress-spinner/BUILD.bazel
index c1535578c6e0..2de0e6ce53c4 100644
--- a/src/material/progress-spinner/BUILD.bazel
+++ b/src/material/progress-spinner/BUILD.bazel
@@ -22,6 +22,7 @@ ng_module(
deps = [
"//src/cdk/coercion",
"//src/cdk/platform",
+ "//src/cdk/scrolling",
"//src/material/core",
"@npm//@angular/animations",
"@npm//@angular/common",
diff --git a/src/material/progress-spinner/progress-spinner.html b/src/material/progress-spinner/progress-spinner.html
index 29c093050abd..9156f5d44a4c 100644
--- a/src/material/progress-spinner/progress-spinner.html
+++ b/src/material/progress-spinner/progress-spinner.html
@@ -14,7 +14,8 @@
preserveAspectRatio="xMidYMid meet"
focusable="false"
[ngSwitch]="mode === 'indeterminate'"
- aria-hidden="true">
+ aria-hidden="true"
+ #svg>
diff --git a/src/material/slide-toggle/slide-toggle.scss b/src/material/slide-toggle/slide-toggle.scss
index 5a68bf0df822..f84cf16a2629 100644
--- a/src/material/slide-toggle/slide-toggle.scss
+++ b/src/material/slide-toggle/slide-toggle.scss
@@ -121,6 +121,7 @@ $bar-track-width: $bar-width - $thumb-size;
height: $thumb-size;
width: $thumb-size;
border-radius: 50%;
+ display: block;
}
// Horizontal bar for the slide-toggle.
diff --git a/src/material/slider/_slider-theme.scss b/src/material/slider/_slider-theme.scss
index a916b5d89516..745564eb7fd2 100644
--- a/src/material/slider/_slider-theme.scss
+++ b/src/material/slider/_slider-theme.scss
@@ -52,16 +52,18 @@
background-color: $mat-slider-off-color;
}
- .mat-primary {
- @include _inner-content-theme($primary);
- }
+ .mat-slider {
+ &.mat-primary {
+ @include _inner-content-theme($primary);
+ }
- .mat-accent {
- @include _inner-content-theme($accent);
- }
+ &.mat-accent {
+ @include _inner-content-theme($accent);
+ }
- .mat-warn {
- @include _inner-content-theme($warn);
+ &.mat-warn {
+ @include _inner-content-theme($warn);
+ }
}
.mat-slider:hover,
@@ -71,7 +73,7 @@
}
}
- .mat-slider-disabled {
+ .mat-slider.mat-slider-disabled {
.mat-slider-track-background,
.mat-slider-track-fill,
.mat-slider-thumb {
@@ -85,7 +87,7 @@
}
}
- .mat-slider-min-value {
+ .mat-slider.mat-slider-min-value {
.mat-slider-focus-ring {
$opacity: 0.12;
$color: theming.get-color-from-palette($foreground, base, $opacity);
diff --git a/src/material/slider/slider.spec.ts b/src/material/slider/slider.spec.ts
index a9acb3be6232..04e92cd4fbb8 100644
--- a/src/material/slider/slider.spec.ts
+++ b/src/material/slider/slider.spec.ts
@@ -18,7 +18,8 @@ import {
dispatchKeyboardEvent,
dispatchMouseEvent,
createKeyboardEvent,
-} from '../../cdk/testing/private';
+ createTouchEvent,
+} from '@angular/cdk/testing/private';
import {Component, DebugElement, Type, ViewChild} from '@angular/core';
import {ComponentFixture, fakeAsync, flush, TestBed} from '@angular/core/testing';
import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms';
@@ -242,6 +243,17 @@ describe('MatSlider', () => {
it('should have a focus indicator', () => {
expect(sliderNativeElement.classList.contains('mat-focus-indicator')).toBe(true);
});
+
+ it('should not try to preventDefault on a non-cancelable event', () => {
+ const event = createTouchEvent('touchstart');
+ const spy = spyOn(event, 'preventDefault');
+ Object.defineProperty(event, 'cancelable', {value: false});
+
+ dispatchEvent(sliderNativeElement, event);
+ fixture.detectChanges();
+
+ expect(spy).not.toHaveBeenCalled();
+ });
});
describe('disabled slider', () => {
@@ -438,6 +450,26 @@ describe('MatSlider', () => {
expect(sliderInstance.percent).toBe(1);
},
);
+ it('should properly update ticks when max value changed to 0', () => {
+ testComponent.min = 0;
+ testComponent.max = 100;
+ fixture.detectChanges();
+
+ dispatchMouseenterEvent(sliderNativeElement);
+ fixture.detectChanges();
+
+ expect(ticksElement.style.backgroundSize).toBe('6% 2px');
+ expect(ticksElement.style.transform).toContain('translateX(3%)');
+
+ testComponent.max = 0;
+ fixture.detectChanges();
+
+ dispatchMouseenterEvent(sliderNativeElement);
+ fixture.detectChanges();
+
+ expect(ticksElement.style.backgroundSize).toBe('0% 2px');
+ expect(ticksElement.style.transform).toContain('translateX(0%)');
+ });
});
describe('slider with set value', () => {
@@ -962,6 +994,7 @@ describe('MatSlider', () => {
let sliderNativeElement: HTMLElement;
let testComponent: SliderWithChangeHandler;
let sliderInstance: MatSlider;
+ let trackFillElement: HTMLElement;
beforeEach(() => {
fixture = createComponent(SliderWithChangeHandler);
@@ -974,6 +1007,7 @@ describe('MatSlider', () => {
sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider))!;
sliderNativeElement = sliderDebugElement.nativeElement;
sliderInstance = sliderDebugElement.injector.get(MatSlider);
+ trackFillElement = sliderNativeElement.querySelector('.mat-slider-track-fill') as HTMLElement;
});
it('should increment slider by 1 on up arrow pressed', () => {
@@ -1028,6 +1062,21 @@ describe('MatSlider', () => {
expect(sliderInstance.value).toBe(99);
});
+ it('should decrement from max when interacting after out-of-bounds value is assigned', () => {
+ sliderInstance.max = 100;
+ sliderInstance.value = 200;
+ fixture.detectChanges();
+
+ expect(sliderInstance.value).toBe(200);
+ expect(trackFillElement.style.transform).toContain('scale3d(1, 1, 1)');
+
+ dispatchKeyboardEvent(sliderNativeElement, 'keydown', LEFT_ARROW);
+ fixture.detectChanges();
+
+ expect(sliderInstance.value).toBe(99);
+ expect(trackFillElement.style.transform).toContain('scale3d(0.99, 1, 1)');
+ });
+
it('should increment slider by 10 on page up pressed', () => {
expect(testComponent.onChange).not.toHaveBeenCalled();
diff --git a/src/material/slider/slider.ts b/src/material/slider/slider.ts
index 4fb7650df091..019043801915 100644
--- a/src/material/slider/slider.ts
+++ b/src/material/slider/slider.ts
@@ -672,7 +672,6 @@ export class MatSlider
const oldValue = this.value;
this._isSliding = 'pointer';
this._lastPointerEvent = event;
- event.preventDefault();
this._focusHostElement();
this._onMouseenter(); // Simulate mouseenter in case this is a mobile device.
this._bindGlobalEvents(event);
@@ -680,6 +679,13 @@ export class MatSlider
this._updateValueFromPosition(pointerPosition);
this._valueOnSlideStart = oldValue;
+ // Despite the fact that we explicitly bind active events, in some cases the browser
+ // still dispatches non-cancelable events which cause this call to throw an error.
+ // There doesn't appear to be a good way of avoiding them. See #23820.
+ if (event.cancelable) {
+ event.preventDefault();
+ }
+
// Emit a change and input event if the value changed.
if (oldValue != this.value) {
this._emitInputEvent();
@@ -793,7 +799,10 @@ export class MatSlider
/** Increments the slider by the given number of steps (negative number decrements). */
private _increment(numSteps: number) {
- this.value = this._clamp((this.value || 0) + this.step * numSteps, this.min, this.max);
+ // Pre-clamp the current value since it's allowed to be
+ // out of bounds when assigned programmatically.
+ const clampedValue = this._clamp(this.value || 0, this.min, this.max);
+ this.value = this._clamp(clampedValue + this.step * numSteps, this.min, this.max);
}
/** Calculate the new value from the new physical location. The value will always be snapped. */
@@ -851,15 +860,17 @@ export class MatSlider
return;
}
+ let tickIntervalPercent: number;
if (this.tickInterval == 'auto') {
let trackSize = this.vertical ? this._sliderDimensions.height : this._sliderDimensions.width;
let pixelsPerStep = (trackSize * this.step) / (this.max - this.min);
let stepsPerTick = Math.ceil(MIN_AUTO_TICK_SEPARATION / pixelsPerStep);
let pixelsPerTick = stepsPerTick * this.step;
- this._tickIntervalPercent = pixelsPerTick / trackSize;
+ tickIntervalPercent = pixelsPerTick / trackSize;
} else {
- this._tickIntervalPercent = (this.tickInterval * this.step) / (this.max - this.min);
+ tickIntervalPercent = (this.tickInterval * this.step) / (this.max - this.min);
}
+ this._tickIntervalPercent = isSafeNumber(tickIntervalPercent) ? tickIntervalPercent : 0;
}
/** Creates a slider change object from the specified value. */
@@ -874,7 +885,8 @@ export class MatSlider
/** Calculates the percentage of the slider that a value is. */
private _calculatePercentage(value: number | null) {
- return ((value || 0) - this.min) / (this.max - this.min);
+ const percentage = ((value || 0) - this.min) / (this.max - this.min);
+ return isSafeNumber(percentage) ? percentage : 0;
}
/** Calculates the value a percentage of the slider corresponds to. */
@@ -945,6 +957,11 @@ export class MatSlider
}
}
+/** Checks if number is safe for calculation */
+function isSafeNumber(value: number) {
+ return !isNaN(value) && isFinite(value);
+}
+
/** Returns whether an event is a touch event. */
function isTouchEvent(event: MouseEvent | TouchEvent): event is TouchEvent {
// This function is called for every pixel that the user has dragged so we need it to be
diff --git a/src/material/snack-bar/simple-snack-bar.html b/src/material/snack-bar/simple-snack-bar.html
index a581b91d6dd4..c0326acdba11 100644
--- a/src/material/snack-bar/simple-snack-bar.html
+++ b/src/material/snack-bar/simple-snack-bar.html
@@ -1,4 +1,4 @@
-{{data.message}}
+{{data.message}}
diff --git a/src/material/snack-bar/simple-snack-bar.scss b/src/material/snack-bar/simple-snack-bar.scss
index 50188497237a..977dc8556ae2 100644
--- a/src/material/snack-bar/simple-snack-bar.scss
+++ b/src/material/snack-bar/simple-snack-bar.scss
@@ -30,3 +30,8 @@ $button-vertical-margin: -(math.div($button-height - $line-height, 2));
margin-right: $button-horizontal-margin;
}
}
+
+.mat-simple-snack-bar-content {
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
diff --git a/src/material/snack-bar/snack-bar.ts b/src/material/snack-bar/snack-bar.ts
index 0ba152441b13..b9ac058fa914 100644
--- a/src/material/snack-bar/snack-bar.ts
+++ b/src/material/snack-bar/snack-bar.ts
@@ -44,11 +44,8 @@ export function MAT_SNACK_BAR_DEFAULT_OPTIONS_FACTORY(): MatSnackBarConfig {
return new MatSnackBarConfig();
}
-/**
- * Service to dispatch Material Design snack bar messages.
- */
-@Injectable({providedIn: MatSnackBarModule})
-export class MatSnackBar implements OnDestroy {
+@Injectable()
+export abstract class _MatSnackBarBase implements OnDestroy {
/**
* Reference to the current snack bar in the view *at this level* (in the Angular injector tree).
* If there is a parent snack-bar service, all operations should delegate to that parent
@@ -57,13 +54,13 @@ export class MatSnackBar implements OnDestroy {
private _snackBarRefAtThisLevel: MatSnackBarRef | null = null;
/** The component that should be rendered as the snack bar's simple component. */
- protected simpleSnackBarComponent: Type = SimpleSnackBar;
+ protected abstract simpleSnackBarComponent: Type;
/** The container component that attaches the provided template or component. */
- protected snackBarContainerComponent: Type<_SnackBarContainer> = MatSnackBarContainer;
+ protected abstract snackBarContainerComponent: Type<_SnackBarContainer>;
/** The CSS class to apply for handset mode. */
- protected handsetCssClass = 'mat-snack-bar-handset';
+ protected abstract handsetCssClass: string;
/** Reference to the currently opened snackbar at *any* level. */
get _openedSnackBarRef(): MatSnackBarRef | null {
@@ -84,7 +81,7 @@ export class MatSnackBar implements OnDestroy {
private _live: LiveAnnouncer,
private _injector: Injector,
private _breakpointObserver: BreakpointObserver,
- @Optional() @SkipSelf() private _parentSnackBar: MatSnackBar,
+ @Optional() @SkipSelf() private _parentSnackBar: _MatSnackBarBase,
@Inject(MAT_SNACK_BAR_DEFAULT_OPTIONS) private _defaultConfig: MatSnackBarConfig,
) {}
@@ -311,3 +308,24 @@ export class MatSnackBar implements OnDestroy {
});
}
}
+
+/**
+ * Service to dispatch Material Design snack bar messages.
+ */
+@Injectable({providedIn: MatSnackBarModule})
+export class MatSnackBar extends _MatSnackBarBase {
+ protected simpleSnackBarComponent = SimpleSnackBar;
+ protected snackBarContainerComponent = MatSnackBarContainer;
+ protected handsetCssClass = 'mat-snack-bar-handset';
+
+ constructor(
+ overlay: Overlay,
+ live: LiveAnnouncer,
+ injector: Injector,
+ breakpointObserver: BreakpointObserver,
+ @Optional() @SkipSelf() parentSnackBar: MatSnackBar,
+ @Inject(MAT_SNACK_BAR_DEFAULT_OPTIONS) defaultConfig: MatSnackBarConfig,
+ ) {
+ super(overlay, live, injector, breakpointObserver, parentSnackBar, defaultConfig);
+ }
+}
diff --git a/src/material/table/table-data-source.ts b/src/material/table/table-data-source.ts
index 22d01b86e661..119968bab0ac 100644
--- a/src/material/table/table-data-source.ts
+++ b/src/material/table/table-data-source.ts
@@ -85,6 +85,7 @@ export class _MatTableDataSource<
return this._data.value;
}
set data(data: T[]) {
+ data = Array.isArray(data) ? data : [];
this._data.next(data);
// Normally the `filteredData` is updated by the re-render
// subscription, but that won't happen if it's inactive.
diff --git a/src/material/table/table.spec.ts b/src/material/table/table.spec.ts
index e47337e73a68..0b5d0cb14069 100644
--- a/src/material/table/table.spec.ts
+++ b/src/material/table/table.spec.ts
@@ -576,6 +576,45 @@ describe('MatTable', () => {
['Footer A', 'Footer B', 'Footer C'],
]);
});
+
+ it('should fall back to empty table if invalid data is passed in', () => {
+ component.underlyingDataSource.addData();
+ fixture.detectChanges();
+ expectTableToMatchContent(tableElement, [
+ ['Column A', 'Column B', 'Column C'],
+ ['a_1', 'b_1', 'c_1'],
+ ['a_2', 'b_2', 'c_2'],
+ ['a_3', 'b_3', 'c_3'],
+ ['a_4', 'b_4', 'c_4'],
+ ['Footer A', 'Footer B', 'Footer C'],
+ ]);
+
+ dataSource.data = null!;
+ fixture.detectChanges();
+ expectTableToMatchContent(tableElement, [
+ ['Column A', 'Column B', 'Column C'],
+ ['Footer A', 'Footer B', 'Footer C'],
+ ]);
+
+ component.underlyingDataSource.addData();
+ fixture.detectChanges();
+ expectTableToMatchContent(tableElement, [
+ ['Column A', 'Column B', 'Column C'],
+ ['a_1', 'b_1', 'c_1'],
+ ['a_2', 'b_2', 'c_2'],
+ ['a_3', 'b_3', 'c_3'],
+ ['a_4', 'b_4', 'c_4'],
+ ['a_5', 'b_5', 'c_5'],
+ ['Footer A', 'Footer B', 'Footer C'],
+ ]);
+
+ dataSource.data = {} as any;
+ fixture.detectChanges();
+ expectTableToMatchContent(tableElement, [
+ ['Column A', 'Column B', 'Column C'],
+ ['Footer A', 'Footer B', 'Footer C'],
+ ]);
+ });
});
});
diff --git a/src/material/tabs/tab-body.scss b/src/material/tabs/tab-body.scss
index a92aaf492e20..ff6a49468662 100644
--- a/src/material/tabs/tab-body.scss
+++ b/src/material/tabs/tab-body.scss
@@ -5,4 +5,15 @@
.mat-tab-group-dynamic-height & {
overflow: hidden;
}
+
+ // Usually the `visibility: hidden` added by the animation is enough to prevent focus from
+ // entering the collapsed content, but children with their own `visibility` can override it.
+ // This is a fallback that completely hides the content when the element becomes hidden.
+ // Note that we can't do this in the animation definition, because the style gets recomputed too
+ // late, breaking the animation because Angular didn't have time to figure out the target height.
+ // This can also be achieved with JS, but it has issues when when starting an animation before
+ // the previous one has finished.
+ &[style*='visibility: hidden'] {
+ display: none;
+ }
}
diff --git a/src/material/tabs/tab-body.ts b/src/material/tabs/tab-body.ts
index 731f7380b22e..36b2129f11a4 100644
--- a/src/material/tabs/tab-body.ts
+++ b/src/material/tabs/tab-body.ts
@@ -93,7 +93,9 @@ export class MatTabBodyPortal extends CdkPortalOutlet implements OnInit, OnDestr
});
this._leavingSub = this._host._afterLeavingCenter.subscribe(() => {
- this.detach();
+ if (!this._host.preserveContent) {
+ this.detach();
+ }
});
}
@@ -149,6 +151,9 @@ export abstract class _MatTabBodyBase implements OnInit, OnDestroy {
/** Duration for the tab's animation. */
@Input() animationDuration: string = '500ms';
+ /** Whether the tab's content should be kept in the DOM while it's off-screen. */
+ @Input() preserveContent: boolean = false;
+
/** The shifted index position of the tab body, where zero represents the active center tab. */
@Input()
set position(position: number) {
diff --git a/src/material/tabs/tab-config.ts b/src/material/tabs/tab-config.ts
index 5c6711aa2c40..f974e1d480e9 100644
--- a/src/material/tabs/tab-config.ts
+++ b/src/material/tabs/tab-config.ts
@@ -29,6 +29,13 @@ export interface MatTabsConfig {
/** `tabindex` to be set on the inner element that wraps the tab content. */
contentTabIndex?: number;
+
+ /**
+ * By default tabs remove their content from the DOM while it's off-screen.
+ * Setting this to `true` will keep it in the DOM which will prevent elements
+ * like iframes and videos from reloading next time it comes back into the view.
+ */
+ preserveContent?: boolean;
}
/** Injection token that can be used to provide the default options the tabs module. */
diff --git a/src/material/tabs/tab-group.html b/src/material/tabs/tab-group.html
index 268ed3076d1e..c39e3528f2a0 100644
--- a/src/material/tabs/tab-group.html
+++ b/src/material/tabs/tab-group.html
@@ -4,17 +4,19 @@
[disablePagination]="disablePagination"
(indexFocused)="_focusChanged($event)"
(selectFocusedIndex)="selectedIndex = $event">
-
-
+
- {{tab.textLabel}}
+ {{tab.textLabel}}
@@ -43,10 +45,12 @@
[attr.tabindex]="(contentTabIndex != null && selectedIndex === i) ? contentTabIndex : null"
[attr.aria-labelledby]="_getTabLabelId(i)"
[class.mat-tab-body-active]="selectedIndex === i"
+ [ngClass]="tab.bodyClass"
[content]="tab.content!"
[position]="tab.position!"
[origin]="tab.origin"
[animationDuration]="animationDuration"
+ [preserveContent]="preserveContent"
(_onCentered)="_removeTabBodyWrapperHeight()"
(_onCentering)="_setTabBodyWrapperHeight($event)">
diff --git a/src/material/tabs/tab-group.spec.ts b/src/material/tabs/tab-group.spec.ts
index d91b51ca43cf..1be121f13985 100644
--- a/src/material/tabs/tab-group.spec.ts
+++ b/src/material/tabs/tab-group.spec.ts
@@ -1,6 +1,6 @@
import {LEFT_ARROW} from '@angular/cdk/keycodes';
import {dispatchFakeEvent, dispatchKeyboardEvent} from '../../cdk/testing/private';
-import {Component, OnInit, QueryList, ViewChild, ViewChildren} from '@angular/core';
+import {Component, DebugElement, OnInit, QueryList, ViewChild, ViewChildren} from '@angular/core';
import {
waitForAsync,
ComponentFixture,
@@ -40,6 +40,7 @@ describe('MatTabGroup', () => {
TabGroupWithIndirectDescendantTabs,
TabGroupWithSpaceAbove,
NestedTabGroupWithLabel,
+ TabsWithClassesTestApp,
],
});
@@ -419,11 +420,16 @@ describe('MatTabGroup', () => {
expect(tab.getAttribute('aria-label')).toBe('Fruit');
expect(tab.hasAttribute('aria-labelledby')).toBe(false);
+
+ fixture.componentInstance.ariaLabel = 'Veggie';
+ fixture.detectChanges();
+ expect(tab.getAttribute('aria-label')).toBe('Veggie');
});
});
describe('disable tabs', () => {
let fixture: ComponentFixture;
+
beforeEach(() => {
fixture = TestBed.createComponent(DisabledTabsTestApp);
});
@@ -659,6 +665,56 @@ describe('MatTabGroup', () => {
expect(tabGroupNode.classList).toContain('mat-tab-group-inverted-header');
});
+
+ it('should be able to opt into keeping the inactive tab content in the DOM', fakeAsync(() => {
+ fixture.componentInstance.preserveContent = true;
+ fixture.detectChanges();
+
+ expect(fixture.nativeElement.textContent).toContain('Pizza, fries');
+ expect(fixture.nativeElement.textContent).not.toContain('Peanuts');
+
+ tabGroup.selectedIndex = 3;
+ fixture.detectChanges();
+ tick();
+
+ expect(fixture.nativeElement.textContent).toContain('Pizza, fries');
+ expect(fixture.nativeElement.textContent).toContain('Peanuts');
+ }));
+
+ it('should visibly hide the content of inactive tabs', fakeAsync(() => {
+ const contentElements: HTMLElement[] = Array.from(
+ fixture.nativeElement.querySelectorAll('.mat-tab-body-content'),
+ );
+
+ expect(contentElements.map(element => element.style.visibility)).toEqual([
+ '',
+ 'hidden',
+ 'hidden',
+ 'hidden',
+ ]);
+
+ tabGroup.selectedIndex = 2;
+ fixture.detectChanges();
+ tick();
+
+ expect(contentElements.map(element => element.style.visibility)).toEqual([
+ 'hidden',
+ 'hidden',
+ '',
+ 'hidden',
+ ]);
+
+ tabGroup.selectedIndex = 1;
+ fixture.detectChanges();
+ tick();
+
+ expect(contentElements.map(element => element.style.visibility)).toEqual([
+ 'hidden',
+ '',
+ 'hidden',
+ 'hidden',
+ ]);
+ }));
});
describe('lazy loaded tabs', () => {
@@ -779,6 +835,62 @@ describe('MatTabGroup', () => {
}));
});
+ describe('tabs with custom css classes', () => {
+ let fixture: ComponentFixture;
+ let labelElements: DebugElement[];
+ let bodyElements: DebugElement[];
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(TabsWithClassesTestApp);
+ fixture.detectChanges();
+ labelElements = fixture.debugElement.queryAll(By.css('.mat-tab-label'));
+ bodyElements = fixture.debugElement.queryAll(By.css('mat-tab-body'));
+ });
+
+ it('should apply label/body classes', () => {
+ expect(labelElements[1].nativeElement.classList).toContain('hardcoded-label-class');
+ expect(bodyElements[1].nativeElement.classList).toContain('hardcoded-body-class');
+ });
+
+ it('should set classes as strings dynamically', () => {
+ expect(labelElements[0].nativeElement.classList).not.toContain('custom-label-class');
+ expect(bodyElements[0].nativeElement.classList).not.toContain('custom-body-class');
+
+ fixture.componentInstance.labelClassList = 'custom-label-class';
+ fixture.componentInstance.bodyClassList = 'custom-body-class';
+ fixture.detectChanges();
+
+ expect(labelElements[0].nativeElement.classList).toContain('custom-label-class');
+ expect(bodyElements[0].nativeElement.classList).toContain('custom-body-class');
+
+ delete fixture.componentInstance.labelClassList;
+ delete fixture.componentInstance.bodyClassList;
+ fixture.detectChanges();
+
+ expect(labelElements[0].nativeElement.classList).not.toContain('custom-label-class');
+ expect(bodyElements[0].nativeElement.classList).not.toContain('custom-body-class');
+ });
+
+ it('should set classes as strings array dynamically', () => {
+ expect(labelElements[0].nativeElement.classList).not.toContain('custom-label-class');
+ expect(bodyElements[0].nativeElement.classList).not.toContain('custom-body-class');
+
+ fixture.componentInstance.labelClassList = ['custom-label-class'];
+ fixture.componentInstance.bodyClassList = ['custom-body-class'];
+ fixture.detectChanges();
+
+ expect(labelElements[0].nativeElement.classList).toContain('custom-label-class');
+ expect(bodyElements[0].nativeElement.classList).toContain('custom-body-class');
+
+ delete fixture.componentInstance.labelClassList;
+ delete fixture.componentInstance.bodyClassList;
+ fixture.detectChanges();
+
+ expect(labelElements[0].nativeElement.classList).not.toContain('custom-label-class');
+ expect(bodyElements[0].nativeElement.classList).not.toContain('custom-body-class');
+ });
+ });
+
/**
* Checks that the `selectedIndex` has been updated; checks that the label and body have their
* respective `active` classes
@@ -960,7 +1072,6 @@ class BindedTabsTestApp {
}
@Component({
- selector: 'test-app',
template: `
@@ -1011,7 +1122,7 @@ class AsyncTabsTestApp implements OnInit {
@Component({
template: `
-
+
Pizza, fries
Broccoli, spinach
{{otherContent}}
@@ -1020,13 +1131,13 @@ class AsyncTabsTestApp implements OnInit {
`,
})
class TabGroupWithSimpleApi {
+ preserveContent = false;
otherLabel = 'Fruit';
otherContent = 'Apples, grapes';
@ViewChild('legumes') legumes: any;
}
@Component({
- selector: 'nested-tabs',
template: `
Tab one content
@@ -1045,7 +1156,6 @@ class NestedTabs {
}
@Component({
- selector: 'template-tabs',
template: `
@@ -1149,3 +1259,21 @@ class TabGroupWithSpaceAbove {
`,
})
class NestedTabGroupWithLabel {}
+
+@Component({
+ template: `
+
+
+ Tab one content
+
+
+ Tab two content
+
+
+ `,
+})
+class TabsWithClassesTestApp {
+ labelClassList?: string | string[];
+ bodyClassList?: string | string[];
+}
diff --git a/src/material/tabs/tab-group.ts b/src/material/tabs/tab-group.ts
index 318ae407779f..11b077499bc8 100644
--- a/src/material/tabs/tab-group.ts
+++ b/src/material/tabs/tab-group.ts
@@ -71,7 +71,8 @@ const _MatTabGroupMixinBase = mixinColor(
);
interface MatTabGroupBaseHeader {
- _alignInkBarToSelectedTab: () => void;
+ _alignInkBarToSelectedTab(): void;
+ updatePagination(): void;
focusIndex: number;
}
@@ -162,6 +163,14 @@ export abstract class _MatTabGroupBase
@Input()
disablePagination: boolean;
+ /**
+ * By default tabs remove their content from the DOM while it's off-screen.
+ * Setting this to `true` will keep it in the DOM which will prevent elements
+ * like iframes and videos from reloading next time it comes back into the view.
+ */
+ @Input()
+ preserveContent: boolean;
+
/** Background color of the tab group. */
@Input()
get backgroundColor(): ThemePalette {
@@ -213,6 +222,7 @@ export abstract class _MatTabGroupBase
this.dynamicHeight =
defaultConfig && defaultConfig.dynamicHeight != null ? defaultConfig.dynamicHeight : false;
this.contentTabIndex = defaultConfig?.contentTabIndex ?? null;
+ this.preserveContent = !!defaultConfig?.preserveContent;
}
/**
@@ -327,6 +337,19 @@ export abstract class _MatTabGroupBase
}
}
+ /**
+ * Recalculates the tab group's pagination dimensions.
+ *
+ * WARNING: Calling this method can be very costly in terms of performance. It should be called
+ * as infrequently as possible from outside of the Tabs component as it causes a reflow of the
+ * page.
+ */
+ updatePagination() {
+ if (this._tabHeader) {
+ this._tabHeader.updatePagination();
+ }
+ }
+
/**
* Sets focus to a particular tab.
* @param index Index of the tab to be focused.
diff --git a/src/material/tabs/tab.ts b/src/material/tabs/tab.ts
index 5378b7f898d6..b58e5e61f893 100644
--- a/src/material/tabs/tab.ts
+++ b/src/material/tabs/tab.ts
@@ -81,6 +81,18 @@ export class MatTab extends _MatTabBase implements OnInit, CanDisable, OnChanges
*/
@Input('aria-labelledby') ariaLabelledby: string;
+ /**
+ * Classes to be passed to the tab label inside the mat-tab-header container.
+ * Supports string and string array values, same as `ngClass`.
+ */
+ @Input() labelClass: string | string[];
+
+ /**
+ * Classes to be passed to the tab mat-tab-body container.
+ * Supports string and string array values, same as `ngClass`.
+ */
+ @Input() bodyClass: string | string[];
+
/** Portal that will be the hosted content of the tab */
private _contentPortal: TemplatePortal | null = null;
diff --git a/src/material/tabs/tabs-animations.ts b/src/material/tabs/tabs-animations.ts
index 2b338f126f83..5e7955d4a373 100644
--- a/src/material/tabs/tabs-animations.ts
+++ b/src/material/tabs/tabs-animations.ts
@@ -23,26 +23,43 @@ export const matTabsAnimations: {
} = {
/** Animation translates a tab along the X axis. */
translateTab: trigger('translateTab', [
- // Note: transitions to `none` instead of 0, because some browsers might blur the content.
+ // Transitions to `none` instead of 0, because some browsers might blur the content.
state('center, void, left-origin-center, right-origin-center', style({transform: 'none'})),
// If the tab is either on the left or right, we additionally add a `min-height` of 1px
// in order to ensure that the element has a height before its state changes. This is
// necessary because Chrome does seem to skip the transition in RTL mode if the element does
// not have a static height and is not rendered. See related issue: #9465
- state('left', style({transform: 'translate3d(-100%, 0, 0)', minHeight: '1px'})),
- state('right', style({transform: 'translate3d(100%, 0, 0)', minHeight: '1px'})),
+ state(
+ 'left',
+ style({
+ transform: 'translate3d(-100%, 0, 0)',
+ minHeight: '1px',
+
+ // Normally this is redundant since we detach the content from the DOM, but if the user
+ // opted into keeping the content in the DOM, we have to hide it so it isn't focusable.
+ visibility: 'hidden',
+ }),
+ ),
+ state(
+ 'right',
+ style({
+ transform: 'translate3d(100%, 0, 0)',
+ minHeight: '1px',
+ visibility: 'hidden',
+ }),
+ ),
transition(
'* => left, * => right, left => center, right => center',
animate('{{animationDuration}} cubic-bezier(0.35, 0, 0.25, 1)'),
),
transition('void => left-origin-center', [
- style({transform: 'translate3d(-100%, 0, 0)'}),
+ style({transform: 'translate3d(-100%, 0, 0)', visibility: 'hidden'}),
animate('{{animationDuration}} cubic-bezier(0.35, 0, 0.25, 1)'),
]),
transition('void => right-origin-center', [
- style({transform: 'translate3d(100%, 0, 0)'}),
+ style({transform: 'translate3d(100%, 0, 0)', visibility: 'hidden'}),
animate('{{animationDuration}} cubic-bezier(0.35, 0, 0.25, 1)'),
]),
]),
diff --git a/src/material/tabs/tabs.md b/src/material/tabs/tabs.md
index f91bcbb9332c..08d83e6227c2 100644
--- a/src/material/tabs/tabs.md
+++ b/src/material/tabs/tabs.md
@@ -84,6 +84,15 @@ duration can be configured globally using the `MAT_TABS_CONFIG` injection token.
"file": "tab-group-animations-example.html",
"region": "slow-animation-duration"}) -->
+### Keeping the tab content inside the DOM while it's off-screen
+By default the `` will remove the content of off-screen tabs from the DOM until they
+come into the view. This is optimal for most cases since it keeps the DOM size smaller, but it
+isn't great for others like when a tab has an `