1
- import { Platform } from '@angular/cdk/platform' ;
2
- import { Component , ViewChild , TemplateRef , ViewContainerRef } from '@angular/core' ;
1
+ import { Platform , _supportsShadowDom } from '@angular/cdk/platform' ;
2
+ import {
3
+ Component ,
4
+ ViewChild ,
5
+ TemplateRef ,
6
+ ViewContainerRef ,
7
+ ViewEncapsulation ,
8
+ } from '@angular/core' ;
3
9
import { waitForAsync , ComponentFixture , TestBed } from '@angular/core/testing' ;
4
10
import { PortalModule , CdkPortalOutlet , TemplatePortal } from '@angular/cdk/portal' ;
5
11
import { A11yModule , FocusTrap , CdkTrapFocus } from '../index' ;
12
+ import { By } from '@angular/platform-browser' ;
6
13
7
14
8
15
describe ( 'FocusTrap' , ( ) => {
@@ -19,6 +26,7 @@ describe('FocusTrap', () => {
19
26
FocusTrapWithAutoCapture ,
20
27
FocusTrapUnfocusableTarget ,
21
28
FocusTrapInsidePortal ,
29
+ FocusTrapWithAutoCaptureInShadowDom ,
22
30
] ,
23
31
} ) ;
24
32
@@ -40,7 +48,7 @@ describe('FocusTrap', () => {
40
48
// focus event handler directly.
41
49
const result = focusTrapInstance . focusFirstTabbableElement ( ) ;
42
50
43
- expect ( document . activeElement ! . nodeName . toLowerCase ( ) )
51
+ expect ( getActiveElement ( ) . nodeName . toLowerCase ( ) )
44
52
. toBe ( 'input' , 'Expected input element to be focused' ) ;
45
53
expect ( result ) . toBe ( true , 'Expected return value to be true if focus was shifted.' ) ;
46
54
} ) ;
@@ -54,7 +62,7 @@ describe('FocusTrap', () => {
54
62
// In iOS button elements are never tabbable, so the last element will be the input.
55
63
const lastElement = platform . IOS ? 'input' : 'button' ;
56
64
57
- expect ( document . activeElement ! . nodeName . toLowerCase ( ) )
65
+ expect ( getActiveElement ( ) . nodeName . toLowerCase ( ) )
58
66
. toBe ( lastElement , `Expected ${ lastElement } element to be focused` ) ;
59
67
60
68
expect ( result ) . toBe ( true , 'Expected return value to be true if focus was shifted.' ) ;
@@ -126,21 +134,21 @@ describe('FocusTrap', () => {
126
134
// Because we can't mimic a real tab press focus change in a unit test, just call the
127
135
// focus event handler directly.
128
136
focusTrapInstance . focusInitialElement ( ) ;
129
- expect ( document . activeElement ! . id ) . toBe ( 'middle' ) ;
137
+ expect ( getActiveElement ( ) . id ) . toBe ( 'middle' ) ;
130
138
} ) ;
131
139
132
140
it ( 'should be able to prioritize the first focus target' , ( ) => {
133
141
// Because we can't mimic a real tab press focus change in a unit test, just call the
134
142
// focus event handler directly.
135
143
focusTrapInstance . focusFirstTabbableElement ( ) ;
136
- expect ( document . activeElement ! . id ) . toBe ( 'first' ) ;
144
+ expect ( getActiveElement ( ) . id ) . toBe ( 'first' ) ;
137
145
} ) ;
138
146
139
147
it ( 'should be able to prioritize the last focus target' , ( ) => {
140
148
// Because we can't mimic a real tab press focus change in a unit test, just call the
141
149
// focus event handler directly.
142
150
focusTrapInstance . focusLastTabbableElement ( ) ;
143
- expect ( document . activeElement ! . id ) . toBe ( 'last' ) ;
151
+ expect ( getActiveElement ( ) . id ) . toBe ( 'last' ) ;
144
152
} ) ;
145
153
146
154
it ( 'should warn if the initial focus target is not focusable' , ( ) => {
@@ -176,16 +184,16 @@ describe('FocusTrap', () => {
176
184
177
185
const buttonOutsideTrappedRegion = fixture . nativeElement . querySelector ( 'button' ) ;
178
186
buttonOutsideTrappedRegion . focus ( ) ;
179
- expect ( document . activeElement ) . toBe ( buttonOutsideTrappedRegion ) ;
187
+ expect ( getActiveElement ( ) ) . toBe ( buttonOutsideTrappedRegion ) ;
180
188
181
189
fixture . componentInstance . showTrappedRegion = true ;
182
190
fixture . detectChanges ( ) ;
183
191
184
192
fixture . whenStable ( ) . then ( ( ) => {
185
- expect ( document . activeElement ! . id ) . toBe ( 'auto-capture-target' ) ;
193
+ expect ( getActiveElement ( ) . id ) . toBe ( 'auto-capture-target' ) ;
186
194
187
195
fixture . destroy ( ) ;
188
- expect ( document . activeElement ) . toBe ( buttonOutsideTrappedRegion ) ;
196
+ expect ( getActiveElement ( ) ) . toBe ( buttonOutsideTrappedRegion ) ;
189
197
} ) ;
190
198
} ) ) ;
191
199
@@ -197,19 +205,71 @@ describe('FocusTrap', () => {
197
205
198
206
const buttonOutsideTrappedRegion = fixture . nativeElement . querySelector ( 'button' ) ;
199
207
buttonOutsideTrappedRegion . focus ( ) ;
200
- expect ( document . activeElement ) . toBe ( buttonOutsideTrappedRegion ) ;
208
+ expect ( getActiveElement ( ) ) . toBe ( buttonOutsideTrappedRegion ) ;
201
209
202
210
fixture . componentInstance . autoCaptureEnabled = true ;
203
211
fixture . detectChanges ( ) ;
204
212
205
213
fixture . whenStable ( ) . then ( ( ) => {
206
- expect ( document . activeElement ! . id ) . toBe ( 'auto-capture-target' ) ;
214
+ expect ( getActiveElement ( ) . id ) . toBe ( 'auto-capture-target' ) ;
207
215
208
216
fixture . destroy ( ) ;
209
- expect ( document . activeElement ) . toBe ( buttonOutsideTrappedRegion ) ;
217
+ expect ( getActiveElement ( ) ) . toBe ( buttonOutsideTrappedRegion ) ;
210
218
} ) ;
211
219
} ) ) ;
212
220
221
+ it ( 'should automatically capture and return focus on init / destroy inside the shadow DOM' ,
222
+ waitForAsync ( ( ) => {
223
+ if ( ! _supportsShadowDom ( ) ) {
224
+ return ;
225
+ }
226
+
227
+ const fixture = TestBed . createComponent ( FocusTrapWithAutoCaptureInShadowDom ) ;
228
+ fixture . detectChanges ( ) ;
229
+
230
+ const buttonOutsideTrappedRegion =
231
+ fixture . debugElement . query ( By . css ( 'button' ) ) . nativeElement ;
232
+ buttonOutsideTrappedRegion . focus ( ) ;
233
+ expect ( getActiveElement ( ) ) . toBe ( buttonOutsideTrappedRegion ) ;
234
+
235
+ fixture . componentInstance . showTrappedRegion = true ;
236
+ fixture . detectChanges ( ) ;
237
+
238
+ fixture . whenStable ( ) . then ( ( ) => {
239
+ expect ( getActiveElement ( ) . id ) . toBe ( 'auto-capture-target' ) ;
240
+
241
+ fixture . destroy ( ) ;
242
+ expect ( getActiveElement ( ) ) . toBe ( buttonOutsideTrappedRegion ) ;
243
+ } ) ;
244
+ } ) ) ;
245
+
246
+ it ( 'should capture focus if auto capture is enabled later on inside the shadow DOM' ,
247
+ waitForAsync ( ( ) => {
248
+ if ( ! _supportsShadowDom ( ) ) {
249
+ return ;
250
+ }
251
+
252
+ const fixture = TestBed . createComponent ( FocusTrapWithAutoCaptureInShadowDom ) ;
253
+ fixture . componentInstance . autoCaptureEnabled = false ;
254
+ fixture . componentInstance . showTrappedRegion = true ;
255
+ fixture . detectChanges ( ) ;
256
+
257
+ const buttonOutsideTrappedRegion =
258
+ fixture . debugElement . query ( By . css ( 'button' ) ) . nativeElement ;
259
+ buttonOutsideTrappedRegion . focus ( ) ;
260
+ expect ( getActiveElement ( ) ) . toBe ( buttonOutsideTrappedRegion ) ;
261
+
262
+ fixture . componentInstance . autoCaptureEnabled = true ;
263
+ fixture . detectChanges ( ) ;
264
+
265
+ fixture . whenStable ( ) . then ( ( ) => {
266
+ expect ( getActiveElement ( ) . id ) . toBe ( 'auto-capture-target' ) ;
267
+
268
+ fixture . destroy ( ) ;
269
+ expect ( getActiveElement ( ) ) . toBe ( buttonOutsideTrappedRegion ) ;
270
+ } ) ;
271
+ } ) ) ;
272
+
213
273
} ) ;
214
274
215
275
it ( 'should put anchors inside the outlet when set at the root of a template portal' , ( ) => {
@@ -234,6 +294,11 @@ describe('FocusTrap', () => {
234
294
} ) ;
235
295
} ) ;
236
296
297
+ /** Gets the currently-focused element while accounting for the shadow DOM. */
298
+ function getActiveElement ( ) {
299
+ const activeElement = document . activeElement as HTMLElement | null ;
300
+ return activeElement ?. shadowRoot ?. activeElement as HTMLElement || activeElement ;
301
+ }
237
302
238
303
@Component ( {
239
304
template : `
@@ -247,21 +312,27 @@ class SimpleFocusTrap {
247
312
@ViewChild ( CdkTrapFocus ) focusTrapDirective : CdkTrapFocus ;
248
313
}
249
314
250
- @ Component ( {
251
- template : `
252
- <button type="button">Toggle</button >
253
- <div *ngIf="showTrappedRegion" cdkTrapFocus [cdkTrapFocusAutoCapture]="autoCaptureEnabled ">
254
- <input id="auto-capture-target" >
255
- <button>SAVE</button >
256
- </div>
257
- `
258
- } )
315
+ const AUTO_FOCUS_TEMPLATE = `
316
+ <button type="button">Toggle</button>
317
+ <div *ngIf="showTrappedRegion" cdkTrapFocus [cdkTrapFocusAutoCapture]="autoCaptureEnabled" >
318
+ <input id="auto-capture-target ">
319
+ <button>SAVE</button >
320
+ </div >
321
+ ` ;
322
+
323
+ @ Component ( { template : AUTO_FOCUS_TEMPLATE } )
259
324
class FocusTrapWithAutoCapture {
260
325
@ViewChild ( CdkTrapFocus ) focusTrapDirective : CdkTrapFocus ;
261
326
showTrappedRegion = false ;
262
327
autoCaptureEnabled = true ;
263
328
}
264
329
330
+ @Component ( {
331
+ template : AUTO_FOCUS_TEMPLATE ,
332
+ encapsulation : ViewEncapsulation . ShadowDom
333
+ } )
334
+ class FocusTrapWithAutoCaptureInShadowDom extends FocusTrapWithAutoCapture {
335
+ }
265
336
266
337
@Component ( {
267
338
template : `
0 commit comments