Skip to content

Commit

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

Fixes ng-bootstrap#3483
  • Loading branch information
maxokorokov committed Jan 9, 2020
1 parent 6780d62 commit a3485c8
Show file tree
Hide file tree
Showing 5 changed files with 125 additions and 3 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 focused element 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(focusableEl);
});

it('should fallback to focused element 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(focusableEl);
});
});

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

beforeEach(() => {
Expand Down
24 changes: 21 additions & 3 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 {isFocusable, 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 an `HTMLElement` or a selector to focus. If the element doesn't exist or not
* focusable, we'll still focus previously focused element.
*/
@Input() restoreFocus: true | string | HTMLElement;

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

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

elementToFocus.focus();
}
}
Expand Down Expand Up @@ -469,8 +487,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
4 changes: 4 additions & 0 deletions src/util/util.ts
Expand Up @@ -34,6 +34,10 @@ export function padNumber(value: number) {
}
}

export function isFocusable(element: any) {
return element instanceof HTMLElement && element['focus'];
}

export function regExpEscape(text) {
return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
}
Expand Down

0 comments on commit a3485c8

Please sign in to comment.