Skip to content

Commit 3f247ab

Browse files
committed
feat(DatePicker, DateRangePicker, MultiSelect, TimePicker): improve keyboard support
1 parent 6556c6b commit 3f247ab

File tree

3 files changed

+50
-6
lines changed

3 files changed

+50
-6
lines changed

js/src/date-range-picker.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const DATA_KEY = 'coreui.date-range-picker'
2424
const EVENT_KEY = `.${DATA_KEY}`
2525
const DATA_API_KEY = '.data-api'
2626

27+
const ESCAPE_KEY = 'Escape'
2728
const TAB_KEY = 'Tab'
2829
const RIGHT_MOUSE_BUTTON = 2
2930

@@ -32,6 +33,7 @@ const EVENT_END_DATE_CHANGE = `endDateChange${EVENT_KEY}`
3233
const EVENT_HIDE = `hide${EVENT_KEY}`
3334
const EVENT_HIDDEN = `hidden${EVENT_KEY}`
3435
const EVENT_INPUT = 'input'
36+
const EVENT_KEYDOWN = `keydown${EVENT_KEY}`
3537
const EVENT_RESIZE = 'resize'
3638
const EVENT_SHOW = `show${EVENT_KEY}`
3739
const EVENT_SHOWN = `shown${EVENT_KEY}`
@@ -327,6 +329,12 @@ class DateRangePicker extends BaseComponent {
327329
}
328330
})
329331

332+
EventHandler.on(this._element, EVENT_KEYDOWN, event => {
333+
if (event.key === ESCAPE_KEY) {
334+
this.hide()
335+
}
336+
})
337+
330338
EventHandler.on(this._startInput, EVENT_CLICK, () => {
331339
this._selectEndDate = false
332340
this._calendar.update(this._getCalendarConfig())

js/src/multi-select.js

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,12 @@ import BaseComponent from './base-component.js'
1010
import Data from './dom/data.js'
1111
import EventHandler from './dom/event-handler.js'
1212
import SelectorEngine from './dom/selector-engine.js'
13-
import { defineJQueryPlugin, isRTL } from './util/index.js'
13+
import {
14+
defineJQueryPlugin,
15+
getNextActiveElement,
16+
isVisible,
17+
isRTL
18+
} from './util/index.js'
1419

1520
/**
1621
* ------------------------------------------------------------------------
@@ -23,8 +28,11 @@ const DATA_KEY = 'coreui.multi-select'
2328
const EVENT_KEY = `.${DATA_KEY}`
2429
const DATA_API_KEY = '.data-api'
2530

31+
const ESCAPE_KEY = 'Escape'
2632
const TAB_KEY = 'Tab'
27-
const RIGHT_MOUSE_BUTTON = 2
33+
const ARROW_UP_KEY = 'ArrowUp'
34+
const ARROW_DOWN_KEY = 'ArrowDown'
35+
const RIGHT_MOUSE_BUTTON = 2 // MouseEvent.button value for the secondary button, usually the right button
2836

2937
const SELECTOR_CLEANER = '.form-multi-select-cleaner'
3038
const SELECTOR_OPTGROUP = '.form-multi-select-optgroup'
@@ -34,6 +42,7 @@ const SELECTOR_OPTIONS_EMPTY = '.form-multi-select-options-empty'
3442
const SELECTOR_SEARCH = '.form-multi-select-search'
3543
const SELECTOR_SELECT = '.form-multi-select'
3644
const SELECTOR_SELECTION = '.form-multi-select-selection'
45+
const SELECTOR_VISIBLE_ITEMS = '.form-multi-select-options .form-multi-select-option:not(.disabled):not(:disabled)'
3746

3847
const EVENT_CHANGED = `changed${EVENT_KEY}`
3948
const EVENT_CLICK = `click${EVENT_KEY}`
@@ -261,6 +270,12 @@ class MultiSelect extends BaseComponent {
261270
}
262271
})
263272

273+
EventHandler.on(this._clone, EVENT_KEYDOWN, event => {
274+
if (event.key === ESCAPE_KEY) {
275+
this.hide()
276+
}
277+
})
278+
264279
EventHandler.on(this._indicatorElement, EVENT_CLICK, event => {
265280
event.preventDefault()
266281
event.stopPropagation()
@@ -306,9 +321,11 @@ class MultiSelect extends BaseComponent {
306321

307322
if (key === 13) {
308323
this._onOptionsClick(event.target)
309-
if (this._config.search) {
310-
SelectorEngine.findOne(SELECTOR_SEARCH, this._clone).focus()
311-
}
324+
}
325+
326+
if ([ARROW_UP_KEY, ARROW_DOWN_KEY].includes(event.key)) {
327+
event.preventDefault()
328+
this._selectMenuItem(event)
312329
}
313330
})
314331
}
@@ -881,6 +898,18 @@ class MultiSelect extends BaseComponent {
881898
}
882899
}
883900

901+
_selectMenuItem({ key, target }) {
902+
const items = SelectorEngine.find(SELECTOR_VISIBLE_ITEMS, this._menu).filter(element => isVisible(element))
903+
904+
if (!items.length) {
905+
return
906+
}
907+
908+
// if target isn't included in items (e.g. when expanding the dropdown)
909+
// allow cycling to get the last item in case key equals ARROW_UP_KEY
910+
getNextActiveElement(items, target, key === ARROW_DOWN_KEY, !items.includes(target)).focus()
911+
}
912+
884913
// Static
885914

886915
static multiSelectInterface(element, config) {
@@ -949,7 +978,6 @@ EventHandler.on(window, EVENT_LOAD_DATA_API, () => {
949978
}
950979
}
951980
})
952-
953981
EventHandler.on(document, EVENT_CLICK_DATA_API, MultiSelect.clearMenus)
954982
EventHandler.on(document, EVENT_KEYUP_DATA_API, MultiSelect.clearMenus)
955983

js/src/time-picker.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ const EVENT_KEY = `.${DATA_KEY}`
3030
const DATA_API_KEY = '.data-api'
3131

3232
const ENTER_KEY = 'Enter'
33+
const ESCAPE_KEY = 'Escape'
3334
const SPACE_KEY = 'Space'
3435
const TAB_KEY = 'Tab'
3536
const RIGHT_MOUSE_BUTTON = 2
@@ -38,6 +39,7 @@ const EVENT_CLICK = `click${EVENT_KEY}`
3839
const EVENT_HIDE = `hide${EVENT_KEY}`
3940
const EVENT_HIDDEN = `hidden${EVENT_KEY}`
4041
const EVENT_INPUT = 'input'
42+
const EVENT_KEYDOWN = `keydown${EVENT_KEY}`
4143
const EVENT_SHOW = `show${EVENT_KEY}`
4244
const EVENT_SHOWN = `shown${EVENT_KEY}`
4345
const EVENT_SUBMIT = 'submit'
@@ -257,6 +259,12 @@ class TimePicker extends BaseComponent {
257259
}
258260
})
259261

262+
EventHandler.on(this._element, EVENT_KEYDOWN, event => {
263+
if (event.key === ESCAPE_KEY) {
264+
this.hide()
265+
}
266+
})
267+
260268
EventHandler.on(this._element, 'timeChange.coreui.time-picker', () => {
261269
if (this._config.variant === 'roll') {
262270
this._setUpRolls()

0 commit comments

Comments
 (0)