Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
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';

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 <button onClick={lockNavExpanded}>Lock</button>;
}

describe('DefaultNavigationPresenceProvider', () => {
afterEach(() => jest.restoreAllMocks());

it('renders wrapper with class setting --widget-margin-top-max by default', () => {
const {container} = renderInEntry(
<DefaultNavigationPresenceProvider configuration={{}}>
Expand Down Expand Up @@ -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(
<DefaultNavigationPresenceProvider configuration={{}}>
<div />
</DefaultNavigationPresenceProvider>
);
it('toggles expanded class based on scroll direction', () => {
const {container} = renderInEntry(
<DefaultNavigationPresenceProvider configuration={{}}>
<div />
</DefaultNavigationPresenceProvider>
);

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(
<DefaultNavigationPresenceProvider configuration={{}}>
<LockNavButton />
</DefaultNavigationPresenceProvider>
);

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(
<DefaultNavigationPresenceProvider configuration={{}}>
<LockNavButton />
</DefaultNavigationPresenceProvider>
);

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(
<DefaultNavigationPresenceProvider configuration={{}}>
<LockNavButton />
</DefaultNavigationPresenceProvider>
);

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);
});
});
});
4 changes: 3 additions & 1 deletion entry_types/scrolled/package/src/frontend/Chapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div id={props.chapterSlug}>
<div id={props.chapterSlug} className={styles.wrapper}>
{renderSections(props.sections,
props.currentSectionIndex,
props.setCurrentSection)}
Expand Down
3 changes: 3 additions & 0 deletions entry_types/scrolled/package/src/frontend/Chapter.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.wrapper {
scroll-margin-top: var(--widget-margin-top);
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

.Section {
position: relative;
scroll-margin-top: var(--widget-margin-top);

--section-max-width:
var(--theme-section-max-width);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -78,6 +78,7 @@ export function DefaultNavigation({
};

function handleMenuClick(chapterLinkId) {
lockNavExpanded();
setMenuOpen(false);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
usePhonePlatform
} from 'pageflow-scrolled/frontend';

import {useTimeoutFlag} from './useTimeoutFlag';
import styles from './presenceClassNames.module.css';

const DefaultNavigationContext = createContext({
Expand All @@ -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
Expand Down Expand Up @@ -51,8 +60,8 @@ export function DefaultNavigationPresenceProvider({configuration, children}) {
);

const contextValue = useMemo(
() => ({navExpanded, setNavExpanded}),
[navExpanded]
() => ({navExpanded, setNavExpanded, lockNavExpanded}),
[navExpanded, lockNavExpanded]
);

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -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];
}
Loading