Skip to content
Open
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
24 changes: 15 additions & 9 deletions js/src/modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import Backdrop from './util/backdrop.js'
import { enableDismissTrigger } from './util/component-functions.js'
import FocusTrap from './util/focustrap.js'
import {
defineJQueryPlugin, isRTL, isVisible, reflow
defineJQueryPlugin, execute, isRTL, isVisible, reflow
} from './util/index.js'
import ScrollBarHelper from './util/scrollbar.js'

Expand Down Expand Up @@ -54,9 +54,9 @@ const Default = {
}

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

/**
Expand Down Expand Up @@ -157,7 +157,7 @@ class Modal extends BaseComponent {
// Private
_initializeBackDrop() {
return new Backdrop({
isVisible: Boolean(this._config.backdrop), // 'static' option will be translated to true, and booleans will keep their value,
isVisible: Boolean(this._resolvePossibleFunction(this._config.backdrop)), // 'static' option will be translated to true, and booleans will keep their value
isAnimated: this._isAnimated()
})
}
Expand Down Expand Up @@ -190,7 +190,7 @@ class Modal extends BaseComponent {
this._element.classList.add(CLASS_NAME_SHOW)

const transitionComplete = () => {
if (this._config.focus) {
if (this._resolvePossibleFunction(this._config.focus)) {
this._focustrap.activate()
}

Expand All @@ -209,7 +209,7 @@ class Modal extends BaseComponent {
return
}

if (this._config.keyboard) {
if (this._resolvePossibleFunction(this._config.keyboard)) {
this.hide()
return
}
Expand All @@ -230,12 +230,14 @@ class Modal extends BaseComponent {
return
}

if (this._config.backdrop === 'static') {
const backdrop = this._resolvePossibleFunction(this._config.backdrop)

if (backdrop === 'static') {
this._triggerBackdropTransition()
return
}

if (this._config.backdrop) {
if (backdrop) {
this.hide()
}
})
Expand Down Expand Up @@ -314,6 +316,10 @@ class Modal extends BaseComponent {
this._element.style.paddingRight = ''
}

_resolvePossibleFunction(arg) {
return execute(arg, [this])
}

// Static
static jQueryInterface(config, relatedTarget) {
return this.each(function () {
Expand Down
166 changes: 166 additions & 0 deletions js/tests/unit/modal.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ describe('Modal', () => {
modal.show()
})
})

it('should set is transitioning if fade class is present', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = '<div class="modal fade"><div class="modal-dialog"></div></div>'
Expand Down Expand Up @@ -550,6 +551,7 @@ describe('Modal', () => {
modal.show()
})
})

it('should close modal when escape key is pressed with keyboard = true and backdrop is static', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
Expand Down Expand Up @@ -677,6 +679,44 @@ describe('Modal', () => {
modal.show()
})
})

it('should call .focus() when config function returns true', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'

const modalEl = fixtureEl.querySelector('.modal')
const focusSpy = spyOn(modalEl, 'focus')
const modal = new Modal(modalEl, {
focus: () => true
})

modalEl.addEventListener('shown.bs.modal', () => {
expect(focusSpy).toHaveBeenCalled()
resolve()
})

modal.show()
})
})

it('should NOT call .focus() when config function returns false', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'

const modalEl = fixtureEl.querySelector('.modal')
const focusSpy = spyOn(modalEl, 'focus')
const modal = new Modal(modalEl, {
focus: () => false
})

modalEl.addEventListener('shown.bs.modal', () => {
expect(focusSpy).not.toHaveBeenCalled()
resolve()
})

modal.show()
})
})
})

describe('hide', () => {
Expand Down Expand Up @@ -766,6 +806,129 @@ describe('Modal', () => {
})
})

it('should not close on Escape when "keyboard" option is dynamically changed to false', () => {
return new Promise((resolve, reject) => {
fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'

const config = { keyboard: 'closing' }

const modalEl = fixtureEl.querySelector('.modal')
const modal = new Modal(modalEl, {
keyboard: () => config.keyboard === 'closing'
})

modalEl.addEventListener('shown.bs.modal', () => {
config.keyboard = 'nothing'

const keydownEscape = createEvent('keydown')
keydownEscape.key = 'Escape'
modalEl.dispatchEvent(keydownEscape)

expect(modal._isShown).toBeTrue()
resolve()
})

modalEl.addEventListener('hidden.bs.modal', () => {
reject(new Error('Should not hide a modal'))
})

modal.show()
})
})

it('should close on Escape when "keyboard" option is dynamically changed to true', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'

const config = { keyboard: 'nothing' }

const modalEl = fixtureEl.querySelector('.modal')
const modal = new Modal(modalEl, {
keyboard: () => config.keyboard === 'closing'
})

modalEl.addEventListener('shown.bs.modal', () => {
config.keyboard = 'closing'

const keydownEscape = createEvent('keydown')
keydownEscape.key = 'Escape'
modalEl.dispatchEvent(keydownEscape)
})

modalEl.addEventListener('hidden.bs.modal', () => {
resolve()
})

modal.show()
})
})

it('should close by backdrop click when option dynamically changed to true', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'

const config = { backdrop: 'static' }
const modalEl = fixtureEl.querySelector('.modal')
const modal = new Modal(modalEl, {
backdrop: () => config.backdrop
})

const backdropSpy = spyOn(modal._backdrop, 'hide').and.callThrough()
EventHandler.one(modalEl, 'click', () => {
if (config.backdrop === false) {
modal.hide()
}
})

modalEl.addEventListener('shown.bs.modal', () => {
config.backdrop = false

modalEl.click()
})

modalEl.addEventListener('hidden.bs.modal', () => {
expect(backdropSpy).toHaveBeenCalled()
resolve()
})

modal.show()
})
})

it('should not close by backdrop click when option dynamically changed to "static"', () => {
return new Promise((resolve, reject) => {
fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'

const config = { backdrop: false }
const modalEl = fixtureEl.querySelector('.modal')
const modal = new Modal(modalEl, {
backdrop: () => config.backdrop
})

const backdropSpy = spyOn(modal._backdrop, 'hide').and.callThrough()
EventHandler.one(modalEl, 'click', () => {
if (config.backdrop === false) {
modal.hide()
}
})

modalEl.addEventListener('shown.bs.modal', () => {
config.backdrop = 'static'

modalEl.click()

expect(backdropSpy).not.toHaveBeenCalled()
resolve()
})

modalEl.addEventListener('hidden.bs.modal', () => {
reject(new Error('Should not hide a modal'))
})

modal.show()
})
})

it('should do nothing is the modal is not shown', () => {
fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'

Expand Down Expand Up @@ -1077,6 +1240,7 @@ describe('Modal', () => {
modal.show()
})
})

it('should not focus the trigger if the modal is not visible', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = [
Expand Down Expand Up @@ -1109,6 +1273,7 @@ describe('Modal', () => {
trigger.click()
})
})

it('should not focus the trigger if the modal is not shown', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = [
Expand Down Expand Up @@ -1162,6 +1327,7 @@ describe('Modal', () => {
})
})
})

describe('jQueryInterface', () => {
it('should create a modal', () => {
fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
Expand Down
2 changes: 2 additions & 0 deletions site/src/components/shortcodes/JsDataAttributes.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ As options can be passed via data attributes or JavaScript, you can append an op
As of Bootstrap 5.2.0, all components support an **experimental** reserved data attribute `data-bs-config` that can house simple component configuration as a JSON string. When an element has `data-bs-config='{"delay":0, "title":123}'` and `data-bs-title="456"` attributes, the final `title` value will be `456` and the separate data attributes will override values given on `data-bs-config`. In addition, existing data attributes are able to house JSON values like `data-bs-delay='{"show":0,"hide":150}'`.

The final configuration object is the merged result of `data-bs-config`, `data-bs-`, and `js object` where the latest given key-value overrides the others.

You can pass a callback function there, which undertakes to return the value type specified for the parameter. This will allow you to change the logic of the modal window in runtime mode after you have passed the configuration object.
39 changes: 36 additions & 3 deletions site/src/content/docs/components/modal.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -782,11 +782,44 @@ const myModalAlternative = new bootstrap.Modal('#myModal', options)
<BsTable>
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `backdrop` | boolean, `’static'` | `true` | Includes a modal-backdrop element. Alternatively, specify `static` for a backdrop which doesn’t close the modal when clicked. |
| `focus` | boolean | `true` | Puts the focus on the modal when initialized. |
| `keyboard` | boolean | `true` | Closes the modal when escape key is pressed. |
| `backdrop` | true, `’static'`, <br /> function | `true` | Includes a modal-backdrop element. Alternatively, specify `static` for a backdrop which doesn’t close the modal when clicked. |
| `focus` | boolean, function | `true` | Puts the focus on the modal when initialized. |
| `keyboard` | boolean, function | `true` | Closes the modal when escape key is pressed. |
</BsTable>

For example, if you want to prevent the user from closing the modal window using the Escape key to avoid accidental loss
of important data if they are entered, you can use instead:

```js
const confirmInput = document.getElementById('confirmInput')
const modalElement = document.getElementById('confirmModal')

new bootstrap.Modal(modalElement, { keyboard: true })

const keydownBlocker = event => {
if (event.key === 'Escape' && confirmInput.value.toLowerCase() === 'save') {
event.stopPropagation()
}
}

modalElement.addEventListener('show.bs.modal', () => {
document.addEventListener('keydown', keydownBlocker, true)
})

modalElement.addEventListener('hide.bs.modal', () => {
document.removeEventListener('keydown', keydownBlocker, true)
})
```

The next option is to transfer the function:

```js
const confirmInput = document.getElementById('confirmInput')
const modalElement = document.getElementById('confirmModal')

new bootstrap.Modal(modalElement, { keyboard: () => confirmInput.value.toLowerCase() !== 'save' })
```

### Methods

<Callout name="danger-async-methods" type="danger" />
Expand Down