diff --git a/js/src/dropdown.js b/js/src/dropdown.js index 96094a3e6577..7e0df8922a62 100644 --- a/js/src/dropdown.js +++ b/js/src/dropdown.js @@ -71,6 +71,7 @@ const PLACEMENT_BOTTOMCENTER = 'bottom' const Default = { autoClose: true, boundary: 'clippingParents', + cycling: true, display: 'dynamic', offset: [0, 2], popperConfig: null, @@ -80,6 +81,7 @@ const Default = { const DefaultType = { autoClose: '(boolean|string)', boundary: '(string|element)', + cycling: 'boolean', display: 'string', offset: '(array|string|function)', popperConfig: '(null|object|function)', @@ -331,9 +333,8 @@ class Dropdown extends BaseComponent { return } - // 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() + // Allow cycling with up and down arrows + getNextActiveElement(items, target, key === ARROW_DOWN_KEY, this._config.cycling || !items.includes(target)).focus() } // Static diff --git a/js/tests/unit/dropdown.spec.js b/js/tests/unit/dropdown.spec.js index 63ae4bd102bc..934c81551fd3 100644 --- a/js/tests/unit/dropdown.spec.js +++ b/js/tests/unit/dropdown.spec.js @@ -1819,6 +1819,81 @@ describe('Dropdown', () => { }) }) + it('should cycle and focus on the last item when using ArrowUp for the first time, respectively with ArrowDown', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const triggerItem1 = fixtureEl.querySelector('#item1') + const triggerItem2 = fixtureEl.querySelector('#item2') + + const keydown = createEvent('keydown') + keydown.key = 'ArrowDown' + + const keydown2 = createEvent('keydown') + keydown2.key = 'ArrowUp' + + triggerDropdown.dispatchEvent(keydown) + triggerItem1.dispatchEvent(keydown2) + + setTimeout(() => { + expect(document.activeElement).toEqual(triggerItem2, 'item2 is focused') + triggerItem2.dispatchEvent(keydown) + + setTimeout(() => { + expect(document.activeElement).toEqual(triggerItem1, 'item1 is focused') + resolve() + }, 20) + }, 20) + }) + }) + + it('should not cycle and stay focus on the first item when using ArrowUp and respectively with last item and ArrowDown', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const triggerItem1 = fixtureEl.querySelector('#item1') + const triggerItem2 = fixtureEl.querySelector('#item2') + + const keydown = createEvent('keydown') + keydown.key = 'ArrowDown' + + const keydown2 = createEvent('keydown') + keydown2.key = 'ArrowUp' + + triggerDropdown.dispatchEvent(keydown) + triggerItem1.dispatchEvent(keydown2) + + setTimeout(() => { + expect(document.activeElement).toEqual(triggerItem1, 'item1 is focused') + triggerItem1.dispatchEvent(keydown) + triggerItem2.dispatchEvent(keydown) + + setTimeout(() => { + expect(document.activeElement).toEqual(triggerItem2, 'item2 is focused') + resolve() + }, 20) + }, 20) + }) + }) + it('should not close the dropdown if the user clicks on a text field within dropdown-menu', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ diff --git a/site/content/docs/5.3/components/dropdowns.md b/site/content/docs/5.3/components/dropdowns.md index 78a6043c22ba..089778169561 100644 --- a/site/content/docs/5.3/components/dropdowns.md +++ b/site/content/docs/5.3/components/dropdowns.md @@ -1078,6 +1078,7 @@ const dropdownList = [...dropdownElementList].map(dropdownToggleEl => new bootst | --- | --- | --- | --- | | `autoClose` | boolean, string | `true` | Configure the auto close behavior of the dropdown: Note: the dropdown can always be closed with the Esc key. | | `boundary` | string, element | `'clippingParents'` | Overflow constraint boundary of the dropdown menu (applies only to Popper's preventOverflow modifier). By default it's `clippingParents` and can accept an HTMLElement reference (via JavaScript only). For more information refer to Popper's [detectOverflow docs](https://popper.js.org/docs/v2/utils/detect-overflow/#boundary). | +| `cycling` | boolean | `true` | Configure cycling among `.dropdown-item` using `up` and `down` arrow. | | `display` | string | `'dynamic'` | By default, we use Popper for dynamic positioning. Disable this with `static`. | | `offset` | array, string, function | `[0, 2]` | Offset of the dropdown relative to its target. You can pass a string in data attributes with comma separated values like: `data-bs-offset="10,20"`. When a function is used to determine the offset, it is called with an object containing the popper placement, the reference, and popper rects as its first argument. The triggering element DOM node is passed as the second argument. The function must return an array with two numbers: [skidding](https://popper.js.org/docs/v2/modifiers/offset/#skidding-1), [distance](https://popper.js.org/docs/v2/modifiers/offset/#distance-1). For more information refer to Popper's [offset docs](https://popper.js.org/docs/v2/modifiers/offset/#options). | | `popperConfig` | null, object, function | `null` | To change Bootstrap's default Popper config, see [Popper's configuration](https://popper.js.org/docs/v2/constructors/#options). When a function is used to create the Popper configuration, it's called with an object that contains the Bootstrap's default Popper configuration. It helps you use and merge the default with your own configuration. The function must return a configuration object for Popper. |