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 configuring "root element" for modal #33018

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 13 additions & 7 deletions js/src/modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import {
defineJQueryPlugin,
getElement,
getElementFromSelector,
isRTL,
isVisible,
Expand Down Expand Up @@ -35,13 +36,15 @@ const ESCAPE_KEY = 'Escape'
const Default = {
backdrop: true,
keyboard: true,
focus: true
focus: true,
rootElement: 'body'
}

const DefaultType = {
backdrop: '(boolean|string)',
keyboard: 'boolean',
focus: 'boolean'
focus: 'boolean',
rootElement: 'element'
}

const EVENT_HIDE = `hide${EVENT_KEY}`
Expand Down Expand Up @@ -83,7 +86,7 @@ class Modal extends BaseComponent {
this._isShown = false
this._ignoreBackdropClick = false
this._isTransitioning = false
this._scrollBar = new ScrollBarHelper()
this._scrollBar = new ScrollBarHelper(this._config.rootElement)
}

// Getters
Expand Down Expand Up @@ -123,7 +126,7 @@ class Modal extends BaseComponent {

this._scrollBar.hide()

document.body.classList.add(CLASS_NAME_OPEN)
this._config.rootElement.classList.add(CLASS_NAME_OPEN)

this._adjustDialog()

Expand Down Expand Up @@ -202,7 +205,8 @@ class Modal extends BaseComponent {
_initializeBackDrop() {
return new Backdrop({
isVisible: Boolean(this._config.backdrop), // 'static' option will be translated to true, and booleans will keep their value
isAnimated: this._isAnimated()
isAnimated: this._isAnimated(),
rootElement: this._config.rootElement
})
}

Expand All @@ -212,6 +216,8 @@ class Modal extends BaseComponent {
...Manipulator.getDataAttributes(this._element),
...(typeof config === 'object' ? config : {})
}

config.rootElement = getElement(config.rootElement)
typeCheckConfig(NAME, config, DefaultType)
return config
}
Expand All @@ -222,7 +228,7 @@ class Modal extends BaseComponent {

if (!this._element.parentNode || this._element.parentNode.nodeType !== Node.ELEMENT_NODE) {
// Don't move modal's DOM position
document.body.appendChild(this._element)
this._config.rootElement.appendChild(this._element)
}

this._element.style.display = 'block'
Expand Down Expand Up @@ -300,7 +306,7 @@ class Modal extends BaseComponent {
this._element.removeAttribute('role')
this._isTransitioning = false
this._backdrop.hide(() => {
document.body.classList.remove(CLASS_NAME_OPEN)
this._config.rootElement.classList.remove(CLASS_NAME_OPEN)
this._resetAdjustments()
this._scrollBar.reset()
EventHandler.trigger(this._element, EVENT_HIDDEN)
Expand Down
2 changes: 1 addition & 1 deletion js/src/util/backdrop.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const DefaultType = {
className: 'string',
isVisible: 'boolean',
isAnimated: 'boolean',
rootElement: '(element|string)',
rootElement: 'element',
clickCallback: '(function|null)'
}
const NAME = 'backdrop'
Expand Down
20 changes: 11 additions & 9 deletions js/src/util/scrollbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,20 @@ const SELECTOR_FIXED_CONTENT = '.fixed-top, .fixed-bottom, .is-fixed, .sticky-to
const SELECTOR_STICKY_CONTENT = '.sticky-top'

class ScrollBarHelper {
constructor() {
this._element = document.body
constructor(element = document.body) {
this._isBody = [document.body, document.documentElement].includes(element)

this._element = this._isBody ? document.body : element
}

getWidth() {
// https://developer.mozilla.org/en-US/docs/Web/API/Window/innerWidth#usage_notes
const documentWidth = document.documentElement.clientWidth
return Math.abs(window.innerWidth - documentWidth)
if (this._isBody) {
// https://developer.mozilla.org/en-US/docs/Web/API/Window/innerWidth#usage_notes
const documentWidth = document.documentElement.clientWidth
return Math.abs(window.innerWidth - documentWidth)
}

return Math.abs(this._element.offsetWidth - this._element.clientWidth)
}

hide() {
Expand Down Expand Up @@ -88,10 +94,6 @@ class ScrollBarHelper {
SelectorEngine.find(selector, this._element).forEach(callBack)
}
}

isOverflowing() {
return this.getWidth() > 0
}
}

export default ScrollBarHelper
2 changes: 1 addition & 1 deletion js/tests/integration/bundle.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Tooltip } from '../../../dist/js/bootstrap.esm.js'
import { Tooltip } from './../../../dist/js/bootstrap.esm.js'
Copy link
Contributor Author

@spicalous spicalous Jun 22, 2021

Choose a reason for hiding this comment

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

I had to add this, otherwise the tests was failing for me :/


window.addEventListener('load', () => {
[].concat(...document.querySelectorAll('[data-bs-toggle="tooltip"]'))
Expand Down
22 changes: 22 additions & 0 deletions js/tests/unit/modal.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -714,6 +714,28 @@ describe('Modal', () => {
})
})

describe('config rootElement', () => {
it('should append backdrop to root element', done => {
fixtureEl.innerHTML = [
'<div id="modal-root">',
' <div class="modal"><div class="modal-dialog"></div></div>',
'</div>'
].join('')

const modalEl = fixtureEl.querySelector('.modal')
const rootElement = fixtureEl.querySelector('#modal-root')
const modal = new Modal(modalEl, { rootElement })

modalEl.addEventListener('shown.bs.modal', () => {
expect(fixtureEl.querySelector('.modal-open')).toEqual(rootElement)
expect(rootElement.querySelector('.modal-backdrop')).not.toBeNull()
done()
})

modal.show()
})
})

describe('handleUpdate', () => {
it('should call adjust dialog', () => {
fixtureEl.innerHTML = '<div id="exampleModal" class="modal"><div class="modal-dialog"></div></div>'
Expand Down
97 changes: 50 additions & 47 deletions js/tests/unit/util/scrollbar.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,61 +48,64 @@ describe('ScrollBar', () => {
clearBodyAndDocument()
})

describe('isBodyOverflowing', () => {
it('should return true if body is overflowing', () => {
document.documentElement.style.overflowY = 'scroll'
document.body.style.overflowY = 'scroll'
fixtureEl.innerHTML = [
'<div style="height: 110vh; width: 100%"></div>'
].join('')
const result = new ScrollBarHelper().isOverflowing()

if (isScrollBarHidden()) {
expect(result).toEqual(false)
} else {
expect(result).toEqual(true)
}
})
describe('getWidth', () => {
describe('Body', () => {
it('should return an integer greater than zero, if body is overflowing', () => {
doc.style.overflowY = 'scroll'
document.body.style.overflowY = 'scroll'
fixtureEl.innerHTML = [
'<div style="height: 110vh; width: 100%"></div>'
].join('')
const result = new ScrollBarHelper().getWidth()

it('should return false if body is not overflowing', () => {
doc.style.overflowY = 'hidden'
document.body.style.overflowY = 'hidden'
fixtureEl.innerHTML = [
'<div style="height: 110vh; width: 100%"></div>'
].join('')
const scrollBar = new ScrollBarHelper()
const result = scrollBar.isOverflowing()
if (isScrollBarHidden()) {
expect(result).toBe(0)
} else {
expect(result).toBeGreaterThan(1)
}
})

expect(result).toEqual(false)
})
})
it('should return 0 if body is not overflowing', () => {
document.documentElement.style.overflowY = 'hidden'
document.body.style.overflowY = 'hidden'
fixtureEl.innerHTML = [
'<div style="height: 110vh; width: 100%"></div>'
].join('')

describe('getWidth', () => {
it('should return an integer greater than zero, if body is overflowing', () => {
doc.style.overflowY = 'scroll'
document.body.style.overflowY = 'scroll'
fixtureEl.innerHTML = [
'<div style="height: 110vh; width: 100%"></div>'
].join('')
const result = new ScrollBarHelper().getWidth()
const result = new ScrollBarHelper().getWidth()

if (isScrollBarHidden()) {
expect(result).toBe(0)
} else {
expect(result).toBeGreaterThan(1)
}
expect(result).toEqual(0)
})
})

it('should return 0 if body is not overflowing', () => {
document.documentElement.style.overflowY = 'hidden'
document.body.style.overflowY = 'hidden'
fixtureEl.innerHTML = [
'<div style="height: 110vh; width: 100%"></div>'
].join('')
describe('Element', () => {
it('should return an integer greater than zero, if wrapper element is overflowing', () => {
fixtureEl.innerHTML = [
'<div class="wrapper" style="height: 100px; width: 100%; overflow: scroll">' +
' <div style="height: 120px; width: 100%"></div>' +
'</div>'
].join('')
const wrapper = fixtureEl.querySelector('.wrapper')
const result = new ScrollBarHelper(wrapper).getWidth()

if (isScrollBarHidden()) {
expect(result).toBe(0)
} else {
expect(result).toBeGreaterThan(1)
}
})

const result = new ScrollBarHelper().getWidth()
it('should return 0 if wrapper element is not overflowing', () => {
fixtureEl.innerHTML = [
'<div class="wrapper" style="height: 100px; width: 100%">' +
' <div style="height: 20px; width: 100%"></div>' +
'</div>'
].join('')
const wrapper = fixtureEl.querySelector('.wrapper')
const result = new ScrollBarHelper(wrapper).getWidth()

expect(result).toEqual(0)
expect(result).toEqual(0)
})
})
})

Expand Down
10 changes: 10 additions & 0 deletions scss/_modal.scss
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,16 @@
@include overlay-backdrop($zindex-modal-backdrop, $modal-backdrop-bg, $modal-backdrop-opacity);
}

// custom root specified
.modal-open:not(body) {
.modal,
.modal-backdrop {
position: absolute;
width: 100%;
height: 100%;
}
}

// Modal header
// Top section of the modal w/ title and dismiss
.modal-header {
Expand Down
44 changes: 44 additions & 0 deletions site/content/docs/5.0/components/modal.md
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,44 @@ Be sure to add `aria-labelledby="..."`, referencing the modal title, to `.modal`

Embedding YouTube videos in modals requires additional JavaScript not in Bootstrap to automatically stop playback and more. [See this helpful Stack Overflow post](https://stackoverflow.com/questions/18622508/bootstrap-3-and-youtube-in-modal) for more information.

### Configuring root element

Specify `data-bs-root-element` attribute
```html
<div id="another-element" class="position-relative">
<div class="modal ..." data-bs-root-element="#another-element" tabindex="-1" aria-hidden="true">
```

Or pass it in the config via JavaScript

```js
var modal = bootstrap.Modal(document.getElementById('modal'), {
rootElement: document.getElementById('another-element')
});
```
{{< example >}}
<!-- modal will appear within this element and not document.body -->
<div id="another-element" class="position-relative border" style="height: 300px;">

<!-- modal -->
<div id="modal-root-example" class="modal fade" tabindex="-1" aria-labelledby="modal-root-example-title" aria-hidden="true" data-bs-root-element="#another-element">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title h4" id="modal-root-example-title">Modal in a container</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
...
</div>
</div>
</div>
</div>
</div>
<button type="button" class="btn btn-primary mt-3" data-bs-toggle="modal" data-bs-target="#modal-root-example">
Show modal in another element
</button>
{{< /example >}}
## Optional sizes

Modals have three optional sizes, available via modifier classes to be placed on a `.modal-dialog`. These sizes kick in at certain breakpoints to avoid horizontal scrollbars on narrower viewports.
Expand Down Expand Up @@ -877,6 +915,12 @@ Options can be passed via data attributes or JavaScript. For data attributes, ap
<td><code>true</code></td>
<td>Puts the focus on the modal when initialized.</td>
</tr>
<tr>
<td><code>rootElement</code></td>
<td>string or element</td>
<td><code>document.body</code></td>
<td>Configure the container that the modal resides in.</td>
</tr>
</tbody>
</table>

Expand Down