Skip to content

Commit d4984fd

Browse files
committed
fix: address feedback
1 parent d944dde commit d4984fd

File tree

7 files changed

+91
-59
lines changed

7 files changed

+91
-59
lines changed

src/lib/core/a11y/focus-trap.spec.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {ComponentFixture, TestBed, async} from '@angular/core/testing';
22
import {Component, ViewChild} from '@angular/core';
3-
import {FocusTrapService, FocusTrapDirective, FocusTrap} from './focus-trap';
3+
import {FocusTrapFactory, FocusTrapDirective, FocusTrap} from './focus-trap';
44
import {InteractivityChecker} from './interactivity-checker';
55
import {Platform} from '../platform/platform';
66

@@ -16,7 +16,7 @@ describe('FocusTrap', () => {
1616
beforeEach(async(() => {
1717
TestBed.configureTestingModule({
1818
declarations: [FocusTrapDirective, FocusTrapTestApp],
19-
providers: [InteractivityChecker, Platform, FocusTrapService]
19+
providers: [InteractivityChecker, Platform, FocusTrapFactory]
2020
});
2121

2222
TestBed.compileComponents();
@@ -65,7 +65,7 @@ describe('FocusTrap', () => {
6565

6666
expect(anchors.every(current => current.getAttribute('tabindex') === '0')).toBe(true);
6767

68-
fixture.componentInstance.isFocusTrapDisabled = true;
68+
fixture.componentInstance.isFocusTrapEnabled = false;
6969
fixture.detectChanges();
7070

7171
expect(anchors.every(current => current.getAttribute('tabindex') === '-1')).toBe(true);
@@ -79,7 +79,7 @@ describe('FocusTrap', () => {
7979
beforeEach(async(() => {
8080
TestBed.configureTestingModule({
8181
declarations: [FocusTrapDirective, FocusTrapTargetTestApp],
82-
providers: [InteractivityChecker, Platform, FocusTrapService]
82+
providers: [InteractivityChecker, Platform, FocusTrapFactory]
8383
});
8484

8585
TestBed.compileComponents();
@@ -108,27 +108,27 @@ describe('FocusTrap', () => {
108108

109109
@Component({
110110
template: `
111-
<cdk-focus-trap *ngIf="renderFocusTrap" [disabled]="isFocusTrapDisabled">
111+
<div *ngIf="renderFocusTrap" [cdkTrapFocus]="isFocusTrapEnabled">
112112
<input>
113113
<button>SAVE</button>
114-
</cdk-focus-trap>
114+
</div>
115115
`
116116
})
117117
class FocusTrapTestApp {
118118
@ViewChild(FocusTrapDirective) focusTrapDirective: FocusTrapDirective;
119119
renderFocusTrap = true;
120-
isFocusTrapDisabled = false;
120+
isFocusTrapEnabled = true;
121121
}
122122

123123

124124
@Component({
125125
template: `
126-
<cdk-focus-trap>
126+
<div cdkTrapFocus>
127127
<input>
128128
<button id="last" cdk-focus-end></button>
129129
<button id="first" cdk-focus-start>SAVE</button>
130130
<input>
131-
</cdk-focus-trap>
131+
</div>
132132
`
133133
})
134134
class FocusTrapTargetTestApp {

src/lib/core/a11y/focus-trap.ts

Lines changed: 42 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,12 @@ export class FocusTrap {
2424
private _endAnchor: HTMLElement = this._createAnchor();
2525

2626
/** Whether the focus trap is active. */
27-
get disabled(): boolean { return this._disabled; }
28-
set disabled(val: boolean) {
29-
this._disabled = val;
30-
this._startAnchor.tabIndex = this._endAnchor.tabIndex = this._disabled ? -1 : 0;
31-
}
32-
private _disabled: boolean = false;
33-
34-
/** Element to which the focus trap is attached. */
35-
get element(): HTMLElement {
36-
return this._element;
27+
get enabled(): boolean { return this._enabled; }
28+
set enabled(val: boolean) {
29+
this._enabled = val;
30+
this._startAnchor.tabIndex = this._endAnchor.tabIndex = this._enabled ? 0 : -1;
3731
}
32+
private _enabled: boolean = false;
3833

3934
constructor(
4035
private _element: HTMLElement,
@@ -170,10 +165,10 @@ export class FocusTrap {
170165

171166

172167
/**
173-
* Service that allows easy instantiation of focus traps.
168+
* Factory that allows easy instantiation of focus traps.
174169
*/
175170
@Injectable()
176-
export class FocusTrapService {
171+
export class FocusTrapFactory {
177172
constructor(private _checker: InteractivityChecker, private _ngZone: NgZone) { }
178173

179174
attach(element: HTMLElement, deferAnchors = false): FocusTrap {
@@ -184,22 +179,51 @@ export class FocusTrapService {
184179

185180
/**
186181
* Directive for trapping focus within a region.
182+
* @deprecated
187183
*/
188184
@Directive({
189-
selector: 'cdk-focus-trap, [cdkFocusTrap]',
185+
selector: 'cdk-focus-trap',
190186
})
191-
export class FocusTrapDirective implements OnDestroy, AfterContentInit {
187+
export class FocusTrapDeprecatedDirective implements OnDestroy, AfterContentInit {
192188
focusTrap: FocusTrap;
193189

194190
/** Whether the focus trap is active. */
195191
@Input()
196-
get disabled(): boolean { return this.focusTrap.disabled; }
192+
get disabled(): boolean { return !this.focusTrap.enabled; }
197193
set disabled(val: boolean) {
198-
this.focusTrap.disabled = coerceBooleanProperty(val);
194+
this.focusTrap.enabled = !coerceBooleanProperty(val);
195+
}
196+
197+
constructor(private _elementRef: ElementRef, private _focusTrapFactory: FocusTrapFactory) {
198+
this.focusTrap = this._focusTrapFactory.attach(this._elementRef.nativeElement, true);
199+
}
200+
201+
ngOnDestroy() {
202+
this.focusTrap.destroy();
203+
}
204+
205+
ngAfterContentInit() {
206+
this.focusTrap.attachAnchors();
199207
}
208+
}
209+
210+
211+
/**
212+
* Directive for trapping focus within a region.
213+
*/
214+
@Directive({
215+
selector: '[cdkTrapFocus]'
216+
})
217+
export class FocusTrapDirective implements OnDestroy, AfterContentInit {
218+
focusTrap: FocusTrap;
219+
220+
/** Whether the focus trap is active. */
221+
@Input('cdkTrapFocus')
222+
get enabled(): boolean { return this.focusTrap.enabled; }
223+
set enabled(val: boolean) { this.focusTrap.enabled = val; }
200224

201-
constructor(private _elementRef: ElementRef, private _focusTrapService: FocusTrapService) {
202-
this.focusTrap = this._focusTrapService.attach(this._elementRef.nativeElement, true);
225+
constructor(private _elementRef: ElementRef, private _focusTrapFactory: FocusTrapFactory) {
226+
this.focusTrap = this._focusTrapFactory.attach(this._elementRef.nativeElement, true);
203227
}
204228

205229
ngOnDestroy() {

src/lib/core/a11y/index.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import {NgModule, ModuleWithProviders} from '@angular/core';
2-
import {FocusTrapDirective, FocusTrapService} from './focus-trap';
2+
import {FocusTrapDirective, FocusTrapDeprecatedDirective, FocusTrapFactory} from './focus-trap';
33
import {LIVE_ANNOUNCER_PROVIDER} from './live-announcer';
44
import {InteractivityChecker} from './interactivity-checker';
55
import {CommonModule} from '@angular/common';
66
import {PlatformModule} from '../platform/index';
77

88
@NgModule({
99
imports: [CommonModule, PlatformModule],
10-
declarations: [FocusTrapDirective],
11-
exports: [FocusTrapDirective],
12-
providers: [InteractivityChecker, FocusTrapService, LIVE_ANNOUNCER_PROVIDER]
10+
declarations: [FocusTrapDirective, FocusTrapDeprecatedDirective],
11+
exports: [FocusTrapDirective, FocusTrapDeprecatedDirective],
12+
providers: [InteractivityChecker, FocusTrapFactory, LIVE_ANNOUNCER_PROVIDER]
1313
})
1414
export class A11yModule {
1515
/** @deprecated */

src/lib/dialog/dialog-container.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {BasePortalHost, ComponentPortal, PortalHostDirective, TemplatePortal} fr
1212
import {MdDialogConfig} from './dialog-config';
1313
import {MdDialogRef} from './dialog-ref';
1414
import {MdDialogContentAlreadyAttachedError} from './dialog-errors';
15-
import {FocusTrapService, FocusTrap} from '../core/a11y/focus-trap';
15+
import {FocusTrapFactory, FocusTrap} from '../core/a11y/focus-trap';
1616
import 'rxjs/add/operator/first';
1717

1818

@@ -51,7 +51,7 @@ export class MdDialogContainer extends BasePortalHost implements OnDestroy {
5151
private _ngZone: NgZone,
5252
private _renderer: Renderer,
5353
private _elementRef: ElementRef,
54-
private _focusTrapService: FocusTrapService) {
54+
private _focusTrapFactory: FocusTrapFactory) {
5555

5656
super();
5757
}
@@ -90,7 +90,7 @@ export class MdDialogContainer extends BasePortalHost implements OnDestroy {
9090
*/
9191
private _trapFocus() {
9292
if (!this._focusTrap) {
93-
this._focusTrap = this._focusTrapService.attach(this._elementRef.nativeElement);
93+
this._focusTrap = this._focusTrapFactory.attach(this._elementRef.nativeElement);
9494
}
9595

9696
// If were to attempt to focus immediately, then the content of the dialog would not yet be

src/lib/sidenav/sidenav.html

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1 @@
1-
<cdk-focus-trap class="mat-sidenav-focus-trap" [disabled]="isFocusTrapDisabled">
2-
<ng-content></ng-content>
3-
</cdk-focus-trap>
1+
<ng-content></ng-content>

src/lib/sidenav/sidenav.scss

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,9 @@
100100
z-index: 3;
101101
min-width: 5vw;
102102
outline: 0;
103+
box-sizing: border-box;
104+
height: 100%;
105+
overflow-y: auto; // TODO(kara): revisit scrolling behavior for sidenavs
103106

104107
@include mat-sidenav-transition(0, -100%);
105108

@@ -133,16 +136,6 @@
133136
}
134137
}
135138

136-
.mat-sidenav-focus-trap {
137-
box-sizing: border-box;
138-
height: 100%;
139-
display: block;
140-
overflow-y: auto; // TODO(kara): revisit scrolling behavior for sidenavs
141-
142-
// Prevents unnecessary repaints while scrolling.
143-
transform: translateZ(0);
144-
}
145-
146139
.mat-sidenav-invalid {
147140
display: none;
148141
}

src/lib/sidenav/sidenav.ts

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,13 @@ import {
1313
EventEmitter,
1414
Renderer,
1515
ViewEncapsulation,
16-
ViewChild,
17-
NgZone
16+
NgZone,
17+
OnDestroy,
1818
} from '@angular/core';
1919
import {CommonModule} from '@angular/common';
2020
import {Dir, MdError, coerceBooleanProperty, CompatibilityModule} from '../core';
2121
import {A11yModule} from '../core/a11y/index';
22-
import {FocusTrapDirective} from '../core/a11y/focus-trap';
22+
import {FocusTrapFactory, FocusTrap} from '../core/a11y/focus-trap';
2323
import {ESCAPE} from '../core/keyboard/keycodes';
2424
import {OverlayModule} from '../core/overlay/overlay-directives';
2525
import 'rxjs/add/operator/first';
@@ -71,8 +71,8 @@ export class MdSidenavToggleResult {
7171
changeDetection: ChangeDetectionStrategy.OnPush,
7272
encapsulation: ViewEncapsulation.None,
7373
})
74-
export class MdSidenav implements AfterContentInit {
75-
@ViewChild(FocusTrapDirective) _focusTrapDirective: FocusTrapDirective;
74+
export class MdSidenav implements AfterContentInit, OnDestroy {
75+
private _focusTrap: FocusTrap;
7676

7777
/** Alignment of the sidenav (direction neutral); whether 'start' or 'end'. */
7878
private _align: 'start' | 'end' = 'start';
@@ -138,21 +138,25 @@ export class MdSidenav implements AfterContentInit {
138138
*/
139139
private _resolveToggleAnimationPromise: (animationFinished: boolean) => void = null;
140140

141-
get isFocusTrapDisabled() {
141+
get isFocusTrapEnabled() {
142142
// The focus trap is only enabled when the sidenav is open in any mode other than side.
143-
return !this.opened || this.mode == 'side';
143+
return this.opened && this.mode !== 'side';
144144
}
145145

146146
/**
147147
* @param _elementRef The DOM element reference. Used for transition and width calculation.
148148
* If not available we do not hook on transitions.
149149
*/
150-
constructor(private _elementRef: ElementRef, private _renderer: Renderer) {
150+
constructor(
151+
private _elementRef: ElementRef,
152+
private _renderer: Renderer,
153+
private _focusTrapFactory: FocusTrapFactory) {
154+
151155
this.onOpen.subscribe(() => {
152156
this._elementFocusedBeforeSidenavWasOpened = document.activeElement as HTMLElement;
153157

154-
if (!this.isFocusTrapDisabled) {
155-
this._focusTrapDirective.focusTrap.focusFirstTabbableElementWhenReady();
158+
if (this.isFocusTrapEnabled && this._focusTrap) {
159+
this._focusTrap.focusFirstTabbableElementWhenReady();
156160
}
157161
});
158162

@@ -168,14 +172,23 @@ export class MdSidenav implements AfterContentInit {
168172
}
169173

170174
ngAfterContentInit() {
171-
// This can happen when the sidenav is set to opened in the template and the transition
172-
// isn't ended.
175+
this._focusTrap = this._focusTrapFactory.attach(this._elementRef.nativeElement);
176+
this._focusTrap.enabled = this.isFocusTrapEnabled;
177+
178+
// This can happen when the sidenav is set to opened in
179+
// the template and the transition hasn't ended.
173180
if (this._toggleAnimationPromise) {
174181
this._resolveToggleAnimationPromise(true);
175182
this._toggleAnimationPromise = this._resolveToggleAnimationPromise = null;
176183
}
177184
}
178185

186+
ngOnDestroy() {
187+
if (this._focusTrap) {
188+
this._focusTrap.destroy();
189+
}
190+
}
191+
179192
/**
180193
* Whether the sidenav is opened. We overload this because we trigger an event when it
181194
* starts or end.
@@ -220,6 +233,10 @@ export class MdSidenav implements AfterContentInit {
220233

221234
this._opened = isOpen;
222235

236+
if (this._focusTrap) {
237+
this._focusTrap.enabled = this.isFocusTrapEnabled;
238+
}
239+
223240
if (isOpen) {
224241
this.onOpenStart.emit();
225242
} else {

0 commit comments

Comments
 (0)