-
-
Notifications
You must be signed in to change notification settings - Fork 78.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* 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>
- Loading branch information
1 parent
b9e51dc
commit 548be2e
Showing
20 changed files
with
1,201 additions
and
20 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
Oops, something went wrong.