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

Use the backdrop util in offcanvas, enforcing consistency #33545

Merged
merged 2 commits into from Apr 19, 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
60 changes: 33 additions & 27 deletions js/src/offcanvas.js
Expand Up @@ -7,8 +7,8 @@

import {
defineJQueryPlugin,
emulateTransitionEnd,
getElementFromSelector,
getSelectorFromElement,
getTransitionDurationFromElement,
isDisabled,
isVisible,
Expand All @@ -20,6 +20,7 @@ import EventHandler from './dom/event-handler'
import BaseComponent from './base-component'
import SelectorEngine from './dom/selector-engine'
import Manipulator from './dom/manipulator'
import Backdrop from './util/backdrop'

/**
* ------------------------------------------------------------------------
Expand All @@ -46,11 +47,8 @@ const DefaultType = {
scroll: 'boolean'
}

const CLASS_NAME_BACKDROP_BODY = 'offcanvas-backdrop'
const CLASS_NAME_SHOW = 'show'
const CLASS_NAME_TOGGLING = 'offcanvas-toggling'
const OPEN_SELECTOR = '.offcanvas.show'
const ACTIVE_SELECTOR = `${OPEN_SELECTOR}, .${CLASS_NAME_TOGGLING}`

const EVENT_SHOW = `show${EVENT_KEY}`
const EVENT_SHOWN = `shown${EVENT_KEY}`
Expand All @@ -59,6 +57,7 @@ const EVENT_HIDDEN = `hidden${EVENT_KEY}`
const EVENT_FOCUSIN = `focusin${EVENT_KEY}`
const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`
const EVENT_CLICK_DISMISS = `click.dismiss${EVENT_KEY}`
const EVENT_KEYDOWN_DISMISS = `keydown.dismiss${EVENT_KEY}`

const SELECTOR_DATA_DISMISS = '[data-bs-dismiss="offcanvas"]'
const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="offcanvas"]'
Expand All @@ -75,6 +74,7 @@ class Offcanvas extends BaseComponent {

this._config = this._getConfig(config)
this._isShown = false
this._backdrop = this._initializeBackDrop()
this._addEventListeners()
}

Expand Down Expand Up @@ -108,27 +108,25 @@ class Offcanvas extends BaseComponent {
this._isShown = true
this._element.style.visibility = 'visible'

if (this._config.backdrop) {
document.body.classList.add(CLASS_NAME_BACKDROP_BODY)
}
this._backdrop.show()

if (!this._config.scroll) {
scrollBarHide()
}

this._element.classList.add(CLASS_NAME_TOGGLING)
this._element.removeAttribute('aria-hidden')
this._element.setAttribute('aria-modal', true)
this._element.setAttribute('role', 'dialog')
this._element.classList.add(CLASS_NAME_SHOW)

const completeCallBack = () => {
this._element.classList.remove(CLASS_NAME_TOGGLING)
EventHandler.trigger(this._element, EVENT_SHOWN, { relatedTarget })
this._enforceFocusOnElement(this._element)
}

setTimeout(completeCallBack, getTransitionDurationFromElement(this._element))
const transitionDuration = getTransitionDurationFromElement(this._element)
EventHandler.one(this._element, 'transitionend', completeCallBack)
emulateTransitionEnd(this._element, transitionDuration)
}

hide() {
Expand All @@ -142,31 +140,37 @@ class Offcanvas extends BaseComponent {
return
}

this._element.classList.add(CLASS_NAME_TOGGLING)
EventHandler.off(document, EVENT_FOCUSIN)
this._element.blur()
this._isShown = false
this._element.classList.remove(CLASS_NAME_SHOW)
this._backdrop.hide()

const completeCallback = () => {
this._element.setAttribute('aria-hidden', true)
this._element.removeAttribute('aria-modal')
this._element.removeAttribute('role')
this._element.style.visibility = 'hidden'

if (this._config.backdrop) {
document.body.classList.remove(CLASS_NAME_BACKDROP_BODY)
}

if (!this._config.scroll) {
scrollBarReset()
}

EventHandler.trigger(this._element, EVENT_HIDDEN)
this._element.classList.remove(CLASS_NAME_TOGGLING)
}

setTimeout(completeCallback, getTransitionDurationFromElement(this._element))
const transitionDuration = getTransitionDurationFromElement(this._element)
EventHandler.one(this._element, 'transitionend', completeCallback)
emulateTransitionEnd(this._element, transitionDuration)
}

dispose() {
this._backdrop.dispose()
super.dispose()
EventHandler.off(document, EVENT_FOCUSIN)

this._config = null
this._backdrop = null
}

// Private
Expand All @@ -181,6 +185,15 @@ class Offcanvas extends BaseComponent {
return config
}

_initializeBackDrop() {
return new Backdrop({
isVisible: this._config.backdrop,
isAnimated: true,
rootElement: this._element.parentNode,
clickCallback: () => this.hide()
})
}

_enforceFocusOnElement(element) {
EventHandler.off(document, EVENT_FOCUSIN) // guard against infinite focus loop
EventHandler.on(document, EVENT_FOCUSIN, event => {
Expand All @@ -196,18 +209,11 @@ class Offcanvas extends BaseComponent {
_addEventListeners() {
EventHandler.on(this._element, EVENT_CLICK_DISMISS, SELECTOR_DATA_DISMISS, () => this.hide())

EventHandler.on(document, 'keydown', event => {
EventHandler.on(this._element, EVENT_KEYDOWN_DISMISS, event => {
if (this._config.keyboard && event.key === ESCAPE_KEY) {
this.hide()
}
})

EventHandler.on(document, EVENT_CLICK_DATA_API, event => {
const target = SelectorEngine.findOne(getSelectorFromElement(event.target))
if (!this._element.contains(event.target) && target !== this._element) {
this.hide()
}
})
}

// Static
Expand Down Expand Up @@ -254,9 +260,9 @@ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (
})

// avoid conflict when clicking a toggler of an offcanvas, while another is open
const allReadyOpen = SelectorEngine.findOne(ACTIVE_SELECTOR)
const allReadyOpen = SelectorEngine.findOne(OPEN_SELECTOR)
if (allReadyOpen && allReadyOpen !== target) {
return
Offcanvas.getInstance(allReadyOpen).hide()
}

const data = Data.get(target, DATA_KEY) || new Offcanvas(target)
Expand Down
14 changes: 12 additions & 2 deletions js/src/util/backdrop.js
Expand Up @@ -11,19 +11,23 @@ import { emulateTransitionEnd, execute, getTransitionDurationFromElement, reflow
const Default = {
isVisible: true, // if false, we use the backdrop helper without adding any element to the dom
isAnimated: false,
rootElement: document.body // give the choice to place backdrop under different elements
rootElement: document.body, // give the choice to place backdrop under different elements
clickCallback: null
}

const DefaultType = {
isVisible: 'boolean',
isAnimated: 'boolean',
rootElement: 'element'
rootElement: 'element',
clickCallback: '(function|null)'
}
const NAME = 'backdrop'
const CLASS_NAME_BACKDROP = 'modal-backdrop'
const CLASS_NAME_FADE = 'fade'
const CLASS_NAME_SHOW = 'show'

const EVENT_MOUSEDOWN = `mousedown.bs.${NAME}`

class Backdrop {
constructor(config) {
this._config = this._getConfig(config)
Expand Down Expand Up @@ -96,6 +100,10 @@ class Backdrop {

this._config.rootElement.appendChild(this._getElement())

EventHandler.on(this._getElement(), EVENT_MOUSEDOWN, () => {
execute(this._config.clickCallback)
})

this._isAppended = true
}

Expand All @@ -104,6 +112,8 @@ class Backdrop {
return
}

EventHandler.off(this._element, EVENT_MOUSEDOWN)

this._getElement().parentNode.removeChild(this._element)
this._isAppended = false
}
Expand Down