Skip to content

Trap focus inside modal #10383

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 38 commits into from
Nov 9, 2021
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
70a9d08
feat(overlay): initial implementation FocusTrapDirective#8961
PlamenaMiteva Oct 27, 2021
fa9e804
Merge branch 'master' into PMiteva/overlay-focus
wnvko Nov 1, 2021
3b99448
feat(overlay): addressing comments#8961
PlamenaMiteva Nov 3, 2021
644e923
Merge branch 'PMiteva/overlay-focus' of https://github.com/IgniteUI/i…
PlamenaMiteva Nov 3, 2021
a925123
chore(overlay): fix lint errors#8961
PlamenaMiteva Nov 3, 2021
f22c49c
chore(overlay): fix lint errors#8961
PlamenaMiteva Nov 3, 2021
5c3a66c
test(igxFocusTrap): add directive tests#8961
PlamenaMiteva Nov 3, 2021
c06b6b9
chore(igxFocusTrap): fix lint errors#8961
PlamenaMiteva Nov 3, 2021
12d4bc5
test(igxFocusTrap): split igxFocusTrap tests#8961
PlamenaMiteva Nov 4, 2021
ff3634f
Merge branch 'master' into PMiteva/overlay-focus
wnvko Nov 4, 2021
6ae3d69
chore(igxFocusTrap): add changelog#8961
PlamenaMiteva Nov 5, 2021
aee91e0
Merge branch 'PMiteva/overlay-focus' of https://github.com/IgniteUI/i…
PlamenaMiteva Nov 5, 2021
46bdd0d
chore(igxFocusTrap): remove unnecessary imports#8961
PlamenaMiteva Nov 5, 2021
3173424
chore(igxFocusTrap): remove unnecessary imports#8961
PlamenaMiteva Nov 5, 2021
a25beaa
Merge branch 'PMiteva/overlay-focus' of https://github.com/IgniteUI/i…
PlamenaMiteva Nov 5, 2021
0ffa803
chore(igxFocusTrap): add README#8961
PlamenaMiteva Nov 5, 2021
78925a1
Merge branch 'master' into PMiteva/overlay-focus
wnvko Nov 5, 2021
a14e9f5
feat(igxFocusTrap): addressing comments#8961
PlamenaMiteva Nov 5, 2021
f46e56c
Merge branch 'PMiteva/overlay-focus' of https://github.com/IgniteUI/i…
PlamenaMiteva Nov 5, 2021
49f4bbc
feat(igxFousTrap): addressing comments#8961
PlamenaMiteva Nov 8, 2021
ee89aa2
chore(igxFousTrap): fix lint error#8961
PlamenaMiteva Nov 8, 2021
7c584fe
chore(igxFousTrap): change readme & changelog#8961
PlamenaMiteva Nov 8, 2021
a391f0e
Merge branch 'master' into PMiteva/overlay-focus
Lipata Nov 8, 2021
596d1d9
chore(focus-trap): fix dialog focus trap binding
wnvko Nov 8, 2021
435e450
Merge branch 'master' of https://github.com/IgniteUI/igniteui-angular…
PlamenaMiteva Nov 8, 2021
daebe2a
Merge branch 'PMiteva/overlay-focus' of https://github.com/IgniteUI/i…
PlamenaMiteva Nov 8, 2021
148e682
test(igxFousTrap): call clearOverlay() after each test#8961
PlamenaMiteva Nov 8, 2021
2238287
test(nav-drawer): relative parent container in test
Lipata Nov 8, 2021
0b44130
test(nav-drawer): absolute parent container in test
Lipata Nov 8, 2021
34c73e3
test(nav-drawer): set height to parent container
PlamenaMiteva Nov 9, 2021
9811c99
test(nav-drawer): use documentElement.clientHeight instead of window …
PlamenaMiteva Nov 9, 2021
9cd9c74
Merge branch 'master' into PMiteva/overlay-focus
Lipata Nov 9, 2021
425a474
test(nav-drawer): remove scroll from body
PlamenaMiteva Nov 9, 2021
db3122c
Merge branch 'PMiteva/overlay-focus' of https://github.com/IgniteUI/i…
PlamenaMiteva Nov 9, 2021
437e56c
chore(igxFousTrap): fix lint error#8961
PlamenaMiteva Nov 9, 2021
34119d6
test(nav-drawer): subtract scroll height from window height
PlamenaMiteva Nov 9, 2021
4e28017
test(nav-drawer): remove position relative style
PlamenaMiteva Nov 9, 2021
640ded7
test(nav-drawer): fix failing test
PlamenaMiteva Nov 9, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,24 @@ All notable changes for each version of this project will be documented in this

