1
0
mirror of https://github.com/twbs/bootstrap.git synced 2025-08-22 05:03:16 +02:00

Offcanvas as component (#29017)

* Add a new offcanvas component

* offcanvas.js: switch to string constants and `event.key`

* Remove unneeded code

* Sass optimizations

* Fixes

Make sure the element is hidden and not offscreen when inactive
fix close icon negative margins
Add content in right & bottom examples
Re-fix bottom offcanvas height not to cover all viewport

* Wording tweaks

* update tests and offcanvas class

* separate scrollbar functionality and use it in offcanvas

* Update .bundlewatch.config.json

* fix focus

* update btn-close / fix focus on close

* add aria-modal and role
return focus on trigger when offcanvas is closed
change body scrolling timings

* move common code to reusable functions

* add aria-labelledby

* Replace lorem ipsum text

* fix focus when offcanvas is closed

* updates

* revert modal, add tests for scrollbar

* show backdrop by default

* Update offcanvas.md

* Update offcanvas CSS to better match modals

- Add background-clip for borders
- Move from outline to border (less clever, more consistent)
- Add scss-docs in vars

* Revamp offcanvas docs

- Add static example to show and explain the components
- Split live examples and rename them
- Simplify example content
- Expand docs notes elsewhere
- Add sass docs

* Add .offcanvas-title instead of .modal-title

* Rename offcanvas example to offcanvas-navbar to reflect it's purpose

* labelledby references title and not header

* Add default shadow to offcanvas

* enable offcanvas-body to fill all the remaining wrapper area

* Be more descriptive, on Accessibility area

* remove redundant classes

* ensure in case of an already open offcanvas, not to open another one

* bring back backdrop|scroll combinations

* bring back toggling class

* refactor scrollbar method, plus tests

* add check if element is not full-width, according to #30621

* revert all in modal

* use documentElement innerWidth

* Rename classes to -start and -end

Also copyedit some docs wording

* omit some things on scrollbar

* PASS BrowserStack tests

-- IOS devices, Android devices and Browsers on Mac, hide scrollbar by default and appear it, only while scrolling.

* Rename '_handleClosing' to '_addEventListeners'

* change pipe usage to comma

* change Data.getData to Data.get

Co-authored-by: XhmikosR <xhmikosr@gmail.com>
Co-authored-by: Martijn Cuppens <martijn.cuppens@gmail.com>
Co-authored-by: Mark Otto <markdotto@gmail.com>
This commit is contained in:
GeoSot
2021-03-02 19:10:10 +02:00
committed by GitHub
parent b9e51dc3c4
commit 548be2ed66
20 changed files with 1201 additions and 20 deletions

View File

@@ -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'
@@ -24,6 +25,7 @@ export {
Collapse,
Dropdown,
Modal,
OffCanvas,
Popover,
ScrollSpy,
Tab,

View File

@@ -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'
@@ -24,6 +25,7 @@ export default {
Collapse,
Dropdown,
Modal,
OffCanvas,
Popover,
ScrollSpy,
Tab,

239
js/src/offcanvas.js Normal file
View File

@@ -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'
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}"`)
}
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)) {
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

70
js/src/util/scrollbar.js Normal file
View File

@@ -0,0 +1,70 @@
/**
* --------------------------------------------------------------------------
* Bootstrap (v5.0.0-beta2): util/scrollBar.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
import SelectorEngine from '../dom/selector-engine'
import Manipulator from '../dom/manipulator'
const SELECTOR_FIXED_CONTENT = '.fixed-top, .fixed-bottom, .is-fixed'
const SELECTOR_STICKY_CONTENT = '.sticky-top'
const 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)
}
const hide = (width = getWidth()) => {
document.body.style.overflow = 'hidden'
_setElementAttributes(SELECTOR_FIXED_CONTENT, 'paddingRight', calculatedValue => calculatedValue + width)
_setElementAttributes(SELECTOR_STICKY_CONTENT, 'marginRight', calculatedValue => calculatedValue - width)
_setElementAttributes('body', 'paddingRight', calculatedValue => calculatedValue + width)
}
const _setElementAttributes = (selector, styleProp, callback) => {
const scrollbarWidth = getWidth()
SelectorEngine.find(selector)
.forEach(element => {
if (element !== document.body && window.innerWidth > element.clientWidth + scrollbarWidth) {
return
}
const actualValue = element.style[styleProp]
const calculatedValue = window.getComputedStyle(element)[styleProp]
Manipulator.setDataAttribute(element, styleProp, actualValue)
element.style[styleProp] = callback(Number.parseFloat(calculatedValue)) + 'px'
})
}
const reset = () => {
document.body.style.overflow = 'auto'
_resetElementAttributes(SELECTOR_FIXED_CONTENT, 'paddingRight')
_resetElementAttributes(SELECTOR_STICKY_CONTENT, 'marginRight')
_resetElementAttributes('body', 'paddingRight')
}
const _resetElementAttributes = (selector, styleProp) => {
SelectorEngine.find(selector).forEach(element => {
const value = Manipulator.getDataAttribute(element, styleProp)
if (typeof value === 'undefined' && element === document.body) {
element.style.removeProperty(styleProp)
} else {
Manipulator.removeDataAttribute(element, styleProp)
element.style[styleProp] = value
}
})
}
const isBodyOverflowing = () => {
return getWidth() > 0
}
export {
getWidth,
hide,
isBodyOverflowing,
reset
}

View File

@@ -0,0 +1,324 @@
import OffCanvas from '../../src/offcanvas'
import EventHandler from '../../src/dom/event-handler'
/** Test helpers */
import { clearFixture, getFixture, jQueryMock, createEvent } from '../helpers/fixture'
describe('OffCanvas', () => {
let fixtureEl
beforeAll(() => {
fixtureEl = getFixture()
})
afterEach(() => {
clearFixture()
document.body.classList.remove('offcanvas-open')
})
describe('VERSION', () => {
it('should return plugin version', () => {
expect(OffCanvas.VERSION).toEqual(jasmine.any(String))
})
})
describe('constructor', () => {
it('should call hide when a element with data-bs-dismiss="offcanvas" is clicked', () => {
fixtureEl.innerHTML = [
'<div class="offcanvas">',
' <a href="#" data-bs-dismiss="offcanvas">Close</a>',
'</div>'
].join('')
const offCanvasEl = fixtureEl.querySelector('.offcanvas')
const closeEl = fixtureEl.querySelector('a')
const offCanvas = new OffCanvas(offCanvasEl)
spyOn(offCanvas, 'hide')
closeEl.click()
expect(offCanvas.hide).toHaveBeenCalled()
})
it('should hide if esc is pressed', () => {
fixtureEl.innerHTML = '<div class="offcanvas"></div>'
const offCanvasEl = fixtureEl.querySelector('.offcanvas')
const offCanvas = new OffCanvas(offCanvasEl)
const keyDownEsc = createEvent('keydown')
keyDownEsc.key = 'Escape'
spyOn(offCanvas, 'hide')
document.dispatchEvent(keyDownEsc)
expect(offCanvas.hide).toHaveBeenCalled()
})
it('should not hide if esc is not pressed', () => {
fixtureEl.innerHTML = '<div class="offcanvas"></div>'
const offCanvasEl = fixtureEl.querySelector('.offcanvas')
const offCanvas = new OffCanvas(offCanvasEl)
const keydownTab = createEvent('keydown')
keydownTab.key = 'Tab'
spyOn(offCanvas, 'hide')
document.dispatchEvent(keydownTab)
expect(offCanvas.hide).not.toHaveBeenCalled()
})
})
describe('toggle', () => {
it('should call show method if show class is not present', () => {
fixtureEl.innerHTML = '<div class="offcanvas"></div>'
const offCanvasEl = fixtureEl.querySelector('.offcanvas')
const offCanvas = new OffCanvas(offCanvasEl)
spyOn(offCanvas, 'show')
offCanvas.toggle()
expect(offCanvas.show).toHaveBeenCalled()
})
it('should call hide method if show class is present', () => {
fixtureEl.innerHTML = '<div class="offcanvas show"></div>'
const offCanvasEl = fixtureEl.querySelector('.show')
const offCanvas = new OffCanvas(offCanvasEl)
spyOn(offCanvas, 'hide')
offCanvas.toggle()
expect(offCanvas.hide).toHaveBeenCalled()
})
})
describe('show', () => {
it('should do nothing if already shown', () => {
fixtureEl.innerHTML = '<div class="offcanvas show"></div>'
spyOn(EventHandler, 'trigger')
const offCanvasEl = fixtureEl.querySelector('div')
const offCanvas = new OffCanvas(offCanvasEl)
offCanvas.show()
expect(EventHandler.trigger).not.toHaveBeenCalled()
})
it('should show a hidden element', done => {
fixtureEl.innerHTML = '<div class="offcanvas"></div>'
const offCanvasEl = fixtureEl.querySelector('div')
const offCanvas = new OffCanvas(offCanvasEl)
offCanvasEl.addEventListener('shown.bs.offcanvas', () => {
expect(offCanvasEl.classList.contains('show')).toEqual(true)
done()
})
offCanvas.show()
})
it('should not fire shown when show is prevented', done => {
fixtureEl.innerHTML = '<div class="offcanvas"></div>'
const offCanvasEl = fixtureEl.querySelector('div')
const offCanvas = new OffCanvas(offCanvasEl)
const expectEnd = () => {
setTimeout(() => {
expect().nothing()
done()
}, 10)
}
offCanvasEl.addEventListener('show.bs.offcanvas', e => {
e.preventDefault()
expectEnd()
})
offCanvasEl.addEventListener('shown.bs.offcanvas', () => {
throw new Error('should not fire shown event')
})
offCanvas.show()
})
})
describe('hide', () => {
it('should do nothing if already shown', () => {
fixtureEl.innerHTML = '<div class="offcanvas"></div>'
spyOn(EventHandler, 'trigger')
const offCanvasEl = fixtureEl.querySelector('div')
const offCanvas = new OffCanvas(offCanvasEl)
offCanvas.hide()
expect(EventHandler.trigger).not.toHaveBeenCalled()
})
it('should hide a shown element', done => {
fixtureEl.innerHTML = '<div class="offcanvas show"></div>'
const offCanvasEl = fixtureEl.querySelector('div')
const offCanvas = new OffCanvas(offCanvasEl)
offCanvasEl.addEventListener('hidden.bs.offcanvas', () => {
expect(offCanvasEl.classList.contains('show')).toEqual(false)
done()
})
offCanvas.hide()
})
it('should not fire hidden when hide is prevented', done => {
fixtureEl.innerHTML = '<div class="offcanvas show"></div>'
const offCanvasEl = fixtureEl.querySelector('div')
const offCanvas = new OffCanvas(offCanvasEl)
const expectEnd = () => {
setTimeout(() => {
expect().nothing()
done()
}, 10)
}
offCanvasEl.addEventListener('hide.bs.offcanvas', e => {
e.preventDefault()
expectEnd()
})
offCanvasEl.addEventListener('hidden.bs.offcanvas', () => {
throw new Error('should not fire hidden event')
})
offCanvas.hide()
})
})
describe('data-api', () => {
it('should not prevent event for input', done => {
fixtureEl.innerHTML = [
'<input type="checkbox" data-bs-toggle="offcanvas" data-bs-target="#offcanvasdiv1" />',
'<div id="offcanvasdiv1" class="offcanvas"></div>'
].join('')
const target = fixtureEl.querySelector('input')
const offCanvasEl = fixtureEl.querySelector('#offcanvasdiv1')
offCanvasEl.addEventListener('shown.bs.offcanvas', () => {
expect(offCanvasEl.classList.contains('show')).toEqual(true)
expect(target.checked).toEqual(true)
done()
})
target.click()
})
it('should not call toggle on disabled elements', () => {
fixtureEl.innerHTML = [
'<a href="#" data-bs-toggle="offcanvas" data-bs-target="#offcanvasdiv1" class="disabled"></a>',
'<div id="offcanvasdiv1" class="offcanvas"></div>'
].join('')
const target = fixtureEl.querySelector('a')
spyOn(OffCanvas.prototype, 'toggle')
target.click()
expect(OffCanvas.prototype.toggle).not.toHaveBeenCalled()
})
})
describe('jQueryInterface', () => {
it('should create an offcanvas', () => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
jQueryMock.fn.offcanvas = OffCanvas.jQueryInterface
jQueryMock.elements = [div]
jQueryMock.fn.offcanvas.call(jQueryMock)
expect(OffCanvas.getInstance(div)).toBeDefined()
})
it('should not re create an offcanvas', () => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
const offCanvas = new OffCanvas(div)
jQueryMock.fn.offcanvas = OffCanvas.jQueryInterface
jQueryMock.elements = [div]
jQueryMock.fn.offcanvas.call(jQueryMock)
expect(OffCanvas.getInstance(div)).toEqual(offCanvas)
})
it('should throw error on undefined method', () => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
const action = 'undefinedMethod'
jQueryMock.fn.offcanvas = OffCanvas.jQueryInterface
jQueryMock.elements = [div]
try {
jQueryMock.fn.offcanvas.call(jQueryMock, action)
} catch (error) {
expect(error.message).toEqual(`No method named "${action}"`)
}
})
it('should call offcanvas method', () => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
spyOn(OffCanvas.prototype, 'show')
jQueryMock.fn.offcanvas = OffCanvas.jQueryInterface
jQueryMock.elements = [div]
jQueryMock.fn.offcanvas.call(jQueryMock, 'show')
expect(OffCanvas.prototype.show).toHaveBeenCalled()
})
})
describe('getInstance', () => {
it('should return offcanvas instance', () => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
const offCanvas = new OffCanvas(div)
expect(OffCanvas.getInstance(div)).toEqual(offCanvas)
expect(OffCanvas.getInstance(div)).toBeInstanceOf(OffCanvas)
})
it('should return null when there is no offcanvas instance', () => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
expect(OffCanvas.getInstance(div)).toEqual(null)
})
})
})

