Skip to content

Commit

Permalink
feat(datepicker): allow focusing calendar days and navigation separately
Browse files Browse the repository at this point in the history
Fixes #1716
Closes #2270

BREAKING CHANGE: The datepicker is no longer focusable as a whole component. Instead, the focus is allowed on each element inside the datepicker (navigation buttons, select boxes, focusable day) in the natural order. The datepicker `.focus()` method will now only focus one day and not the whole component.
  • Loading branch information
fbasso authored and maxokorokov committed Apr 11, 2018
1 parent bc79f9b commit 2daf038
Show file tree
Hide file tree
Showing 12 changed files with 252 additions and 129 deletions.
2 changes: 1 addition & 1 deletion src/datepicker/datepicker-input.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ describe('NgbInputDatepicker', () => {
const button = fixture.nativeElement.querySelector('button');
button.click();
fixture.detectChanges();
expect(document.activeElement).toBe(fixture.nativeElement.querySelector('ngb-datepicker'));
expect(document.activeElement).toBe(fixture.nativeElement.querySelector('div.ngb-dp-day[tabindex="0"]'));
});

it('should close datepicker on date selection', () => {
Expand Down
2 changes: 2 additions & 0 deletions src/datepicker/datepicker-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,8 @@ export class NgbInputDatepicker implements OnChanges,
this._onChange(selectedDate);
});

this._cRef.changeDetectorRef.detectChanges();

// focus handling
this._cRef.instance.focus();

Expand Down
30 changes: 20 additions & 10 deletions src/datepicker/datepicker-month-view.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,8 @@ class TestComponent {
disabled: false,
focused: false,
selected: false
}
},
tabindex: -1
},
{
date: new NgbDate(2016, 8, 23),
Expand All @@ -239,7 +240,8 @@ class TestComponent {
disabled: false,
focused: false,
selected: false
}
},
tabindex: -1
}
]
}]
Expand All @@ -264,7 +266,8 @@ class TestComponent {
disabled: false,
focused: false,
selected: false
}
},
tabindex: -1
},
{
date: new NgbDate(2016, 8, 1),
Expand All @@ -274,7 +277,8 @@ class TestComponent {
disabled: false,
focused: false,
selected: false
}
},
tabindex: -1
}
]
},
Expand All @@ -290,7 +294,8 @@ class TestComponent {
disabled: false,
focused: false,
selected: false
}
},
tabindex: -1
},
{
date: new NgbDate(2016, 8, 3),
Expand All @@ -300,7 +305,8 @@ class TestComponent {
disabled: false,
focused: false,
selected: false
}
},
tabindex: -1
}
]
},
Expand All @@ -316,7 +322,8 @@ class TestComponent {
disabled: false,
focused: false,
selected: false
}
},
tabindex: -1
},
{
date: new NgbDate(2016, 9, 1),
Expand All @@ -326,7 +333,8 @@ class TestComponent {
disabled: false,
focused: false,
selected: false
}
},
tabindex: -1
}
]
},
Expand All @@ -342,7 +350,8 @@ class TestComponent {
disabled: false,
focused: false,
selected: false
}
},
tabindex: -1
},
{
date: new NgbDate(2016, 9, 3),
Expand All @@ -352,7 +361,8 @@ class TestComponent {
disabled: false,
focused: false,
selected: false
}
},
tabindex: -1
}
]
}
Expand Down
10 changes: 6 additions & 4 deletions src/datepicker/datepicker-month-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {DayTemplateContext} from './datepicker-day-template-context';

