Skip to content

Commit b39b665

Browse files
authored
Automatically select an item in the dropdown when using arrow keys (#34052)
1 parent 8033975 commit b39b665

File tree

5 files changed

+59
-24
lines changed

5 files changed

+59
-24
lines changed

.bundlewatch.config.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
},
3535
{
3636
"path": "./dist/js/bootstrap.bundle.js",
37-
"maxSize": "41.25 kB"
37+
"maxSize": "41.5 kB"
3838
},
3939
{
4040
"path": "./dist/js/bootstrap.bundle.min.js",
@@ -50,7 +50,7 @@
5050
},
5151
{
5252
"path": "./dist/js/bootstrap.js",
53-
"maxSize": "27.25 kB"
53+
"maxSize": "27.5 kB"
5454
},
5555
{
5656
"path": "./dist/js/bootstrap.min.js",

js/src/dropdown.js

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -354,18 +354,16 @@ class Dropdown extends BaseComponent {
354354
}
355355
}
356356

357-
_selectMenuItem(event) {
358-
if (![ARROW_UP_KEY, ARROW_DOWN_KEY].includes(event.key)) {
359-
return
360-
}
361-
357+
_selectMenuItem({ key, target }) {
362358
const items = SelectorEngine.find(SELECTOR_VISIBLE_ITEMS, this._menu).filter(isVisible)
363359

364360
if (!items.length) {
365361
return
366362
}
367363

368-
getNextActiveElement(items, event.target, event.key === ARROW_DOWN_KEY, false).focus()
364+
// if target isn't included in items (e.g. when expanding the dropdown)
365+
// allow cycling to get the last item in case key equals ARROW_UP_KEY
366+
getNextActiveElement(items, target, key === ARROW_DOWN_KEY, !items.includes(target)).focus()
369367
}
370368

371369
// Static
@@ -480,17 +478,18 @@ class Dropdown extends BaseComponent {
480478
return
481479
}
482480

483-
if (!isActive && (event.key === ARROW_UP_KEY || event.key === ARROW_DOWN_KEY)) {
484-
getToggleButton().click()
481+
if (event.key === ARROW_UP_KEY || event.key === ARROW_DOWN_KEY) {
482+
if (!isActive) {
483+
getToggleButton().click()
484+
}
485+
486+
Dropdown.getInstance(getToggleButton())._selectMenuItem(event)
485487
return
486488
}
487489

488490
if (!isActive || event.key === SPACE_KEY) {
489491
Dropdown.clearMenus()
490-
return
491492
}
492-
493-
Dropdown.getInstance(getToggleButton())._selectMenuItem(event)
494493
}
495494
}
496495

js/src/util/index.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -264,9 +264,9 @@ const execute = callback => {
264264
const getNextActiveElement = (list, activeElement, shouldGetNext, isCycleAllowed) => {
265265
let index = list.indexOf(activeElement)
266266

267-
// if the element does not exist in the list initialize it as the first element
267+
// if the element does not exist in the list return an element depending on the direction and if cycle is allowed
268268
if (index === -1) {
269-
return list[0]
269+
return list[!shouldGetNext && isCycleAllowed ? list.length - 1 : 0]
270270
}
271271

272272
const listLength = list.length

js/tests/unit/dropdown.spec.js

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1561,7 +1561,7 @@ describe('Dropdown', () => {
15611561
triggerDropdown.click()
15621562
})
15631563

1564-
it('should focus on the first element when using ArrowUp for the first time', done => {
1564+
it('should open the dropdown and focus on the last item when using ArrowUp for the first time', done => {
15651565
fixtureEl.innerHTML = [
15661566
'<div class="dropdown">',
15671567
' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>',
@@ -1573,19 +1573,44 @@ describe('Dropdown', () => {
15731573
].join('')
15741574

15751575
const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
1576-
const item1 = fixtureEl.querySelector('#item1')
1576+
const lastItem = fixtureEl.querySelector('#item2')
15771577

15781578
triggerDropdown.addEventListener('shown.bs.dropdown', () => {
1579-
const keydown = createEvent('keydown')
1580-
keydown.key = 'ArrowUp'
1579+
setTimeout(() => {
1580+
expect(document.activeElement).toEqual(lastItem, 'item2 is focused')
1581+
done()
1582+
})
1583+
})
15811584

1582-
document.activeElement.dispatchEvent(keydown)
1583-
expect(document.activeElement).toEqual(item1, 'item1 is focused')
1585+
const keydown = createEvent('keydown')
1586+
keydown.key = 'ArrowUp'
1587+
triggerDropdown.dispatchEvent(keydown)
1588+
})
15841589

1585-
done()
1590+
it('should open the dropdown and focus on the first item when using ArrowDown for the first time', done => {
1591+
fixtureEl.innerHTML = [
1592+
'<div class="dropdown">',
1593+
' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>',
1594+
' <div class="dropdown-menu">',
1595+
' <a id="item1" class="dropdown-item" href="#">A link</a>',
1596+
' <a id="item2" class="dropdown-item" href="#">Another link</a>',
1597+
' </div>',
1598+
'</div>'
1599+
].join('')
1600+
1601+
const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
1602+
const firstItem = fixtureEl.querySelector('#item1')
1603+
1604+
triggerDropdown.addEventListener('shown.bs.dropdown', () => {
1605+
setTimeout(() => {
1606+
expect(document.activeElement).toEqual(firstItem, 'item1 is focused')
1607+
done()
1608+
})
15861609
})
15871610

1588-
triggerDropdown.click()
1611+
const keydown = createEvent('keydown')
1612+
keydown.key = 'ArrowDown'
1613+
triggerDropdown.dispatchEvent(keydown)
15891614
})
15901615

15911616
it('should not close the dropdown if the user clicks on a text field within dropdown-menu', done => {

js/tests/unit/util/index.spec.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -661,11 +661,22 @@ describe('Util', () => {
661661
})
662662

663663
describe('getNextActiveElement', () => {
664-
it('should return first element if active not exists or not given', () => {
664+
it('should return first element if active not exists or not given and shouldGetNext is either true, or false with cycling being disabled', () => {
665665
const array = ['a', 'b', 'c', 'd']
666666

667667
expect(Util.getNextActiveElement(array, '', true, true)).toEqual('a')
668668
expect(Util.getNextActiveElement(array, 'g', true, true)).toEqual('a')
669+
expect(Util.getNextActiveElement(array, '', true, false)).toEqual('a')
670+
expect(Util.getNextActiveElement(array, 'g', true, false)).toEqual('a')
671+
expect(Util.getNextActiveElement(array, '', false, false)).toEqual('a')
672+
expect(Util.getNextActiveElement(array, 'g', false, false)).toEqual('a')
673+
})
674+
675+
it('should return last element if active not exists or not given and shouldGetNext is false but cycling is enabled', () => {
676+
const array = ['a', 'b', 'c', 'd']
677+
678+
expect(Util.getNextActiveElement(array, '', false, true)).toEqual('d')
679+
expect(Util.getNextActiveElement(array, 'g', false, true)).toEqual('d')
669680
})
670681

671682
it('should return next element or same if is last', () => {

0 commit comments

Comments
 (0)