]> git.ipfire.org Git - thirdparty/bootstrap.git/commitdiff
Automatically select an item in the dropdown when using arrow keys (#34052)
authoralpadev <2838324+alpadev@users.noreply.github.com>
Sat, 22 May 2021 07:58:52 +0000 (09:58 +0200)
committerGitHub <noreply@github.com>
Sat, 22 May 2021 07:58:52 +0000 (10:58 +0300)
.bundlewatch.config.json
js/src/dropdown.js
js/src/util/index.js
js/tests/unit/dropdown.spec.js
js/tests/unit/util/index.spec.js

index 81badf254cb11eaccfd148a4cfc1e4d07a71e23e..988accd7f9c5698c0d71cfe7e76c2bd2ab0a7bab 100644 (file)
@@ -34,7 +34,7 @@
     },
     {
       "path": "./dist/js/bootstrap.bundle.js",
-      "maxSize": "41.25 kB"
+      "maxSize": "41.5 kB"
     },
     {
       "path": "./dist/js/bootstrap.bundle.min.js",
@@ -50,7 +50,7 @@
     },
     {
       "path": "./dist/js/bootstrap.js",
-      "maxSize": "27.25 kB"
+      "maxSize": "27.5 kB"
     },
     {
       "path": "./dist/js/bootstrap.min.js",
index cab2d018bb030cfa7b275f02f0912503777c652a..34beb6512916de9d1d140cebf6923b65f8c13828 100644 (file)
@@ -354,18 +354,16 @@ class Dropdown extends BaseComponent {
     }
   }
 
-  _selectMenuItem(event) {
-    if (![ARROW_UP_KEY, ARROW_DOWN_KEY].includes(event.key)) {
-      return
-    }
-
+  _selectMenuItem({ key, target }) {
     const items = SelectorEngine.find(SELECTOR_VISIBLE_ITEMS, this._menu).filter(isVisible)
 
     if (!items.length) {
       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
@@ -480,17 +478,18 @@ class Dropdown extends BaseComponent {
       return
     }
 
-    if (!isActive && (event.key === ARROW_UP_KEY || event.key === ARROW_DOWN_KEY)) {
-      getToggleButton().click()
+    if (event.key === ARROW_UP_KEY || event.key === ARROW_DOWN_KEY) {
+      if (!isActive) {
+        getToggleButton().click()
+      }
+
+      Dropdown.getInstance(getToggleButton())._selectMenuItem(event)
       return
     }
 
     if (!isActive || event.key === SPACE_KEY) {
       Dropdown.clearMenus()
-      return
     }
-
-    Dropdown.getInstance(getToggleButton())._selectMenuItem(event)
   }
 }
 
index 77bdc072fc6e1487db7c14827e5be77f7b620cee..4d077b21f933ce46fc5a9d35226f2f89571676c9 100644 (file)
@@ -264,9 +264,9 @@ const execute = callback => {
 const getNextActiveElement = (list, activeElement, shouldGetNext, isCycleAllowed) => {
   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) {
-    return list[0]
+    return list[!shouldGetNext && isCycleAllowed ? list.length - 1 : 0]
   }
 
   const listLength = list.length
index 5275f1a5567031654841a046df1c656000d9cca9..390cddfbfaee25f7161075be62ace42893322406 100644 (file)
@@ -1561,7 +1561,7 @@ describe('Dropdown', () => {
       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 = [
         '<div class="dropdown">',
         '  <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>',
@@ -1573,19 +1573,44 @@ describe('Dropdown', () => {
       ].join('')
 
       const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
-      const item1 = fixtureEl.querySelector('#item1')
+      const lastItem = fixtureEl.querySelector('#item2')
 
       triggerDropdown.addEventListener('shown.bs.dropdown', () => {
-        const keydown = createEvent('keydown')
-        keydown.key = 'ArrowUp'
+        setTimeout(() => {
+          expect(document.activeElement).toEqual(lastItem, 'item2 is focused')
+          done()
+        })
+      })
 
-        document.activeElement.dispatchEvent(keydown)
-        expect(document.activeElement).toEqual(item1, 'item1 is focused')
+      const keydown = createEvent('keydown')
+      keydown.key = 'ArrowUp'
+      triggerDropdown.dispatchEvent(keydown)
+    })
 
-        done()
+    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()
+        })
       })
 
-      triggerDropdown.click()
+      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 => {
index 774945d1f92a46453e978a5e63371a209a7adcaa..737ecacfde6c5f8075de6c6d16b8b60a70de4e8d 100644 (file)
@@ -661,11 +661,22 @@ describe('Util', () => {
   })
 
   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']
 
       expect(Util.getNextActiveElement(array, '', 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', () => {