diff --git a/entry_types/scrolled/package/spec/widgets/defaultNavigation/DefaultNavigationPresenceProvider-spec.js b/entry_types/scrolled/package/spec/widgets/defaultNavigation/DefaultNavigationPresenceProvider-spec.js index fede12920a..ae48ef61b8 100644 --- a/entry_types/scrolled/package/spec/widgets/defaultNavigation/DefaultNavigationPresenceProvider-spec.js +++ b/entry_types/scrolled/package/spec/widgets/defaultNavigation/DefaultNavigationPresenceProvider-spec.js @@ -1,6 +1,9 @@ import React from 'react'; -import {DefaultNavigationPresenceProvider} from 'widgets/defaultNavigation/DefaultNavigationPresenceProvider'; +import { + DefaultNavigationPresenceProvider, + useDefaultNavigationState +} from 'widgets/defaultNavigation/DefaultNavigationPresenceProvider'; import styles from 'widgets/defaultNavigation/presenceClassNames.module.css'; @@ -8,8 +11,14 @@ import {renderInEntry} from 'pageflow-scrolled/testHelpers'; import {act} from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; +function LockNavButton() { + const {lockNavExpanded} = useDefaultNavigationState(); + return ; +} + describe('DefaultNavigationPresenceProvider', () => { afterEach(() => jest.restoreAllMocks()); + it('renders wrapper with class setting --widget-margin-top-max by default', () => { const {container} = renderInEntry( @@ -66,41 +75,114 @@ describe('DefaultNavigationPresenceProvider', () => { expect(container.firstChild).toHaveClass(styles.expanded); }); - it('toggles expanded class based on scroll direction', () => { - Object.defineProperty(window, 'scrollY', { - writable: true, - value: 0 + describe('scroll direction', () => { + beforeEach(() => { + jest.useFakeTimers(); + + Object.defineProperty(window, 'scrollY', { + writable: true, + value: 0 + }); + + jest.spyOn(document.body, 'getBoundingClientRect').mockImplementation(() => ({ + top: -window.scrollY, + left: 0, + right: 1024, + bottom: 768 - window.scrollY, + width: 1024, + height: 768 + })); }); - jest.spyOn(document.body, 'getBoundingClientRect').mockImplementation(() => ({ - top: -window.scrollY, - left: 0, - right: 1024, - bottom: 768 - window.scrollY, - width: 1024, - height: 768 - })); + afterEach(() => { + jest.useRealTimers(); + }); - const {container} = renderInEntry( - -
- - ); + it('toggles expanded class based on scroll direction', () => { + const {container} = renderInEntry( + +
+ + ); - expect(container.firstChild).toHaveClass(styles.expanded); + expect(container.firstChild).toHaveClass(styles.expanded); + + act(() => { + window.scrollY = 100; + window.dispatchEvent(new Event('scroll')); + }); + + expect(container.firstChild).not.toHaveClass(styles.expanded); + + act(() => { + window.scrollY = 50; + window.dispatchEvent(new Event('scroll')); + }); + + expect(container.firstChild).toHaveClass(styles.expanded); + }); - act(() => { - window.scrollY = 100; - window.dispatchEvent(new Event('scroll')); + it('stays expanded during scroll lock', () => { + const {container, getByText} = renderInEntry( + + + + ); + + act(() => { + getByText('Lock').click(); + }); + + act(() => { + window.scrollY = 100; + window.dispatchEvent(new Event('scroll')); + }); + + expect(container.firstChild).toHaveClass(styles.expanded); }); - expect(container.firstChild).not.toHaveClass(styles.expanded); + it('resumes collapsing after scroll lock timeout', () => { + const {container, getByText} = renderInEntry( + + + + ); + + act(() => { + getByText('Lock').click(); + }); + + act(() => { + jest.advanceTimersByTime(200); + }); - act(() => { - window.scrollY = 50; - window.dispatchEvent(new Event('scroll')); + act(() => { + window.scrollY = 100; + window.dispatchEvent(new Event('scroll')); + }); + + expect(container.firstChild).not.toHaveClass(styles.expanded); }); - expect(container.firstChild).toHaveClass(styles.expanded); + it('re-expands nav when lockNavExpanded is called while collapsed', () => { + const {container, getByText} = renderInEntry( + + + + ); + + act(() => { + window.scrollY = 100; + window.dispatchEvent(new Event('scroll')); + }); + + expect(container.firstChild).not.toHaveClass(styles.expanded); + + act(() => { + getByText('Lock').click(); + }); + + expect(container.firstChild).toHaveClass(styles.expanded); + }); }); }); diff --git a/entry_types/scrolled/package/src/frontend/Chapter.js b/entry_types/scrolled/package/src/frontend/Chapter.js index b7d9dd9e5e..49996ac040 100644 --- a/entry_types/scrolled/package/src/frontend/Chapter.js +++ b/entry_types/scrolled/package/src/frontend/Chapter.js @@ -3,9 +3,11 @@ import React from 'react'; import {Section} from './Section'; import {EventContextDataProvider} from './useEventContextData'; +import styles from './Chapter.module.css'; + export default function Chapter(props) { return ( -
+
{renderSections(props.sections, props.currentSectionIndex, props.setCurrentSection)} diff --git a/entry_types/scrolled/package/src/frontend/Chapter.module.css b/entry_types/scrolled/package/src/frontend/Chapter.module.css new file mode 100644 index 0000000000..f2a8eee755 --- /dev/null +++ b/entry_types/scrolled/package/src/frontend/Chapter.module.css @@ -0,0 +1,3 @@ +.wrapper { + scroll-margin-top: var(--widget-margin-top); +} diff --git a/entry_types/scrolled/package/src/frontend/Section.module.css b/entry_types/scrolled/package/src/frontend/Section.module.css index 2d5f47bd5b..e01376c086 100644 --- a/entry_types/scrolled/package/src/frontend/Section.module.css +++ b/entry_types/scrolled/package/src/frontend/Section.module.css @@ -7,6 +7,7 @@ .Section { position: relative; + scroll-margin-top: var(--widget-margin-top); --section-max-width: var(--theme-section-max-width); diff --git a/entry_types/scrolled/package/src/widgets/defaultNavigation/DefaultNavigation.js b/entry_types/scrolled/package/src/widgets/defaultNavigation/DefaultNavigation.js index 6f115e35f6..5ffdad1c11 100644 --- a/entry_types/scrolled/package/src/widgets/defaultNavigation/DefaultNavigation.js +++ b/entry_types/scrolled/package/src/widgets/defaultNavigation/DefaultNavigation.js @@ -34,7 +34,7 @@ export function DefaultNavigation({ logo, omitChapterNavigation }) { - const {navExpanded, setNavExpanded} = useDefaultNavigationState(); + const {navExpanded, setNavExpanded, lockNavExpanded} = useDefaultNavigationState(); const [menuOpen, setMenuOpen] = useState(!!configuration.defaultMobileNavVisible); const [readingProgress, setReadingProgress] = useState(0); @@ -78,6 +78,7 @@ export function DefaultNavigation({ }; function handleMenuClick(chapterLinkId) { + lockNavExpanded(); setMenuOpen(false); }; diff --git a/entry_types/scrolled/package/src/widgets/defaultNavigation/DefaultNavigationPresenceProvider.js b/entry_types/scrolled/package/src/widgets/defaultNavigation/DefaultNavigationPresenceProvider.js index 34833d4206..db44c2429f 100644 --- a/entry_types/scrolled/package/src/widgets/defaultNavigation/DefaultNavigationPresenceProvider.js +++ b/entry_types/scrolled/package/src/widgets/defaultNavigation/DefaultNavigationPresenceProvider.js @@ -7,6 +7,7 @@ import { usePhonePlatform } from 'pageflow-scrolled/frontend'; +import {useTimeoutFlag} from './useTimeoutFlag'; import styles from './presenceClassNames.module.css'; const DefaultNavigationContext = createContext({ @@ -21,9 +22,17 @@ export function useDefaultNavigationState() { export function DefaultNavigationPresenceProvider({configuration, children}) { const [navExpanded, setNavExpanded] = useState(true); const isPhonePlatform = usePhonePlatform(); + const [scrollLockRef, activateScrollLock] = useTimeoutFlag(200); + + const lockNavExpanded = useCallback(() => { + activateScrollLock(); + setNavExpanded(true); + }, [activateScrollLock]); useScrollPosition( ({prevPos, currPos}) => { + if (scrollLockRef.current) return; + const expand = currPos.y > prevPos.y || // Mobile Safari reports positive scroll position // during scroll bounce animation when scrolling @@ -51,8 +60,8 @@ export function DefaultNavigationPresenceProvider({configuration, children}) { ); const contextValue = useMemo( - () => ({navExpanded, setNavExpanded}), - [navExpanded] + () => ({navExpanded, setNavExpanded, lockNavExpanded}), + [navExpanded, lockNavExpanded] ); return ( diff --git a/entry_types/scrolled/package/src/widgets/defaultNavigation/useTimeoutFlag.js b/entry_types/scrolled/package/src/widgets/defaultNavigation/useTimeoutFlag.js new file mode 100644 index 0000000000..975163f0f7 --- /dev/null +++ b/entry_types/scrolled/package/src/widgets/defaultNavigation/useTimeoutFlag.js @@ -0,0 +1,20 @@ +import {useCallback, useEffect, useRef} from 'react'; + +export function useTimeoutFlag(duration) { + const flagRef = useRef(false); + const timeoutRef = useRef(null); + + const activate = useCallback(() => { + flagRef.current = true; + clearTimeout(timeoutRef.current); + timeoutRef.current = setTimeout(() => { + flagRef.current = false; + }, duration); + }, [duration]); + + useEffect(() => { + return () => clearTimeout(timeoutRef.current); + }, []); + + return [flagRef, activate]; +}