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

Modal | JS | Add page scrolling option #1304

Merged
merged 8 commits into from
Aug 5, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
{% set javascript %}
<script>
// If the browser body has scrollbar, set padding on the element the width of the scrollbar
const setScrollbarPadding = function(element, hasScrollbar, scrollbarWidth) {
if (!element) {
return;
}

if (hasScrollbar) {
const originalPadding = element.style.paddingRight;
const calculatedPadding = window.getComputedStyle(element)[
'padding-right'
];

// Save original padding value for later
element.setAttribute('data-padding-right', originalPadding);
element.style.paddingRight = parseFloat(calculatedPadding) + scrollbarWidth + 'px';
}
}

// Reset the padding on that element after modal is hidden
const resetScrollbarPadding = function(element) {
if (!element) {
return;
}

const padding = element.getAttribute('data-padding-right');

element.style.paddingRight = '';

if (typeof padding === 'undefined') {
element.style.paddingRight = '';
} else {
element.removeAttribute('data-padding-right');
// Restore original padding value
element.style.paddingRight = padding;
}
}

// Arbitrary fixed element, could be .c-page-header
const fixedElement = document.querySelector('.placeholder-fixed-element');

// Listen for 'modal:show' on the body, event will bubble up from the modal
document.body.addEventListener('modal:show', function(e) {
setScrollbarPadding(fixedElement, e.detail.hasScrollbar, e.detail.scrollbarWidth);
});

// Listen for 'modal:hidden', fires after the modal has animated out
document.body.addEventListener('modal:hidden', function(e) {
resetScrollbarPadding(fixedElement);
});
</script>
{% endset %}

<bolt-text headline font-size="xlarge" style="margin-top: 120px;">Modal Custom Events</bolt-text>
<bolt-text>Bolt Modal emits the following custom events: <code>modal:show</code>, <code>modal:shown</code>, <code>modal:hide</code>, <code>modal:hidden</code>.</bolt-text>

<div class="placeholder-fixed-element" style="background: yellow; padding: 2rem; text-align: center; position: fixed; top: 0; height: 100px; width: 100%;">
Placeholder "fixed" element, should not shift when modal shows/hides.
</div>

<bolt-text headline font-size="large">Demo</bolt-text>
<div class="t-bolt-light u-bolt-padding-medium u-bolt-margin-bottom-small">
{% 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 %}
<bolt-text headline font-size="large">Set padding on a "fixed" element when modal shows/hides</bolt-text>
<bolt-text>Use the <code>modal:show</code> and <code>modal:hidden</code> events to set padding on a fixed element to prevent it from shifting.</bolt-text>
<bolt-text>Note: for this example, you must use <code>modal:hidden</code> not <code>modal:hide</code> event, as <code>modal:hidden</code> fires after the modal animation, and that is key to getting the correct <code>hasScrollbar</code> state.</bolt-text>
{% 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 %}
</div>

<bolt-text headline font-size="large">Custom Javascript</bolt-text>
<bolt-code-snippet syntax="dark" lang="html">{% spaceless %}
{{ javascript | replace({
'<': '&lt;',
'>': '&gt;',
}) | trim | raw }}
{% endspaceless %}</bolt-code-snippet>
1 change: 0 additions & 1 deletion packages/components/bolt-modal/modal.schema.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
86 changes: 39 additions & 47 deletions packages/components/bolt-modal/src/modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand 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');
Expand All @@ -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),
);
}

/**
Expand All @@ -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
Expand Down Expand Up @@ -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;
}

/**
Expand Down Expand Up @@ -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
Expand Down
12 changes: 1 addition & 11 deletions packages/components/bolt-modal/src/modal.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

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

+1 for commenting on this

height: 100vh;
pointer-events: none;
transition: opacity $bolt-modal-transition;
Expand Down Expand Up @@ -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;
}
1 change: 1 addition & 0 deletions packages/core/utils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Loading