From 5fe3a04287714b59bc438756336c146d2f57cb90 Mon Sep 17 00:00:00 2001 From: zhangted Date: Fri, 7 Jul 2023 15:54:24 -0400 Subject: [PATCH] fix scroll when popstate from a full screen code block (#19674) --- .../codeFullscreenModeSwitcher.test.js | 106 ++++++++++++++++++ .../utilities/codeFullscreenModeSwitcher.js | 23 +++- 2 files changed, 127 insertions(+), 2 deletions(-) create mode 100644 app/javascript/utilities/__tests__/codeFullscreenModeSwitcher.test.js diff --git a/app/javascript/utilities/__tests__/codeFullscreenModeSwitcher.test.js b/app/javascript/utilities/__tests__/codeFullscreenModeSwitcher.test.js new file mode 100644 index 000000000..70f50da44 --- /dev/null +++ b/app/javascript/utilities/__tests__/codeFullscreenModeSwitcher.test.js @@ -0,0 +1,106 @@ +const importModule = () => import('@utilities/codeFullscreenModeSwitcher'); + +describe('CodeFullScreenModeSwitcher Utility', () => { + let addFullScreenModeControl, getFullScreenModeStatus; + let onPressEscape, onPopstate; + + const getFullScreenElements = () => + ['.js-fullscreen-code.is-open', '.js-code-highlight.is-fullscreen'].map( + (selector) => document.body.querySelector(selector), + ); + + const testFullScreenElements = ({ exists }) => { + for (const element of getFullScreenElements()) { + if (exists) expect(element).not.toBeNull(); + else expect(element).toBeNull(); + } + }; + + const testNonFullScreen = () => { + expect(getFullScreenModeStatus()).toBe(false); + testFullScreenElements({ exists: false }); + expect(document.body.style.overflow).toBe(''); + }; + + const testFullScreen = () => { + expect(getFullScreenModeStatus()).toBe(true); + testFullScreenElements({ exists: true }); + expect(document.body.style.overflow).toBe('hidden'); + }; + + const getEnterFullScreenButtons = () => + document.getElementsByClassName('js-fullscreen-code-action'); + + beforeAll(() => { + global.scrollTo = jest.fn(); + + document.body.innerHTML = ` +
+
+
+
+ `; + }); + + beforeAll(async () => { + ({ addFullScreenModeControl, getFullScreenModeStatus } = + await importModule()); + addFullScreenModeControl(getEnterFullScreenButtons()); + }); + + beforeAll(async () => { + // eventListener functions + ({ onPressEscape, onPopstate } = await importModule()); + }); + + it('starts in non-fullscreen mode', testNonFullScreen); + + it('enters fullscreen mode on click', () => { + const goFullScreenButtons = getEnterFullScreenButtons(); + const spyDoc = jest.spyOn(document.body, 'addEventListener'); + const spyWindow = jest.spyOn(window, 'addEventListener'); + + goFullScreenButtons[0].click(); + + testFullScreen(); + expect(spyDoc).toHaveBeenCalledWith('keyup', onPressEscape); + expect(spyWindow).toHaveBeenCalledWith('popstate', onPopstate); + }); + + it('exits fullscreen mode on Escape key', () => { + const spyDocRemove = jest.spyOn(document.body, 'removeEventListener'); + const spyWindowRemove = jest.spyOn(window, 'removeEventListener'); + + testFullScreen(); + document.body.dispatchEvent(new KeyboardEvent('keyup', { key: 'Escape' })); + testNonFullScreen(); + + expect(spyDocRemove).toHaveBeenCalledWith('keyup', onPressEscape); + expect(spyWindowRemove).toHaveBeenCalledWith('popstate', onPopstate); + }); + + it('re-enters fullscreen mode on click', () => { + const goFullScreenButtons = getEnterFullScreenButtons(); + const spyDoc = jest.spyOn(document.body, 'addEventListener'); + const spyWindow = jest.spyOn(window, 'addEventListener'); + + testNonFullScreen(); + goFullScreenButtons[0].click(); + testFullScreen(); + + expect(spyDoc).toHaveBeenCalledWith('keyup', onPressEscape); + expect(spyWindow).toHaveBeenCalledWith('popstate', onPopstate); + }); + + it('exits fullscreen mode on popstate event', () => { + const spyDocRemove = jest.spyOn(document.body, 'removeEventListener'); + const spyWindowRemove = jest.spyOn(window, 'removeEventListener'); + + testFullScreen(); + window.dispatchEvent(new PopStateEvent('popstate', { state: { key: '' } })); + testNonFullScreen(); + + expect(spyDocRemove).toHaveBeenCalledWith('keyup', onPressEscape); + expect(spyWindowRemove).toHaveBeenCalledWith('popstate', onPopstate); + }); +}); diff --git a/app/javascript/utilities/codeFullscreenModeSwitcher.js b/app/javascript/utilities/codeFullscreenModeSwitcher.js index 14d6a3526..5e42a6404 100644 --- a/app/javascript/utilities/codeFullscreenModeSwitcher.js +++ b/app/javascript/utilities/codeFullscreenModeSwitcher.js @@ -4,6 +4,10 @@ const fullScreenWindow = document.getElementsByClassName('js-fullscreen-code')[0]; const { body } = document; +export function getFullScreenModeStatus() { + return isFullScreenModeCodeOn; +} + function setAfterFullScreenScrollPosition() { window.scrollTo(0, screenScroll); } @@ -12,7 +16,7 @@ function getBeforeFullScreenScrollPosition() { screenScroll = window.scrollY; } -function onPressEscape(event) { +export function onPressEscape(event) { if (event.key == 'Escape') { fullScreenModeControl(event); } @@ -26,6 +30,18 @@ function listenToKeyboardForEscape(listen) { } } +export function onPopstate() { + fullScreenModeControl(); +} + +function listenToWindowForPopstate(listen) { + if (listen) { + window.addEventListener('popstate', onPopstate); + } else { + window.removeEventListener('popstate', onPopstate); + } +} + function toggleOverflowForDocument(overflow) { if (overflow) { body.style.overflow = 'hidden'; @@ -51,7 +67,7 @@ function removeFullScreenModeControl(elements) { } function fullScreenModeControl(event) { - const codeBlock = event.currentTarget.closest('.js-code-highlight') + const codeBlock = event?.currentTarget.closest('.js-code-highlight') ? event.currentTarget.closest('.js-code-highlight').cloneNode(true) : null; const codeBlockControls = codeBlock @@ -62,6 +78,7 @@ function fullScreenModeControl(event) { toggleOverflowForDocument(false); setAfterFullScreenScrollPosition(); listenToKeyboardForEscape(false); + listenToWindowForPopstate(false); removeFullScreenModeControl(codeBlockControls); fullScreenWindow.classList.remove('is-open'); @@ -72,6 +89,8 @@ function fullScreenModeControl(event) { toggleOverflowForDocument(true); getBeforeFullScreenScrollPosition(); listenToKeyboardForEscape(true); + listenToWindowForPopstate(true); + codeBlock.classList.add('is-fullscreen'); fullScreenWindow.appendChild(codeBlock); fullScreenWindow.classList.add('is-open');