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
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
76 changes: 25 additions & 51 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 @@ -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,
}
: {};
}

/**
Expand All @@ -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');
Expand Down Expand Up @@ -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),
);
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
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
62 changes: 62 additions & 0 deletions packages/core/utils/scrollbar.js
Original file line number Diff line number Diff line change
@@ -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';
Copy link
Contributor

Choose a reason for hiding this comment

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

Make sure to remove this ;)

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
Copy link
Contributor

Choose a reason for hiding this comment

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

👏 we need to start doing this for all net-new JS coming in. Nice!!

*/
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;
sghoweri marked this conversation as resolved.
Show resolved Hide resolved
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
}
};