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
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>
5 changes: 4 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,10 @@ properties:
uuid:
type: string
description: Unique ID for modal, randomly generated if not provided.

prevent_body_scroll:
Copy link
Contributor

Choose a reason for hiding this comment

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

@danielamorse would no_body_scroll make more sense here?

type: boolean
description: Prevent background page content from scrolling when modal is open
hidden: true
Copy link
Collaborator

Choose a reason for hiding this comment

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

Sort of a nitpick, but can we make the default value (false) explicit the schema?

Copy link
Collaborator

@remydenton remydenton Aug 2, 2019

Choose a reason for hiding this comment

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

Although, now that I think about it, do we ultimately want it to actually default to true? That's the ideal UX after all once we can get it working...

I suppose the way you have it is the path of least resistance though. Don't let me derail it.

# @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 @@ -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
Expand All @@ -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
Expand All @@ -83,6 +93,18 @@ class BoltModal extends withLitHtml() {
this.dispatchEvent(new CustomEvent('modal:ready'));
}

get _toggleEventOptions() {
return this.props.preventBodyScroll
? {
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.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');
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.props.preventBodyScroll && 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