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; +}