- 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).

- Added `IgxFocusTrap` directive, which traps the Tab key focus within an element.

```html
<div #wrapper [igxFocusTrap]="true" tabindex="0">
<input type="text" placeholder="Enter Username" name="uname">
<input type="password" placeholder="Enter Password" name="psw">
<button>SIGN IN</button>
</div>
```

- `IgxCsvExporterService`, `IgxExcelExporterService`
- Exporter services are no longer required to be provided in the application since they are now injected on a root level.
- `IgxGridToolbarPinningComponent`, `IgxGridToolbarHidingComponent`
- Exposed new input `buttonText` which sets the text that is displayed inside the dropdown button in the toolbar.
- `IgxCombo`
- Added `groupSortingDirection` input, which allows you to set groups sorting order.
- `IgxDialog`
- Added `focusTrap` input to set whether the Tab key focus is trapped within the dialog when opened. Defaults to `true`.

### General

Expand Down
38 changes: 19 additions & 19 deletions README.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<div tabindex="0" #dialog class="igx-dialog" igxToggle (click)="onDialogSelected($event)">
<div tabindex="0" #dialog class="igx-dialog" igxToggle [igxFocusTrap]="focusTrap" (click)="onDialogSelected($event)">
<div #dialogWindow class="igx-dialog__window" [attr.role]="role" [attr.aria-labelledby]="titleId">

<div *ngIf="title" [attr.id]="titleId" class="igx-dialog__window-title">
Expand Down
63 changes: 63 additions & 0 deletions projects/igniteui-angular/src/lib/dialog/dialog.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { configureTestSuite } from '../test-utils/configure-suite';
import { useAnimation } from '@angular/animations';
import { PositionSettings, HorizontalAlignment, VerticalAlignment } from '../services/overlay/utilities';
import { slideOutBottom, slideInTop } from '../animations/main';
import { IgxToggleDirective } from '../directives/toggle/toggle.directive';

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

it('Should focus focusable elements in dialog on Tab key pressed', () => {
const fix = TestBed.createComponent(DialogComponent);
fix.detectChanges();

const dialog = fix.componentInstance.dialog;
dialog.open();
fix.detectChanges();

const buttons = fix.debugElement.queryAll(By.css('button'));
const toggle = fix.debugElement.query(By.directive(IgxToggleDirective));

UIInteractions.triggerEventHandlerKeyDown('Tab', toggle);
fix.detectChanges();
expect(document.activeElement).toEqual(buttons[0].nativeElement);

UIInteractions.triggerEventHandlerKeyDown('Tab', toggle);
fix.detectChanges();
expect(document.activeElement).toEqual(buttons[1].nativeElement);

UIInteractions.triggerEventHandlerKeyDown('Tab', toggle);
fix.detectChanges();
expect(document.activeElement).toEqual(buttons[0].nativeElement);

UIInteractions.triggerEventHandlerKeyDown('Tab', toggle, false, true);
fix.detectChanges();
expect(document.activeElement).toEqual(buttons[1].nativeElement);

UIInteractions.triggerEventHandlerKeyDown('Tab', toggle, false, true);
fix.detectChanges();
expect(document.activeElement).toEqual(buttons[0].nativeElement);

UIInteractions.triggerEventHandlerKeyDown('Tab', toggle, false, true);
fix.detectChanges();
expect(document.activeElement).toEqual(buttons[1].nativeElement);
});

it('should trap focus on dialog modal with non-focusable elements', () => {
const fix = TestBed.createComponent(AlertComponent);
fix.detectChanges();

const dialog = fix.componentInstance.dialog;
dialog.leftButtonLabel = '';
fix.detectChanges();

dialog.open();
fix.detectChanges();

const toggle = fix.debugElement.query(By.directive(IgxToggleDirective));

UIInteractions.triggerEventHandlerKeyDown('Tab', toggle);
fix.detectChanges();
expect(document.activeElement).toEqual(toggle.nativeElement);

UIInteractions.triggerEventHandlerKeyDown('Tab', toggle, false, true);
fix.detectChanges();
expect(document.activeElement).toEqual(toggle.nativeElement);

UIInteractions.triggerEventHandlerKeyDown('Tab', toggle);
fix.detectChanges();
expect(document.activeElement).toEqual(toggle.nativeElement);
});

