0
0
mirror of https://github.com/twbs/bootstrap.git synced 2024-12-10 22:24:19 +01:00

Automatically select an item in the dropdown when using arrow keys (#34052)

This commit is contained in:
alpadev 2021-05-22 09:58:52 +02:00 committed by GitHub
parent 8033975548
commit b39b665072
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 61 additions and 26 deletions

View File

@ -34,7 +34,7 @@
}, },
{ {
"path": "./dist/js/bootstrap.bundle.js", "path": "./dist/js/bootstrap.bundle.js",
"maxSize": "41.25 kB" "maxSize": "41.5 kB"
}, },
{ {
"path": "./dist/js/bootstrap.bundle.min.js", "path": "./dist/js/bootstrap.bundle.min.js",
@ -50,7 +50,7 @@
}, },
{ {
"path": "./dist/js/bootstrap.js", "path": "./dist/js/bootstrap.js",
"maxSize": "27.25 kB" "maxSize": "27.5 kB"
}, },
{ {
"path": "./dist/js/bootstrap.min.js", "path": "./dist/js/bootstrap.min.js",

View File

@ -354,18 +354,16 @@ class Dropdown extends BaseComponent {
} }
} }
_selectMenuItem(event) { _selectMenuItem({ key, target }) {
if (![ARROW_UP_KEY, ARROW_DOWN_KEY].includes(event.key)) {
return
}
const items = SelectorEngine.find(SELECTOR_VISIBLE_ITEMS, this._menu).filter(isVisible) const items = SelectorEngine.find(SELECTOR_VISIBLE_ITEMS, this._menu).filter(isVisible)
if (!items.length) { if (!items.length) {
return return
} }
getNextActiveElement(items, event.target, event.key === ARROW_DOWN_KEY, false).focus() // if target isn't included in items (e.g. when expanding the dropdown)
// allow cycling to get the last item in case key equals ARROW_UP_KEY
getNextActiveElement(items, target, key === ARROW_DOWN_KEY, !items.includes(target)).focus()
} }
// Static // Static
@ -480,17 +478,18 @@ class Dropdown extends BaseComponent {
return return
} }
if (!isActive && (event.key === ARROW_UP_KEY || event.key === ARROW_DOWN_KEY)) { if (event.key === ARROW_UP_KEY || event.key === ARROW_DOWN_KEY) {
if (!isActive) {
getToggleButton().click() getToggleButton().click()
}
Dropdown.getInstance(getToggleButton())._selectMenuItem(event)
return return
} }
if (!isActive || event.key === SPACE_KEY) { if (!isActive || event.key === SPACE_KEY) {
Dropdown.clearMenus() Dropdown.clearMenus()
return
} }
Dropdown.getInstance(getToggleButton())._selectMenuItem(event)
} }
} }

View File

@ -264,9 +264,9 @@ const execute = callback => {
const getNextActiveElement = (list, activeElement, shouldGetNext, isCycleAllowed) => { const getNextActiveElement = (list, activeElement, shouldGetNext, isCycleAllowed) => {
let index = list.indexOf(activeElement) let index = list.indexOf(activeElement)
// if the element does not exist in the list initialize it as the first element // if the element does not exist in the list return an element depending on the direction and if cycle is allowed
if (index === -1) { if (index === -1) {
return list[0] return list[!shouldGetNext && isCycleAllowed ? list.length - 1 : 0]
} }
const listLength = list.length const listLength = list.length

View File

@ -1561,7 +1561,7 @@ describe('Dropdown', () => {
triggerDropdown.click() triggerDropdown.click()
}) })
it('should focus on the first element when using ArrowUp for the first time', done => { it('should open the dropdown and focus on the last item when using ArrowUp for the first time', done => {
fixtureEl.innerHTML = [ fixtureEl.innerHTML = [
'<div class="dropdown">', '<div class="dropdown">',
' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>',
@ -1573,19 +1573,44 @@ describe('Dropdown', () => {
].join('') ].join('')
const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
const item1 = fixtureEl.querySelector('#item1') const lastItem = fixtureEl.querySelector('#item2')
triggerDropdown.addEventListener('shown.bs.dropdown', () => { triggerDropdown.addEventListener('shown.bs.dropdown', () => {
const keydown = createEvent('keydown') setTimeout(() => {
keydown.key = 'ArrowUp' expect(document.activeElement).toEqual(lastItem, 'item2 is focused')
document.activeElement.dispatchEvent(keydown)
expect(document.activeElement).toEqual(item1, 'item1 is focused')
done() done()
}) })
})
triggerDropdown.click() const keydown = createEvent('keydown')
keydown.key = 'ArrowUp'
triggerDropdown.dispatchEvent(keydown)
})
it('should open the dropdown and focus on the first item when using ArrowDown for the first time', done => {
fixtureEl.innerHTML = [
'<div class="dropdown">',
' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>',
' <div class="dropdown-menu">',
' <a id="item1" class="dropdown-item" href="#">A link</a>',
' <a id="item2" class="dropdown-item" href="#">Another link</a>',
' </div>',
'</div>'
].join('')
const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
const firstItem = fixtureEl.querySelector('#item1')
triggerDropdown.addEventListener('shown.bs.dropdown', () => {
setTimeout(() => {
expect(document.activeElement).toEqual(firstItem, 'item1 is focused')
done()
})
})
const keydown = createEvent('keydown')
keydown.key = 'ArrowDown'
triggerDropdown.dispatchEvent(keydown)
}) })
it('should not close the dropdown if the user clicks on a text field within dropdown-menu', done => { it('should not close the dropdown if the user clicks on a text field within dropdown-menu', done => {

View File

@ -661,11 +661,22 @@ describe('Util', () => {
}) })
describe('getNextActiveElement', () => { describe('getNextActiveElement', () => {
it('should return first element if active not exists or not given', () => { it('should return first element if active not exists or not given and shouldGetNext is either true, or false with cycling being disabled', () => {
const array = ['a', 'b', 'c', 'd'] const array = ['a', 'b', 'c', 'd']
expect(Util.getNextActiveElement(array, '', true, true)).toEqual('a') expect(Util.getNextActiveElement(array, '', true, true)).toEqual('a')
expect(Util.getNextActiveElement(array, 'g', true, true)).toEqual('a') expect(Util.getNextActiveElement(array, 'g', true, true)).toEqual('a')
expect(Util.getNextActiveElement(array, '', true, false)).toEqual('a')
expect(Util.getNextActiveElement(array, 'g', true, false)).toEqual('a')
expect(Util.getNextActiveElement(array, '', false, false)).toEqual('a')
expect(Util.getNextActiveElement(array, 'g', false, false)).toEqual('a')
})
it('should return last element if active not exists or not given and shouldGetNext is false but cycling is enabled', () => {
const array = ['a', 'b', 'c', 'd']
expect(Util.getNextActiveElement(array, '', false, true)).toEqual('d')
expect(Util.getNextActiveElement(array, 'g', false, true)).toEqual('d')
}) })
it('should return next element or same if is last', () => { it('should return next element or same if is last', () => {