Skip to content

Commit 52ad18a

Browse files
committed
feat(datepicker): Tabbing to/from an inline datepicker
1 parent 540cb34 commit 52ad18a

File tree

6 files changed

+218
-36
lines changed

6 files changed

+218
-36
lines changed

src/components/Datepicker.vue

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,12 @@ export default {
343343
this.setInitialView()
344344
}
345345
},
346+
isActive(hasJustBecomeActive) {
347+
if (hasJustBecomeActive && this.inline) {
348+
this.setNavElementsFocusedIndex()
349+
this.tabToCorrectInlineCell()
350+
}
351+
},
346352
openDate() {
347353
this.setPageDate()
348354
},

src/mixins/navMixin.vue

Lines changed: 137 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export default {
77
delay: 0,
88
refs: [],
99
},
10+
inlineTabbableCell: null,
1011
isActive: false,
1112
isRevertingToOpenDate: false,
1213
navElements: [],
@@ -47,6 +48,17 @@ export default {
4748
},
4849
},
4950
methods: {
51+
/**
52+
* Returns true, unless tabbing should be focus-trapped
53+
* @return {Boolean}
54+
*/
55+
allowNormalTabbing(event) {
56+
if (!this.isOpen) {
57+
return true
58+
}
59+
60+
return this.isTabbingAwayFromInlineDatepicker(event)
61+
},
5062
/**
5163
* Focuses the first non-disabled element found in the `focus.refs` array and sets `navElementsFocusedIndex`
5264
*/
@@ -63,6 +75,22 @@ export default {
6375
}
6476
}
6577
},
78+
/**
79+
* Ensures the most recently focused tabbable cell is focused when tabbing backwards to an inline calendar
80+
* If no element has previously been focused, the tabbable cell is reset and focused
81+
*/
82+
focusInlineTabbableCell() {
83+
if (this.inlineTabbableCell) {
84+
this.inlineTabbableCell.focus()
85+
86+
return
87+
}
88+
89+
this.resetTabbableCell = true
90+
this.setTabbableCell()
91+
this.tabbableCell.focus()
92+
this.resetTabbableCell = false
93+
},
6694
/**
6795
* Returns the currently focused cell element, if there is one...
6896
*/
@@ -181,7 +209,7 @@ export default {
181209
document.datepickerId = this.datepickerId
182210
183211
this.isActive = true
184-
212+
this.setInlineTabbableCell()
185213
this.setAllElements()
186214
this.setNavElements()
187215
},
@@ -198,6 +226,57 @@ export default {
198226
hasArrowedToNewPage() {
199227
return this.focus.refs && this.focus.refs[0] === 'arrow-to-cell'
200228
},
229+
/**
230+
* Returns true if the user is tabbing away from an inline datepicker
231+
* @return {Boolean}
232+
*/
233+
isTabbingAwayFromInlineDatepicker(event) {
234+
if (!this.inline) {
235+
return false
236+
}
237+
238+
if (this.isTabbingAwayFromFirstNavElement(event)) {
239+
this.tabAwayFromFirstElement()
240+
241+
return true
242+
}
243+
244+
if (this.isTabbingAwayFromLastNavElement(event)) {
245+
this.tabAwayFromLastElement()
246+
247+
return true
248+
}
249+
250+
return false
251+
},
252+
/**
253+
* Used for inline calendars; returns true if the user tabs backwards from the first focusable element
254+
* @param {object} event Used to determine whether we are tabbing forwards or backwards
255+
* @return {Boolean}
256+
*/
257+
isTabbingAwayFromFirstNavElement(event) {
258+
if (!event.shiftKey) {
259+
return false
260+
}
261+
262+
const firstNavElement = this.navElements[0]
263+
264+
return document.activeElement === firstNavElement
265+
},
266+
/**
267+
* Used for inline calendars; returns true if the user tabs forwards from the last focusable element
268+
* @param {object} event Used to determine whether we are tabbing forwards or backwards
269+
* @return {Boolean}
270+
*/
271+
isTabbingAwayFromLastNavElement(event) {
272+
if (event.shiftKey) {
273+
return false
274+
}
275+
276+
const lastNavElement = this.navElements[this.navElements.length - 1]
277+
278+
return document.activeElement === lastNavElement
279+
},
201280
/**
202281
* Resets the focus to the open date
203282
*/
@@ -239,6 +318,17 @@ export default {
239318
this.resetTabbableCell = false
240319
})
241320
},
321+
/**
322+
* Stores the current tabbableCell of an inline datepicker
323+
* N.B. This is used when tabbing back (shift + tab) to an inline calendar from further down the page
324+
*/
325+
setInlineTabbableCell() {
326+
if (!this.inline) {
327+
return
328+
}
329+
330+
this.inlineTabbableCell = this.tabbableCell
331+
},
242332
/**
243333
* Sets the direction of the slide transition and whether or not to delay application of the focus
244334
* @param {Date|Number} startDate The date from which to measure
@@ -339,6 +429,26 @@ export default {
339429
this.transitionName = isInTheFuture ? 'slide-right' : 'slide-left'
340430
}
341431
},
432+
/**
433+
* Focuses the first focusable element in the datepicker, so that the previous element on the page will be tabbed to
434+
*/
435+
tabAwayFromFirstElement() {
436+
const firstElement = this.allElements[0]
437+
438+
firstElement.focus()
439+
440+
this.tabbableCell = this.inlineTabbableCell
441+
},
442+
/**
443+
* Focuses the last focusable element in the datepicker, so that the next element on the page will be tabbed to
444+
*/
445+
tabAwayFromLastElement() {
446+
const lastElement = this.allElements[this.allElements.length - 1]
447+
448+
lastElement.focus()
449+
450+
this.tabbableCell = this.inlineTabbableCell
451+
},
342452
/**
343453
* Tab backwards through the focus-trapped elements
344454
*/
@@ -368,10 +478,10 @@ export default {
368478
* @param event
369479
*/
370480
tabThroughNavigation(event) {
371-
// Allow normal tabbing when closed
372-
if (!this.isOpen) {
481+
if (this.allowNormalTabbing(event)) {
373482
return
374483
}
484+
375485
event.preventDefault()
376486
377487
if (event.shiftKey) {
@@ -380,6 +490,30 @@ export default {
380490
this.tabForwards()
381491
}
382492
},
493+
/**
494+
* Special cases for when tabbing to an inline datepicker
495+
*/
496+
tabToCorrectInlineCell() {
497+
const lastElement = this.allElements[this.allElements.length - 1]
498+
const isACell = this.hasClass(lastElement, 'cell')
499+
const isLastElementFocused = document.activeElement === lastElement
500+
501+
// If there are no focusable elements in the footer slots and the inline datepicker has been tabbed to (backwards)
502+
if (isACell && isLastElementFocused) {
503+
this.focusInlineTabbableCell()
504+
return
505+
}
506+
507+
// If `show-header` is false and the inline datepicker has been tabbed to (forwards)
508+
this.$nextTick(() => {
509+
const isFirstCell =
510+
document.activeElement.getAttribute('data-id') === '0'
511+
512+
if (isFirstCell) {
513+
this.focusInlineTabbableCell()
514+
}
515+
})
516+
},
383517
/**
384518
* Update which cell in the picker should be focus-trapped
385519
*/

test/FEATURES.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
| Focus state | Calendar state | Typeable | Input date | Show on button click | Show on focus | Other state | Action | Calendar state | Focus state | Other state changes | Feature file | Test id |
2-
| --------------- | -------------- | -------- | ---------- | -------------------- | ------------- | ------------------------------------------------------- | -------------------------- | -------------- | ------------------------------- | ------------------------- | ------------------------- |---------|
2+
|-----------------|----------------|----------|------------|----------------------|---------------|---------------------------------------------------------|----------------------------|----------------|---------------------------------|---------------------------|---------------------------|---------|
33
| ANY | closed | | | | | initialView=day | Open calendar | open | focusable-cell (day) | | InitialFocus | 1#1 |
44
| | closed | | | | | initialView=month | Open calendar | open | focusable-cell (month) | | InitialFocus | 1#2 |
55
| | closed | | | | | initialView=year | Open calendar | open | focusable-cell (year) | | InitialFocus | 1#3 |
@@ -147,5 +147,7 @@
147147
| | open | | | | | | Keydown right | open | cell right on next page | Focusable cell, page up | CellNavigation | 2#4 |
148148
| | open | | | | | | Keydown tab | open | prev | | FocusTrap | 7 |
149149
| | open | | | | | | Keydown shift + tab | open | next | | FocusTrap | 8 |
150+
| | open | | | | | isInline, last element is focused | Keydown tab | open | Previous element on page | | FocusTrap | 9 |
151+
| | open | | | | | isInline, first element is focused | Keydown shift + tab | open | Next element on page | | FocusTrap | 10 |
150152
| --------------- | -------------- | -------- | ---------- | -------------------- | ------------- | --------------------------------- | -------------------------- | -------------- | -------------------------- | ------------------------- | ------------------------- | ------- |
151153
| Focus state | Calendar state | Typeable | Input date | Show on button click | Show on focus | Other state | Action | Calendar state | Focus state | Other state changes | Feature file | Test id |

test/e2e/specs/FocusTrap.feature

Lines changed: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,47 +13,59 @@ Feature: Focus Trap
1313
#
1414
#
1515
# @id-1
16-
# Scenario: Press tab when the previous button is focused
17-
# When the user focuses the previous button and presses tab
16+
# Scenario: Tab forwards when the previous button is focused
17+
# When the user focuses the previous button and tabs forwards
1818
# Then the up button has focus
1919
#
2020
# @id-2
21-
# Scenario: Press shift + tab when the previous button is focused
22-
# When the user focuses the previous button and presses shift + tab
21+
# Scenario: Tab backwards when the previous button is focused
22+
# When the user focuses the previous button and tabs backwards
2323
# Then the tabbable cell has focus
2424
#
2525
#
2626
# @id-3
27-
# Scenario: Press tab when the up button is focused
28-
# When the user focuses the up button and presses tab
27+
# Scenario: Tab forwards when the up button is focused
28+
# When the user focuses the up button and tabs forwards
2929
# Then the next button has focus
3030
#
3131
#
3232
# @id-4
33-
# Scenario: Press shift + tab when the up button is focused
34-
# When the user focuses the up button and presses shift + tab
33+
# Scenario: Tab backwards when the up button is focused
34+
# When the user focuses the up button and tabs backwards
3535
# Then the previous button has focus
3636
#
3737
#
3838
# @id-5
39-
# Scenario: Press tab when the next button is focused
40-
# When the user focuses the next button and presses tab
39+
# Scenario: Tab forwards when the next button is focused
40+
# When the user focuses the next button and tabs forwards
4141
# Then the tabbable cell has focus
4242
#
4343
#
4444
# @id-6
45-
# Scenario: Press shift + tab when the next button is focused
46-
# When the user focuses the next button and presses shift + tab
45+
# Scenario: Tab backwards when the next button is focused
46+
# When the user focuses the next button and tabs backwards
4747
# Then the up button has focus
4848
#
4949
#
5050
# @id-7
51-
# Scenario: Press tab when today's cell is focused
52-
# When the user focuses the tabbable cell and presses tab
51+
# Scenario: Tab forwards when today's cell is focused
52+
# When the user focuses the tabbable cell and tabs forwards
5353
# Then the previous button has focus
5454
#
5555
#
5656
# @id-8
57-
# Scenario: Press shift + tab when today's cell is focused
58-
# When the user focuses the tabbable cell and presses shift + tab
57+
# Scenario: Tab backwards when today's cell is focused
58+
# When the user focuses the tabbable cell and tabs backwards
5959
# Then the next button has focus
60+
#
61+
#
62+
# @id-9
63+
# Scenario: Inline calendar: Tab forwards when last element is focused
64+
# When the user focuses the last element and tabs forwards
65+
# Then the next element on the page has focus
66+
#
67+
#
68+
# @id-10
69+
# Scenario: Inline calendar: Tab backwards when first element is focused
70+
# When the user focuses the first element and tabs backwards
71+
# Then the previous element on the page has focus

0 commit comments

Comments
 (0)