mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-01-20 22:42:53 -05:00
Fixed layout shift issue when Portal popup appears (#21895)
ref DES-547 - when Portal popup is opened and the browser scroll bar is visible, it used to make layout shift, because we were hiding the scrollbar - now it applies right margin to body element and the trigger button by calculating the scrollbar width only when the browser scroll bar is visible - it also preservers the current right margin for those elements and makes the calculation based on that
This commit is contained in:
parent
3d65bfa38d
commit
4bc85e2ff2
2 changed files with 56 additions and 5 deletions
|
@ -57,11 +57,15 @@ export default class App extends React.Component {
|
||||||
initStatus: 'running',
|
initStatus: 'running',
|
||||||
lastPage: null,
|
lastPage: null,
|
||||||
customSiteUrl: props.customSiteUrl,
|
customSiteUrl: props.customSiteUrl,
|
||||||
locale: props.locale
|
locale: props.locale,
|
||||||
|
scrollbarWidth: 0
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
const scrollbarWidth = this.getScrollbarWidth();
|
||||||
|
this.setState({scrollbarWidth});
|
||||||
|
|
||||||
this.initSetup();
|
this.initSetup();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,10 +79,19 @@ export default class App extends React.Component {
|
||||||
if (this.state.showPopup) {
|
if (this.state.showPopup) {
|
||||||
/** When modal is opened, store current overflow and set as hidden */
|
/** When modal is opened, store current overflow and set as hidden */
|
||||||
this.bodyScroll = window.document?.body?.style?.overflow;
|
this.bodyScroll = window.document?.body?.style?.overflow;
|
||||||
|
this.bodyMargin = window.getComputedStyle(document.body).getPropertyValue('margin-right');
|
||||||
window.document.body.style.overflow = 'hidden';
|
window.document.body.style.overflow = 'hidden';
|
||||||
|
if (this.state.scrollbarWidth) {
|
||||||
|
window.document.body.style.marginRight = `calc(${this.bodyMargin} + ${this.state.scrollbarWidth}px)`;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
/** When the modal is hidden, reset overflow property for body */
|
/** When the modal is hidden, reset overflow property for body */
|
||||||
window.document.body.style.overflow = this.bodyScroll || '';
|
window.document.body.style.overflow = this.bodyScroll || '';
|
||||||
|
if (!this.bodyMargin || this.bodyMargin === '0px') {
|
||||||
|
window.document.body.style.marginRight = '';
|
||||||
|
} else {
|
||||||
|
window.document.body.style.marginRight = this.bodyMargin;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
/** Ignore any errors for scroll handling */
|
/** Ignore any errors for scroll handling */
|
||||||
|
@ -115,6 +128,27 @@ export default class App extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// User for adding trailing margin to prevent layout shift when popup appears
|
||||||
|
getScrollbarWidth() {
|
||||||
|
// Create a temporary div
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.style.visibility = 'hidden';
|
||||||
|
div.style.overflow = 'scroll'; // forcing scrollbar to appear
|
||||||
|
document.body.appendChild(div);
|
||||||
|
|
||||||
|
// Create an inner div
|
||||||
|
// const inner = document.createElement('div');
|
||||||
|
document.body.appendChild(div);
|
||||||
|
|
||||||
|
// Calculate the width difference
|
||||||
|
const scrollbarWidth = div.offsetWidth - div.clientWidth;
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
document.body.removeChild(div);
|
||||||
|
|
||||||
|
return scrollbarWidth;
|
||||||
|
}
|
||||||
|
|
||||||
/** Setup custom trigger buttons handling on page */
|
/** Setup custom trigger buttons handling on page */
|
||||||
setupCustomTriggerButton() {
|
setupCustomTriggerButton() {
|
||||||
// Handler for custom buttons
|
// Handler for custom buttons
|
||||||
|
@ -161,7 +195,7 @@ export default class App extends React.Component {
|
||||||
const {site, member, page, showPopup, popupNotification, lastPage, pageQuery, pageData} = await this.fetchData();
|
const {site, member, page, showPopup, popupNotification, lastPage, pageQuery, pageData} = await this.fetchData();
|
||||||
const i18nLanguage = this.props.siteI18nEnabled ? this.props.locale || site.locale || 'en' : 'en';
|
const i18nLanguage = this.props.siteI18nEnabled ? this.props.locale || site.locale || 'en' : 'en';
|
||||||
const i18n = i18nLib(i18nLanguage, 'portal');
|
const i18n = i18nLib(i18nLanguage, 'portal');
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
site,
|
site,
|
||||||
member,
|
member,
|
||||||
|
@ -926,7 +960,7 @@ export default class App extends React.Component {
|
||||||
|
|
||||||
/**Get final App level context from App state*/
|
/**Get final App level context from App state*/
|
||||||
getContextFromState() {
|
getContextFromState() {
|
||||||
const {site, member, action, page, lastPage, showPopup, pageQuery, pageData, popupNotification, customSiteUrl, t, dir} = this.state;
|
const {site, member, action, page, lastPage, showPopup, pageQuery, pageData, popupNotification, customSiteUrl, t, dir, scrollbarWidth} = this.state;
|
||||||
const contextPage = this.getContextPage({site, page, member});
|
const contextPage = this.getContextPage({site, page, member});
|
||||||
const contextMember = this.getContextMember({page: contextPage, member, customSiteUrl});
|
const contextMember = this.getContextMember({page: contextPage, member, customSiteUrl});
|
||||||
return {
|
return {
|
||||||
|
@ -944,6 +978,7 @@ export default class App extends React.Component {
|
||||||
customSiteUrl,
|
customSiteUrl,
|
||||||
t,
|
t,
|
||||||
dir,
|
dir,
|
||||||
|
scrollbarWidth,
|
||||||
onAction: (_action, data) => this.dispatchAction(_action, data)
|
onAction: (_action, data) => this.dispatchAction(_action, data)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -223,6 +223,18 @@ export default class TriggerButton extends React.Component {
|
||||||
this.state = {
|
this.state = {
|
||||||
width: null
|
width: null
|
||||||
};
|
};
|
||||||
|
this.buttonRef = React.createRef();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (this.buttonRef.current) {
|
||||||
|
const iframeElement = this.buttonRef.current.node;
|
||||||
|
if (iframeElement) {
|
||||||
|
this.buttonMargin = window.getComputedStyle(iframeElement).getPropertyValue('margin-right');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
onWidthChange(width) {
|
onWidthChange(width) {
|
||||||
|
@ -251,7 +263,7 @@ export default class TriggerButton extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
const site = this.context.site;
|
const site = this.context.site;
|
||||||
const {portal_button: portalButton} = site;
|
const {portal_button: portalButton} = site;
|
||||||
const {showPopup} = this.context;
|
const {showPopup, scrollbarWidth} = this.context;
|
||||||
|
|
||||||
if (!portalButton || !isSigninAllowed({site}) || hasMode(['offerPreview'])) {
|
if (!portalButton || !isSigninAllowed({site}) || hasMode(['offerPreview'])) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -268,8 +280,12 @@ export default class TriggerButton extends React.Component {
|
||||||
frameStyle.width = `${updatedWidth}px`;
|
frameStyle.width = `${updatedWidth}px`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (scrollbarWidth && showPopup) {
|
||||||
|
frameStyle.marginRight = `calc(${scrollbarWidth}px + ${this.buttonMargin})`;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Frame dataTestId='portal-trigger-frame' className='gh-portal-triggerbtn-iframe' style={frameStyle} title="portal-trigger" head={this.renderFrameStyles()}>
|
<Frame ref={this.buttonRef} dataTestId='portal-trigger-frame' className='gh-portal-triggerbtn-iframe' style={frameStyle} title="portal-trigger" head={this.renderFrameStyles()}>
|
||||||
<TriggerButtonContent isPopupOpen={showPopup} updateWidth={width => this.onWidthChange(width)} />
|
<TriggerButtonContent isPopupOpen={showPopup} updateWidth={width => this.onWidthChange(width)} />
|
||||||
</Frame>
|
</Frame>
|
||||||
);
|
);
|
||||||
|
|
Loading…
Add table
Reference in a new issue