Skip to content

Commit

Permalink
feat(datepicker): add 'restoreFocus' input (#3539)
Browse files Browse the repository at this point in the history
restoreFocus input will define re-focusing strategy when closing the input datepicker.

```
@input() restoreFocus: true | string | HTMLElement; // defaults to true
```

By default it re-focuses element focused at the moment of datepicker opening.
Otherwise accepts either selector or element reference.

Fixes #3483
  • Loading branch information
maxokorokov committed Jan 10, 2020
1 parent bbd8b51 commit 31402a3
Show file tree
Hide file tree
Showing 4 changed files with 123 additions and 4 deletions.
1 change: 1 addition & 0 deletions src/datepicker/datepicker-input-config.spec.ts
Expand Up @@ -8,5 +8,6 @@ describe('NgbInputDatepickerConfig', () => {
expect(config.container).toBeUndefined();
expect(config.positionTarget).toBeUndefined();
expect(config.placement).toEqual(['bottom-left', 'bottom-right', 'top-left', 'top-right']);
expect(config.restoreFocus).toBe(true);
});
});
1 change: 1 addition & 0 deletions src/datepicker/datepicker-input-config.ts
Expand Up @@ -15,4 +15,5 @@ export class NgbInputDatepickerConfig extends NgbDatepickerConfig {
container: null | 'body';
positionTarget: string | HTMLElement;
placement: PlacementArray = ['bottom-left', 'bottom-right', 'top-left', 'top-right'];
restoreFocus: true | HTMLElement | string = true;
}
98 changes: 98 additions & 0 deletions src/datepicker/datepicker-input.spec.ts
Expand Up @@ -910,6 +910,104 @@ describe('NgbInputDatepicker', () => {
}));
});

describe('focus restore', () => {

function open(fixture: ComponentFixture<TestComponent>) {
const dp = fixture.debugElement.query(By.directive(NgbInputDatepicker)).injector.get(NgbInputDatepicker);
dp.open();
fixture.detectChanges();
}

function selectDateAndClose(fixture: ComponentFixture<TestComponent>) {
fixture.nativeElement.querySelectorAll('.ngb-dp-day')[3].click(); // 1 MAR 2018
fixture.detectChanges();
}

it('should focus previously focused element', () => {
const fixture = createTestCmpt(`
<div tabindex="0" id="focusable"></div>
<input ngbDatepicker [startDate]="{year: 2018, month: 3}"/>
`);

// initial focus
const focusableEl = fixture.nativeElement.querySelector('#focusable');
focusableEl.focus();
expect(document.activeElement).toBe(focusableEl);

open(fixture);
expect(document.activeElement).not.toBe(focusableEl);

selectDateAndClose(fixture);
expect(document.activeElement).toBe(focusableEl);
});

it('should focus using selector provided via [restoreFocus]', () => {
const fixture = createTestCmpt(`
<div tabindex="0" id="focusable"></div>
<input ngbDatepicker restoreFocus="#focusable" [startDate]="{year: 2018, month: 3}"/>
`);

const focusableEl = fixture.nativeElement.querySelector('#focusable');
expect(document.activeElement).not.toBe(focusableEl);

open(fixture);
expect(document.activeElement).not.toBe(focusableEl);

selectDateAndClose(fixture);
expect(document.activeElement).toBe(focusableEl);
});

it('should focus using element provided via [restoreFocus]', () => {
const fixture = createTestCmpt(`
<div #el tabindex="0" id="focusable"></div>
<input ngbDatepicker [restoreFocus]="el" [startDate]="{year: 2018, month: 3}"/>
`);

const focusableEl = fixture.nativeElement.querySelector('#focusable');
expect(document.activeElement).not.toBe(focusableEl);

open(fixture);
expect(document.activeElement).not.toBe(focusableEl);

selectDateAndClose(fixture);
expect(document.activeElement).toBe(focusableEl);
});

it('should fallback to body if [restoreFocus] selector is invalid', () => {
const fixture = createTestCmpt(`
<div tabindex="0" id="focusable"></div>
<input ngbDatepicker restoreFocus=".invalid-element" [startDate]="{year: 2018, month: 3}"/>
`);

const focusableEl = fixture.nativeElement.querySelector('#focusable');
focusableEl.focus();
expect(document.activeElement).toBe(focusableEl);

open(fixture);
expect(document.activeElement).not.toBe(focusableEl);

selectDateAndClose(fixture);
expect(document.activeElement).toBe(document.body);
});

it('should fallback to body if [restoreFocus] value is invalid', () => {
const fixture = createTestCmpt(`
<div tabindex="0" id="focusable"></div>
<input ngbDatepicker [restoreFocus]="null" [startDate]="{year: 2018, month: 3}"/>
`);

const focusableEl = fixture.nativeElement.querySelector('#focusable');
focusableEl.focus();
expect(document.activeElement).toBe(focusableEl);

open(fixture);
expect(document.activeElement).not.toBe(focusableEl);

selectDateAndClose(fixture);
expect(document.activeElement).toBe(document.body);
});
});

describe('Native adapter', () => {

beforeEach(() => {
Expand Down
27 changes: 23 additions & 4 deletions src/datepicker/datepicker-input.ts
Expand Up @@ -33,6 +33,7 @@ import {NgbDateParserFormatter} from './ngb-date-parser-formatter';
import {NgbDateStruct} from './ngb-date-struct';
import {NgbInputDatepickerConfig} from './datepicker-input-config';
import {NgbDatepickerConfig} from './datepicker-config';
import {isString} from '../util/util';

const NGB_DATEPICKER_VALUE_ACCESSOR = {
provide: NG_VALUE_ACCESSOR,
Expand Down Expand Up @@ -183,6 +184,14 @@ export class NgbInputDatepicker implements OnChanges,
*/
@Input() placement: PlacementArray;

/**
* If `true`, when closing datepicker will focus element that was focused before datepicker was opened.
*
* Alternatively you could provide a selector or an `HTMLElement` to focus. If the element doesn't exist or invalid,
* we'll fallback to focus document body.
*/
@Input() restoreFocus: true | string | HTMLElement;

/**
* If `true`, weekdays will be displayed.
*/
Expand Down Expand Up @@ -373,8 +382,18 @@ export class NgbInputDatepicker implements OnChanges,
this._changeDetector.markForCheck();

// restore focus
const elementToFocus = this._elWithFocus && this._elWithFocus['focus'] ? this._elWithFocus : this._document.body;
elementToFocus.focus();
let elementToFocus = this._elWithFocus;
if (isString(this.restoreFocus)) {
elementToFocus = this._document.querySelector(this.restoreFocus);
} else if (this.restoreFocus !== undefined) {
elementToFocus = this.restoreFocus;
}

if (elementToFocus) {
elementToFocus.focus();
} else {
this._document.body.focus();
}
}
}

Expand Down Expand Up @@ -469,8 +488,8 @@ export class NgbInputDatepicker implements OnChanges,
}

let hostElement: HTMLElement;
if (typeof this.positionTarget === 'string') {
hostElement = window.document.querySelector(this.positionTarget);
if (isString(this.positionTarget)) {
hostElement = this._document.querySelector(this.positionTarget);
} else if (this.positionTarget instanceof HTMLElement) {
hostElement = this.positionTarget;
} else {
Expand Down

0 comments on commit 31402a3

Please sign in to comment.