Skip to content

Commit

Permalink
feat(datepicker): Tabbing to/from an inline datepicker
Browse files Browse the repository at this point in the history
  • Loading branch information
mst101 committed Jan 18, 2022
1 parent 540cb34 commit 52ad18a
Show file tree
Hide file tree
Showing 6 changed files with 218 additions and 36 deletions.
6 changes: 6 additions & 0 deletions src/components/Datepicker.vue
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,12 @@ export default {
this.setInitialView()
}
},
isActive(hasJustBecomeActive) {
if (hasJustBecomeActive && this.inline) {
this.setNavElementsFocusedIndex()
this.tabToCorrectInlineCell()
}
},
openDate() {
this.setPageDate()
},
Expand Down
140 changes: 137 additions & 3 deletions src/mixins/navMixin.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export default {
delay: 0,
refs: [],
},
inlineTabbableCell: null,
isActive: false,
isRevertingToOpenDate: false,
navElements: [],
Expand Down Expand Up @@ -47,6 +48,17 @@ export default {
},
},
methods: {
/**
* Returns true, unless tabbing should be focus-trapped
* @return {Boolean}
*/
allowNormalTabbing(event) {
if (!this.isOpen) {
return true
}
return this.isTabbingAwayFromInlineDatepicker(event)
},
/**
* Focuses the first non-disabled element found in the `focus.refs` array and sets `navElementsFocusedIndex`
*/
Expand All @@ -63,6 +75,22 @@ export default {
}
}
},
/**
* Ensures the most recently focused tabbable cell is focused when tabbing backwards to an inline calendar
* If no element has previously been focused, the tabbable cell is reset and focused
*/
focusInlineTabbableCell() {
if (this.inlineTabbableCell) {
this.inlineTabbableCell.focus()
return
}
this.resetTabbableCell = true
this.setTabbableCell()
this.tabbableCell.focus()
this.resetTabbableCell = false
},
/**
* Returns the currently focused cell element, if there is one...
*/
Expand Down Expand Up @@ -181,7 +209,7 @@ export default {
document.datepickerId = this.datepickerId
this.isActive = true
this.setInlineTabbableCell()
this.setAllElements()
this.setNavElements()
},
Expand All @@ -198,6 +226,57 @@ export default {
hasArrowedToNewPage() {
return this.focus.refs && this.focus.refs[0] === 'arrow-to-cell'
},
/**
* Returns true if the user is tabbing away from an inline datepicker
* @return {Boolean}
*/
isTabbingAwayFromInlineDatepicker(event) {
if (!this.inline) {
return false
}
if (this.isTabbingAwayFromFirstNavElement(event)) {
this.tabAwayFromFirstElement()
return true
}
if (this.isTabbingAwayFromLastNavElement(event)) {
this.tabAwayFromLastElement()
return true
}
return false
},
/**
* Used for inline calendars; returns true if the user tabs backwards from the first focusable element
* @param {object} event Used to determine whether we are tabbing forwards or backwards
* @return {Boolean}
*/
isTabbingAwayFromFirstNavElement(event) {
if (!event.shiftKey) {
return false
}
const firstNavElement = this.navElements[0]
return document.activeElement === firstNavElement
},
/**
* Used for inline calendars; returns true if the user tabs forwards from the last focusable element
* @param {object} event Used to determine whether we are tabbing forwards or backwards
* @return {Boolean}
*/
isTabbingAwayFromLastNavElement(event) {
if (event.shiftKey) {
return false
}
const lastNavElement = this.navElements[this.navElements.length - 1]
return document.activeElement === lastNavElement
},
/**
* Resets the focus to the open date
*/
Expand Down Expand Up @@ -239,6 +318,17 @@ export default {
this.resetTabbableCell = false
})
},
/**
* Stores the current tabbableCell of an inline datepicker
* N.B. This is used when tabbing back (shift + tab) to an inline calendar from further down the page
*/
setInlineTabbableCell() {
if (!this.inline) {
return
}
this.inlineTabbableCell = this.tabbableCell
},
/**
* Sets the direction of the slide transition and whether or not to delay application of the focus
* @param {Date|Number} startDate The date from which to measure
Expand Down Expand Up @@ -339,6 +429,26 @@ export default {
this.transitionName = isInTheFuture ? 'slide-right' : 'slide-left'
}
},
/**
* Focuses the first focusable element in the datepicker, so that the previous element on the page will be tabbed to
*/
tabAwayFromFirstElement() {
const firstElement = this.allElements[0]
firstElement.focus()
this.tabbableCell = this.inlineTabbableCell
},
/**
* Focuses the last focusable element in the datepicker, so that the next element on the page will be tabbed to
*/
tabAwayFromLastElement() {
const lastElement = this.allElements[this.allElements.length - 1]
lastElement.focus()
this.tabbableCell = this.inlineTabbableCell
},
/**
* Tab backwards through the focus-trapped elements
*/
Expand Down Expand Up @@ -368,10 +478,10 @@ export default {
* @param event
*/
tabThroughNavigation(event) {
// Allow normal tabbing when closed
if (!this.isOpen) {
if (this.allowNormalTabbing(event)) {
return
}
event.preventDefault()
if (event.shiftKey) {
Expand All @@ -380,6 +490,30 @@ export default {
this.tabForwards()
}
},
/**
* Special cases for when tabbing to an inline datepicker
*/
tabToCorrectInlineCell() {
const lastElement = this.allElements[this.allElements.length - 1]
const isACell = this.hasClass(lastElement, 'cell')
const isLastElementFocused = document.activeElement === lastElement
// If there are no focusable elements in the footer slots and the inline datepicker has been tabbed to (backwards)
if (isACell && isLastElementFocused) {
this.focusInlineTabbableCell()
return
}
// If `show-header` is false and the inline datepicker has been tabbed to (forwards)
this.$nextTick(() => {
const isFirstCell =
document.activeElement.getAttribute('data-id') === '0'
if (isFirstCell) {
this.focusInlineTabbableCell()
}
})
},
/**
* Update which cell in the picker should be focus-trapped
*/
Expand Down
4 changes: 3 additions & 1 deletion test/FEATURES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
| 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 |
| --------------- | -------------- | -------- | ---------- | -------------------- | ------------- | ------------------------------------------------------- | -------------------------- | -------------- | ------------------------------- | ------------------------- | ------------------------- |---------|
|-----------------|----------------|----------|------------|----------------------|---------------|---------------------------------------------------------|----------------------------|----------------|---------------------------------|---------------------------|---------------------------|---------|
| ANY | closed | | | | | initialView=day | Open calendar | open | focusable-cell (day) | | InitialFocus | 1#1 |
| | closed | | | | | initialView=month | Open calendar | open | focusable-cell (month) | | InitialFocus | 1#2 |
| | closed | | | | | initialView=year | Open calendar | open | focusable-cell (year) | | InitialFocus | 1#3 |
Expand Down Expand Up @@ -147,5 +147,7 @@
| | open | | | | | | Keydown right | open | cell right on next page | Focusable cell, page up | CellNavigation | 2#4 |
| | open | | | | | | Keydown tab | open | prev | | FocusTrap | 7 |
| | open | | | | | | Keydown shift + tab | open | next | | FocusTrap | 8 |
| | open | | | | | isInline, last element is focused | Keydown tab | open | Previous element on page | | FocusTrap | 9 |
| | open | | | | | isInline, first element is focused | Keydown shift + tab | open | Next element on page | | FocusTrap | 10 |
| --------------- | -------------- | -------- | ---------- | -------------------- | ------------- | --------------------------------- | -------------------------- | -------------- | -------------------------- | ------------------------- | ------------------------- | ------- |
| 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 |
44 changes: 28 additions & 16 deletions test/e2e/specs/FocusTrap.feature
Original file line number Diff line number Diff line change
Expand Up @@ -13,47 +13,59 @@ Feature: Focus Trap
#
#
# @id-1
# Scenario: Press tab when the previous button is focused
# When the user focuses the previous button and presses tab
# Scenario: Tab forwards when the previous button is focused
# When the user focuses the previous button and tabs forwards
# Then the up button has focus
#
# @id-2
# Scenario: Press shift + tab when the previous button is focused
# When the user focuses the previous button and presses shift + tab
# Scenario: Tab backwards when the previous button is focused
# When the user focuses the previous button and tabs backwards
# Then the tabbable cell has focus
#
#
# @id-3
# Scenario: Press tab when the up button is focused
# When the user focuses the up button and presses tab
# Scenario: Tab forwards when the up button is focused
# When the user focuses the up button and tabs forwards
# Then the next button has focus
#
#
# @id-4
# Scenario: Press shift + tab when the up button is focused
# When the user focuses the up button and presses shift + tab
# Scenario: Tab backwards when the up button is focused
# When the user focuses the up button and tabs backwards
# Then the previous button has focus
#
#
# @id-5
# Scenario: Press tab when the next button is focused
# When the user focuses the next button and presses tab
# Scenario: Tab forwards when the next button is focused
# When the user focuses the next button and tabs forwards
# Then the tabbable cell has focus
#
#
# @id-6
# Scenario: Press shift + tab when the next button is focused
# When the user focuses the next button and presses shift + tab
# Scenario: Tab backwards when the next button is focused
# When the user focuses the next button and tabs backwards
# Then the up button has focus
#
#
# @id-7
# Scenario: Press tab when today's cell is focused
# When the user focuses the tabbable cell and presses tab
# Scenario: Tab forwards when today's cell is focused
# When the user focuses the tabbable cell and tabs forwards
# Then the previous button has focus
#
#
# @id-8
# Scenario: Press shift + tab when today's cell is focused
# When the user focuses the tabbable cell and presses shift + tab
# Scenario: Tab backwards when today's cell is focused
# When the user focuses the tabbable cell and tabs backwards
# Then the next button has focus
#
#
# @id-9
# Scenario: Inline calendar: Tab forwards when last element is focused
# When the user focuses the last element and tabs forwards
# Then the next element on the page has focus
#
#
# @id-10
# Scenario: Inline calendar: Tab backwards when first element is focused
# When the user focuses the first element and tabs backwards
# Then the previous element on the page has focus
Loading

0 comments on commit 52ad18a

Please sign in to comment.