Skip to content

Commit 9db1333

Browse files
vanessanschmittjelbourn
authored andcommitted
chore: add FocusTrap demo page (#17858)
Note that much of this demo page is known to not be working with the current focus trap implementation (ex. elements in the focus trap with tabindex > 0 send focus out of the trap, elements in iframes/shadow DOM get skipped when focus wraps, all focus traps can be escaped by clicking on the URL and then pressing tab). The demo page will be helpful for research into FocusTrap improvements (see #13054).
1 parent 03a9c1d commit 9db1333

11 files changed

+368
-0
lines changed

.github/CODEOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@
149149
/src/dev-app/examples-page/** @andrewseguin
150150
/src/dev-app/expansion/** @jelbourn
151151
/src/dev-app/focus-origin/** @mmalerba
152+
/src/dev-app/focus-trap/** @jelbourn
152153
/src/dev-app/google-map/** @mbehrlich
153154
/src/dev-app/grid-list/** @jelbourn
154155
/src/dev-app/icon/** @jelbourn

src/dev-app/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ ng_module(
3737
"//src/dev-app/examples-page",
3838
"//src/dev-app/expansion",
3939
"//src/dev-app/focus-origin",
40+
"//src/dev-app/focus-trap",
4041
"//src/dev-app/google-map",
4142
"//src/dev-app/grid-list",
4243
"//src/dev-app/icon",

src/dev-app/dev-app/dev-app-layout.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export class DevAppLayout {
3838
{name: 'Drag and Drop', route: '/drag-drop'},
3939
{name: 'Expansion Panel', route: '/expansion'},
4040
{name: 'Focus Origin', route: '/focus-origin'},
41+
{name: 'Focus Trap', route: '/focus-trap'},
4142
{name: 'Google Map', route: '/google-map'},
4243
{name: 'Grid List', route: '/grid-list'},
4344
{name: 'Icon', route: '/icon'},

src/dev-app/dev-app/routes.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ export const DEV_APP_ROUTES: Routes = [
3939
path: 'focus-origin',
4040
loadChildren: 'focus-origin/focus-origin-demo-module#FocusOriginDemoModule'
4141
},
42+
{
43+
path: 'focus-trap',
44+
loadChildren: 'focus-trap/focus-trap-demo-module#FocusTrapDemoModule'
45+
},
4246
{path: 'google-map', loadChildren: 'google-map/google-map-demo-module#GoogleMapDemoModule'},
4347
{path: 'grid-list', loadChildren: 'grid-list/grid-list-demo-module#GridListDemoModule'},
4448
{path: 'icon', loadChildren: 'icon/icon-demo-module#IconDemoModule'},

src/dev-app/focus-trap/BUILD.bazel

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package(default_visibility = ["//visibility:public"])
2+
3+
load("//tools:defaults.bzl", "ng_module", "sass_binary")
4+
5+
ng_module(
6+
name = "focus-trap",
7+
srcs = glob(["**/*.ts"]),
8+
assets = [
9+
"focus-trap-demo.html",
10+
"focus-trap-dialog-demo.html",
11+
":focus_trap_demo_scss",
12+
":focus_trap_dialog_demo_scss",
13+
],
14+
deps = [
15+
"//src/cdk/a11y",
16+
"//src/material/button",
17+
"//src/material/card",
18+
"//src/material/dialog",
19+
"//src/material/toolbar",
20+
"@npm//@angular/router",
21+
],
22+
)
23+
24+
sass_binary(
25+
name = "focus_trap_demo_scss",
26+
src = "focus-trap-demo.scss",
27+
)
28+
29+
sass_binary(
30+
name = "focus_trap_dialog_demo_scss",
31+
src = "focus-trap-dialog-demo.scss",
32+
)
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {A11yModule} from '@angular/cdk/a11y';
10+
import {CommonModule} from '@angular/common';
11+
import {NgModule} from '@angular/core';
12+
import {MatButtonModule} from '@angular/material/button';
13+
import {MatCardModule} from '@angular/material/card';
14+
import {MatDialogModule} from '@angular/material/dialog';
15+
import {MatToolbarModule} from '@angular/material/toolbar';
16+
import {RouterModule} from '@angular/router';
17+
import {FocusTrapDemo, FocusTrapShadowDomDemo, FocusTrapDialogDemo} from './focus-trap-demo';
18+
19+
@NgModule({
20+
imports: [
21+
A11yModule,
22+
CommonModule,
23+
MatButtonModule,
24+
MatCardModule,
25+
MatDialogModule,
26+
MatToolbarModule,
27+
RouterModule.forChild([{path: '', component: FocusTrapDemo}]),
28+
],
29+
declarations: [FocusTrapDemo, FocusTrapShadowDomDemo, FocusTrapDialogDemo],
30+
entryComponents: [FocusTrapDialogDemo],
31+
})
32+
export class FocusTrapDemoModule {
33+
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
<div>
2+
<mat-card class="demo-mat-card">
3+
<mat-toolbar color="primary">Basic</mat-toolbar>
4+
<mat-card-content class="demo-mat-card-content">
5+
<button mat-raised-button (click)="toggleFocus(basicFocusTrap)">
6+
{{basicFocusTrap && basicFocusTrap.enabled ? "Disable" : "Enable"}} FocusTrap
7+
</button>
8+
<div class="demo-focus-trap-region" #basicDemoRegion
9+
[class.demo-focus-trap-enabled]="(basicFocusTrap && basicFocusTrap.enabled) || false">
10+
<textarea class="demo-focus-trap-element" placeholder="One"></textarea>
11+
<textarea class="demo-focus-trap-element" placeholder="Two"></textarea>
12+
</div>
13+
</mat-card-content>
14+
</mat-card>
15+
16+
<mat-card class="demo-mat-card">
17+
<mat-toolbar color="primary">Nested</mat-toolbar>
18+
<mat-card-content class="demo-mat-card-content">
19+
<button mat-raised-button (click)="toggleFocus(nestedOuterFocusTrap)">
20+
{{nestedOuterFocusTrap && nestedOuterFocusTrap.enabled ? "Disable" : "Enable"}} outer FocusTrap
21+
</button>
22+
<div class="demo-focus-trap-region" #nestedOuterDemoRegion
23+
[class.demo-focus-trap-enabled]="(nestedOuterFocusTrap && nestedOuterFocusTrap.enabled) || false">
24+
<textarea class="demo-focus-trap-element" placeholder="One"></textarea>
25+
<textarea class="demo-focus-trap-element" placeholder="Two"></textarea>
26+
<button mat-raised-button class="demo-focus-trap-element"
27+
(click)="toggleFocus(nestedInnerFocusTrap)">
28+
{{nestedInnerFocusTrap && nestedInnerFocusTrap.enabled ? "Disable" : "Enable"}} inner FocusTrap
29+
</button>
30+
<div class="demo-focus-trap-region" #nestedInnerDemoRegion
31+
[class.demo-focus-trap-enabled]="(nestedInnerFocusTrap && nestedInnerFocusTrap.enabled) || false">
32+
<textarea class="demo-focus-trap-element" placeholder="Three"></textarea>
33+
<textarea class="demo-focus-trap-element" placeholder="Four"></textarea>
34+
</div>
35+
</div>
36+
</mat-card-content>
37+
</mat-card>
38+
39+
<mat-card class="demo-mat-card">
40+
<mat-toolbar color="primary">Tabindex > 0</mat-toolbar>
41+
<mat-card-content class="demo-mat-card-content">
42+
<button mat-raised-button (click)="toggleFocus(tabIndexFocusTrap)">
43+
{{tabIndexFocusTrap && tabIndexFocusTrap.enabled ? "Disable" : "Enable"}} FocusTrap
44+
</button>
45+
<div class="demo-focus-trap-region" #tabIndexDemoRegion
46+
[class.demo-focus-trap-enabled]="(tabIndexFocusTrap && tabIndexFocusTrap.enabled) || false">
47+
<textarea class="demo-focus-trap-element" tabindex="1"
48+
placeholder="I have tabindex 1"></textarea>
49+
<textarea class="demo-focus-trap-element" placeholder="One"></textarea>
50+
<textarea class="demo-focus-trap-element" placeholder="Two"></textarea>
51+
</div>
52+
<textarea class="demo-focus-trap-element" tabindex="1"
53+
placeholder="I have tabindex 1"></textarea>
54+
</mat-card-content>
55+
</mat-card>
56+
57+
<mat-card class="demo-mat-card">
58+
<mat-toolbar color="primary">Shadow DOMs</mat-toolbar>
59+
<mat-card-content class="demo-mat-card-content">
60+
<button mat-raised-button (click)="toggleFocus(shadowDomFocusTrap)">
61+
{{shadowDomFocusTrap && shadowDomFocusTrap.enabled ? "Disable" : "Enable"}} FocusTrap
62+
</button>
63+
<div class="demo-focus-trap-region" #shadowDomDemoRegion
64+
[class.demo-focus-trap-enabled]="(shadowDomFocusTrap && shadowDomFocusTrap.enabled) || false">
65+
<shadow-dom-demo>
66+
<textarea placeholder="I am in a shadow DOM"></textarea>
67+
</shadow-dom-demo>
68+
<textarea class="demo-focus-trap-element" placeholder="One"></textarea>
69+
<textarea class="demo-focus-trap-element" placeholder="Two"></textarea>
70+
</div>
71+
<shadow-dom-demo>
72+
<textarea class="demo-focus-trap-element" placeholder="I am in a shadow DOM"></textarea>
73+
</shadow-dom-demo>
74+
</mat-card-content>
75+
</mat-card>
76+
77+
<mat-card class="demo-mat-card">
78+
<mat-toolbar color="primary">iframes</mat-toolbar>
79+
<mat-card-content class="demo-mat-card-content">
80+
<button mat-raised-button (click)="toggleFocus(iframeFocusTrap)">
81+
{{iframeFocusTrap && iframeFocusTrap.enabled ? "Disable" : "Enable"}} FocusTrap
82+
</button>
83+
<div class="demo-focus-trap-region" #iframeDemoRegion
84+
[class.demo-focus-trap-enabled]="(iframeFocusTrap && iframeFocusTrap.enabled) || false">
85+
<iframe class="demo-focus-trap-element"
86+
srcdoc="<textarea placeholder='I am in an iframe'></textarea>">
87+
</iframe>
88+
<textarea class="demo-focus-trap-element" placeholder="One"></textarea>
89+
<textarea class="demo-focus-trap-element" placeholder="Two"></textarea>
90+
</div>
91+
<iframe srcdoc="<textarea placeholder='I am in an iframe'></textarea>"></iframe>
92+
</mat-card-content>
93+
</mat-card>
94+
95+
<mat-card class="demo-mat-card">
96+
<mat-toolbar color="primary">Dynamic page content</mat-toolbar>
97+
<mat-card-content class="demo-mat-card-content">
98+
<button mat-raised-button (click)="toggleFocus(dynamicFocusTrap)">
99+
{{dynamicFocusTrap && dynamicFocusTrap.enabled ? "Disable" : "Enable"}} FocusTrap
100+
</button>
101+
<div class="demo-focus-trap-region" #dynamicDemoRegion
102+
[class.demo-focus-trap-enabled]="(dynamicFocusTrap && dynamicFocusTrap.enabled) || false">
103+
<textarea class="demo-focus-trap-element" placeholder="One"></textarea>
104+
<textarea class="demo-focus-trap-element" placeholder="Two"></textarea>
105+
<button mat-raised-button class="demo-focus-trap-element" (click)="addNewElement()">
106+
Click to add more focusable elements to the page
107+
</button>
108+
</div>
109+
<div #newElements></div>
110+
</mat-card-content>
111+
</mat-card>
112+
113+
<mat-card class="demo-mat-card">
114+
<mat-toolbar color="primary">Dialog-on-dialog</mat-toolbar>
115+
<mat-card-content class="demo-mat-card-content">
116+
<button mat-raised-button (click)="openDialog()">Open dialog</button>
117+
</mat-card-content>
118+
</mat-card>
119+
</div>
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
.demo-focus-trap-region {
2+
outline: 2px dashed lightgray;
3+
padding: 4px;
4+
margin: 12px 0;
5+
6+
7+
&.demo-focus-trap-enabled {
8+
outline: 2px solid red;
9+
}
10+
11+
.demo-focus-trap-element, .demo-focus-trap-shadow-root {
12+
display: block;
13+
margin: 4px;
14+
}
15+
16+
.demo-focus-trap-region {
17+
margin: 12px 4px;
18+
}
19+
}
20+
21+
.demo-focus-trap-shadow-root {
22+
display: block;
23+
padding: 4px;
24+
background-color: lightgrey;
25+
}
26+
27+
.demo-mat-card {
28+
padding: 0;
29+
margin: 16px;
30+
31+
& .demo-mat-card-content {
32+
padding: 24px;
33+
}
34+
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {FocusTrap, FocusTrapFactory} from '@angular/cdk/a11y';
10+
import {
11+
AfterViewInit,
12+
Component,
13+
ElementRef,
14+
ViewChild,
15+
ViewEncapsulation} from '@angular/core';
16+
import {MatDialog} from '@angular/material/dialog';
17+
18+
@Component({
19+
selector: 'shadow-dom-demo',
20+
template: '<ng-content></ng-content>',
21+
host: {'class': 'demo-focus-trap-shadow-root'},
22+
encapsulation: ViewEncapsulation.ShadowDom
23+
})
24+
export class FocusTrapShadowDomDemo {}
25+
26+
@Component({
27+
selector: 'focus-trap-demo',
28+
templateUrl: 'focus-trap-demo.html',
29+
styleUrls: ['focus-trap-demo.css'],
30+
})
31+
export class FocusTrapDemo implements AfterViewInit {
32+
33+
basicFocusTrap: FocusTrap;
34+
@ViewChild('basicDemoRegion', {static: false}) private readonly _basicDemoRegion!: ElementRef;
35+
36+
nestedOuterFocusTrap: FocusTrap;
37+
@ViewChild('nestedOuterDemoRegion', {static: false})
38+
private readonly _nestedOuterDemoRegion!: ElementRef;
39+
nestedInnerFocusTrap: FocusTrap;
40+
@ViewChild('nestedInnerDemoRegion', {static: false})
41+
private readonly _nestedInnerDemoRegion!: ElementRef;
42+
43+
tabIndexFocusTrap: FocusTrap;
44+
@ViewChild('tabIndexDemoRegion', {static: false})
45+
private readonly _tabIndexDemoRegion!: ElementRef;
46+
47+
shadowDomFocusTrap: FocusTrap;
48+
@ViewChild('shadowDomDemoRegion', {static: false})
49+
private readonly _shadowDomDemoRegion!: ElementRef;
50+
51+
iframeFocusTrap: FocusTrap;
52+
@ViewChild('iframeDemoRegion', {static: false})
53+
private readonly _iframeDemoRegion!: ElementRef;
54+
55+
dynamicFocusTrap: FocusTrap;
56+
@ViewChild('dynamicDemoRegion', {static: false})
57+
private readonly _dynamicDemoRegion!: ElementRef;
58+
@ViewChild('newElements', {static: false}) private readonly _newElements!: ElementRef;
59+
60+
constructor(
61+
public dialog: MatDialog,
62+
private _focusTrapFactory: FocusTrapFactory) {}
63+
64+
ngAfterViewInit() {
65+
this.basicFocusTrap = this._focusTrapFactory.create(this._basicDemoRegion.nativeElement);
66+
this.basicFocusTrap.enabled = false;
67+
68+
this.nestedOuterFocusTrap = this._focusTrapFactory.create(
69+
this._nestedOuterDemoRegion.nativeElement);
70+
this.nestedOuterFocusTrap.enabled = false;
71+
72+
this.nestedInnerFocusTrap = this._focusTrapFactory.create(
73+
this._nestedInnerDemoRegion.nativeElement);
74+
this.nestedInnerFocusTrap.enabled = false;
75+
76+
this.tabIndexFocusTrap = this._focusTrapFactory.create(
77+
this._tabIndexDemoRegion.nativeElement);
78+
this.tabIndexFocusTrap.enabled = false;
79+
80+
this.shadowDomFocusTrap = this._focusTrapFactory.create(
81+
this._shadowDomDemoRegion.nativeElement);
82+
this.shadowDomFocusTrap.enabled = false;
83+
84+
this.iframeFocusTrap = this._focusTrapFactory.create(this._iframeDemoRegion.nativeElement);
85+
this.iframeFocusTrap.enabled = false;
86+
87+
this.dynamicFocusTrap = this._focusTrapFactory.create(this._dynamicDemoRegion.nativeElement);
88+
this.dynamicFocusTrap.enabled = false;
89+
}
90+
91+
toggleFocus(focusTrap: FocusTrap) {
92+
focusTrap.enabled = !focusTrap.enabled;
93+
if (focusTrap.enabled) {
94+
focusTrap.focusInitialElementWhenReady();
95+
}
96+
}
97+
98+
addNewElement() {
99+
const textarea = document.createElement('textarea');
100+
textarea.setAttribute('placeholder', 'I am a new element!');
101+
this._newElements.nativeElement.appendChild(textarea);
102+
}
103+
104+
openDialog() {
105+
this.dialog.open(FocusTrapDialogDemo);
106+
}
107+
}
108+
109+
let dialogCount = 0;
110+
111+
@Component({
112+
selector: 'focus-trap-dialog-demo',
113+
styleUrls: ['focus-trap-dialog-demo.css'],
114+
templateUrl: 'focus-trap-dialog-demo.html',
115+
})
116+
export class FocusTrapDialogDemo {
117+
id = dialogCount++;
118+
constructor(public dialog: MatDialog) {}
119+
120+
openAnotherDialog() {
121+
this.dialog.open(FocusTrapDialogDemo);
122+
}
123+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<h2 mat-dialog-title>Dialog {{id}}</h2>
2+
3+
<mat-dialog-content>
4+
<textarea class="demo-dialog-textarea" placeholder="One"></textarea>
5+
<textarea class="demo-dialog-textarea" placeholder="Two"></textarea>
6+
</mat-dialog-content>
7+
8+
<mat-dialog-actions>
9+
<button mat-raised-button mat-dialog-close>
10+
Close
11+
</button>
12+
13+
<button mat-raised-button (click)="openAnotherDialog()">
14+
Open another dialog
15+
</button>
16+
</mat-dialog-actions>

0 commit comments

Comments
 (0)