Skip to content

Commit 55a23c0

Browse files
committed
feat(datepicker): Revert to open date/close on escape
1 parent 47bc71d commit 55a23c0

File tree

7 files changed

+217
-5
lines changed

7 files changed

+217
-5
lines changed

src/components/DateInput.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
@focus="handleInputFocus"
4747
@keydown.down.prevent="handleKeydownDown"
4848
@keydown.enter.prevent="handleKeydownEnter"
49-
@keydown.escape.prevent="$emit('close')"
49+
@keydown.esc.prevent="clearDate"
5050
@keydown.space="handleKeydownSpace($event)"
5151
@keyup="handleKeyup"
5252
@keyup.space="handleKeyupSpace($event)"
@@ -170,6 +170,7 @@ export default {
170170
* Emits a `clear-date` event
171171
*/
172172
clearDate() {
173+
this.input.value = ''
173174
this.$emit('clear-date')
174175
},
175176
/**

src/components/Datepicker.vue

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
class="vdp-datepicker"
66
:class="[wrapperClass, { rtl: isRtl }]"
77
@focusin="handleFocusChange($event)"
8+
@keydown.esc="clearDate"
89
@keydown.tab="tabThroughNavigation($event)"
910
>
1011
<DateInput
@@ -380,8 +381,15 @@ export default {
380381
* Clear the selected date
381382
*/
382383
clearDate() {
384+
if (this.isResetFocus()) {
385+
this.resetFocusToOpenDate()
386+
return
387+
}
388+
383389
this.selectedDate = null
384-
this.setPageDate()
390+
this.focus.refs = ['input']
391+
this.close()
392+
385393
this.$emit('selected', null)
386394
this.$emit('input', null)
387395
this.$emit('cleared')
@@ -529,6 +537,17 @@ export default {
529537
date,
530538
)
531539
},
540+
/**
541+
* Returns true if we should reset the focus to the open date
542+
* @returns {Boolean}
543+
*/
544+
isResetFocus() {
545+
return (
546+
this.isOpen &&
547+
this.hasClass(document.activeElement, 'cell') &&
548+
(!this.isMinimumView || !this.hasClass(document.activeElement, 'open'))
549+
)
550+
},
532551
/**
533552
* Opens the calendar with the relevant view: 'day', 'month', or 'year'
534553
*/

src/components/PickerCells.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
<button
44
v-for="(cell, id) in cells"
55
:key="cell.timestamp"
6+
:ref="cell.isOpenDate ? 'openDate' : null"
67
:class="cellClasses(cell)"
78
:data-id="id"
89
:data-test-tabbable-cell="id === tabbableCellId"

src/mixins/navMixin.vue

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export default {
77
delay: 0,
88
refs: [],
99
},
10+
isRevertingToOpenDate: false,
1011
navElements: [],
1112
navElementsFocusedIndex: 0,
1213
resetTabbableCell: false,
@@ -77,6 +78,9 @@ export default {
7778
if (ref === 'calendarButton') {
7879
return this.$refs.dateInput.$refs.calendarButton
7980
}
81+
if (ref === 'openDate') {
82+
return this.$refs.picker.$refs.cells.$refs.openDate[0]
83+
}
8084
if (this.showHeader) {
8185
if (ref === 'up') {
8286
return this.$refs.picker && this.$refs.picker.$refs.up.$el
@@ -168,6 +172,22 @@ export default {
168172
hasArrowedToNewPage() {
169173
return this.focus.refs && this.focus.refs[0] === 'arrow-to-cell'
170174
},
175+
/**
176+
* Resets the focus to the open date
177+
*/
178+
resetFocusToOpenDate() {
179+
this.focus.refs = ['openDate']
180+
181+
if (!this.isMinimumView) {
182+
this.isRevertingToOpenDate = true
183+
this.view = this.minimumView
184+
}
185+
186+
this.setTabbableCell()
187+
this.reviewFocus()
188+
this.selectedDate = null
189+
this.setPageDate()
190+
},
171191
/**
172192
* Sets the correct focus on next tick
173193
*/
@@ -189,6 +209,24 @@ export default {
189209
this.resetTabbableCell = false
190210
})
191211
},
212+
/**
213+
* Sets the direction of the slide transition and whether or not to delay application of the focus
214+
* @param {Date|Number} startDate The date from which to measure
215+
* @param {Date|Number} endDate Is this before or after the startDate? And is it on the same page?
216+
*/
217+
setTransitionAndFocusDelay(startDate, endDate) {
218+
const startPageDate = this.utils.setDate(new Date(startDate), 1)
219+
const endPageDate = this.utils.setDate(new Date(endDate), 1)
220+
const isInTheFuture = startPageDate < endPageDate
221+
222+
if (this.isMinimumView) {
223+
this.focus.delay = isInTheFuture ? this.slideDuration : 0
224+
} else {
225+
this.focus.delay = 0
226+
}
227+
228+
this.setTransitionName(endDate - startDate)
229+
},
192230
/**
193231
* Records all focusable elements (so that we know whether any element in the datepicker is focused)
194232
*/

test/unit/specs/DateInput/DateInput.spec.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -160,11 +160,11 @@ describe('DateInput', () => {
160160
expect(wrapper.find('input').element.value).toEqual('!')
161161
})
162162

163-
it('emits close when escape is pressed', async () => {
163+
it('emits `clear-date` when escape is pressed', async () => {
164164
const input = wrapper.find('input')
165-
await input.trigger('keydown.escape')
165+
await input.trigger('keydown.esc')
166166

167-
expect(wrapper.emitted('close')).toBeTruthy()
167+
expect(wrapper.emitted('clear-date')).toBeTruthy()
168168
})
169169

170170
it('opens the calendar on click', async () => {

test/unit/specs/DateInput/typedDates.spec.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,28 @@ describe('Datepicker mount', () => {
235235
expect(wrapper.vm.selectedDate).toEqual(new Date(2016, 1, 15))
236236
})
237237

238+
it('clears the date when escape is pressed', async () => {
239+
await wrapper.setProps({
240+
value: new Date(2020, 0, 1),
241+
})
242+
243+
const input = wrapper.find('input')
244+
expect(wrapper.vm.selectedDate).toStrictEqual(new Date(2020, 0, 1))
245+
246+
await input.trigger('keydown.esc')
247+
expect(wrapper.vm.selectedDate).toBeNull()
248+
})
249+
250+
it('closes the calendar when escape is pressed', async () => {
251+
const input = wrapper.find('input')
252+
253+
await input.trigger('click')
254+
expect(wrapper.vm.isOpen).toBeTruthy()
255+
256+
await input.trigger('keydown.esc')
257+
expect(wrapper.vm.isOpen).toBeFalsy()
258+
})
259+
238260
it('opens the calendar when the space bar is pressed on the input field', async () => {
239261
const input = wrapper.find('input')
240262
await input.trigger('keydown.space')

test/unit/specs/Datepicker/Datepicker.spec.js

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -729,6 +729,137 @@ describe('Datepicker mounted to body with openDate', () => {
729729
jest.advanceTimersByTime(250)
730730
expect(document.activeElement).toBe(cellDown.element)
731731
})
732+
733+
it('reverts focus to the `open-date` when another date on the same page has focus and the `escape` key is pressed', async () => {
734+
const input = wrapper.find('input')
735+
await input.trigger('click')
736+
737+
jest.advanceTimersByTime(250)
738+
739+
const openDateCell = wrapper.find('button.open')
740+
expect(document.activeElement).toBe(openDateCell.element)
741+
742+
await openDateCell.trigger('keydown.down')
743+
744+
const downCell = wrapper.findAll('button.cell').at(10)
745+
await downCell.trigger('keydown.esc')
746+
747+
jest.advanceTimersByTime(250)
748+
749+
expect(document.activeElement).toBe(openDateCell.element)
750+
})
751+
752+
it('reverts focus to the `open-date` when a date on a different page has focus and the `escape` key is pressed', async () => {
753+
const input = wrapper.find('input')
754+
await input.trigger('click')
755+
756+
jest.advanceTimersByTime(250)
757+
758+
let openDateCell = wrapper.find('button.open')
759+
expect(document.activeElement).toStrictEqual(openDateCell.element)
760+
761+
await openDateCell.trigger('keydown.up')
762+
jest.advanceTimersByTime(250)
763+
764+
const upCell = wrapper.findAll('button.cell').at(24)
765+
expect(document.activeElement).toBe(upCell.element)
766+
767+
await upCell.trigger('keydown.esc')
768+
769+
jest.advanceTimersByTime(250)
770+
openDateCell = wrapper.find('button.open')
771+
expect(document.activeElement).toBe(openDateCell.element)
772+
})
773+
774+
it('reverts focus to the `open-date` when a month has focus and the `escape` key is pressed', async () => {
775+
const input = wrapper.find('input')
776+
await input.trigger('click')
777+
jest.advanceTimersByTime(250)
778+
expect(wrapper.vm.view).toBe('day')
779+
780+
let upButton = wrapper.find('button.vdp-datepicker__up')
781+
await upButton.trigger('click')
782+
jest.advanceTimersByTime(250)
783+
expect(wrapper.vm.view).toBe('month')
784+
785+
upButton = wrapper.find('button.vdp-datepicker__up')
786+
await upButton.trigger('keydown.down')
787+
788+
const firstCell = wrapper.find('button.cell:not(.muted)')
789+
await firstCell.trigger('keydown.esc')
790+
jest.advanceTimersByTime(250)
791+
expect(wrapper.vm.view).toBe('day')
792+
793+
const openDateCell = wrapper.find('button.open')
794+
expect(document.activeElement).toBe(openDateCell.element)
795+
})
796+
797+
it('reverts focus to the `open-date` when a year has focus and the `escape` key is pressed', async () => {
798+
const input = wrapper.find('input')
799+
await input.trigger('click')
800+
jest.advanceTimersByTime(250)
801+
expect(wrapper.vm.view).toBe('day')
802+
803+
let upButton = wrapper.find('button.vdp-datepicker__up')
804+
await upButton.trigger('click')
805+
jest.advanceTimersByTime(250)
806+
expect(wrapper.vm.view).toBe('month')
807+
808+
upButton = wrapper.find('button.vdp-datepicker__up')
809+
await upButton.trigger('click')
810+
jest.advanceTimersByTime(250)
811+
expect(wrapper.vm.view).toBe('year')
812+
813+
const firstCell = wrapper.find('button.cell:not(.muted)')
814+
upButton = wrapper.find('button.vdp-datepicker__up')
815+
await upButton.trigger('keydown.down')
816+
817+
await firstCell.element.focus()
818+
await firstCell.trigger('keydown.esc')
819+
jest.advanceTimersByTime(250)
820+
expect(wrapper.vm.view).toBe('day')
821+
822+
const openDateCell = wrapper.find('button.open')
823+
expect(document.activeElement).toBe(openDateCell.element)
824+
})
825+
826+
it('clears the date and closes the calendar on pressing the `escape` key, if the `open-date` is focused', async () => {
827+
const input = wrapper.find('input')
828+
await input.trigger('click')
829+
jest.advanceTimersByTime(250)
830+
831+
expect(wrapper.vm.isOpen).toBeTruthy()
832+
const openDateCell = wrapper.find('button.open')
833+
await openDateCell.trigger('focus')
834+
expect(document.activeElement).toStrictEqual(openDateCell.element)
835+
836+
await openDateCell.trigger('keydown.esc')
837+
expect(wrapper.vm.isOpen).toBeFalsy()
838+
expect(wrapper.vm.selectedDate).toEqual(null)
839+
})
840+
841+
it('clears the date and reverts the calendar to `minimumView` on pressing the `escape` key', async () => {
842+
const input = wrapper.find('input')
843+
844+
await input.trigger('click')
845+
expect(wrapper.vm.isOpen).toBeTruthy()
846+
847+
const upButton = wrapper.find('button.vdp-datepicker__up')
848+
await upButton.trigger('click')
849+
expect(wrapper.vm.view).toBe('month')
850+
851+
const firstCell = wrapper.find('button.cell')
852+
await firstCell.element.focus()
853+
expect(document.activeElement).toBe(firstCell.element)
854+
855+
await firstCell.trigger('keydown.esc')
856+
jest.advanceTimersByTime(250)
857+
858+
const openDate = wrapper.find('button.open')
859+
expect(wrapper.vm.view).toBe('day')
860+
expect(wrapper.vm.selectedDate).toEqual(null)
861+
expect(document.activeElement).toBe(openDate.element)
862+
})
732863
})
733864

734865
describe('Datepicker shallowMounted', () => {

0 commit comments

Comments
 (0)