Skip to content
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

feat(datepicker): add 'restoreFocus' input #3539

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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();
}
}
maxokorokov marked this conversation as resolved.
Show resolved Hide resolved
}

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