diff --git a/docs-site/src/components/pattern-lab-hacks/pattern-lab-hacks.scss b/docs-site/src/components/pattern-lab-hacks/pattern-lab-hacks.scss
index efa983ece7..dfcaf7da82 100644
--- a/docs-site/src/components/pattern-lab-hacks/pattern-lab-hacks.scss
+++ b/docs-site/src/components/pattern-lab-hacks/pattern-lab-hacks.scss
@@ -39,6 +39,10 @@
max-height: 0px; // prevent background from showing up
}
+.pl-c-pattern {
+ position: static;
+}
+
.pl-c-category,
.pl-c-category__title,
.pl-c-pattern[id*=-docs] > .pl-c-pattern__header,
diff --git a/docs-site/src/pages/pattern-lab/_patterns/02-components/modal/custom-events/_modal-usage-custom-events.twig b/docs-site/src/pages/pattern-lab/_patterns/02-components/modal/custom-events/_modal-usage-custom-events.twig
new file mode 100644
index 0000000000..d21e15fb4a
--- /dev/null
+++ b/docs-site/src/pages/pattern-lab/_patterns/02-components/modal/custom-events/_modal-usage-custom-events.twig
@@ -0,0 +1,132 @@
+{% set javascript %}
+
+{% endset %}
+
+Modal Custom Events
+Bolt Modal emits the following custom events: modal:show
, modal:shown
, modal:hide
, modal:hidden
.
+
+
+ Placeholder "fixed" element, should not shift when modal shows/hides.
+
+
+Demo
+
+ {% set modal_content %}
+ {% include "@bolt-components-video/video.twig" with {
+ videoId: "3861325118001",
+ accountId: "1900410236",
+ playerId: "r1CAdLzTW",
+ showMeta: true,
+ showMetaTitle: true,
+ attributes: {
+ class: "js-modal-video-123"
+ }
+ } only %}
+ {% endset %}
+
+ {% set trigger %}
+ {% include "@bolt-components-button/button.twig" with {
+ text: "Play the video",
+ size: "small",
+ width: "full",
+ attributes: {
+ "on-click": "show",
+ "on-click-target": "js-modal-123"
+ }
+ } only %}
+ {% include "@bolt-components-modal/modal.twig" with {
+ attributes: {
+ class: "js-modal-123"
+ },
+ content: modal_content,
+ width: "optimal",
+ spacing: "none",
+ theme: "none",
+ scroll: "overall",
+ } only %}
+ {{ javascript }}
+ {% endset %}
+ {% set description %}
+ Set padding on a "fixed" element when modal shows/hides
+ Use the modal:show
and modal:hidden
events to set padding on a fixed element to prevent it from shifting.
+ Note: for this example, you must use modal:hidden
not modal:hide
event, as modal:hidden
fires after the modal animation, and that is key to getting the correct hasScrollbar
state.
+ {% endset %}
+ {% include "@bolt-components-grid/grid.twig" with {
+ items: [
+ {
+ column_start: "1 1@small",
+ column_span: "12 8@small 9@medium",
+ row_start: "2 1@small",
+ row_span: "1",
+ valign: "center",
+ content: description,
+ },
+ {
+ column_start: "1 10@small 11@medium",
+ column_span: "6 3@small 2@medium",
+ row_start: "1 1@small",
+ row_span: "1",
+ valign: "center",
+ content: trigger,
+ },
+ ]
+ } only %}
+
+
+Custom Javascript
+{% spaceless %}
+ {{ javascript | replace({
+ '<': '<',
+ '>': '>',
+ }) | trim | raw }}
+{% endspaceless %}
diff --git a/packages/components/bolt-modal/modal.schema.yml b/packages/components/bolt-modal/modal.schema.yml
index 0199bbdd38..f99c801fb0 100644
--- a/packages/components/bolt-modal/modal.schema.yml
+++ b/packages/components/bolt-modal/modal.schema.yml
@@ -58,7 +58,6 @@ properties:
uuid:
type: string
description: Unique ID for modal, randomly generated if not provided.
-
# @todo: persistent and hide close button props are not ready.
# persistent:
# type: boolean
diff --git a/packages/components/bolt-modal/src/modal.js b/packages/components/bolt-modal/src/modal.js
index a08b91b55f..dc44930851 100644
--- a/packages/components/bolt-modal/src/modal.js
+++ b/packages/components/bolt-modal/src/modal.js
@@ -5,6 +5,10 @@ import {
define,
hasNativeShadowDomSupport,
getTransitionDuration,
+ bodyHasScrollbar,
+ getScrollbarWidth,
+ setScrollbarPadding,
+ resetScrollbarPadding,
} from '@bolt/core/utils';
import { html, withLitHtml } from '@bolt/core/renderers/renderer-lit-html';
import classNames from 'classnames/bind';
@@ -50,15 +54,21 @@ class BoltModal extends withLitHtml() {
self.show = self.show.bind(this);
self.hide = self.hide.bind(this);
self._handleKeyPresseskeypress = this._handleKeyPresseskeypress.bind(this);
+ self._noBodyScroll = false; // Internal switch to enable 'no-body-scroll' feature which is not ready for release
return self;
}
+ static scrollbarWidth = getScrollbarWidth();
+
+ static get bodyHasScrollbar() {
+ return bodyHasScrollbar();
+ }
+
connecting() {
super.connecting && super.connecting();
document.addEventListener('keydown', this._handleKeyPresseskeypress);
this.setAttribute('ready', '');
- this.scrollbarWidth = this._getScrollbarWidth();
}
// Initialise everything needed for the dialog to work properly
@@ -83,6 +93,18 @@ class BoltModal extends withLitHtml() {
this.dispatchEvent(new CustomEvent('modal:ready'));
}
+ get _toggleEventOptions() {
+ return this._noBodyScroll
+ ? {
+ detail: {
+ hasScrollbar: BoltModal.bodyHasScrollbar,
+ scrollbarWidth: BoltModal.scrollbarWidth,
+ },
+ bubbles: true,
+ }
+ : {};
+ }
+
/**
* Show the dialog element, disable all the targets (siblings), trap the
* current focus within it, listen for some specific key presses and fire all
@@ -101,7 +123,9 @@ class BoltModal extends withLitHtml() {
// triggers re-render
this.open = true;
- this._setScrollbar();
+ this.dispatchEvent(new CustomEvent('modal:show', this._toggleEventOptions));
+
+ this._noBodyScroll && this._setScrollbar();
// @todo: re-evaluate if the trigger element used needs to have it's tabindex messed with
// this.querySelector('[slot="trigger"]').setAttribute('tabindex', '-1');
@@ -113,7 +137,9 @@ class BoltModal extends withLitHtml() {
// this.dialog.setAttribute('open', '');
// this.container.removeAttribute('aria-hidden');
- this.dispatchEvent(new CustomEvent('modal:show'));
+ this.dispatchEvent(
+ new CustomEvent('modal:shown', this._toggleEventOptions),
+ );
}
/**
@@ -133,13 +159,19 @@ class BoltModal extends withLitHtml() {
this.focusTrap.active = false;
this.open = false;
this.ready = false;
+
+ this.dispatchEvent(new CustomEvent('modal:hide', this._toggleEventOptions));
+
this.transitionDuration = getTransitionDuration(
this.renderRoot.querySelector('.c-bolt-modal'),
);
// Wait until after transition or modal will shift
setTimeout(() => {
- this._resetScrollbar();
+ this._noBodyScroll && this._resetScrollbar();
+ this.dispatchEvent(
+ new CustomEvent('modal:hidden', this._toggleEventOptions),
+ );
}, this.transitionDuration);
// @todo: refactor this to be more component / element agnostic
@@ -170,13 +202,6 @@ class BoltModal extends withLitHtml() {
// target.removeAttribute('aria-hidden');
// });
}
-
- this.dispatchEvent(new CustomEvent('modal:hide'));
- }
-
- get _bodyHasScrollbar() {
- const bodyRect = document.body.getBoundingClientRect();
- return bodyRect.left + bodyRect.right < window.innerWidth;
}
/**
@@ -223,51 +248,18 @@ class BoltModal extends withLitHtml() {
}
_setScrollbar() {
- // Technique inspired by Bootstrap Modal: https://github.com/twbs/bootstrap/blob/master/js/src/modal/modal.js
-
- if (this._bodyHasScrollbar) {
- const originalPadding = document.body.style.paddingRight;
- const calculatedPadding = window.getComputedStyle(document.body)[
- 'padding-right'
- ];
-
- // Save original padding value for later
- document.body.setAttribute('data-padding-right', originalPadding);
- document.body.style.paddingRight = `${parseFloat(calculatedPadding) +
- this.scrollbarWidth}px`;
- }
+ BoltModal.bodyHasScrollbar &&
+ setScrollbarPadding(document.body, BoltModal.scrollbarWidth);
document.body.classList.add('u-bolt-overflow-hidden');
}
_resetScrollbar() {
- const padding = document.body.getAttribute('data-padding-right');
-
- document.body.style.paddingRight = '';
-
- if (typeof padding === 'undefined') {
- document.body.style.paddingRight = '';
- } else {
- document.body.removeAttribute('data-padding-right');
- // Restore original padding value
- document.body.style.paddingRight = padding;
- }
+ resetScrollbarPadding(document.body);
document.body.classList.remove('u-bolt-overflow-hidden');
}
- // @todo: refactor into core JS/CSS
- _getScrollbarWidth() {
- // https://davidwalsh.name/detect-scrollbar-width
- const scrollDiv = document.createElement('div');
- scrollDiv.className = 'c-bolt-modal__scrollbar-measure';
- document.body.appendChild(scrollDiv);
- const scrollbarWidth =
- scrollDiv.getBoundingClientRect().width - scrollDiv.clientWidth;
- document.body.removeChild(scrollDiv);
- return scrollbarWidth;
- }
-
/**
* Set the focus to the first element with `autofocus` or the first focusable
* child of the given element
diff --git a/packages/components/bolt-modal/src/modal.scss b/packages/components/bolt-modal/src/modal.scss
index 15ffecbc13..a97df76529 100644
--- a/packages/components/bolt-modal/src/modal.scss
+++ b/packages/components/bolt-modal/src/modal.scss
@@ -52,7 +52,7 @@ bolt-modal:not([ready]) {
position: fixed;
top: 0;
left: 0;
- width: 100vw;
+ width: 100%; // Use % instead of vh or modal scrollbar is hidden behind bold scrollbar in IE11
height: 100vh;
pointer-events: none;
transition: opacity $bolt-modal-transition;
@@ -419,13 +419,3 @@ bolt-modal:not([ready]) {
.c-bolt-modal__dialog-title {
@include bolt-visuallyhidden;
}
-
-// https://davidwalsh.name/detect-scrollbar-width
-// @todo: refactor into core JS/CSS
-.c-bolt-modal__scrollbar-measure {
- position: absolute;
- top: -9999px;
- width: 100px;
- height: 100px;
- overflow: scroll;
-}
diff --git a/packages/core/utils/index.js b/packages/core/utils/index.js
index 89a20e4796..3a65056ec9 100644
--- a/packages/core/utils/index.js
+++ b/packages/core/utils/index.js
@@ -11,6 +11,7 @@ export * from './is-valid-selector';
export * from './rgb2hex';
export * from './rename-key';
export * from './sanitize-classes';
+export * from './scrollbar';
export * from './supports-css-vars';
export * from './supports-passive-event-listener';
export * from './which-transition-event';
diff --git a/packages/core/utils/scrollbar.js b/packages/core/utils/scrollbar.js
new file mode 100644
index 0000000000..ce24516204
--- /dev/null
+++ b/packages/core/utils/scrollbar.js
@@ -0,0 +1,61 @@
+/**
+ * Determine whether or not the BODY has a visible scrollbar
+ * @returns {Boolean} - Returns true if element has scrollbar
+ */
+export const bodyHasScrollbar = () => {
+ const bodyRect = document.body.getBoundingClientRect();
+ return bodyRect.left + bodyRect.right < window.innerWidth;
+};
+
+// https://davidwalsh.name/detect-scrollbar-width
+/**
+ * Get scrollbar width by temporarily adding element with scrollbar to page then removing it
+ * @returns {number} - Width of the scrollbar in pixels, without unit, e.g 15
+ */
+export const getScrollbarWidth = () => {
+ const scrollDiv = document.createElement('div');
+ scrollDiv.style.cssText =
+ 'position: absolute; top: -9999px; width: 100px; height: 100px; overflow: scroll;';
+ document.body.appendChild(scrollDiv);
+ const scrollbarWidth =
+ scrollDiv.getBoundingClientRect().width - scrollDiv.clientWidth;
+ document.body.removeChild(scrollDiv);
+
+ return scrollbarWidth;
+};
+
+// Technique inspired by Bootstrap Modal: https://github.com/twbs/bootstrap/blob/master/js/src/modal/modal.js
+/**
+ * Set padding on a given element equal to the browser's scrollbar width, used to prevent content shifting when scrollbars are turned off, e.g. when a modal opens
+ * @param {HTMLElement} element - The target element
+ * @param {(string\|number)} scrollbarWidth - The scrollbar width, with or without unit, e.g. 15 or 15px
+ */
+export const setScrollbarPadding = (element, scrollbarWidth) => {
+ if (!element) {
+ return;
+ }
+
+ const originalPaddingRight = element.style.paddingRight;
+ const calculatedPadding = window.getComputedStyle(element)['padding-right'];
+
+ // Save original padding value for later
+ element.originalPaddingRight = originalPaddingRight;
+ element.style.paddingRight =
+ parseFloat(calculatedPadding) + scrollbarWidth + 'px';
+};
+
+/**
+ * Reset padding on a given element, will remove current padding style and restore original padding style if necessary, used after modal closes
+ * @param {HTMLElement} element - The target element
+ */
+export const resetScrollbarPadding = element => {
+ if (!element) {
+ return;
+ }
+
+ if (typeof element.originalPaddingRight === 'undefined') {
+ element.style.paddingRight = '';
+ } else {
+ element.style.paddingRight = element.originalPaddingRight; // Restore original padding value
+ }
+};