Skip to content

Commit 86372bc

Browse files
committed
refactor(DatePicker, DateRangePicker): add focus trap for better accessibility handling
1 parent 080dfc0 commit 86372bc

File tree

3 files changed

+59
-4
lines changed

3 files changed

+59
-4
lines changed

js/src/date-picker.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ class DatePicker extends DateRangePicker {
140140
const composedPath = event.composedPath()
141141

142142
if (
143-
composedPath.includes(context._element)
143+
composedPath.includes(context._element) || composedPath.includes(context._menu)
144144
) {
145145
continue
146146
}

js/src/date-range-picker.js

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import EventHandler from './dom/event-handler.js'
1313
import Manipulator from './dom/manipulator.js'
1414
import SelectorEngine from './dom/selector-engine.js'
1515
import { defineJQueryPlugin, getElement, isRTL } from './util/index.js'
16+
import FocusTrap from './util/focustrap.js'
1617
import {
1718
convertToDateObject, getDateBySelectionType, getLocalDateFromString, isDateDisabled
1819
} from './util/calendar.js'
@@ -218,6 +219,8 @@ class DateRangePicker extends BaseComponent {
218219
this._createDateRangePickerCalendars()
219220
this._addEventListeners()
220221
this._addCalendarEventListeners()
222+
223+
this._focustrap = this._initializeFocusTrap()
221224
}
222225

223226
// Getters
@@ -254,6 +257,7 @@ class DateRangePicker extends BaseComponent {
254257
this._menu.classList.add(CLASS_NAME_SHOW)
255258
}
256259

260+
this._focustrap.activate()
257261
EventHandler.trigger(this._element, EVENT_SHOWN)
258262

259263
this._createPopper()
@@ -273,6 +277,7 @@ class DateRangePicker extends BaseComponent {
273277
this._menu.classList.remove(CLASS_NAME_SHOW)
274278
}
275279

280+
this._focustrap.deactivate()
276281
EventHandler.trigger(this._element, EVENT_HIDDEN)
277282
}
278283

@@ -289,6 +294,8 @@ class DateRangePicker extends BaseComponent {
289294
clearTimeout(this._endInputTimeout)
290295
}
291296

297+
this._focustrap.deactivate()
298+
292299
super.dispose()
293300
}
294301

@@ -335,6 +342,13 @@ class DateRangePicker extends BaseComponent {
335342
}
336343

337344
// Private
345+
_initializeFocusTrap() {
346+
return new FocusTrap({
347+
additionalElement: this._config.container ? this._menu : null,
348+
trapElement: this._element
349+
})
350+
}
351+
338352
_addEventListeners() {
339353
EventHandler.on(this._indicatorElement, EVENT_CLICK, () => {
340354
if (!this._config.disabled) {
@@ -357,6 +371,7 @@ class DateRangePicker extends BaseComponent {
357371
EventHandler.on(this._element, EVENT_KEYDOWN, event => {
358372
if (event.key === ESCAPE_KEY) {
359373
this.hide()
374+
this._startInput.focus()
360375
}
361376
})
362377

@@ -460,7 +475,7 @@ class DateRangePicker extends BaseComponent {
460475
}
461476

462477
_addCalendarEventListeners() {
463-
for (const calendar of SelectorEngine.find(SELECTOR_CALENDAR, this._element)) {
478+
for (const calendar of SelectorEngine.find(SELECTOR_CALENDAR, this._menu)) {
464479
EventHandler.on(calendar, 'startDateChange.coreui.calendar', event => {
465480
this._changeStartDate(event.date)
466481

@@ -1068,7 +1083,7 @@ class DateRangePicker extends BaseComponent {
10681083
const composedPath = event.composedPath()
10691084

10701085
if (
1071-
composedPath.includes(context._element)
1086+
composedPath.includes(context._element) || composedPath.includes(context._menu)
10721087
) {
10731088
continue
10741089
}

js/src/util/focustrap.js

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,13 @@ const TAB_NAV_FORWARD = 'forward'
2727
const TAB_NAV_BACKWARD = 'backward'
2828

2929
const Default = {
30+
additionalElement: null,
3031
autofocus: true,
3132
trapElement: null // The element to trap focus inside of
3233
}
3334

3435
const DefaultType = {
36+
additionalElement: '(element|null|undefined)',
3537
autofocus: 'boolean',
3638
trapElement: 'element'
3739
}
@@ -89,12 +91,16 @@ class FocusTrap extends Config {
8991

9092
// Private
9193
_handleFocusin(event) {
92-
const { trapElement } = this._config
94+
const { additionalElement, trapElement } = this._config
9395

9496
if (event.target === document || event.target === trapElement || trapElement.contains(event.target)) {
9597
return
9698
}
9799

100+
if (additionalElement && (event.target === additionalElement || additionalElement.contains(event.target))) {
101+
return
102+
}
103+
98104
const elements = SelectorEngine.focusableChildren(trapElement)
99105

100106
if (elements.length === 0) {
@@ -112,6 +118,40 @@ class FocusTrap extends Config {
112118
}
113119

114120
this._lastTabNavDirection = event.shiftKey ? TAB_NAV_BACKWARD : TAB_NAV_FORWARD
121+
122+
const { additionalElement, trapElement } = this._config
123+
124+
if (!additionalElement) {
125+
return
126+
}
127+
128+
const trapElements = SelectorEngine.focusableChildren(trapElement)
129+
const additionalElements = SelectorEngine.focusableChildren(additionalElement)
130+
131+
if (trapElements.length === 0 || additionalElements.length === 0) {
132+
return
133+
}
134+
135+
event.preventDefault()
136+
137+
if (trapElements.indexOf(event.target) === trapElements.length - 1 && !event.shiftKey) {
138+
additionalElements[0].focus()
139+
return
140+
}
141+
142+
if (trapElements.indexOf(event.target) === 0 && event.shiftKey) {
143+
additionalElements[additionalElements.length - 1].focus()
144+
return
145+
}
146+
147+
if (additionalElements.indexOf(event.target) === additionalElements.length - 1 && !event.shiftKey) {
148+
trapElements[0].focus()
149+
return
150+
}
151+
152+
if (additionalElements.indexOf(event.target) === 0 && event.shiftKey) {
153+
trapElements[trapElements.length - 1].focus()
154+
}
115155
}
116156
}
117157

0 commit comments

Comments
 (0)