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

Offcanvas as component #29017

Merged
merged 44 commits into from Mar 2, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
b4b99d7
Add a new offcanvas component
GeoSot Oct 12, 2019
15ea3fc
offcanvas.js: switch to string constants and `event.key`
XhmikosR Apr 1, 2020
b3dce19
Remove unneeded code
XhmikosR Apr 1, 2020
f504db0
Sass optimizations
MartijnCuppens Apr 4, 2020
bcc2b96
Fixes
GeoSot Jun 13, 2020
2b57bbe
Wording tweaks
XhmikosR Dec 6, 2020
de607c7
update
GeoSot Dec 8, 2020
7d12110
update tests and offcanvas class
GeoSot Dec 8, 2020
18a4aad
separate scrollbar functionality and use it in offcanvas
GeoSot Dec 9, 2020
3960459
Update .bundlewatch.config.json
GeoSot Dec 9, 2020
722c1c8
fix focus
GeoSot Dec 14, 2020
6f25645
update btn-close / fix focus on close
GeoSot Jan 6, 2021
4a99294
add aria-modal and role
GeoSot Jan 6, 2021
56e245b
move common code to reusable functions
GeoSot Jan 21, 2021
22f538e
add aria-labelledby
GeoSot Jan 21, 2021
2f9a60f
Replace lorem ipsum text
GeoSot Jan 21, 2021
cbba2dd
fix focus when offcanvas is closed
GeoSot Feb 8, 2021
f0d9ae9
updates
GeoSot Feb 10, 2021
cafb8e7
revert modal, add tests for scrollbar
GeoSot Feb 12, 2021
334cf93
show backdrop by default
GeoSot Feb 13, 2021
3b48c6a
Update offcanvas.md
XhmikosR Feb 16, 2021
bb42e9d
Update offcanvas CSS to better match modals
mdo Feb 18, 2021
3de767a
Revamp offcanvas docs
mdo Feb 18, 2021
aad78ec
Add .offcanvas-title instead of .modal-title
mdo Feb 18, 2021
95cd2d1
Rename offcanvas example to offcanvas-navbar to reflect it's purpose
mdo Feb 18, 2021
5c20649
labelledby references title and not header
mdo Feb 18, 2021
6f40385
Add default shadow to offcanvas
mdo Feb 18, 2021
e25e9d1
enable offcanvas-body to fill all the remaining wrapper area
GeoSot Feb 19, 2021
e07cf43
Be more descriptive, on Accessibility area
GeoSot Feb 19, 2021
8445e36
remove redundant classes
GeoSot Feb 19, 2021
97db642
ensure in case of an already open offcanvas, not to open another one
GeoSot Feb 19, 2021
a06dbbb
bring back backdrop|scroll combinations
GeoSot Feb 19, 2021
0228f65
Update offcanvas.md
XhmikosR Feb 19, 2021
3eb6b34
bring back toggling class
GeoSot Feb 21, 2021
f7f35fb
refactor scrollbar method, plus tests
GeoSot Feb 21, 2021
df053ae
add check if element is not full-width, according to #30621
GeoSot Feb 24, 2021
311e97f
revert all in modal
GeoSot Feb 24, 2021
39babe2
use documentElement innerWidth
GeoSot Feb 24, 2021
aaef5b5
Rename classes to -start and -end
mdo Feb 26, 2021
a1a3fea
omit some things on scrollbar
GeoSot Feb 27, 2021
f9a01a1
PASS BrowserStack tests
GeoSot Feb 27, 2021
9c04ec8
Rename '_handleClosing' to '_addEventListeners'
GeoSot Feb 28, 2021
b2b3d04
change pipe usage to comma
GeoSot Feb 28, 2021
d0a4cbb
Data.get
XhmikosR Mar 2, 2021
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
16 changes: 8 additions & 8 deletions .bundlewatch.config.json
Expand Up @@ -26,35 +26,35 @@
},
{
"path": "./dist/css/bootstrap.css",
"maxSize": "24 kB"
"maxSize": "24.25 kB"
},
{
"path": "./dist/css/bootstrap.min.css",
"maxSize": "22 kB"
"maxSize": "22.25 kB"
},
{
"path": "./dist/js/bootstrap.bundle.js",
"maxSize": "42 kB"
"maxSize": "43 kB"
},
{
"path": "./dist/js/bootstrap.bundle.min.js",
"maxSize": "22 kB"
"maxSize": "22.5 kB"
},
{
"path": "./dist/js/bootstrap.esm.js",
"maxSize": "27 kB"
"maxSize": "28.5 kB"
},
{
"path": "./dist/js/bootstrap.esm.min.js",
"maxSize": "18 kB"
"maxSize": "19 kB"
},
{
"path": "./dist/js/bootstrap.js",
"maxSize": "28 kB"
"maxSize": "29 kB"
},
{
"path": "./dist/js/bootstrap.min.js",
"maxSize": "15.75 kB"
"maxSize": "16.25 kB"
}
],
"ci": {
Expand Down
3 changes: 2 additions & 1 deletion build/build-plugins.js
Expand Up @@ -35,6 +35,7 @@ const bsPlugins = {
Collapse: path.resolve(__dirname, '../js/src/collapse.js'),
Dropdown: path.resolve(__dirname, '../js/src/dropdown.js'),
Modal: path.resolve(__dirname, '../js/src/modal.js'),
OffCanvas: path.resolve(__dirname, '../js/src/offcanvas.js'),
Popover: path.resolve(__dirname, '../js/src/popover.js'),
ScrollSpy: path.resolve(__dirname, '../js/src/scrollspy.js'),
Tab: path.resolve(__dirname, '../js/src/tab.js'),
Expand Down Expand Up @@ -71,7 +72,7 @@ const getConfigByPluginKey = pluginKey => {
}
}

if (pluginKey === 'Alert' || pluginKey === 'Tab') {
if (pluginKey === 'Alert' || pluginKey === 'Tab' || pluginKey === 'OffCanvas') {
return defaultPluginConfig
}

Expand Down
2 changes: 2 additions & 0 deletions js/index.esm.js
Expand Up @@ -11,6 +11,7 @@ import Carousel from './src/carousel'
import Collapse from './src/collapse'
import Dropdown from './src/dropdown'
import Modal from './src/modal'
import OffCanvas from './src/offcanvas'
import Popover from './src/popover'
import ScrollSpy from './src/scrollspy'
import Tab from './src/tab'
Expand All @@ -24,6 +25,7 @@ export {
Collapse,
Dropdown,
Modal,
OffCanvas,
Popover,
ScrollSpy,
Tab,
Expand Down
2 changes: 2 additions & 0 deletions js/index.umd.js
Expand Up @@ -11,6 +11,7 @@ import Carousel from './src/carousel'
import Collapse from './src/collapse'
import Dropdown from './src/dropdown'
import Modal from './src/modal'
import OffCanvas from './src/offcanvas'
import Popover from './src/popover'
import ScrollSpy from './src/scrollspy'
import Tab from './src/tab'
Expand All @@ -24,6 +25,7 @@ export default {
Collapse,
Dropdown,
Modal,
OffCanvas,
Popover,
ScrollSpy,
Tab,
Expand Down
239 changes: 239 additions & 0 deletions js/src/offcanvas.js
@@ -0,0 +1,239 @@
/**
* --------------------------------------------------------------------------
* Bootstrap (v5.0.0-beta2): offcanvas.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* --------------------------------------------------------------------------
*/

import {
defineJQueryPlugin,
getElementFromSelector,
getSelectorFromElement,
getTransitionDurationFromElement,
isVisible
} from './util/index'
import { hide as scrollBarHide, reset as scrollBarReset } from './util/scrollbar'
import Data from './dom/data'
import EventHandler from './dom/event-handler'
import BaseComponent from './base-component'
import SelectorEngine from './dom/selector-engine'

/**
* ------------------------------------------------------------------------
* Constants
* ------------------------------------------------------------------------
*/

const NAME = 'offcanvas'
const DATA_KEY = 'bs.offcanvas'
const EVENT_KEY = `.${DATA_KEY}`
const DATA_API_KEY = '.data-api'
const ESCAPE_KEY = 'Escape'
const DATA_BODY_ACTIONS = 'data-bs-body'
Copy link
Collaborator

Choose a reason for hiding this comment

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

Better if you can accept this option in the configuration object (Like other plugins) 🤔

Copy link
Member Author

Choose a reason for hiding this comment

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

@Rohit. I really would prefer it to keep it simple right now, in order to proceed and to avoid more mess.
If you find it so important, I can change it after. I am not very confident with the Config object, as every component uses it, in a different way


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

const EVENT_SHOW = `show${EVENT_KEY}`
const EVENT_SHOWN = `shown${EVENT_KEY}`
const EVENT_HIDE = `hide${EVENT_KEY}`
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 SELECTOR_DATA_DISMISS = '[data-bs-dismiss="offcanvas"]'
const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="offcanvas"]'

/**
* ------------------------------------------------------------------------
* Class Definition
* ------------------------------------------------------------------------
*/

class OffCanvas extends BaseComponent {
constructor(element) {
super(element)

this._isShown = element.classList.contains(CLASS_NAME_SHOW)
this._bodyOptions = element.getAttribute(DATA_BODY_ACTIONS) || ''
this._addEventListeners()
}

// Public

toggle(relatedTarget) {
return this._isShown ? this.hide() : this.show(relatedTarget)
}

show(relatedTarget) {
if (this._isShown) {
return
}

const showEvent = EventHandler.trigger(this._element, EVENT_SHOW, { relatedTarget })

if (showEvent.defaultPrevented) {
return
}

this._isShown = true
this._element.style.visibility = 'visible'

if (this._bodyOptionsHas('backdrop') || !this._bodyOptions.length) {
document.body.classList.add(CLASS_NAME_BACKDROP_BODY)
}

if (!this._bodyOptionsHas('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))
}

hide() {
if (!this._isShown) {
return
}

const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE)

if (hideEvent.defaultPrevented) {
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)

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

if (this._bodyOptionsHas('backdrop') || !this._bodyOptions.length) {
document.body.classList.remove(CLASS_NAME_BACKDROP_BODY)
}

if (!this._bodyOptionsHas('scroll')) {
scrollBarReset()
}

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

setTimeout(completeCallback, getTransitionDurationFromElement(this._element))
}

_enforceFocusOnElement(element) {
EventHandler.off(document, EVENT_FOCUSIN) // guard against infinite focus loop
EventHandler.on(document, EVENT_FOCUSIN, event => {
if (document !== event.target &&
element !== event.target &&
!element.contains(event.target)) {
element.focus()
}
})
element.focus()
}

_bodyOptionsHas(option) {
return this._bodyOptions.split(',').includes(option)
}

_addEventListeners() {
EventHandler.on(this._element, EVENT_CLICK_DISMISS, SELECTOR_DATA_DISMISS, () => this.hide())

EventHandler.on(document, 'keydown', event => {
if (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

static jQueryInterface(config) {
return this.each(function () {
const data = Data.get(this, DATA_KEY) || new OffCanvas(this)

if (typeof config === 'string') {
if (typeof data[config] === 'undefined') {
throw new TypeError(`No method named "${config}"`)
}

GeoSot marked this conversation as resolved.
Show resolved Hide resolved
data[config](this)
}
})
}
}

/**
* ------------------------------------------------------------------------
* Data Api implementation
* ------------------------------------------------------------------------
*/

EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {
const target = getElementFromSelector(this)

if (['A', 'AREA'].includes(this.tagName)) {
GeoSot marked this conversation as resolved.
Show resolved Hide resolved
event.preventDefault()
}

if (this.disabled || this.classList.contains(CLASS_NAME_DISABLED)) {
return
}

EventHandler.one(target, EVENT_HIDDEN, () => {
// focus on trigger when it is closed
if (isVisible(this)) {
this.focus()
}
})

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

const data = Data.get(target, DATA_KEY) || new OffCanvas(target)
data.toggle(this)
})

/**
* ------------------------------------------------------------------------
* jQuery
* ------------------------------------------------------------------------
*/

defineJQueryPlugin(NAME, OffCanvas)

export default OffCanvas