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 menu alignment fixes #32986

Merged
merged 3 commits into from Feb 9, 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
15 changes: 13 additions & 2 deletions js/src/dropdown.js
Expand Up @@ -156,7 +156,9 @@ class Dropdown extends BaseComponent {
}

// Totally disable Popper for Dropdowns in Navbar
if (!this._inNavbar) {
if (this._inNavbar) {
Manipulator.setDataAttribute(this._menu, 'popper', 'none')
} else {
if (typeof Popper === 'undefined') {
throw new TypeError('Bootstrap\'s dropdowns require Popper (https://popper.js.org)')
}
Expand All @@ -176,7 +178,14 @@ class Dropdown extends BaseComponent {
referenceElement = this._config.reference
}

this._popper = Popper.createPopper(referenceElement, this._menu, this._getPopperConfig())
const popperConfig = this._getPopperConfig()
const isDisplayStatic = popperConfig.modifiers.find(modifier => modifier.name === 'applyStyles' && modifier.enabled === false)

if (isDisplayStatic) {
Manipulator.setDataAttribute(this._menu, 'popper', 'static')
}

this._popper = Popper.createPopper(referenceElement, this._menu, popperConfig)
}

// If this is a touch-enabled device we add extra
Expand Down Expand Up @@ -218,6 +227,7 @@ class Dropdown extends BaseComponent {

this._menu.classList.toggle(CLASS_NAME_SHOW)
this._element.classList.toggle(CLASS_NAME_SHOW)
Manipulator.removeDataAttribute(this._menu, 'popper')
EventHandler.trigger(this._element, EVENT_HIDDEN, relatedTarget)
}

Expand Down Expand Up @@ -421,6 +431,7 @@ class Dropdown extends BaseComponent {

dropdownMenu.classList.remove(CLASS_NAME_SHOW)
toggles[i].classList.remove(CLASS_NAME_SHOW)
Manipulator.removeDataAttribute(dropdownMenu, 'popper')
EventHandler.trigger(toggles[i], EVENT_HIDDEN, relatedTarget)
}
}
Expand Down
60 changes: 59 additions & 1 deletion js/tests/unit/dropdown.spec.js
Expand Up @@ -1006,13 +1006,44 @@ describe('Dropdown', () => {

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

btnDropdown.addEventListener('shown.bs.dropdown', () => {
expect(dropdown._popper).toBeNull()
expect(dropdownMenu.getAttribute('style')).toEqual(null, 'no inline style applied by Popper')
done()
})

btnDropdown.click()
dropdown.show()
})

it('should manage bs attribute `data-bs-popper`="none" when dropdown is in navbar', done => {
fixtureEl.innerHTML = [
'<nav class="navbar navbar-expand-md navbar-light bg-light">',
' <div class="dropdown">',
' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>',
' <div class="dropdown-menu">',
' <a class="dropdown-item" href="#">Secondary link</a>',
' </div>',
' </div>',
'</nav>'
].join('')

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

btnDropdown.addEventListener('shown.bs.dropdown', () => {
expect(dropdownMenu.getAttribute('data-bs-popper')).toEqual('none')
dropdown.hide()
})

btnDropdown.addEventListener('hidden.bs.dropdown', () => {
expect(dropdownMenu.getAttribute('data-bs-popper')).toBeNull()
done()
})

dropdown.show()
})

