From 2584e00d15b72675d638740a56941467bcd488d6 Mon Sep 17 00:00:00 2001 From: Daniel Morse Date: Tue, 30 Jul 2019 21:59:42 -0400 Subject: [PATCH 1/8] feat: update custom events to bubble and include details about body scrollbar --- packages/components/bolt-modal/src/modal.js | 24 ++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/packages/components/bolt-modal/src/modal.js b/packages/components/bolt-modal/src/modal.js index a08b91b55f..9a682cdf7e 100644 --- a/packages/components/bolt-modal/src/modal.js +++ b/packages/components/bolt-modal/src/modal.js @@ -83,6 +83,16 @@ class BoltModal extends withLitHtml() { this.dispatchEvent(new CustomEvent('modal:ready')); } + get _toggleEventOptions() { + return { + detail: { + hasScrollbar: this._bodyHasScrollbar, + scrollbarWidth: this.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,6 +111,8 @@ class BoltModal extends withLitHtml() { // triggers re-render this.open = true; + this.dispatchEvent(new CustomEvent('modal:show', this._toggleEventOptions)); + this._setScrollbar(); // @todo: re-evaluate if the trigger element used needs to have it's tabindex messed with @@ -113,7 +125,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,6 +147,9 @@ 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'), ); @@ -140,6 +157,9 @@ class BoltModal extends withLitHtml() { // Wait until after transition or modal will shift setTimeout(() => { this._resetScrollbar(); + this.dispatchEvent( + new CustomEvent('modal:hidden', this._toggleEventOptions), + ); }, this.transitionDuration); // @todo: refactor this to be more component / element agnostic @@ -170,8 +190,6 @@ class BoltModal extends withLitHtml() { // target.removeAttribute('aria-hidden'); // }); } - - this.dispatchEvent(new CustomEvent('modal:hide')); } get _bodyHasScrollbar() { From 90071dfd246fe55ce8277160b26f7b6e6eacf461 Mon Sep 17 00:00:00 2001 From: Daniel Morse Date: Tue, 30 Jul 2019 22:00:41 -0400 Subject: [PATCH 2/8] docs: add demo page showing how to set padding on element when modal shows/hides --- .../-modal-usage-custom-events.twig | 132 ++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 docs-site/src/pages/pattern-lab/_patterns/02-components/modal/custom-events/-modal-usage-custom-events.twig 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 %} From 5a731b09efa098d6bcd2fdc801be3279b4786faa Mon Sep 17 00:00:00 2001 From: Daniel Morse Date: Fri, 2 Aug 2019 13:48:15 -0400 Subject: [PATCH 3/8] feat: move scrollbar helpers to core, scrollbar calc functions to Class, add 'preventBodyScroll' as prop --- .../components/bolt-modal/modal.schema.yml | 5 +- packages/components/bolt-modal/src/modal.js | 76 ++++++------------- packages/components/bolt-modal/src/modal.scss | 12 +-- packages/core/utils/index.js | 1 + packages/core/utils/scrollbar.js | 62 +++++++++++++++ 5 files changed, 93 insertions(+), 63 deletions(-) create mode 100644 packages/core/utils/scrollbar.js diff --git a/packages/components/bolt-modal/modal.schema.yml b/packages/components/bolt-modal/modal.schema.yml index 0199bbdd38..0b871398fa 100644 --- a/packages/components/bolt-modal/modal.schema.yml +++ b/packages/components/bolt-modal/modal.schema.yml @@ -58,7 +58,10 @@ properties: uuid: type: string description: Unique ID for modal, randomly generated if not provided. - + prevent_body_scroll: + type: boolean + description: Prevent background page content from scrolling when modal is open + hidden: true # @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 9a682cdf7e..6ee3209de6 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'; @@ -40,6 +44,7 @@ class BoltModal extends withLitHtml() { ...props.boolean, ...{ default: false }, }, + preventBodyScroll: props.boolean, }; // https://github.com/WebReflection/document-register-element#upgrading-the-constructor-context @@ -54,11 +59,16 @@ class BoltModal extends withLitHtml() { 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 @@ -84,13 +94,15 @@ class BoltModal extends withLitHtml() { } get _toggleEventOptions() { - return { - detail: { - hasScrollbar: this._bodyHasScrollbar, - scrollbarWidth: this.scrollbarWidth, - }, - bubbles: true, - }; + return this.props.preventBodyScroll + ? { + detail: { + hasScrollbar: BoltModal.bodyHasScrollbar, + scrollbarWidth: BoltModal.scrollbarWidth, + }, + bubbles: true, + } + : {}; } /** @@ -113,7 +125,7 @@ class BoltModal extends withLitHtml() { this.dispatchEvent(new CustomEvent('modal:show', this._toggleEventOptions)); - this._setScrollbar(); + this.props.preventBodyScroll && 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'); @@ -156,7 +168,7 @@ class BoltModal extends withLitHtml() { // Wait until after transition or modal will shift setTimeout(() => { - this._resetScrollbar(); + this.props.preventBodyScroll && this._resetScrollbar(); this.dispatchEvent( new CustomEvent('modal:hidden', this._toggleEventOptions), ); @@ -192,11 +204,6 @@ class BoltModal extends withLitHtml() { } } - get _bodyHasScrollbar() { - const bodyRect = document.body.getBoundingClientRect(); - return bodyRect.left + bodyRect.right < window.innerWidth; - } - /** * Private event handler used when listening to some specific key presses * (namely ESCAPE and TAB) @@ -241,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..b5eeb29f1f --- /dev/null +++ b/packages/core/utils/scrollbar.js @@ -0,0 +1,62 @@ +/** + * 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.className = 'c-bolt-modal__scrollbar-measure'; + 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 + } +}; From 29b622feebdc11b23c3aed77099360a021b31d15 Mon Sep 17 00:00:00 2001 From: Daniel Morse Date: Fri, 2 Aug 2019 14:51:33 -0400 Subject: [PATCH 4/8] fix: pattern lab override, position 'relative' causing modal choppiness on scroll --- .../src/components/pattern-lab-hacks/pattern-lab-hacks.scss | 4 ++++ 1 file changed, 4 insertions(+) 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, From 353af95ca87c15af1d266fc0b00734bab327d2bb Mon Sep 17 00:00:00 2001 From: Daniel Morse Date: Fri, 2 Aug 2019 15:12:06 -0400 Subject: [PATCH 5/8] docs: hide modal custom-events page for now --- ...l-usage-custom-events.twig => _modal-usage-custom-events.twig} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs-site/src/pages/pattern-lab/_patterns/02-components/modal/custom-events/{-modal-usage-custom-events.twig => _modal-usage-custom-events.twig} (100%) 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 similarity index 100% rename from docs-site/src/pages/pattern-lab/_patterns/02-components/modal/custom-events/-modal-usage-custom-events.twig rename to docs-site/src/pages/pattern-lab/_patterns/02-components/modal/custom-events/_modal-usage-custom-events.twig From 5904236b09c35f82f2c10021c42d21d6cc61cd64 Mon Sep 17 00:00:00 2001 From: Daniel Morse Date: Fri, 2 Aug 2019 16:01:57 -0400 Subject: [PATCH 6/8] fix: remove unused classname --- packages/core/utils/scrollbar.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/core/utils/scrollbar.js b/packages/core/utils/scrollbar.js index b5eeb29f1f..ce24516204 100644 --- a/packages/core/utils/scrollbar.js +++ b/packages/core/utils/scrollbar.js @@ -14,7 +14,6 @@ export const bodyHasScrollbar = () => { */ export const getScrollbarWidth = () => { const scrollDiv = document.createElement('div'); - scrollDiv.className = 'c-bolt-modal__scrollbar-measure'; scrollDiv.style.cssText = 'position: absolute; top: -9999px; width: 100px; height: 100px; overflow: scroll;'; document.body.appendChild(scrollDiv); From 5b6ab74fa425708c5b06bb94442b9bad0f37b70a Mon Sep 17 00:00:00 2001 From: Daniel Morse Date: Fri, 2 Aug 2019 16:25:02 -0400 Subject: [PATCH 7/8] fix: rename 'preventBodyScroll' to 'noBodyScroll' --- packages/components/bolt-modal/modal.schema.yml | 2 +- packages/components/bolt-modal/src/modal.js | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/components/bolt-modal/modal.schema.yml b/packages/components/bolt-modal/modal.schema.yml index 0b871398fa..2109d2476f 100644 --- a/packages/components/bolt-modal/modal.schema.yml +++ b/packages/components/bolt-modal/modal.schema.yml @@ -58,7 +58,7 @@ properties: uuid: type: string description: Unique ID for modal, randomly generated if not provided. - prevent_body_scroll: + no_body_scroll: type: boolean description: Prevent background page content from scrolling when modal is open hidden: true diff --git a/packages/components/bolt-modal/src/modal.js b/packages/components/bolt-modal/src/modal.js index 6ee3209de6..23ffc70cea 100644 --- a/packages/components/bolt-modal/src/modal.js +++ b/packages/components/bolt-modal/src/modal.js @@ -44,7 +44,7 @@ class BoltModal extends withLitHtml() { ...props.boolean, ...{ default: false }, }, - preventBodyScroll: props.boolean, + noBodyScroll: props.boolean, }; // https://github.com/WebReflection/document-register-element#upgrading-the-constructor-context @@ -94,7 +94,7 @@ class BoltModal extends withLitHtml() { } get _toggleEventOptions() { - return this.props.preventBodyScroll + return this.props.noBodyScroll ? { detail: { hasScrollbar: BoltModal.bodyHasScrollbar, @@ -125,7 +125,7 @@ class BoltModal extends withLitHtml() { this.dispatchEvent(new CustomEvent('modal:show', this._toggleEventOptions)); - this.props.preventBodyScroll && this._setScrollbar(); + this.props.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'); @@ -168,7 +168,7 @@ class BoltModal extends withLitHtml() { // Wait until after transition or modal will shift setTimeout(() => { - this.props.preventBodyScroll && this._resetScrollbar(); + this.props.noBodyScroll && this._resetScrollbar(); this.dispatchEvent( new CustomEvent('modal:hidden', this._toggleEventOptions), ); From 702200aefb30baff9054d745ce3edb0e4223f2d7 Mon Sep 17 00:00:00 2001 From: Daniel Morse Date: Sun, 4 Aug 2019 20:24:51 -0400 Subject: [PATCH 8/8] fix: remove 'noBodyScroll' prop in favor of private variable --- packages/components/bolt-modal/modal.schema.yml | 4 ---- packages/components/bolt-modal/src/modal.js | 8 ++++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/components/bolt-modal/modal.schema.yml b/packages/components/bolt-modal/modal.schema.yml index 2109d2476f..f99c801fb0 100644 --- a/packages/components/bolt-modal/modal.schema.yml +++ b/packages/components/bolt-modal/modal.schema.yml @@ -58,10 +58,6 @@ properties: uuid: type: string description: Unique ID for modal, randomly generated if not provided. - no_body_scroll: - type: boolean - description: Prevent background page content from scrolling when modal is open - hidden: true # @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 23ffc70cea..dc44930851 100644 --- a/packages/components/bolt-modal/src/modal.js +++ b/packages/components/bolt-modal/src/modal.js @@ -44,7 +44,6 @@ class BoltModal extends withLitHtml() { ...props.boolean, ...{ default: false }, }, - noBodyScroll: props.boolean, }; // https://github.com/WebReflection/document-register-element#upgrading-the-constructor-context @@ -55,6 +54,7 @@ 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; } @@ -94,7 +94,7 @@ class BoltModal extends withLitHtml() { } get _toggleEventOptions() { - return this.props.noBodyScroll + return this._noBodyScroll ? { detail: { hasScrollbar: BoltModal.bodyHasScrollbar, @@ -125,7 +125,7 @@ class BoltModal extends withLitHtml() { this.dispatchEvent(new CustomEvent('modal:show', this._toggleEventOptions)); - this.props.noBodyScroll && this._setScrollbar(); + 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'); @@ -168,7 +168,7 @@ class BoltModal extends withLitHtml() { // Wait until after transition or modal will shift setTimeout(() => { - this.props.noBodyScroll && this._resetScrollbar(); + this._noBodyScroll && this._resetScrollbar(); this.dispatchEvent( new CustomEvent('modal:hidden', this._toggleEventOptions), );