it('Should open and close dialog when set values to IsOpen', fakeAsync(() => {
const fixture = TestBed.createComponent(AlertComponent);
const dialog = fixture.componentInstance.dialog;
Expand Down
13 changes: 12 additions & 1 deletion projects/igniteui-angular/src/lib/dialog/dialog.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { IgxToggleModule, IgxToggleDirective } from '../directives/toggle/toggle
import { OverlaySettings, GlobalPositionStrategy, NoOpScrollStrategy, PositionSettings } from '../services/public_api';
import {fadeIn, fadeOut} from '../animations/fade/index';
import { IgxFocusModule } from '../directives/focus/focus.directive';
import { IgxFocusTrapModule } from '../directives/focus-trap/focus-trap.directive';
import { CancelableEventArgs, IBaseEventArgs } from '../core/utils';

let DIALOG_ID = 0;
Expand Down Expand Up @@ -110,6 +111,16 @@ export class IgxDialogComponent implements IToggleView, OnInit, OnDestroy, After
this._closeOnEscape = val;
}

/**
* An @Input property to set whether the Tab key focus is trapped within the dialog when opened.
* Defaults to `true`.
* ```html
* <igx-dialog focusTrap="false""></igx-dialog>
* ```
*/
@Input()
public focusTrap = true;

/**
* An @Input property controlling the `title` of the dialog.
* ```html
Expand Down Expand Up @@ -619,6 +630,6 @@ export interface IDialogCancellableEventArgs extends IDialogEventArgs, Cancelabl
@NgModule({
declarations: [IgxDialogComponent, IgxDialogTitleDirective, IgxDialogActionsDirective],
exports: [IgxDialogComponent, IgxDialogTitleDirective, IgxDialogActionsDirective],
imports: [CommonModule, IgxToggleModule, IgxButtonModule, IgxRippleModule, IgxFocusModule]
imports: [CommonModule, IgxToggleModule, IgxButtonModule, IgxRippleModule, IgxFocusModule, IgxFocusTrapModule]
})
export class IgxDialogModule { }
17 changes: 17 additions & 0 deletions projects/igniteui-angular/src/lib/directives/focus-trap/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# IgxFocusTrap Directive

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.

#Usage
```typescript
import { IgxFocusTrapModule } from "igniteui-angular";
```

Basic initialization
```html
<div [igxFocusTrap]="true" tabindex="0">
<input type="text" name="uname">
<input type="password" name="psw">
<button>SIGN IN</button>
</div>
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import { Component } from '@angular/core';
import { fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { IgxFocusTrapDirective, IgxFocusTrapModule } from './focus-trap.directive';

import { configureTestSuite } from '../../test-utils/configure-suite';
import { IgxCheckboxModule } from '../../checkbox/checkbox.component';
import { IgxDatePickerModule } from '../../date-picker/public_api';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { UIInteractions } from '../../test-utils/ui-interactions.spec';

describe('igxFocusTrap', () => {
configureTestSuite();
beforeAll(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [
TrapFocusTestComponent
],
imports: [IgxFocusTrapModule, IgxCheckboxModule, IgxDatePickerModule, NoopAnimationsModule]
}).compileComponents();
}));

it('should focus focusable elements on Tab key pressed', () => {
const fix = TestBed.createComponent(TrapFocusTestComponent);
fix.detectChanges();

const focusTrap = fix.debugElement.query(By.directive(IgxFocusTrapDirective));
const button = fix.debugElement.query(By.css('button'));
const inputs = fix.debugElement.queryAll(By.css('input'));

UIInteractions.triggerEventHandlerKeyDown('Tab', focusTrap);
fix.detectChanges();
expect(document.activeElement).toEqual(inputs[0].nativeElement);

UIInteractions.triggerEventHandlerKeyDown('Tab', focusTrap);
fix.detectChanges();
expect(document.activeElement).toEqual(inputs[1].nativeElement);

UIInteractions.triggerEventHandlerKeyDown('Tab', focusTrap);
fix.detectChanges();
expect(document.activeElement).toEqual(button.nativeElement);

UIInteractions.triggerEventHandlerKeyDown('Tab', focusTrap);
fix.detectChanges();
expect(document.activeElement).toEqual(inputs[0].nativeElement);
});

it('should focus focusable elements in reversed order on Shift + Tab key pressed', () => {
const fix = TestBed.createComponent(TrapFocusTestComponent);
fix.detectChanges();

const focusTrap = fix.debugElement.query(By.directive(IgxFocusTrapDirective));
const button = fix.debugElement.query(By.css('button'));
const inputs = fix.debugElement.queryAll(By.css('input'));

UIInteractions.triggerEventHandlerKeyDown('Tab', focusTrap, false, true);
fix.detectChanges();
expect(document.activeElement).toEqual(button.nativeElement);

UIInteractions.triggerEventHandlerKeyDown('Tab', focusTrap, false, true);
fix.detectChanges();
expect(document.activeElement).toEqual(inputs[1].nativeElement);

UIInteractions.triggerEventHandlerKeyDown('Tab', focusTrap, false, true);
fix.detectChanges();
expect(document.activeElement).toEqual(inputs[0].nativeElement);

UIInteractions.triggerEventHandlerKeyDown('Tab', focusTrap, false, true);
fix.detectChanges();
expect(document.activeElement).toEqual(button.nativeElement);
});

it('should trap focus on element when there is only one focusable element', () => {
const fix = TestBed.createComponent(TrapFocusTestComponent);
fix.detectChanges();

fix.componentInstance.showInput = false;
fix.detectChanges();

const focusTrap = fix.debugElement.query(By.directive(IgxFocusTrapDirective));
const button = fix.debugElement.query(By.css('button'));

UIInteractions.triggerEventHandlerKeyDown('Tab', focusTrap);
fix.detectChanges();
expect(document.activeElement).toEqual(button.nativeElement);

UIInteractions.triggerEventHandlerKeyDown('Tab', focusTrap, false, true);
fix.detectChanges();
expect(document.activeElement).toEqual(button.nativeElement);

UIInteractions.triggerEventHandlerKeyDown('Tab', focusTrap);
fix.detectChanges();
expect(document.activeElement).toEqual(button.nativeElement);
});

it('should trap focus on element with non-focusable elements', fakeAsync(() => {
const fix = TestBed.createComponent(TrapFocusTestComponent);
fix.detectChanges();

fix.componentInstance.showInput = false;
fix.componentInstance.showButton = false;
fix.detectChanges();

const focusTrap = fix.debugElement.query(By.directive(IgxFocusTrapDirective));

UIInteractions.triggerEventHandlerKeyDown('Tab', focusTrap);
tick();
fix.detectChanges();
expect(document.activeElement).toEqual(focusTrap.nativeElement);

UIInteractions.triggerEventHandlerKeyDown('Tab', focusTrap, false, true);
tick();
fix.detectChanges();
expect(document.activeElement).toEqual(focusTrap.nativeElement);

UIInteractions.triggerEventHandlerKeyDown('Tab', focusTrap);
tick();
fix.detectChanges();
expect(document.activeElement).toEqual(focusTrap.nativeElement);
}));

it('should be able to set focusTrap dynamically', fakeAsync(() => {
const fix = TestBed.createComponent(TrapFocusTestComponent);
fix.detectChanges();

const focusTrap = fix.debugElement.query(By.directive(IgxFocusTrapDirective));
const button = fix.debugElement.query(By.css('button'));
const inputs = fix.debugElement.queryAll(By.css('input'));

UIInteractions.triggerEventHandlerKeyDown('Tab', focusTrap);
fix.detectChanges();
expect(document.activeElement).toEqual(inputs[0].nativeElement);

UIInteractions.triggerEventHandlerKeyDown('Tab', focusTrap);
fix.detectChanges();
expect(document.activeElement).toEqual(inputs[1].nativeElement);

UIInteractions.triggerEventHandlerKeyDown('Tab', focusTrap);
fix.detectChanges();
expect(document.activeElement).toEqual(button.nativeElement);

button.nativeElement.blur();
fix.detectChanges();

fix.componentInstance.focusTrap = false;
fix.detectChanges();

UIInteractions.triggerEventHandlerKeyDown('Tab', focusTrap);
fix.detectChanges();
expect(document.activeElement).not.toEqual(inputs[0].nativeElement);

UIInteractions.triggerEventHandlerKeyDown('Tab', focusTrap, false, true);
fix.detectChanges();
expect(document.activeElement).not.toEqual(inputs[1].nativeElement);

fix.componentInstance.focusTrap = true;
fix.detectChanges();

UIInteractions.triggerEventHandlerKeyDown('Tab', focusTrap);
fix.detectChanges();
expect(document.activeElement).toEqual(inputs[0].nativeElement);

UIInteractions.triggerEventHandlerKeyDown('Tab', focusTrap, false, true);
fix.detectChanges();
expect(document.activeElement).toEqual(button.nativeElement);
}));
});


@Component({
template: `<div #wrapper [igxFocusTrap]="focusTrap" tabindex="0">
<label for="uname"><b>Username</b></label><br>
<input type="text" *ngIf="showInput" placeholder="Enter Username" name="uname"><br>
<label for="psw"><b>Password</b></label><br>
<input type="password" *ngIf="showInput" placeholder="Enter Password" name="psw"><br>
<button *ngIf="showButton">SIGN IN</button>
</div>` })
class TrapFocusTestComponent {
public showInput = true;
public showButton = true;
public focusTrap = true;
}
Loading