Skip to content

Commit e1ca089

Browse files
feat(igxFocusTrap): add new focus trap directive and such property in the dialog #8961 (#10383)
1 parent ed6e12f commit e1ca089

File tree

11 files changed

+427
-22
lines changed

11 files changed

+427
-22
lines changed

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,24 @@ All notable changes for each version of this project will be documented in this
2020

2121
- For more information, check out the [README](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/stepper/README.md), [specification](https://github.com/IgniteUI/igniteui-angular/wiki/Stepper-Specification) and [official documentation](https://www.infragistics.com/products/ignite-ui-angular/angular/components/stepper).
2222

23+
- Added `IgxFocusTrap` directive, which traps the Tab key focus within an element.
24+
25+
```html
26+
<div #wrapper [igxFocusTrap]="true" tabindex="0">
27+
<input type="text" placeholder="Enter Username" name="uname">
28+
<input type="password" placeholder="Enter Password" name="psw">
29+
<button>SIGN IN</button>
30+
</div>
31+
```
32+
2333
- `IgxCsvExporterService`, `IgxExcelExporterService`
2434
- Exporter services are no longer required to be provided in the application since they are now injected on a root level.
2535
- `IgxGridToolbarPinningComponent`, `IgxGridToolbarHidingComponent`
2636
- Exposed new input `buttonText` which sets the text that is displayed inside the dropdown button in the toolbar.
2737
- `IgxCombo`
2838
- Added `groupSortingDirection` input, which allows you to set groups sorting order.
39+
- `IgxDialog`
40+
- Added `focusTrap` input to set whether the Tab key focus is trapped within the dialog when opened. Defaults to `true`.
2941

3042
### General
3143

README.md

Lines changed: 19 additions & 19 deletions
Large diffs are not rendered by default.

projects/igniteui-angular/src/lib/dialog/dialog-content.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<div tabindex="0" #dialog class="igx-dialog" igxToggle (click)="onDialogSelected($event)">
1+
<div tabindex="0" #dialog class="igx-dialog" igxToggle [igxFocusTrap]="focusTrap" (click)="onDialogSelected($event)">
22
<div #dialogWindow class="igx-dialog__window" [attr.role]="role" [attr.aria-labelledby]="titleId">
33

44
<div *ngIf="title" [attr.id]="titleId" class="igx-dialog__window-title">

projects/igniteui-angular/src/lib/dialog/dialog.component.spec.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { configureTestSuite } from '../test-utils/configure-suite';
88
import { useAnimation } from '@angular/animations';
99
import { PositionSettings, HorizontalAlignment, VerticalAlignment } from '../services/overlay/utilities';
1010
import { slideOutBottom, slideInTop } from '../animations/main';
11+
import { IgxToggleDirective } from '../directives/toggle/toggle.directive';
1112

1213
const OVERLAY_MAIN_CLASS = 'igx-overlay';
1314
const OVERLAY_WRAPPER_CLASS = `${OVERLAY_MAIN_CLASS}__wrapper`;
@@ -81,6 +82,68 @@ describe('Dialog', () => {
8182
expect(messageDebugElement.nativeElement.textContent.trim()).toEqual(expectedMessage);
8283
});
8384

85+
it('Should focus focusable elements in dialog on Tab key pressed', () => {
86+
const fix = TestBed.createComponent(DialogComponent);
87+
fix.detectChanges();
88+
89+
const dialog = fix.componentInstance.dialog;
90+
dialog.open();
91+
fix.detectChanges();
92+
93+
const buttons = fix.debugElement.queryAll(By.css('button'));
94+
const toggle = fix.debugElement.query(By.directive(IgxToggleDirective));
95+
96+
UIInteractions.triggerEventHandlerKeyDown('Tab', toggle);
97+
fix.detectChanges();
98+
expect(document.activeElement).toEqual(buttons[0].nativeElement);
99+
100+
UIInteractions.triggerEventHandlerKeyDown('Tab', toggle);
101+
fix.detectChanges();
102+
expect(document.activeElement).toEqual(buttons[1].nativeElement);
103+
104+
UIInteractions.triggerEventHandlerKeyDown('Tab', toggle);
105+
fix.detectChanges();
106+
expect(document.activeElement).toEqual(buttons[0].nativeElement);
107+
108+
UIInteractions.triggerEventHandlerKeyDown('Tab', toggle, false, true);
109+
fix.detectChanges();
110+
expect(document.activeElement).toEqual(buttons[1].nativeElement);
111+
112+
UIInteractions.triggerEventHandlerKeyDown('Tab', toggle, false, true);
113+
fix.detectChanges();
114+
expect(document.activeElement).toEqual(buttons[0].nativeElement);
115+
116+
UIInteractions.triggerEventHandlerKeyDown('Tab', toggle, false, true);
117+
fix.detectChanges();
118+
expect(document.activeElement).toEqual(buttons[1].nativeElement);
119+
});
120+
121+
it('should trap focus on dialog modal with non-focusable elements', () => {
122+
const fix = TestBed.createComponent(AlertComponent);
123+
fix.detectChanges();
124+
125+
const dialog = fix.componentInstance.dialog;
126+
dialog.leftButtonLabel = '';
127+
fix.detectChanges();
128+
129+
dialog.open();
130+
fix.detectChanges();
131+
132+
const toggle = fix.debugElement.query(By.directive(IgxToggleDirective));
133+
134+
UIInteractions.triggerEventHandlerKeyDown('Tab', toggle);
135+
fix.detectChanges();
136+
expect(document.activeElement).toEqual(toggle.nativeElement);
137+
138+
UIInteractions.triggerEventHandlerKeyDown('Tab', toggle, false, true);
139+
fix.detectChanges();
140+
expect(document.activeElement).toEqual(toggle.nativeElement);
141+
142+
UIInteractions.triggerEventHandlerKeyDown('Tab', toggle);
143+
fix.detectChanges();
144+
expect(document.activeElement).toEqual(toggle.nativeElement);
145+
});
146+
84147
it('Should open and close dialog when set values to IsOpen', fakeAsync(() => {
85148
const fixture = TestBed.createComponent(AlertComponent);
86149
const dialog = fixture.componentInstance.dialog;

projects/igniteui-angular/src/lib/dialog/dialog.component.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { IgxToggleModule, IgxToggleDirective } from '../directives/toggle/toggle
2323
import { OverlaySettings, GlobalPositionStrategy, NoOpScrollStrategy, PositionSettings } from '../services/public_api';
2424
import {fadeIn, fadeOut} from '../animations/fade/index';
2525
import { IgxFocusModule } from '../directives/focus/focus.directive';
26+
import { IgxFocusTrapModule } from '../directives/focus-trap/focus-trap.directive';
2627
import { CancelableEventArgs, IBaseEventArgs } from '../core/utils';
2728

2829
let DIALOG_ID = 0;
@@ -110,6 +111,16 @@ export class IgxDialogComponent implements IToggleView, OnInit, OnDestroy, After
110111
this._closeOnEscape = val;
111112
}
112113

114+
/**
115+
* An @Input property to set whether the Tab key focus is trapped within the dialog when opened.
116+
* Defaults to `true`.
117+
* ```html
118+
* <igx-dialog focusTrap="false""></igx-dialog>
119+
* ```
120+
*/
121+
@Input()
122+
public focusTrap = true;
123+
113124
/**
114125
* An @Input property controlling the `title` of the dialog.
115126
* ```html
@@ -619,6 +630,6 @@ export interface IDialogCancellableEventArgs extends IDialogEventArgs, Cancelabl
619630
@NgModule({
620631
declarations: [IgxDialogComponent, IgxDialogTitleDirective, IgxDialogActionsDirective],
621632
exports: [IgxDialogComponent, IgxDialogTitleDirective, IgxDialogActionsDirective],
622-
imports: [CommonModule, IgxToggleModule, IgxButtonModule, IgxRippleModule, IgxFocusModule]
633+
imports: [CommonModule, IgxToggleModule, IgxButtonModule, IgxRippleModule, IgxFocusModule, IgxFocusTrapModule]
623634
})
624635
export class IgxDialogModule { }
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# IgxFocusTrap Directive
2+
3+
The **IgxFocusTrap** directive provides functionality to trap the focus within an element. The focus should not leave the element when the user keeps tabbing through the focusable elements. Typically, when the focus leaves the last element, it should move to the first element. And vice versa, when SHIFT + TAB is pressed, when the focus leaves the first element, the last element should be focused. In case the element does not contain any focusable elements, the focus will be trapped on the element itself.
4+
5+
#Usage
6+
```typescript
7+
import { IgxFocusTrapModule } from "igniteui-angular";
8+
```
9+
10+
Basic initialization
11+
```html
12+
<div [igxFocusTrap]="true" tabindex="0">
13+
<input type="text" name="uname">
14+
<input type="password" name="psw">
15+
<button>SIGN IN</button>
16+
</div>
17+
```
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
import { Component } from '@angular/core';
2+
import { fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing';
3+
import { By } from '@angular/platform-browser';
4+
import { IgxFocusTrapDirective, IgxFocusTrapModule } from './focus-trap.directive';
5+
6+
import { configureTestSuite } from '../../test-utils/configure-suite';
7+
import { IgxCheckboxModule } from '../../checkbox/checkbox.component';
8+
import { IgxDatePickerModule } from '../../date-picker/public_api';
9+
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
10+
import { UIInteractions } from '../../test-utils/ui-interactions.spec';
11+
12+
describe('igxFocusTrap', () => {
13+
configureTestSuite();
14+
beforeAll(waitForAsync(() => {
15+
TestBed.configureTestingModule({
16+
declarations: [
17+
TrapFocusTestComponent
18+
],
19+
imports: [IgxFocusTrapModule, IgxCheckboxModule, IgxDatePickerModule, NoopAnimationsModule]
20+
}).compileComponents();
21+
}));
22+
23+
afterEach(() => {
24+
UIInteractions.clearOverlay();
25+
});
26+
27+
it('should focus focusable elements on Tab key pressed', () => {
28+
const fix = TestBed.createComponent(TrapFocusTestComponent);
29+
fix.detectChanges();
30+
31+
const focusTrap = fix.debugElement.query(By.directive(IgxFocusTrapDirective));
32+
const button = fix.debugElement.query(By.css('button'));
33+
const inputs = fix.debugElement.queryAll(By.css('input'));
34+
35+
UIInteractions.triggerEventHandlerKeyDown('Tab', focusTrap);
36+
fix.detectChanges();
37+
expect(document.activeElement).toEqual(inputs[0].nativeElement);
38+
39+
UIInteractions.triggerEventHandlerKeyDown('Tab', focusTrap);
40+
fix.detectChanges();
41+
expect(document.activeElement).toEqual(inputs[1].nativeElement);
42+
43+
UIInteractions.triggerEventHandlerKeyDown('Tab', focusTrap);
44+
fix.detectChanges();
45+
expect(document.activeElement).toEqual(button.nativeElement);
46+
47+
UIInteractions.triggerEventHandlerKeyDown('Tab', focusTrap);
48+
fix.detectChanges();
49+
expect(document.activeElement).toEqual(inputs[0].nativeElement);
50+
});
51+
52+
it('should focus focusable elements in reversed order on Shift + Tab key pressed', () => {
53+
const fix = TestBed.createComponent(TrapFocusTestComponent);
54+
fix.detectChanges();
55+
56+
const focusTrap = fix.debugElement.query(By.directive(IgxFocusTrapDirective));
57+
const button = fix.debugElement.query(By.css('button'));
58+
const inputs = fix.debugElement.queryAll(By.css('input'));
59+
60+
UIInteractions.triggerEventHandlerKeyDown('Tab', focusTrap, false, true);
61+
fix.detectChanges();
62+
expect(document.activeElement).toEqual(button.nativeElement);
63+
64+
UIInteractions.triggerEventHandlerKeyDown('Tab', focusTrap, false, true);
65+
fix.detectChanges();
66+
expect(document.activeElement).toEqual(inputs[1].nativeElement);
67+
68+
UIInteractions.triggerEventHandlerKeyDown('Tab', focusTrap, false, true);
69+
fix.detectChanges();
70+
expect(document.activeElement).toEqual(inputs[0].nativeElement);
71+
72+
UIInteractions.triggerEventHandlerKeyDown('Tab', focusTrap, false, true);
73+
fix.detectChanges();
74+
expect(document.activeElement).toEqual(button.nativeElement);
75+
});
76+
77+
it('should trap focus on element when there is only one focusable element', () => {
78+
const fix = TestBed.createComponent(TrapFocusTestComponent);
79+
fix.detectChanges();
80+
81+
fix.componentInstance.showInput = false;
82+
fix.detectChanges();
83+
84+
const focusTrap = fix.debugElement.query(By.directive(IgxFocusTrapDirective));
85+
const button = fix.debugElement.query(By.css('button'));
86+
87+
UIInteractions.triggerEventHandlerKeyDown('Tab', focusTrap);
88+
fix.detectChanges();
89+
expect(document.activeElement).toEqual(button.nativeElement);
90+
91+
UIInteractions.triggerEventHandlerKeyDown('Tab', focusTrap, false, true);
92+
fix.detectChanges();
93+
expect(document.activeElement).toEqual(button.nativeElement);
94+
95+
UIInteractions.triggerEventHandlerKeyDown('Tab', focusTrap);
96+
fix.detectChanges();
97+
expect(document.activeElement).toEqual(button.nativeElement);
98+
});
99+
100+
it('should trap focus on element with non-focusable elements', fakeAsync(() => {
101+
const fix = TestBed.createComponent(TrapFocusTestComponent);
102+
fix.detectChanges();
103+
104+
fix.componentInstance.showInput = false;
105+
fix.componentInstance.showButton = false;
106+
fix.detectChanges();
107+
108+
const focusTrap = fix.debugElement.query(By.directive(IgxFocusTrapDirective));
109+
110+
UIInteractions.triggerEventHandlerKeyDown('Tab', focusTrap);
111+
tick();
112+
fix.detectChanges();
113+
expect(document.activeElement).toEqual(focusTrap.nativeElement);
114+
115+
UIInteractions.triggerEventHandlerKeyDown('Tab', focusTrap, false, true);
116+
tick();
117+
fix.detectChanges();
118+
expect(document.activeElement).toEqual(focusTrap.nativeElement);
119+
120+
UIInteractions.triggerEventHandlerKeyDown('Tab', focusTrap);
121+
tick();
122+
fix.detectChanges();
123+
expect(document.activeElement).toEqual(focusTrap.nativeElement);
124+
}));
125+
126+
it('should be able to set focusTrap dynamically', fakeAsync(() => {
127+
const fix = TestBed.createComponent(TrapFocusTestComponent);
128+
fix.detectChanges();
129+
130+
const focusTrap = fix.debugElement.query(By.directive(IgxFocusTrapDirective));
131+
const button = fix.debugElement.query(By.css('button'));
132+
const inputs = fix.debugElement.queryAll(By.css('input'));
133+
134+
UIInteractions.triggerEventHandlerKeyDown('Tab', focusTrap);
135+
fix.detectChanges();
136+
expect(document.activeElement).toEqual(inputs[0].nativeElement);
137+
138+
UIInteractions.triggerEventHandlerKeyDown('Tab', focusTrap);
139+
fix.detectChanges();
140+
expect(document.activeElement).toEqual(inputs[1].nativeElement);
141+
142+
UIInteractions.triggerEventHandlerKeyDown('Tab', focusTrap);
143+
fix.detectChanges();
144+
expect(document.activeElement).toEqual(button.nativeElement);
145+
146+
button.nativeElement.blur();
147+
fix.detectChanges();
148+
149+
fix.componentInstance.focusTrap = false;
150+
fix.detectChanges();
151+
152+
UIInteractions.triggerEventHandlerKeyDown('Tab', focusTrap);
153+
fix.detectChanges();
154+
expect(document.activeElement).not.toEqual(inputs[0].nativeElement);
155+
156+
UIInteractions.triggerEventHandlerKeyDown('Tab', focusTrap, false, true);
157+
fix.detectChanges();
158+
expect(document.activeElement).not.toEqual(inputs[1].nativeElement);
159+
160+
fix.componentInstance.focusTrap = true;
161+
fix.detectChanges();
162+
163+
UIInteractions.triggerEventHandlerKeyDown('Tab', focusTrap);
164+
fix.detectChanges();
165+
expect(document.activeElement).toEqual(inputs[0].nativeElement);
166+
167+
UIInteractions.triggerEventHandlerKeyDown('Tab', focusTrap, false, true);
168+
fix.detectChanges();
169+
expect(document.activeElement).toEqual(button.nativeElement);
170+
}));
171+
});
172+
173+
174+
@Component({
175+
template: `<div #wrapper [igxFocusTrap]="focusTrap" tabindex="0">
176+
<label for="uname"><b>Username</b></label><br>
177+
<input type="text" *ngIf="showInput" placeholder="Enter Username" name="uname"><br>
178+
<label for="psw"><b>Password</b></label><br>
179+
<input type="password" *ngIf="showInput" placeholder="Enter Password" name="psw"><br>
180+
<button *ngIf="showButton">SIGN IN</button>
181+
</div>` })
182+
class TrapFocusTestComponent {
183+
public showInput = true;
184+
public showButton = true;
185+
public focusTrap = true;
186+
}

0 commit comments

Comments
 (0)