Skip to content

Commit 66da82a

Browse files
chore(dev-app) Add demo page for FocusTrap
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 2d407a2 commit 66da82a

File tree

9 files changed

+359
-0
lines changed

9 files changed

+359
-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: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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_demo_scss",
11+
],
12+
deps = [
13+
"//src/cdk/a11y",
14+
"//src/material/button",
15+
"//src/material/card",
16+
"//src/material/dialog",
17+
"//src/material/toolbar",
18+
"@npm//@angular/router",
19+
],
20+
)
21+
22+
sass_binary(
23+
name = "focus_trap_demo_scss",
24+
src = "focus-trap-demo.scss",
25+
)
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: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
<div>
2+
<mat-card>
3+
<mat-toolbar color="primary">Basic</mat-toolbar>
4+
<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">
10+
<textarea placeholder="One"></textarea>
11+
<textarea placeholder="Two"></textarea>
12+
</div>
13+
</mat-card-content>
14+
</mat-card>
15+
16+
<mat-card>
17+
<mat-toolbar color="primary">Nested</mat-toolbar>
18+
<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">
24+
<textarea placeholder="One"></textarea>
25+
<textarea placeholder="Two"></textarea>
26+
<button mat-raised-button (click)="toggleFocus(nestedInnerFocusTrap)">
27+
{{nestedInnerFocusTrap && nestedInnerFocusTrap.enabled ? "Disable" : "Enable"}} inner FocusTrap
28+
</button>
29+
<div class="demo-focus-trap-region" #nestedInnerDemoRegion
30+
[class.demo-focus-trap-enabled]="nestedInnerFocusTrap && nestedInnerFocusTrap.enabled">
31+
<textarea placeholder="One"></textarea>
32+
<textarea placeholder="Two"></textarea>
33+
</div>
34+
</div>
35+
</mat-card-content>
36+
</mat-card>
37+
38+
<mat-card>
39+
<mat-toolbar color="primary">Tabindex > 0</mat-toolbar>
40+
<mat-card-content>
41+
<button mat-raised-button (click)="toggleFocus(tabIndexFocusTrap)">
42+
{{tabIndexFocusTrap && tabIndexFocusTrap.enabled ? "Disable" : "Enable"}} FocusTrap
43+
</button>
44+
<div class="demo-focus-trap-region" #tabIndexDemoRegion
45+
[class.demo-focus-trap-enabled]="tabIndexFocusTrap && tabIndexFocusTrap.enabled">
46+
<textarea tabindex="1" placeholder="I have tabindex 1"></textarea>
47+
<textarea placeholder="One"></textarea>
48+
<textarea placeholder="Two"></textarea>
49+
</div>
50+
<textarea tabindex="1" placeholder="I have tabindex 1"></textarea>
51+
</mat-card-content>
52+
</mat-card>
53+
54+
<mat-card>
55+
<mat-toolbar color="primary">Shadow DOMs</mat-toolbar>
56+
<mat-card-content>
57+
<button mat-raised-button (click)="toggleFocus(shadowDOMFocusTrap)">
58+
{{shadowDOMFocusTrap && shadowDOMFocusTrap.enabled ? "Disable" : "Enable"}} FocusTrap
59+
</button>
60+
<div class="demo-focus-trap-region" #shadowDOMDemoRegion
61+
[class.demo-focus-trap-enabled]="shadowDOMFocusTrap && shadowDOMFocusTrap.enabled">
62+
<shadow-dom-demo><textarea placeholder="I am in a shadow DOM"></textarea></shadow-dom-demo>
63+
<textarea placeholder="One"></textarea>
64+
<textarea placeholder="Two"></textarea>
65+
</div>
66+
<shadow-dom-demo><textarea placeholder="I am in a shadow DOM"></textarea></shadow-dom-demo>
67+
</mat-card-content>
68+
</mat-card>
69+
70+
<mat-card>
71+
<mat-toolbar color="primary">iframes</mat-toolbar>
72+
<mat-card-content>
73+
<button mat-raised-button (click)="toggleFocus(iframeFocusTrap)">
74+
{{iframeFocusTrap && iframeFocusTrap.enabled ? "Disable" : "Enable"}} FocusTrap
75+
</button>
76+
<div class="demo-focus-trap-region" #iframeDemoRegion
77+
[class.demo-focus-trap-enabled]="iframeFocusTrap && iframeFocusTrap.enabled">
78+
<iframe srcdoc="<textarea placeholder='I am in an iframe'></textarea>"></iframe>
79+
<textarea placeholder="One"></textarea>
80+
<textarea placeholder="Two"></textarea>
81+
</div>
82+
<iframe srcdoc="<textarea placeholder='I am in an iframe'></textarea>"></iframe>
83+
</mat-card-content>
84+
</mat-card>
85+
86+
<mat-card>
87+
<mat-toolbar color="primary">Dynamic page content</mat-toolbar>
88+
<mat-card-content>
89+
<button mat-raised-button (click)="toggleFocus(dynamicFocusTrap)">
90+
{{dynamicFocusTrap && dynamicFocusTrap.enabled ? "Disable" : "Enable"}} FocusTrap
91+
</button>
92+
<div class="demo-focus-trap-region" #dynamicDemoRegion
93+
[class.demo-focus-trap-enabled]="dynamicFocusTrap && dynamicFocusTrap.enabled">
94+
<textarea placeholder="One"></textarea>
95+
<textarea placeholder="Two"></textarea>
96+
<button mat-raised-button (click)="addNewElement()">Click to add more focusable elements to the page</button>
97+
</div>
98+
<div #newElements></div>
99+
</mat-card-content>
100+
</mat-card>
101+
102+
<mat-card>
103+
<mat-toolbar color="primary">Dialog-on-dialog</mat-toolbar>
104+
<mat-card-content>
105+
<button mat-raised-button (click)="openDialog()">Open dialog</button>
106+
</mat-card-content>
107+
</mat-card>
108+
</div>
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
div.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+
button, textarea, .demo-focus-trap-shadow-root {
12+
display: block;
13+
margin: 4px;
14+
}
15+
16+
div.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+
.mat-card {
28+
padding: 0;
29+
margin: 16px;
30+
31+
& .mat-toolbar {
32+
margin: 0;
33+
}
34+
35+
& .mat-card-content {
36+
padding: 24px;
37+
}
38+
}
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
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+
setTimeout(() => {
66+
this.basicFocusTrap = this._focusTrapFactory.create(this._basicDemoRegion.nativeElement);
67+
this.basicFocusTrap.enabled = false;
68+
69+
this.nestedOuterFocusTrap = this._focusTrapFactory.create(
70+
this._nestedOuterDemoRegion.nativeElement);
71+
this.nestedOuterFocusTrap.enabled = false;
72+
73+
this.nestedInnerFocusTrap = this._focusTrapFactory.create(
74+
this._nestedInnerDemoRegion.nativeElement);
75+
this.nestedInnerFocusTrap.enabled = false;
76+
77+
this.tabIndexFocusTrap = this._focusTrapFactory.create(
78+
this._tabIndexDemoRegion.nativeElement);
79+
this.tabIndexFocusTrap.enabled = false;
80+
81+
this.shadowDOMFocusTrap = this._focusTrapFactory.create(
82+
this._shadowDOMDemoRegion.nativeElement);
83+
this.shadowDOMFocusTrap.enabled = false;
84+
85+
this.iframeFocusTrap = this._focusTrapFactory.create(this._iframeDemoRegion.nativeElement);
86+
this.iframeFocusTrap.enabled = false;
87+
88+
this.dynamicFocusTrap = this._focusTrapFactory.create(this._dynamicDemoRegion.nativeElement);
89+
this.dynamicFocusTrap.enabled = false;
90+
});
91+
}
92+
93+
toggleFocus(focusTrap: FocusTrap) {
94+
focusTrap.enabled = !focusTrap.enabled;
95+
if (focusTrap.enabled) {
96+
focusTrap.focusInitialElementWhenReady();
97+
}
98+
}
99+
100+
addNewElement() {
101+
const textarea = document.createElement('textarea');
102+
textarea.setAttribute('placeholder', 'I am a new element!');
103+
this._newElements.nativeElement.appendChild(textarea);
104+
}
105+
106+
openDialog() {
107+
this.dialog.open(FocusTrapDialogDemo);
108+
}
109+
}
110+
111+
let dialogCount = 0;
112+
113+
@Component({
114+
selector: 'focus-trap-dialog-demo',
115+
styles: [`
116+
textarea {
117+
display: block;
118+
margin: 4px;
119+
}
120+
`],
121+
template: `
122+
<h2 mat-dialog-title>Dialog {{id}}</h2>
123+
124+
<mat-dialog-content>
125+
<textarea placeholder="One"></textarea>
126+
<textarea placeholder="Two"></textarea>
127+
</mat-dialog-content>
128+
129+
<mat-dialog-actions>
130+
<button
131+
mat-raised-button
132+
mat-dialog-close>Close</button>
133+
134+
<button
135+
mat-raised-button
136+
(click)="openAnotherDialog()">
137+
Open another dialog</button>
138+
</mat-dialog-actions>
139+
`
140+
})
141+
export class FocusTrapDialogDemo {
142+
id = dialogCount++;
143+
constructor(public dialog: MatDialog) {}
144+
145+
openAnotherDialog() {
146+
this.dialog.open(FocusTrapDialogDemo);
147+
}
148+
}

0 commit comments

Comments
 (0)