it('should not use Popper if display set to static', done => {
Expand All @@ -1037,6 +1068,33 @@ describe('Dropdown', () => {
btnDropdown.click()
})

it('should manage bs attribute `data-bs-popper`="static" when display set to static', done => {
fixtureEl.innerHTML = [
'<div class="dropdown">',
' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" data-bs-display="static">Dropdown</button>',
' <div class="dropdown-menu">',
' <a class="dropdown-item" href="#">Secondary link</a>',
' </div>',
'</div>'
].join('')

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

btnDropdown.addEventListener('shown.bs.dropdown', () => {
expect(dropdownMenu.getAttribute('data-bs-popper')).toEqual('static')
dropdown.hide()
})

btnDropdown.addEventListener('hidden.bs.dropdown', () => {
expect(dropdownMenu.getAttribute('data-bs-popper')).toBeNull()
done()
})

dropdown.show()
})

it('should remove "show" class if tabbing outside of menu', done => {
fixtureEl.innerHTML = [
'<div class="dropdown">',
Expand Down
20 changes: 12 additions & 8 deletions scss/_dropdown.scss
Expand Up @@ -17,7 +17,6 @@
.dropdown-menu {
position: absolute;
top: 100%;
left: 0;
z-index: $zindex-dropdown;
display: none; // none by default, but block on "open" of the menu
min-width: $dropdown-min-width;
Expand All @@ -33,9 +32,8 @@
@include border-radius($dropdown-border-radius);
@include box-shadow($dropdown-box-shadow);

// Reset positioning when positioned with Popper
&[style] {
right: auto#{"/* rtl:ignore */"} !important; // stylelint-disable-line declaration-no-important
&[data-bs-popper] {
left: 0;
}
}

Expand All @@ -49,14 +47,20 @@

.dropdown-menu#{$infix}-start {
--bs-position: start;
right: auto #{"/* rtl:ignore */"};
left: 0 #{"/* rtl:ignore */"};

&[data-bs-popper] {
right: auto #{"/* rtl:ignore */"};
left: 0 #{"/* rtl:ignore */"};
}
}

.dropdown-menu#{$infix}-end {
--bs-position: end;
right: 0 #{"/* rtl:ignore */"};
left: auto #{"/* rtl:ignore */"};

&[data-bs-popper] {
right: 0 #{"/* rtl:ignore */"};
left: auto #{"/* rtl:ignore */"};
}
}
}
}
Expand Down
93 changes: 89 additions & 4 deletions site/content/docs/5.0/components/dropdowns.md
Expand Up @@ -30,10 +30,10 @@ Any single `.btn` can be turned into a dropdown toggle with some markup changes.

{{< example >}}
<div class="dropdown">
<button class="btn btn-secondary dropdown-toggle" type="button" id="dropdownMenuButton" data-bs-toggle="dropdown" aria-expanded="false">
<button class="btn btn-secondary dropdown-toggle" type="button" id="dropdownMenuButton1" data-bs-toggle="dropdown" aria-expanded="false">
Dropdown button
</button>
<ul class="dropdown-menu" aria-labelledby="dropdownMenuButton">
<ul class="dropdown-menu" aria-labelledby="dropdownMenuButton1">
<li><a class="dropdown-item" href="#">Action</a></li>
<li><a class="dropdown-item" href="#">Another action</a></li>
<li><a class="dropdown-item" href="#">Something else here</a></li>
Expand Down Expand Up @@ -635,10 +635,12 @@ Add `.disabled` to items in the dropdown to **style them as disabled**.

## Menu alignment

By default, a dropdown menu is automatically positioned 100% from the top and along the left side of its parent. Add `.dropdown-menu-end` to a `.dropdown-menu` to right align the dropdown menu. Directions are mirrored when using Bootstrap in RTL, meaning `.dropdown-menu-end` will appear on the left side.
By default, a dropdown menu is automatically positioned 100% from the top and along the left side of its parent. You can change this with the directional `.drop*` classes, but you can also control them with additional modifier classes.

Add `.dropdown-menu-end` to a `.dropdown-menu` to right align the dropdown menu. Directions are mirrored when using Bootstrap in RTL, meaning `.dropdown-menu-end` will appear on the left side.

{{< callout info >}}
**Heads up!** Dropdowns are positioned thanks to Popper (except when they are contained in a navbar).
**Heads up!** Dropdowns are positioned thanks to Popper except when they are contained in a navbar.
{{< /callout >}}

{{< example >}}
Expand Down Expand Up @@ -690,6 +692,89 @@ To align **left** the dropdown menu with the given breakpoint or larger, add `.d

Note that you don't need to add a `data-bs-display="static"` attribute to dropdown buttons in navbars, since Popper isn't used in navbars.

### Alignment options

Taking most of the options shown above, here's a small kitchen sink demo of various dropdown alignment options in one place.

{{< example >}}
<div class="btn-group">
mdo marked this conversation as resolved.
Show resolved Hide resolved
<button class="btn btn-secondary dropdown-toggle" type="button" id="dropdownMenuButton" data-bs-toggle="dropdown" aria-expanded="false">
Dropdown
</button>
<ul class="dropdown-menu" aria-labelledby="dropdownMenuButton">
<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 type="button" class="btn btn-secondary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
Right-aligned menu
</button>
<ul class="dropdown-menu dropdown-menu-end">
<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 type="button" class="btn btn-secondary dropdown-toggle" data-bs-toggle="dropdown" data-bs-display="static" aria-expanded="false">
Left-aligned, right-aligned lg
</button>
<ul class="dropdown-menu dropdown-menu-lg-end">
<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 type="button" class="btn btn-secondary dropdown-toggle" data-bs-toggle="dropdown" data-bs-display="static" aria-expanded="false">
Right-aligned, left-aligned lg
</button>
<ul class="dropdown-menu dropdown-menu-end dropdown-menu-lg-start">
<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 dropstart">
<button type="button" class="btn btn-secondary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
Dropstart
</button>
<ul class="dropdown-menu">
<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 dropend">
<button type="button" class="btn btn-secondary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
Dropend
</button>
<ul class="dropdown-menu">
<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 dropup">
<button type="button" class="btn btn-secondary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
Dropup
</button>
<ul class="dropdown-menu">
<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 >}}

## Menu content

### Headers
Expand Down
1 change: 1 addition & 0 deletions site/content/docs/5.0/migration.md
Expand Up @@ -23,6 +23,7 @@ toc: true
- Restored `offset` option for Dropdown, Popover and Tooltip plugins.
- The default value for the `fallbackPlacements` is changed to `['top', 'right', 'bottom', 'left']` for better placement of popper elements.
- All the events for the dropdown are now triggered on the dropdown toggle button and then bubbled up to the parent element.
- Dropdown menus now have a `data-bs-popper="static"` attribute set when the positioning of the dropdown is static and `data-bs-popper="none"` when dropdown is in the navbar. This is added by our JavaScript and helps us use custom position styles without interfering with Popper's positioning.

## v5.0.0-beta1

Expand Down