0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2024-12-30 22:34:01 -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:
Sodbileg Gansukh 2024-12-17 14:16:39 +08:00 committed by GitHub
parent 3d65bfa38d
commit 4bc85e2ff2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 56 additions and 5 deletions

View file

@ -57,11 +57,15 @@ export default class App extends React.Component {
initStatus: 'running',
lastPage: null,
customSiteUrl: props.customSiteUrl,
locale: props.locale
locale: props.locale,
scrollbarWidth: 0
};
}
componentDidMount() {
const scrollbarWidth = this.getScrollbarWidth();
this.setState({scrollbarWidth});
this.initSetup();
}
@ -75,10 +79,19 @@ export default class App extends React.Component {
if (this.state.showPopup) {
/** When modal is opened, store current overflow and set as hidden */
this.bodyScroll = window.document?.body?.style?.overflow;
this.bodyMargin = window.getComputedStyle(document.body).getPropertyValue('margin-right');
window.document.body.style.overflow = 'hidden';
if (this.state.scrollbarWidth) {
window.document.body.style.marginRight = `calc(${this.bodyMargin} + ${this.state.scrollbarWidth}px)`;
}
} else {
/** When the modal is hidden, reset overflow property for body */
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) {
/** 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 */
setupCustomTriggerButton() {
// 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 i18nLanguage = this.props.siteI18nEnabled ? this.props.locale || site.locale || 'en' : 'en';
const i18n = i18nLib(i18nLanguage, 'portal');
const state = {
site,
member,
@ -926,7 +960,7 @@ export default class App extends React.Component {
/**Get final App level context from App state*/
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 contextMember = this.getContextMember({page: contextPage, member, customSiteUrl});
return {
@ -944,6 +978,7 @@ export default class App extends React.Component {
customSiteUrl,
t,
dir,
scrollbarWidth,
onAction: (_action, data) => this.dispatchAction(_action, data)
};
}

View file

@ -223,6 +223,18 @@ export default class TriggerButton extends React.Component {
this.state = {
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) {
@ -251,7 +263,7 @@ export default class TriggerButton extends React.Component {
render() {
const site = this.context.site;
const {portal_button: portalButton} = site;
const {showPopup} = this.context;
const {showPopup, scrollbarWidth} = this.context;
if (!portalButton || !isSigninAllowed({site}) || hasMode(['offerPreview'])) {
return null;
@ -268,8 +280,12 @@ export default class TriggerButton extends React.Component {
frameStyle.width = `${updatedWidth}px`;
}
if (scrollbarWidth && showPopup) {
frameStyle.marginRight = `calc(${scrollbarWidth}px + ${this.buttonMargin})`;
}
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)} />
</Frame>
);