diff --git a/src/components-examples/material/tabs/index.ts b/src/components-examples/material/tabs/index.ts
index cb5279b3487b..60a688c946e5 100644
--- a/src/components-examples/material/tabs/index.ts
+++ b/src/components-examples/material/tabs/index.ts
@@ -17,6 +17,7 @@ import {TabGroupHarnessExample} from './tab-group-harness/tab-group-harness-exam
import {TabGroupDynamicExample} from './tab-group-dynamic/tab-group-dynamic-example';
import {TabGroupHeaderBelowExample} from './tab-group-header-below/tab-group-header-below-example';
import {TabGroupLazyLoadedExample} from './tab-group-lazy-loaded/tab-group-lazy-loaded-example';
+import {TabGroupPreserveContentExample} from './tab-group-preserve-content/tab-group-preserve-content-example';
import {TabGroupStretchedExample} from './tab-group-stretched/tab-group-stretched-example';
import {TabGroupThemeExample} from './tab-group-theme/tab-group-theme-example';
import {TabNavBarBasicExample} from './tab-nav-bar-basic/tab-nav-bar-basic-example';
@@ -37,6 +38,7 @@ export {
TabGroupThemeExample,
TabNavBarBasicExample,
TabNavBarWithPanelExample,
+ TabGroupPreserveContentExample,
};
const EXAMPLES = [
@@ -54,6 +56,7 @@ const EXAMPLES = [
TabGroupThemeExample,
TabNavBarBasicExample,
TabNavBarWithPanelExample,
+ TabGroupPreserveContentExample,
];
@NgModule({
diff --git a/src/components-examples/material/tabs/tab-group-preserve-content/tab-group-preserve-content-example.html b/src/components-examples/material/tabs/tab-group-preserve-content/tab-group-preserve-content-example.html
new file mode 100644
index 000000000000..3d58eef2e816
--- /dev/null
+++ b/src/components-examples/material/tabs/tab-group-preserve-content/tab-group-preserve-content-example.html
@@ -0,0 +1,14 @@
+
Start the video in the first tab and navigate to the second one to see how it keeps playing.
+
+
+
+
+
+ Note how the video from the previous tab is still playing.
+
diff --git a/src/components-examples/material/tabs/tab-group-preserve-content/tab-group-preserve-content-example.ts b/src/components-examples/material/tabs/tab-group-preserve-content/tab-group-preserve-content-example.ts
new file mode 100644
index 000000000000..8509b2d1fd04
--- /dev/null
+++ b/src/components-examples/material/tabs/tab-group-preserve-content/tab-group-preserve-content-example.ts
@@ -0,0 +1,10 @@
+import {Component} from '@angular/core';
+
+/**
+ * @title Tab group that keeps its content inside the DOM when it's off-screen.
+ */
+@Component({
+ selector: 'tab-group-preserve-content-example',
+ templateUrl: 'tab-group-preserve-content-example.html',
+})
+export class TabGroupPreserveContentExample {}
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-group.html b/src/material-experimental/mdc-tabs/tab-group.html
index 4fa26e85c807..157fe938c340 100644
--- a/src/material-experimental/mdc-tabs/tab-group.html
+++ b/src/material-experimental/mdc-tabs/tab-group.html
@@ -63,6 +63,7 @@
[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 af5b9b55e0b4..dacd95956c01 100644
--- a/src/material-experimental/mdc-tabs/tab-group.spec.ts
+++ b/src/material-experimental/mdc-tabs/tab-group.spec.ts
@@ -666,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', () => {
@@ -1126,7 +1176,7 @@ class AsyncTabsTestApp implements OnInit {
@Component({
template: `
-
+ Pizza, fries Broccoli, spinach {{otherContent}}
@@ -1135,6 +1185,7 @@ class AsyncTabsTestApp implements OnInit {
`,
})
class TabGroupWithSimpleApi {
+ preserveContent = false;
otherLabel = 'Fruit';
otherContent = 'Apples, grapes';
@ViewChild('legumes') legumes: any;
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 ebd767eda5b7..c39e3528f2a0 100644
--- a/src/material/tabs/tab-group.html
+++ b/src/material/tabs/tab-group.html
@@ -50,6 +50,7 @@
[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 6cb53a529583..1be121f13985 100644
--- a/src/material/tabs/tab-group.spec.ts
+++ b/src/material/tabs/tab-group.spec.ts
@@ -665,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', () => {
@@ -1072,7 +1122,7 @@ class AsyncTabsTestApp implements OnInit {
@Component({
template: `
-
+ Pizza, fries Broccoli, spinach {{otherContent}}
@@ -1081,6 +1131,7 @@ class AsyncTabsTestApp implements OnInit {
`,
})
class TabGroupWithSimpleApi {
+ preserveContent = false;
otherLabel = 'Fruit';
otherContent = 'Apples, grapes';
@ViewChild('legumes') legumes: any;
diff --git a/src/material/tabs/tab-group.ts b/src/material/tabs/tab-group.ts
index 7952dabdaa51..11b077499bc8 100644
--- a/src/material/tabs/tab-group.ts
+++ b/src/material/tabs/tab-group.ts
@@ -163,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 {
@@ -214,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;
}
/**
diff --git a/src/material/tabs/tabs-animations.ts b/src/material/tabs/tabs-animations.ts
index 2b338f126f83..0468870cb90f 100644
--- a/src/material/tabs/tabs-animations.ts
+++ b/src/material/tabs/tabs-animations.ts
@@ -23,26 +23,50 @@ 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.
- state('center, void, left-origin-center, right-origin-center', style({transform: 'none'})),
+ state(
+ 'center, void, left-origin-center, right-origin-center',
+ style({
+ // Transitions to `none` instead of 0, because some browsers might blur the content.
+ transform: 'none',
+ // Ensures that the `visibility: hidden` from below is cleared.
+ visibility: '',
+ }),
+ ),
// 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 31ae1e7423b5..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 `