diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index b91805383ffd..8d740347264c 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -149,6 +149,7 @@
/src/dev-app/examples-page/** @andrewseguin
/src/dev-app/expansion/** @jelbourn
/src/dev-app/focus-origin/** @mmalerba
+/src/dev-app/focus-trap/** @jelbourn
/src/dev-app/google-map/** @mbehrlich
/src/dev-app/grid-list/** @jelbourn
/src/dev-app/icon/** @jelbourn
diff --git a/src/dev-app/BUILD.bazel b/src/dev-app/BUILD.bazel
index d091c473a483..7dbbdb4dfcff 100644
--- a/src/dev-app/BUILD.bazel
+++ b/src/dev-app/BUILD.bazel
@@ -37,6 +37,7 @@ ng_module(
"//src/dev-app/examples-page",
"//src/dev-app/expansion",
"//src/dev-app/focus-origin",
+ "//src/dev-app/focus-trap",
"//src/dev-app/google-map",
"//src/dev-app/grid-list",
"//src/dev-app/icon",
diff --git a/src/dev-app/dev-app/dev-app-layout.ts b/src/dev-app/dev-app/dev-app-layout.ts
index 98409e20132e..0a438f7df516 100644
--- a/src/dev-app/dev-app/dev-app-layout.ts
+++ b/src/dev-app/dev-app/dev-app-layout.ts
@@ -38,6 +38,7 @@ export class DevAppLayout {
{name: 'Drag and Drop', route: '/drag-drop'},
{name: 'Expansion Panel', route: '/expansion'},
{name: 'Focus Origin', route: '/focus-origin'},
+ {name: 'Focus Trap', route: '/focus-trap'},
{name: 'Google Map', route: '/google-map'},
{name: 'Grid List', route: '/grid-list'},
{name: 'Icon', route: '/icon'},
diff --git a/src/dev-app/dev-app/routes.ts b/src/dev-app/dev-app/routes.ts
index e831fa929bed..b73299f770b9 100644
--- a/src/dev-app/dev-app/routes.ts
+++ b/src/dev-app/dev-app/routes.ts
@@ -39,6 +39,10 @@ export const DEV_APP_ROUTES: Routes = [
path: 'focus-origin',
loadChildren: 'focus-origin/focus-origin-demo-module#FocusOriginDemoModule'
},
+ {
+ path: 'focus-trap',
+ loadChildren: 'focus-trap/focus-trap-demo-module#FocusTrapDemoModule'
+ },
{path: 'google-map', loadChildren: 'google-map/google-map-demo-module#GoogleMapDemoModule'},
{path: 'grid-list', loadChildren: 'grid-list/grid-list-demo-module#GridListDemoModule'},
{path: 'icon', loadChildren: 'icon/icon-demo-module#IconDemoModule'},
diff --git a/src/dev-app/focus-trap/BUILD.bazel b/src/dev-app/focus-trap/BUILD.bazel
new file mode 100644
index 000000000000..cf493c03c5c8
--- /dev/null
+++ b/src/dev-app/focus-trap/BUILD.bazel
@@ -0,0 +1,32 @@
+package(default_visibility = ["//visibility:public"])
+
+load("//tools:defaults.bzl", "ng_module", "sass_binary")
+
+ng_module(
+ name = "focus-trap",
+ srcs = glob(["**/*.ts"]),
+ assets = [
+ "focus-trap-demo.html",
+ "focus-trap-dialog-demo.html",
+ ":focus_trap_demo_scss",
+ ":focus_trap_dialog_demo_scss",
+ ],
+ deps = [
+ "//src/cdk/a11y",
+ "//src/material/button",
+ "//src/material/card",
+ "//src/material/dialog",
+ "//src/material/toolbar",
+ "@npm//@angular/router",
+ ],
+)
+
+sass_binary(
+ name = "focus_trap_demo_scss",
+ src = "focus-trap-demo.scss",
+)
+
+sass_binary(
+ name = "focus_trap_dialog_demo_scss",
+ src = "focus-trap-dialog-demo.scss",
+)
diff --git a/src/dev-app/focus-trap/focus-trap-demo-module.ts b/src/dev-app/focus-trap/focus-trap-demo-module.ts
new file mode 100644
index 000000000000..34c25d799e61
--- /dev/null
+++ b/src/dev-app/focus-trap/focus-trap-demo-module.ts
@@ -0,0 +1,33 @@
+/**
+ * @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 {A11yModule} from '@angular/cdk/a11y';
+import {CommonModule} from '@angular/common';
+import {NgModule} from '@angular/core';
+import {MatButtonModule} from '@angular/material/button';
+import {MatCardModule} from '@angular/material/card';
+import {MatDialogModule} from '@angular/material/dialog';
+import {MatToolbarModule} from '@angular/material/toolbar';
+import {RouterModule} from '@angular/router';
+import {FocusTrapDemo, FocusTrapShadowDomDemo, FocusTrapDialogDemo} from './focus-trap-demo';
+
+@NgModule({
+ imports: [
+ A11yModule,
+ CommonModule,
+ MatButtonModule,
+ MatCardModule,
+ MatDialogModule,
+ MatToolbarModule,
+ RouterModule.forChild([{path: '', component: FocusTrapDemo}]),
+ ],
+ declarations: [FocusTrapDemo, FocusTrapShadowDomDemo, FocusTrapDialogDemo],
+ entryComponents: [FocusTrapDialogDemo],
+})
+export class FocusTrapDemoModule {
+}
diff --git a/src/dev-app/focus-trap/focus-trap-demo.html b/src/dev-app/focus-trap/focus-trap-demo.html
new file mode 100644
index 000000000000..04bec6ebf188
--- /dev/null
+++ b/src/dev-app/focus-trap/focus-trap-demo.html
@@ -0,0 +1,119 @@
+
+
+ Basic
+
+
+
+
+
+
+
+
+
+
+ Nested
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Tabindex > 0
+
+
+
+
+
+
+
+
+
+
+
+
+ Shadow DOMs
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ iframes
+
+
+
+
+
+
+
+
+
+
+
+
+ Dynamic page content
+
+
+
+
+
+
+
+
+
+
+
+
+ Dialog-on-dialog
+
+
+
+
+
diff --git a/src/dev-app/focus-trap/focus-trap-demo.scss b/src/dev-app/focus-trap/focus-trap-demo.scss
new file mode 100644
index 000000000000..3bcd2d6c088c
--- /dev/null
+++ b/src/dev-app/focus-trap/focus-trap-demo.scss
@@ -0,0 +1,34 @@
+.demo-focus-trap-region {
+ outline: 2px dashed lightgray;
+ padding: 4px;
+ margin: 12px 0;
+
+
+ &.demo-focus-trap-enabled {
+ outline: 2px solid red;
+ }
+
+ .demo-focus-trap-element, .demo-focus-trap-shadow-root {
+ display: block;
+ margin: 4px;
+ }
+
+ .demo-focus-trap-region {
+ margin: 12px 4px;
+ }
+}
+
+.demo-focus-trap-shadow-root {
+ display: block;
+ padding: 4px;
+ background-color: lightgrey;
+}
+
+.demo-mat-card {
+ padding: 0;
+ margin: 16px;
+
+ & .demo-mat-card-content {
+ padding: 24px;
+ }
+}
diff --git a/src/dev-app/focus-trap/focus-trap-demo.ts b/src/dev-app/focus-trap/focus-trap-demo.ts
new file mode 100644
index 000000000000..664c71b8cc86
--- /dev/null
+++ b/src/dev-app/focus-trap/focus-trap-demo.ts
@@ -0,0 +1,123 @@
+/**
+ * @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 {FocusTrap, FocusTrapFactory} from '@angular/cdk/a11y';
+import {
+ AfterViewInit,
+ Component,
+ ElementRef,
+ ViewChild,
+ ViewEncapsulation} from '@angular/core';
+import {MatDialog} from '@angular/material/dialog';
+
+@Component({
+ selector: 'shadow-dom-demo',
+ template: '',
+ host: {'class': 'demo-focus-trap-shadow-root'},
+ encapsulation: ViewEncapsulation.ShadowDom
+})
+export class FocusTrapShadowDomDemo {}
+
+@Component({
+ selector: 'focus-trap-demo',
+ templateUrl: 'focus-trap-demo.html',
+ styleUrls: ['focus-trap-demo.css'],
+})
+export class FocusTrapDemo implements AfterViewInit {
+
+ basicFocusTrap: FocusTrap;
+ @ViewChild('basicDemoRegion', {static: false}) private readonly _basicDemoRegion!: ElementRef;
+
+ nestedOuterFocusTrap: FocusTrap;
+ @ViewChild('nestedOuterDemoRegion', {static: false})
+ private readonly _nestedOuterDemoRegion!: ElementRef;
+ nestedInnerFocusTrap: FocusTrap;
+ @ViewChild('nestedInnerDemoRegion', {static: false})
+ private readonly _nestedInnerDemoRegion!: ElementRef;
+
+ tabIndexFocusTrap: FocusTrap;
+ @ViewChild('tabIndexDemoRegion', {static: false})
+ private readonly _tabIndexDemoRegion!: ElementRef;
+
+ shadowDomFocusTrap: FocusTrap;
+ @ViewChild('shadowDomDemoRegion', {static: false})
+ private readonly _shadowDomDemoRegion!: ElementRef;
+
+ iframeFocusTrap: FocusTrap;
+ @ViewChild('iframeDemoRegion', {static: false})
+ private readonly _iframeDemoRegion!: ElementRef;
+
+ dynamicFocusTrap: FocusTrap;
+ @ViewChild('dynamicDemoRegion', {static: false})
+ private readonly _dynamicDemoRegion!: ElementRef;
+ @ViewChild('newElements', {static: false}) private readonly _newElements!: ElementRef;
+
+ constructor(
+ public dialog: MatDialog,
+ private _focusTrapFactory: FocusTrapFactory) {}
+
+ ngAfterViewInit() {
+ this.basicFocusTrap = this._focusTrapFactory.create(this._basicDemoRegion.nativeElement);
+ this.basicFocusTrap.enabled = false;
+
+ this.nestedOuterFocusTrap = this._focusTrapFactory.create(
+ this._nestedOuterDemoRegion.nativeElement);
+ this.nestedOuterFocusTrap.enabled = false;
+
+ this.nestedInnerFocusTrap = this._focusTrapFactory.create(
+ this._nestedInnerDemoRegion.nativeElement);
+ this.nestedInnerFocusTrap.enabled = false;
+
+ this.tabIndexFocusTrap = this._focusTrapFactory.create(
+ this._tabIndexDemoRegion.nativeElement);
+ this.tabIndexFocusTrap.enabled = false;
+
+ this.shadowDomFocusTrap = this._focusTrapFactory.create(
+ this._shadowDomDemoRegion.nativeElement);
+ this.shadowDomFocusTrap.enabled = false;
+
+ this.iframeFocusTrap = this._focusTrapFactory.create(this._iframeDemoRegion.nativeElement);
+ this.iframeFocusTrap.enabled = false;
+
+ this.dynamicFocusTrap = this._focusTrapFactory.create(this._dynamicDemoRegion.nativeElement);
+ this.dynamicFocusTrap.enabled = false;
+ }
+
+ toggleFocus(focusTrap: FocusTrap) {
+ focusTrap.enabled = !focusTrap.enabled;
+ if (focusTrap.enabled) {
+ focusTrap.focusInitialElementWhenReady();
+ }
+ }
+
+ addNewElement() {
+ const textarea = document.createElement('textarea');
+ textarea.setAttribute('placeholder', 'I am a new element!');
+ this._newElements.nativeElement.appendChild(textarea);
+ }
+
+ openDialog() {
+ this.dialog.open(FocusTrapDialogDemo);
+ }
+}
+
+let dialogCount = 0;
+
+@Component({
+ selector: 'focus-trap-dialog-demo',
+ styleUrls: ['focus-trap-dialog-demo.css'],
+ templateUrl: 'focus-trap-dialog-demo.html',
+})
+export class FocusTrapDialogDemo {
+ id = dialogCount++;
+ constructor(public dialog: MatDialog) {}
+
+ openAnotherDialog() {
+ this.dialog.open(FocusTrapDialogDemo);
+ }
+}
diff --git a/src/dev-app/focus-trap/focus-trap-dialog-demo.html b/src/dev-app/focus-trap/focus-trap-dialog-demo.html
new file mode 100644
index 000000000000..2ee867f515b6
--- /dev/null
+++ b/src/dev-app/focus-trap/focus-trap-dialog-demo.html
@@ -0,0 +1,16 @@
+Dialog {{id}}
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/dev-app/focus-trap/focus-trap-dialog-demo.scss b/src/dev-app/focus-trap/focus-trap-dialog-demo.scss
new file mode 100644
index 000000000000..dfde5b4b5e85
--- /dev/null
+++ b/src/dev-app/focus-trap/focus-trap-dialog-demo.scss
@@ -0,0 +1,4 @@
+.demo-dialog-textarea {
+ display: block;
+ margin: 4px;
+}