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

Support Popper virtual elements #32376

Merged
merged 11 commits into from Dec 21, 2020
11 changes: 10 additions & 1 deletion js/src/dropdown.js
Expand Up @@ -84,7 +84,7 @@ const DefaultType = {
offset: '(number|string|function)',
flip: 'boolean',
boundary: '(string|element)',
reference: '(string|element)',
reference: '(string|element|object)',
display: 'string',
popperConfig: '(null|object)'
}
Expand Down Expand Up @@ -172,6 +172,8 @@ class Dropdown extends BaseComponent {
if (typeof this._config.reference.jquery !== 'undefined') {
referenceElement = this._config.reference[0]
}
} else if (typeof this._config.reference === 'object') {
referenceElement = this._config.reference
}

this._popper = Popper.createPopper(referenceElement, this._menu, this._getPopperConfig())
Expand Down Expand Up @@ -257,6 +259,13 @@ class Dropdown extends BaseComponent {

typeCheckConfig(NAME, config, this.constructor.DefaultType)

if (typeof config.reference === 'object' && !isElement(config.reference) &&
typeof config.reference.getBoundingClientRect !== 'function'
) {
// Popper virtual elements require a getBoundingClientRect method
throw new Error(`${NAME}: Option "reference" provided type "object" without a required "getBoundingClientRect" method.`)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we change this to a TypeError instead of the generic Error?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, missed it 😄
But then it should also be TypeError 🤔

throw new Error(
`${componentName.toUpperCase()}: ` +
`Option "${property}" provided type "${valueType}" ` +
`but expected type "${expectedTypes}".`)
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, let's leave it as is for now and we tackle it later.

}

return config
}

Expand Down
52 changes: 52 additions & 0 deletions js/tests/unit/dropdown.spec.js
Expand Up @@ -367,6 +367,58 @@ describe('Dropdown', () => {
dropdown.toggle()
})

it('should toggle a dropdown with a valid virtual element reference', done => {
fixtureEl.innerHTML = [
'<div class="dropdown">',
' <button class="btn dropdown-toggle visually-hidden" data-bs-toggle="dropdown" aria-expanded="false">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 virtualElement = {
getBoundingClientRect() {
return {
width: 0,
height: 0,
top: 0,
right: 0,
bottom: 0,
left: 0
}
}
}

expect(() => new Dropdown(btnDropdown, {
reference: {}
})).toThrow()

expect(() => new Dropdown(btnDropdown, {
reference: {
getBoundingClientRect: 'not-a-function'
}
})).toThrow()

// use onFirstUpdate as Poppers internal update is executed async
const dropdown = new Dropdown(btnDropdown, {
reference: virtualElement,
popperConfig: {
onFirstUpdate() {
expect(virtualElement.getBoundingClientRect).toHaveBeenCalled()
expect(btnDropdown.classList.contains('show')).toEqual(true)
expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true')
done()
}
}
})

spyOn(virtualElement, 'getBoundingClientRect').and.callThrough()

dropdown.toggle()
})

it('should not toggle a dropdown if the element is disabled', done => {
fixtureEl.innerHTML = [
'<div class="dropdown">',
Expand Down
4 changes: 2 additions & 2 deletions site/content/docs/5.0/components/dropdowns.md
Expand Up @@ -886,9 +886,9 @@ Options can be passed via data attributes or JavaScript. For data attributes, ap
</tr>
<tr>
<td><code>reference</code></td>
<td>string | element</td>
<td>string | element | object</td>
<td><code>'toggle'</code></td>
<td>Reference element of the dropdown menu. Accepts the values of <code>'toggle'</code>, <code>'parent'</code>, or an HTMLElement reference. For more information refer to Popper's <a href="https://popper.js.org/docs/v2/constructors/#createpopper">constructor docs</a>.</td>
<td>Reference element of the dropdown menu. Accepts the values of <code>'toggle'</code>, <code>'parent'</code>, an HTMLElement reference or an object providing <code>getBoundingClientRect</code>. For more information refer to Popper's <a href="https://popper.js.org/docs/v2/constructors/#createpopper">constructor docs</a> and <a href="https://popper.js.org/docs/v2/virtual-elements/">virtual element docs</a>.</td>
</tr>
<tr>
<td><code>display</code></td>
Expand Down