@Component({
selector: 'ngb-datepicker-month-view',
host: {'class': 'd-block'},
host: {'class': 'd-block', 'role': 'grid'},
styles: [`
.ngb-dp-weekday, .ngb-dp-week-number {
line-height: 2rem;
Expand Down Expand Up @@ -43,10 +43,12 @@ import {DayTemplateContext} from './datepicker-day-template-context';
</div>
</div>
<ng-template ngFor let-week [ngForOf]="month.weeks">
<div *ngIf="!isCollapsed(week)" class="ngb-dp-week">
<div *ngIf="!isCollapsed(week)" class="ngb-dp-week" role="row">
<div *ngIf="showWeekNumbers" class="ngb-dp-week-number small text-center font-italic text-muted">{{ week.number }}</div>
<div *ngFor="let day of week.days" (click)="doSelect(day)" class="ngb-dp-day" [class.disabled]="day.context.disabled"
[class.hidden]="isHidden(day)">
<div *ngFor="let day of week.days" (click)="doSelect(day)" class="ngb-dp-day" role="gridcell"
[class.disabled]="day.context.disabled"
[tabindex]="day.tabindex"
[class.hidden]="isHidden(day)">
<ng-template [ngIf]="!isHidden(day)">
<ng-template [ngTemplateOutlet]="dayTemplate" [ngTemplateOutletContext]="day.context"></ng-template>
</ng-template>
Expand Down
6 changes: 2 additions & 4 deletions src/datepicker/datepicker-navigation-select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,13 @@ import {NgbDatepickerI18n} from './datepicker-i18n';
[disabled]="disabled"
class="custom-select"
[value]="date?.month"
(change)="changeMonth($event.target.value)"
tabindex="-1">
(change)="changeMonth($event.target.value)">
<option *ngFor="let m of months" [value]="m">{{ i18n.getMonthShortName(m) }}</option>
</select><select
[disabled]="disabled"
class="custom-select"
[value]="date?.year"
(change)="changeYear($event.target.value)"
tabindex="-1">
(change)="changeYear($event.target.value)">
<option *ngFor="let y of years" [value]="y">{{ y }}</option>
</select>
`
Expand Down
23 changes: 17 additions & 6 deletions src/datepicker/datepicker-navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,24 @@ import {NgbDatepickerI18n} from './datepicker-i18n';
display: -ms-flexbox;
display: flex;
}
.ngb-dp-navigation-chevron::before {
.ngb-dp-navigation-chevron {
border-style: solid;
border-width: 0.2em 0.2em 0 0;
content: '';
display: inline-block;
width: 0.75em;
height: 0.75em;
margin-left: 0.25em;
margin-right: 0.15em;
transform: rotate(-135deg);
-webkit-transform: rotate(-135deg);
-ms-transform: rotate(-135deg);
}
.right .ngb-dp-navigation-chevron:before {
.right .ngb-dp-navigation-chevron {
-webkit-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
margin-left: 0.15em;
margin-right: 0.25em;
}
.ngb-dp-arrow {
display: -webkit-box;
Expand All @@ -51,7 +54,15 @@ import {NgbDatepickerI18n} from './datepicker-i18n';
justify-content: flex-end;
}
.ngb-dp-arrow-btn {
padding: 0rem 1rem;
padding: 0rem 0.25rem;
margin: 0rem 0.5rem;
border: none;
background-color: transparent;
z-index: 1;
}
.ngb-dp-arrow-btn:focus {
outline-style: auto;
outline-width: 1px;
}
.ngb-dp-month-name {
font-size: larger;
Expand All @@ -69,7 +80,7 @@ import {NgbDatepickerI18n} from './datepicker-i18n';
template: `
<div class="ngb-dp-arrow">
<button type="button" class="btn btn-link ngb-dp-arrow-btn"
(click)="!!navigate.emit(navigation.PREV)" [disabled]="prevDisabled" tabindex="-1">
(click)="!!navigate.emit(navigation.PREV)" [disabled]="prevDisabled">
<span class="ngb-dp-navigation-chevron"></span>
</button>
</div>
Expand All @@ -90,7 +101,7 @@ import {NgbDatepickerI18n} from './datepicker-i18n';
</ng-template>
<div class="ngb-dp-arrow right">
<button type="button" class="btn btn-link ngb-dp-arrow-btn"
(click)="!!navigate.emit(navigation.NEXT)" [disabled]="nextDisabled" tabindex="-1">
(click)="!!navigate.emit(navigation.NEXT)" [disabled]="nextDisabled">
<span class="ngb-dp-navigation-chevron"></span>
</button>
</div>
Expand Down
29 changes: 27 additions & 2 deletions src/datepicker/datepicker-service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ describe('ngb-datepicker-service', () => {

let subscriptions: Subscription[];

const getDay = (n: number) => model.months[0].weeks[0].days[n];
const getDayInMonth = (monthIndex: number, weekIndex: number, dayIndex: number) =>
model.months[monthIndex].weeks[weekIndex].days[dayIndex];
const getDay = (n: number) => getDayInMonth(0, 0, n);
const getDayCtx = (n: number) => getDay(n).context;

beforeEach(() => {
Expand Down Expand Up @@ -303,6 +305,21 @@ describe('ngb-datepicker-service', () => {
expect(model.months[0]).toBe(month);
expect(getDay(0).date).toBe(date);
});

it(`should change the tabindex when changing the current month`, () => {
service.displayMonths = 2;
service.focus(new NgbDate(2018, 3, 31));

expect(getDayInMonth(0, 4, 5).tabindex).toEqual(0); // 31 march in the first month block
expect(getDayInMonth(1, 0, 5).tabindex).toEqual(-1); // 31 march in the second month block

service.focusMove('d', 1);
expect(getDayInMonth(0, 4, 5).tabindex).toEqual(-1); // 31 march in the first month block
expect(getDayInMonth(1, 0, 5).tabindex).toEqual(-1); // 31 march in the second month block
expect(getDayInMonth(0, 4, 6).tabindex).toEqual(-1); // 1st april in the first month block
expect(getDayInMonth(1, 0, 6).tabindex).toEqual(0); // 1st april in the second month block

});
});

describe(`disabled`, () => {
Expand Down Expand Up @@ -1053,26 +1070,34 @@ describe('ngb-datepicker-service', () => {
expect(getDayCtx(0).currentMonth).toBe(10);
});

it(`should update 'focused' flag for day template`, () => {
it(`should update 'focused' flag and tabindex for day template`, () => {
// off
service.focus(new NgbDate(2017, 5, 1));
expect(getDayCtx(0).focused).toBeFalsy();
expect(getDayCtx(1).focused).toBeFalsy();
expect(getDay(0).tabindex).toEqual(0);
expect(getDay(1).tabindex).toEqual(-1);

// on
service.focusVisible = true;
expect(getDayCtx(0).focused).toBeTruthy();
expect(getDayCtx(1).focused).toBeFalsy();
expect(getDay(0).tabindex).toEqual(0);
expect(getDay(1).tabindex).toEqual(-1);

// move
service.focusMove('d', 1);
expect(getDayCtx(0).focused).toBeFalsy();
expect(getDayCtx(1).focused).toBeTruthy();
expect(getDay(0).tabindex).toEqual(-1);
expect(getDay(1).tabindex).toEqual(0);

// off
service.focusVisible = false;
expect(getDayCtx(0).focused).toBeFalsy();
expect(getDayCtx(1).focused).toBeFalsy();
expect(getDay(0).tabindex).toEqual(-1);
expect(getDay(1).tabindex).toEqual(0);
});

it(`should update 'selected' flag for day template`, () => {
Expand Down
3 changes: 3 additions & 0 deletions src/datepicker/datepicker-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,9 @@ export class NgbDatepickerService {
day.context.focused = state.focusDate.equals(day.date) && state.focusVisible;
}

day.tabindex =
(!state.disabled && day.date.equals(state.focusDate) && state.focusDate.month === month.number) ? 0 : -1;

// override context disabled
if (state.disabled === true) {
day.context.disabled = true;
Expand Down
3 changes: 2 additions & 1 deletion src/datepicker/datepicker-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,8 @@ export function buildMonth(calendar: NgbCalendar, date: NgbDate, state: Datepick
disabled: disabled,
focused: false,
selected: false
}
},
tabindex: -1
});

date = nextDate;
Expand Down
3 changes: 2 additions & 1 deletion src/datepicker/datepicker-view-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ export type NgbMarkDisabled = (date: NgbDateStruct, current: {year: number, mont

export type DayViewModel = {
date: NgbDate,
context: DayTemplateContext
context: DayTemplateContext,
tabindex: number
}

export type WeekViewModel = {
Expand Down
Loading

0 comments on commit 2daf038

Please sign in to comment.