View File

@@ -0,0 +1,182 @@
import * as Scrollbar from '../../../src/util/scrollbar'
import { clearFixture, getFixture } from '../../helpers/fixture'
describe('ScrollBar', () => {
let fixtureEl
const windowCalculations = () => {
return {
htmlClient: document.documentElement.clientWidth,
htmlOffset: document.documentElement.offsetWidth,
docClient: document.body.clientWidth,
htmlBound: document.documentElement.getBoundingClientRect().width,
bodyBound: document.body.getBoundingClientRect().width,
window: window.innerWidth,
width: Math.abs(window.innerWidth - document.documentElement.clientWidth)
}
}
const isScrollBarHidden = () => { // IOS devices, Android devices and Browsers on Mac, hide scrollbar by default and appear it, only while scrolling. So the tests for scrollbar would fail
const calc = windowCalculations()
return calc.htmlClient === calc.htmlOffset && calc.htmlClient === calc.window
}
beforeAll(() => {
fixtureEl = getFixture()
// custom fixture to avoid extreme style values
fixtureEl.removeAttribute('style')
})
afterAll(() => {
fixtureEl.remove()
document.documentElement.style.overflowY = 'auto'
document.body.style.overflowY = 'auto'
})
afterEach(() => {
clearFixture()
document.documentElement.removeAttribute('style')
})
beforeEach(() => {
document.documentElement.removeAttribute('style')
})
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 = Scrollbar.isBodyOverflowing()
if (isScrollBarHidden()) {
expect(result).toEqual(false)
} else {
expect(result).toEqual(true)
}
})
it('should return false if body is overflowing', () => {
document.documentElement.style.overflowY = 'hidden'
document.body.style.overflowY = 'hidden'
fixtureEl.innerHTML = [
'<div style="height: 110vh; width: 100%"></div>'
].join('')
const result = Scrollbar.isBodyOverflowing()
expect(result).toEqual(false)
})
})
describe('getWidth', () => {
it('should return an integer greater than zero, 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 = Scrollbar.getWidth()
if (isScrollBarHidden()) {
expect(result).toBe(0)
} else {
expect(result).toBeGreaterThan(1)
}
})
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('')
const result = Scrollbar.getWidth()
expect(result).toEqual(0)
})
})
describe('hide - reset', () => {
it('should adjust the inline padding of fixed elements which are full-width', done => {
fixtureEl.innerHTML = [
'<div style="height: 110vh; width: 100%">' +
'<div class="fixed-top" id="fixed1" style="padding-right: 0px; width: 100vw"></div>',
'<div class="fixed-top" id="fixed2" style="padding-right: 5px; width: 100vw"></div>',
'</div>'
].join('')
document.documentElement.style.overflowY = 'scroll'
const fixedEl = fixtureEl.querySelector('#fixed1')
const fixedEl2 = fixtureEl.querySelector('#fixed2')
const originalPadding = Number.parseInt(window.getComputedStyle(fixedEl).paddingRight, 10)
const originalPadding2 = Number.parseInt(window.getComputedStyle(fixedEl2).paddingRight, 10)
const expectedPadding = originalPadding + Scrollbar.getWidth()
const expectedPadding2 = originalPadding2 + Scrollbar.getWidth()
Scrollbar.hide()
let currentPadding = Number.parseInt(window.getComputedStyle(fixedEl).paddingRight, 10)
let currentPadding2 = Number.parseInt(window.getComputedStyle(fixedEl2).paddingRight, 10)
expect(fixedEl.getAttribute('data-bs-padding-right')).toEqual('0px', 'original fixed element padding should be stored in data-bs-padding-right')
expect(fixedEl2.getAttribute('data-bs-padding-right')).toEqual('5px', 'original fixed element padding should be stored in data-bs-padding-right')
expect(currentPadding).toEqual(expectedPadding, 'fixed element padding should be adjusted while opening')
expect(currentPadding2).toEqual(expectedPadding2, 'fixed element padding should be adjusted while opening')
Scrollbar.reset()
currentPadding = Number.parseInt(window.getComputedStyle(fixedEl).paddingRight, 10)
currentPadding2 = Number.parseInt(window.getComputedStyle(fixedEl2).paddingRight, 10)
expect(fixedEl.getAttribute('data-bs-padding-right')).toEqual(null, 'data-bs-padding-right should be cleared after closing')
expect(fixedEl2.getAttribute('data-bs-padding-right')).toEqual(null, 'data-bs-padding-right should be cleared after closing')
expect(currentPadding).toEqual(originalPadding, 'fixed element padding should be reset after closing')
expect(currentPadding2).toEqual(originalPadding2, 'fixed element padding should be reset after closing')
done()
})
it('should adjust the inline margin of sticky elements', done => {
fixtureEl.innerHTML = [
'<div style="height: 110vh">' +
'<div class="sticky-top" style="margin-right: 0px; width: 100vw; height: 10px"></div>',
'</div>'
].join('')
document.documentElement.style.overflowY = 'scroll'
const stickyTopEl = fixtureEl.querySelector('.sticky-top')
const originalMargin = Number.parseInt(window.getComputedStyle(stickyTopEl).marginRight, 10)
const expectedMargin = originalMargin - Scrollbar.getWidth()
Scrollbar.hide()
let currentMargin = Number.parseInt(window.getComputedStyle(stickyTopEl).marginRight, 10)
expect(stickyTopEl.getAttribute('data-bs-margin-right')).toEqual('0px', 'original sticky element margin should be stored in data-bs-margin-right')
expect(currentMargin).toEqual(expectedMargin, 'sticky element margin should be adjusted while opening')
Scrollbar.reset()
currentMargin = Number.parseInt(window.getComputedStyle(stickyTopEl).marginRight, 10)
expect(stickyTopEl.getAttribute('data-bs-margin-right')).toEqual(null, 'data-bs-margin-right should be cleared after closing')
expect(currentMargin).toEqual(originalMargin, 'sticky element margin should be reset after closing')
done()
})
it('should not adjust the inline margin and padding of sticky and fixed elements when element do not have full width', () => {
fixtureEl.innerHTML = [
'<div class="sticky-top" style="margin-right: 0px; padding-right: 0px; width: 50vw"></div>'
].join('')
const stickyTopEl = fixtureEl.querySelector('.sticky-top')
const originalMargin = Number.parseInt(window.getComputedStyle(stickyTopEl).marginRight, 10)
const originalPadding = Number.parseInt(window.getComputedStyle(stickyTopEl).paddingRight, 10)
Scrollbar.hide()
const currentMargin = Number.parseInt(window.getComputedStyle(stickyTopEl).marginRight, 10)
const currentPadding = Number.parseInt(window.getComputedStyle(stickyTopEl).paddingRight, 10)
expect(currentMargin).toEqual(originalMargin, 'sticky element\'s margin should not be adjusted while opening')
expect(currentPadding).toEqual(originalPadding, 'sticky element\'s padding should not be adjusted while opening')
Scrollbar.reset()
})
})
})