-
Notifications
You must be signed in to change notification settings - Fork 1.5k
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 keyboard navigation #1551
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is a great PR. Thank you for your hard work! I have added some remarks. Note that I have not reviewed test files yet.
@@ -20,14 +21,16 @@ import {NgbDateStruct} from './ngb-date-struct'; | |||
'[class.text-white]': 'selected', | |||
'[class.text-muted]': 'isMuted()', | |||
'[class.outside]': 'isMuted()', | |||
'[class.btn-secondary]': '!disabled' | |||
'[class.btn-secondary]': '!disabled', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As discussed, the btn-secondary
class should be there without the !disabled
condition, so that it is possible to see the focus even on disabled dates.
src/datepicker/datepicker-service.ts
Outdated
// rebuilding months | ||
if (startDate) { | ||
const forceRebuild = | ||
!!patch['firstDayOfWeek'] || !!patch['markDisabled'] || !!patch['minDate'] || !!patch['maxDate']; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As discussed, !!patch['firstDayOfWeek']
does not exactly mean that patch
contains firstDayOfWeek
, but that it contains it and it is neither 0 nor any other falsy value... firstDayOfWeek
could be 0
in some calendars (even if 0 is not used in the default calendar), and in that case, we would still need to have forceRebuild = true
.
I think it would be better to use instead the 'firstDayOfWeek' in patch
syntax.
The same should be done everywhere in this function ('minDate' in patch
, forceRebuild = 'firstDayOfWeek' in patch || 'markDisabled' in patch || 'minDate' in patch || 'maxDate' in patch
...) so that we do not have to wonder whether each value can be falsy.
src/datepicker/datepicker.ts
Outdated
}); | ||
|
||
const modelSubscription = _service.model$.subscribe(model => { | ||
const newDate = model.months[0].firstDate; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This could be written more simply: newDate = model.firstDate
and the same for oldDate
.
src/datepicker/datepicker.ts
Outdated
[minDate]="_minDate" | ||
[maxDate]="_maxDate" | ||
[months]="months.length" | ||
[date]="model.months[0].firstDate" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This could be written more simply model.firstDate
.
src/datepicker/datepicker.ts
Outdated
|
||
onNavigateEvent(event: NavigationEvent) { | ||
switch (event) { | ||
case NavigationEvent.PREV: | ||
this._setViewWithinLimits(this._calendar.getPrev(this.months[0].firstDate, 'm')); | ||
this._service.open(this._calendar.getPrev(this.model.months[0].firstDate, 'm', 1)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This could be written more simply this.model.firstDate
(and the same for line 316).
src/datepicker/datepicker-service.ts
Outdated
state.months = months; | ||
state.firstDate = months.length > 0 ? months[0].firstDate : undefined; | ||
state.lastDate = months.length > 0 ? months[months.length - 1].lastDate : undefined; | ||
state.viewDate = state.firstDate; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As viewDate
seems to always be the same as firstDate
, is it really needed to have both? Maybe we could remove viewDate
from the model and only keep firstDate
.
af09af8
to
18ee793
Compare
Thanks, @divdavem! Amended fixes for all your comments. |
18ee793
to
5a12fdb
Compare
Aligned with master, resolved conflicts |
5a12fdb
to
f6b7e02
Compare
@maxokorokov I don't see TravisCI being run on those commits, I'm not sure it is because of getting commits from multiple people. But I want to see "green tick" before merging this one :-) |
class="custom-select d-inline-block" | ||
[value]="date?.month" | ||
(change)="changeMonth($event.target.value)" | ||
tabindex="-1"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why tabindex="-1"
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the current implementation its the ngb-datepicker
itself that captures all events and prevents default behaviour to avoid page scrolling when hitting arrows, space, etc.
Now that I think of, it might be better in future to focus only a specific date instead of everything, so the default navigation behaviour with keyboard select boxes and arrows would continue working as before
src/datepicker/datepicker.spec.ts
Outdated
@@ -541,6 +572,143 @@ describe('ngb-datepicker', () => { | |||
expect(getYearSelect(fixture.nativeElement).value).toBe(`${today.getFullYear()}`); | |||
}); | |||
|
|||
it('should correctly navigate with keyboard', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test is quite big, and if one condition fails we would have hard time figuring out what failed exactly.
Could you split it into multiple small tests? (ideally one test for one navigation key)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Think of it of as a big integration test :)
key-mapping service has unit tests of its own, but yes, we should split this one
Great stuff @divdavem @maxokorokov ! I've left some comments. The diff is big so I can't really comment on every line of code, but I haven't seen anything scary :-) @maxokorokov could you please expand on the "BREAKING CHANGES" (that is, take it from the user's point of view?) |
f6b7e02
to
3e04658
Compare
Split the |
The following shortcuts are available: Arrow left/right: previous/next day Arrow up/down: previous/next week Page up/down: previous/next month Shift+page up/down: previous/next year Home/end: beginning/end of the current view Shift+home/end: min/max selectable date
BREAKING CHANGE: component uses `ChangeDetectionStrategy.OnPush` now for most of the internal implementation. Things like the dynamic internationalization or calendar change might not work anymore as these are injected services. Any internal changes in these services in runtime will not trigger datepicker re-rendering.
3e04658
to
0a2c555
Compare
This essentially contains two things:
Problems with the datepicker before refactoring:
NgbDatepicker
component - became a huge mess after adding of the keyboard handlingRefactoring:
NgbDatepicker
component is as dumb as possible, just displaysDatepickerViewModel
and propagates@Inputs
to the serviceNgbDatepickerService
generates theDatepickerViewModel
with a very simple APINgbDatepickerKeyMappingService
handles key event mapping to actionsservice
OnPush
everywhere for the datepicker componentsSo now it looks something like this:
P.S. It might seem a lot, but 2/3 of the PR are unit tests
P.P.S. Tried lots of different solutions based on observables, but for now they add only boilerplate code...