Skip to content

Commit 5bbf131

Browse files
committed
feat(datepicker): Build an array of navElements to focus trap
1 parent f26a376 commit 5bbf131

File tree

3 files changed

+139
-1
lines changed

3 files changed

+139
-1
lines changed

src/components/Datepicker.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@
6060
@mousedown.prevent
6161
>
6262
<Transition name="view">
63-
<div :key="view">
63+
<div ref="view" :key="view">
6464
<slot name="beforeCalendarHeader" />
6565
<Component
6666
:is="picker"
@@ -442,6 +442,7 @@ export default {
442442
this.setInitialView()
443443
this.$nextTick(() => {
444444
this.setTabbableCell()
445+
this.setNavElements()
445446
})
446447
447448
this.$emit('opened')

src/mixins/navMixin.vue

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,98 @@
22
export default {
33
data() {
44
return {
5+
navElements: [],
56
tabbableCell: null,
67
}
78
},
89
methods: {
10+
/**
11+
* Returns true if the calendar has been passed the given slot
12+
* @param {String} slotName The name of the slot
13+
* @return {Boolean}
14+
*/
15+
hasSlot(slotName) {
16+
return !!this.$slots[slotName]
17+
},
18+
/**
19+
* Returns an array of all HTML elements which should be focus-trapped in the specified slot
20+
* @returns {Array} An array of HTML elements
21+
*/
22+
getElementsFromSlot(slotName) {
23+
if (!this.hasSlot(slotName)) {
24+
return []
25+
}
26+
27+
if (slotName === 'beforeCalendarHeader') {
28+
return this.getFocusableElements(this.$refs.view.children[0])
29+
}
30+
31+
if (slotName === 'calendarFooter') {
32+
return this.getFocusableElements(this.$refs.view.children[2])
33+
}
34+
35+
const isBeforeHeader = slotName.indexOf('beforeCalendarHeader') > -1
36+
const picker = this.$refs.picker.$el
37+
const index = isBeforeHeader ? 0 : picker.children.length - 1
38+
39+
return this.getFocusableElements(picker.children[index])
40+
},
41+
/**
42+
* Returns an array of all HTML elements which should be focus-trapped in the header
43+
* @returns {Array} An array of HTML elements
44+
*/
45+
getElementsFromHeader() {
46+
const view = this.ucFirst(this.view)
47+
const beforeCalendarSlotName = `beforeCalendarHeader${view}`
48+
const picker = this.$refs.picker.$el
49+
const index = this.hasSlot(beforeCalendarSlotName) ? 1 : 0
50+
const fragment = picker.children[index]
51+
52+
return this.showHeader ? this.getFocusableElements(fragment) : []
53+
},
54+
/**
55+
* Returns an array of focusable elements in a given HTML fragment
56+
* @param {Element} fragment The HTML fragment to search
57+
* @returns {Array}
58+
*/
59+
getFocusableElements(fragment) {
60+
const navNodeList = fragment.querySelectorAll(
61+
'button:enabled, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])',
62+
)
63+
64+
return [...Array.prototype.slice.call(navNodeList)]
65+
},
66+
/**
67+
* Returns the input element (when typeable)
68+
* @returns {Element}
69+
*/
70+
getInputField() {
71+
if (!this.typeable || this.inline) {
72+
return null
73+
}
74+
75+
return this.$refs.dateInput.$refs[this.refName]
76+
},
77+
/**
78+
* Determines which elements in datepicker should be focus-trapped
79+
*/
80+
setNavElements() {
81+
if (!this.view) return
82+
83+
const view = this.ucFirst(this.view)
84+
85+
this.navElements = [
86+
this.getInputField(),
87+
this.getElementsFromSlot('beforeCalendarHeader'),
88+
this.getElementsFromSlot(`beforeCalendarHeader${view}`),
89+
this.getElementsFromHeader(),
90+
this.tabbableCell,
91+
this.getElementsFromSlot(`calendarFooter${view}`),
92+
this.getElementsFromSlot('calendarFooter'),
93+
]
94+
.filter((item) => !!item)
95+
.reduce((acc, val) => acc.concat(val), [])
96+
},
997
/**
1098
* Sets the focus-trapped cell in the picker
1199
*/

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

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,55 @@ describe('Datepicker mounted', () => {
9696
})
9797
})
9898

99+
describe('Datepicker mounted with slots', () => {
100+
let wrapper
101+
102+
beforeEach(() => {
103+
const beforeCalendarHeader =
104+
'<div key="0">Example <a href="#">beforeCalendarHeader</a> slot</div>'
105+
const beforeCalendarHeaderDay =
106+
'<div key="1">Example <a href="#">beforeCalendarHeaderDay</a> slot</div>'
107+
const calendarFooterDay =
108+
'<div key="2">Example <a href="#">calendarFooterDay</a> slot</div>'
109+
const beforeCalendarHeaderMonth =
110+
'<div key="3">Example <a href="#">beforeCalendarHeaderMonth</a> slot</div>'
111+
const calendarFooterMonth =
112+
'<div key="4">Example <a href="#">calendarFooterMonth</a> slot</div>'
113+
const beforeCalendarHeaderYear =
114+
'<div key="5">Example <a href="#">beforeCalendarHeaderYear</a> slot</div>'
115+
const calendarFooterYear =
116+
'<div key="6">Example <a href="#">calendarFooterYear</a> slot</div>'
117+
const calendarFooter =
118+
'<div key="7">Example <a href="#">calendarFooter</a> slot</div>'
119+
120+
wrapper = mount(Datepicker, {
121+
slots: {
122+
beforeCalendarHeader,
123+
beforeCalendarHeaderDay,
124+
calendarFooterDay,
125+
beforeCalendarHeaderMonth,
126+
calendarFooterMonth,
127+
beforeCalendarHeaderYear,
128+
calendarFooterYear,
129+
calendarFooter,
130+
},
131+
})
132+
})
133+
134+
afterEach(() => {
135+
wrapper.destroy()
136+
})
137+
138+
it('knows how many navElements there are', async () => {
139+
expect(wrapper.vm.navElements.length).toEqual(0)
140+
141+
const input = wrapper.find('input')
142+
await input.trigger('click')
143+
144+
expect(wrapper.vm.navElements.length).toEqual(8)
145+
})
146+
})
147+
99148
describe('Datepicker shallowMounted', () => {
100149
let wrapper
101150
let date

0 commit comments

Comments
 (0)