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,7 +134,7 @@ 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 pass in focus options to initial focusable element' , ( ) => {
@@ -141,7 +149,7 @@ describe('FocusTrap', () => {
141
149
// Because we can't mimic a real tab press focus change in a unit test, just call the
142
150
// focus event handler directly.
143
151
focusTrapInstance . focusFirstTabbableElement ( ) ;
144
- expect ( document . activeElement ! . id ) . toBe ( 'first' ) ;
152
+ expect ( getActiveElement ( ) . id ) . toBe ( 'first' ) ;
145
153
} ) ;
146
154
147
155
it ( 'should be able to pass in focus options to first focusable element' , ( ) => {
@@ -156,7 +164,7 @@ describe('FocusTrap', () => {
156
164
// Because we can't mimic a real tab press focus change in a unit test, just call the
157
165
// focus event handler directly.
158
166
focusTrapInstance . focusLastTabbableElement ( ) ;
159
- expect ( document . activeElement ! . id ) . toBe ( 'last' ) ;
167
+ expect ( getActiveElement ( ) . id ) . toBe ( 'last' ) ;
160
168
} ) ;
161
169
162
170
it ( 'should be able to pass in focus options to last focusable element' , ( ) => {
@@ -200,16 +208,16 @@ describe('FocusTrap', () => {
200
208
201
209
const buttonOutsideTrappedRegion = fixture . nativeElement . querySelector ( 'button' ) ;
202
210
buttonOutsideTrappedRegion . focus ( ) ;
203
- expect ( document . activeElement ) . toBe ( buttonOutsideTrappedRegion ) ;
211
+ expect ( getActiveElement ( ) ) . toBe ( buttonOutsideTrappedRegion ) ;
204
212
205
213
fixture . componentInstance . showTrappedRegion = true ;
206
214
fixture . detectChanges ( ) ;
207
215
208
216
fixture . whenStable ( ) . then ( ( ) => {
209
- expect ( document . activeElement ! . id ) . toBe ( 'auto-capture-target' ) ;
217
+ expect ( getActiveElement ( ) . id ) . toBe ( 'auto-capture-target' ) ;
210
218
211
219
fixture . destroy ( ) ;
212
- expect ( document . activeElement ) . toBe ( buttonOutsideTrappedRegion ) ;
220
+ expect ( getActiveElement ( ) ) . toBe ( buttonOutsideTrappedRegion ) ;
213
221
} ) ;
214
222
} ) ) ;
215
223
@@ -221,19 +229,71 @@ describe('FocusTrap', () => {
221
229
222
230
const buttonOutsideTrappedRegion = fixture . nativeElement . querySelector ( 'button' ) ;
223
231
buttonOutsideTrappedRegion . focus ( ) ;
224
- expect ( document . activeElement ) . toBe ( buttonOutsideTrappedRegion ) ;
232
+ expect ( getActiveElement ( ) ) . toBe ( buttonOutsideTrappedRegion ) ;
225
233
226
234
fixture . componentInstance . autoCaptureEnabled = true ;
227
235
fixture . detectChanges ( ) ;
228
236
229
237
fixture . whenStable ( ) . then ( ( ) => {
230
- expect ( document . activeElement ! . id ) . toBe ( 'auto-capture-target' ) ;
238
+ expect ( getActiveElement ( ) . id ) . toBe ( 'auto-capture-target' ) ;
231
239
232
240
fixture . destroy ( ) ;
233
- expect ( document . activeElement ) . toBe ( buttonOutsideTrappedRegion ) ;
241
+ expect ( getActiveElement ( ) ) . toBe ( buttonOutsideTrappedRegion ) ;
234
242
} ) ;
235
243
} ) ) ;
236
244
245
+ it ( 'should automatically capture and return focus on init / destroy inside the shadow DOM' ,
246
+ waitForAsync ( ( ) => {
247
+ if ( ! _supportsShadowDom ( ) ) {
248
+ return ;
249
+ }
250
+
251
+ const fixture = TestBed . createComponent ( FocusTrapWithAutoCaptureInShadowDom ) ;
252
+ fixture . detectChanges ( ) ;
253
+
254
+ const buttonOutsideTrappedRegion =
255
+ fixture . debugElement . query ( By . css ( 'button' ) ) . nativeElement ;
256
+ buttonOutsideTrappedRegion . focus ( ) ;
257
+ expect ( getActiveElement ( ) ) . toBe ( buttonOutsideTrappedRegion ) ;
258
+
259
+ fixture . componentInstance . showTrappedRegion = true ;
260
+ fixture . detectChanges ( ) ;
261
+
262
+ fixture . whenStable ( ) . then ( ( ) => {
263
+ expect ( getActiveElement ( ) . id ) . toBe ( 'auto-capture-target' ) ;
264
+
265
+ fixture . destroy ( ) ;
266
+ expect ( getActiveElement ( ) ) . toBe ( buttonOutsideTrappedRegion ) ;
267
+ } ) ;
268
+ } ) ) ;
269
+
270
+ it ( 'should capture focus if auto capture is enabled later on inside the shadow DOM' ,
271
+ waitForAsync ( ( ) => {
272
+ if ( ! _supportsShadowDom ( ) ) {
273
+ return ;
274
+ }
275
+
276
+ const fixture = TestBed . createComponent ( FocusTrapWithAutoCaptureInShadowDom ) ;
277
+ fixture . componentInstance . autoCaptureEnabled = false ;
278
+ fixture . componentInstance . showTrappedRegion = true ;
279
+ fixture . detectChanges ( ) ;
280
+
281
+ const buttonOutsideTrappedRegion =
282
+ fixture . debugElement . query ( By . css ( 'button' ) ) . nativeElement ;
283
+ buttonOutsideTrappedRegion . focus ( ) ;
284
+ expect ( getActiveElement ( ) ) . toBe ( buttonOutsideTrappedRegion ) ;
285
+
286
+ fixture . componentInstance . autoCaptureEnabled = true ;
287
+ fixture . detectChanges ( ) ;
288
+
289
+ fixture . whenStable ( ) . then ( ( ) => {
290
+ expect ( getActiveElement ( ) . id ) . toBe ( 'auto-capture-target' ) ;
291
+
292
+ fixture . destroy ( ) ;
293
+ expect ( getActiveElement ( ) ) . toBe ( buttonOutsideTrappedRegion ) ;
294
+ } ) ;
295
+ } ) ) ;
296
+
237
297
} ) ;
238
298
239
299
it ( 'should put anchors inside the outlet when set at the root of a template portal' , ( ) => {
@@ -258,6 +318,11 @@ describe('FocusTrap', () => {
258
318
} ) ;
259
319
} ) ;
260
320
321
+ /** Gets the currently-focused element while accounting for the shadow DOM. */
322
+ function getActiveElement ( ) {
323
+ const activeElement = document . activeElement as HTMLElement | null ;
324
+ return activeElement ?. shadowRoot ?. activeElement as HTMLElement || activeElement ;
325
+ }
261
326
262
327
@Component ( {
263
328
template : `
@@ -271,21 +336,27 @@ class SimpleFocusTrap {
271
336
@ViewChild ( CdkTrapFocus ) focusTrapDirective : CdkTrapFocus ;
272
337
}
273
338
274
- @ Component ( {
275
- template : `
276
- <button type="button">Toggle</button >
277
- <div *ngIf="showTrappedRegion" cdkTrapFocus [cdkTrapFocusAutoCapture]="autoCaptureEnabled ">
278
- <input id="auto-capture-target" >
279
- <button>SAVE</button >
280
- </div>
281
- `
282
- } )
339
+ const AUTO_FOCUS_TEMPLATE = `
340
+ <button type="button">Toggle</button>
341
+ <div *ngIf="showTrappedRegion" cdkTrapFocus [cdkTrapFocusAutoCapture]="autoCaptureEnabled" >
342
+ <input id="auto-capture-target ">
343
+ <button>SAVE</button >
344
+ </div >
345
+ ` ;
346
+
347
+ @ Component ( { template : AUTO_FOCUS_TEMPLATE } )
283
348
class FocusTrapWithAutoCapture {
284
349
@ViewChild ( CdkTrapFocus ) focusTrapDirective : CdkTrapFocus ;
285
350
showTrappedRegion = false ;
286
351
autoCaptureEnabled = true ;
287
352
}
288
353
354
+ @Component ( {
355
+ template : AUTO_FOCUS_TEMPLATE ,
356
+ encapsulation : ViewEncapsulation . ShadowDom
357
+ } )
358
+ class FocusTrapWithAutoCaptureInShadowDom extends FocusTrapWithAutoCapture {
359
+ }
289
360
290
361
@Component ( {
291
362
template : `
0 commit comments