Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dropdown — Add option to make the dropdown menu clickable #33389

Merged
merged 7 commits into from Apr 20, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
20 changes: 13 additions & 7 deletions js/src/dropdown.js
Expand Up @@ -75,15 +75,17 @@ const Default = {
boundary: 'clippingParents',
reference: 'toggle',
display: 'dynamic',
popperConfig: null
popperConfig: null,
autoClose: true
}

const DefaultType = {
offset: '(array|string|function)',
boundary: '(string|element)',
reference: '(string|element|object)',
display: 'string',
popperConfig: '(null|object|function)'
popperConfig: '(null|object|function)',
autoClose: '(boolean|string)'
}

/**
Expand Down Expand Up @@ -127,9 +129,8 @@ class Dropdown extends BaseComponent {

const isActive = this._element.classList.contains(CLASS_NAME_SHOW)

Dropdown.clearMenus()

if (isActive) {
this.hide()
return
}

Expand Down Expand Up @@ -424,7 +425,7 @@ class Dropdown extends BaseComponent {

for (let i = 0, len = toggles.length; i < len; i++) {
const context = Data.get(toggles[i], DATA_KEY)
if (!context) {
if (!context || context._config.autoClose === false) {
continue
}

Expand All @@ -437,8 +438,13 @@ class Dropdown extends BaseComponent {
}

if (event) {
// Don't close the menu if the clicked element or one of its parents is the dropdown button
if ([context._element].some(element => event.composedPath().includes(element))) {
const composedPath = event.composedPath()
const isMenuTarget = composedPath.includes(context._menu)
if (
composedPath.includes(context._element) ||
(context._config.autoClose === 'inside' && !isMenuTarget) ||
(context._config.autoClose === 'outside' && isMenuTarget)
) {
continue
}

Expand Down
101 changes: 97 additions & 4 deletions js/tests/unit/dropdown.spec.js
Expand Up @@ -216,18 +216,17 @@ describe('Dropdown', () => {
const firstDropdownEl = fixtureEl.querySelector('.first')
const secondDropdownEl = fixtureEl.querySelector('.second')
const dropdown1 = new Dropdown(btnDropdown1)
const dropdown2 = new Dropdown(btnDropdown2)

firstDropdownEl.addEventListener('shown.bs.dropdown', () => {
expect(btnDropdown1.classList.contains('show')).toEqual(true)
spyOn(dropdown1._popper, 'destroy')
dropdown2.toggle()
btnDropdown2.click()
})

secondDropdownEl.addEventListener('shown.bs.dropdown', () => {
secondDropdownEl.addEventListener('shown.bs.dropdown', () => setTimeout(() => {
expect(dropdown1._popper.destroy).toHaveBeenCalled()
done()
})
}))

dropdown1.toggle()
})
Expand Down Expand Up @@ -1739,6 +1738,100 @@ describe('Dropdown', () => {
toggle.dispatchEvent(keydownEscape)
toggle.dispatchEvent(keyupEscape)
})

it('should close dropdown (only) by clicking inside the dropdown menu when it has data-attribute `data-bs-auto-close="inside"`', done => {
fixtureEl.innerHTML = [
'<div class="dropdown">',
' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" data-bs-auto-close="inside">Dropdown toggle</button>',
' <div class="dropdown-menu">',
' <a class="dropdown-item" href="#">Dropdown item</a>',
' </div>',
'</div>'
]

const dropdownToggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
const dropdownMenu = fixtureEl.querySelector('.dropdown-menu')

const expectDropdownToBeOpened = () => setTimeout(() => {
expect(dropdownToggle.classList.contains('show')).toEqual(true)
dropdownMenu.click()
}, 150)

dropdownToggle.addEventListener('shown.bs.dropdown', () => {
document.documentElement.click()
expectDropdownToBeOpened()
})

dropdownToggle.addEventListener('hidden.bs.dropdown', () => setTimeout(() => {
expect(dropdownToggle.classList.contains('show')).toEqual(false)
done()
}))

dropdownToggle.click()
})

it('should close dropdown (only) by clicking outside the dropdown menu when it has data-attribute `data-bs-auto-close="outside"`', done => {
fixtureEl.innerHTML = [
'<div class="dropdown">',
' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" data-bs-auto-close="outside">Dropdown toggle</button>',
' <div class="dropdown-menu">',
' <a class="dropdown-item" href="#">Dropdown item</a>',
' </div>',
'</div>'
]

const dropdownToggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
const dropdownMenu = fixtureEl.querySelector('.dropdown-menu')

const expectDropdownToBeOpened = () => setTimeout(() => {
expect(dropdownToggle.classList.contains('show')).toEqual(true)
document.documentElement.click()
}, 150)

dropdownToggle.addEventListener('shown.bs.dropdown', () => {
dropdownMenu.click()
expectDropdownToBeOpened()
})

dropdownToggle.addEventListener('hidden.bs.dropdown', () => {
expect(dropdownToggle.classList.contains('show')).toEqual(false)
done()
})

dropdownToggle.click()
})

it('should not close dropdown by clicking inside or outside the dropdown menu when it has data-attribute `data-bs-auto-close="false"`', done => {
fixtureEl.innerHTML = [
'<div class="dropdown">',
' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" data-bs-auto-close="false">Dropdown toggle</button>',
' <div class="dropdown-menu">',
' <a class="dropdown-item" href="#">Dropdown item</a>',
' </div>',
'</div>'
]

const dropdownToggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
const dropdownMenu = fixtureEl.querySelector('.dropdown-menu')

const expectDropdownToBeOpened = (shouldTriggerClick = true) => setTimeout(() => {
expect(dropdownToggle.classList.contains('show')).toEqual(true)
if (shouldTriggerClick) {
document.documentElement.click()
} else {
done()
}

expectDropdownToBeOpened(false)
}, 150)

dropdownToggle.addEventListener('shown.bs.dropdown', () => {
dropdownMenu.click()
expectDropdownToBeOpened()
})

dropdownToggle.click()
})
})

describe('jQueryInterface', () => {
Expand Down
66 changes: 65 additions & 1 deletion site/content/docs/5.0/components/dropdowns.md
Expand Up @@ -903,6 +903,56 @@ Use `data-bs-offset` or `data-bs-reference` to change the location of the dropdo
</div>
{{< /example >}}

### Auto close behavior

By default, the dropdown menu is closed when clicking inside or outside the dropdown menu. You can use the `autoClose` option to change this behavior of the dropdown.

{{< example >}}
<div class="btn-group">
<button class="btn btn-secondary dropdown-toggle" type="button" id="defaultDropdown" data-bs-toggle="dropdown" data-bs-auto-close="true" aria-expanded="false">
Default dropdown
</button>
<ul class="dropdown-menu" aria-labelledby="defaultDropdown">
<li><a class="dropdown-item" href="#">Menu item</a></li>
<li><a class="dropdown-item" href="#">Menu item</a></li>
<li><a class="dropdown-item" href="#">Menu item</a></li>
</ul>
</div>

<div class="btn-group">
<button class="btn btn-secondary dropdown-toggle" type="button" id="dropdownMenuClickableOutside" data-bs-toggle="dropdown" data-bs-auto-close="inside" aria-expanded="false">
Clickable outside
</button>
<ul class="dropdown-menu" aria-labelledby="dropdownMenuClickableOutside">
<li><a class="dropdown-item" href="#">Menu item</a></li>
<li><a class="dropdown-item" href="#">Menu item</a></li>
<li><a class="dropdown-item" href="#">Menu item</a></li>
</ul>
</div>

<div class="btn-group">
<button class="btn btn-secondary dropdown-toggle" type="button" id="dropdownMenuClickableInside" data-bs-toggle="dropdown" data-bs-auto-close="outside" aria-expanded="false">
Clickable inside
</button>
<ul class="dropdown-menu" aria-labelledby="dropdownMenuClickableInside">
<li><a class="dropdown-item" href="#">Menu item</a></li>
<li><a class="dropdown-item" href="#">Menu item</a></li>
<li><a class="dropdown-item" href="#">Menu item</a></li>
</ul>
</div>

<div class="btn-group">
<button class="btn btn-secondary dropdown-toggle" type="button" id="dropdownMenuClickable" data-bs-toggle="dropdown" data-bs-auto-close="false" aria-expanded="false">
Manual close
</button>
<ul class="dropdown-menu" aria-labelledby="dropdownMenuClickable">
<li><a class="dropdown-item" href="#">Menu item</a></li>
<li><a class="dropdown-item" href="#">Menu item</a></li>
<li><a class="dropdown-item" href="#">Menu item</a></li>
</ul>
</div>
{{< /example >}}

## Sass

### Variables
Expand Down Expand Up @@ -967,7 +1017,7 @@ Regardless of whether you call your dropdown via JavaScript or instead use the d

### Options

Options can be passed via data attributes or JavaScript. For data attributes, append the option name to `data-bs-`, as in `data-bs-offset=""`.
Options can be passed via data attributes or JavaScript. For data attributes, append the option name to `data-bs-`, as in `data-bs-offset=""`. Make sure to change the case type of the option name from camelCase to kebab-case when passing the options via data attributes. For example, instead of using `data-bs-autoClose="false"`, use `data-bs-auto-close="false"`.

<table class="table">
<thead>
Expand Down Expand Up @@ -1007,6 +1057,20 @@ Options can be passed via data attributes or JavaScript. For data attributes, ap
<p>For more information refer to Popper's <a href="https://popper.js.org/docs/v2/modifiers/offset/#options">offset docs</a>.</p>
</td>
</tr>
<tr>
<td><code>autoClose</code></td>
<td>boolean | string</td>
<td><code>true</code></td>
<td>
<p>Configure the auto close behavior of the dropdown:</p>
<ul>
<li><code>true</code> - the dropdown will be closed by clicking outside or inside the dropdown menu.</li>
<li><code>false</code> - the dropdown will be closed by clicking the toggle button and manually calling <code>hide</code> or <code>toggle</code> method. (Also will not be closed by pressing <kbd>esc</kbd> key)</li>
<li><code>'inside'</code> - the dropdown will be closed (only) by clicking inside the dropdown menu.</li>
<li><code>'outside'</code> - the dropdown will be closed (only) by clicking outside the dropdown menu.</li>
</ul>
</td>
</tr>
<tr>
<td><code>popperConfig</code></td>
<td>null | object | function</td>
Expand Down