Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

report(flow): fix report anchors #13233

Merged
merged 2 commits into from
Nov 1, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
21 changes: 10 additions & 11 deletions flow-report/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,24 @@
*/

import {FunctionComponent} from 'preact';
import {useEffect, useRef, useState} from 'preact/hooks';
import {useLayoutEffect, useRef, useState} from 'preact/hooks';

import {ReportRendererProvider} from './wrappers/report-renderer';
import {Sidebar} from './sidebar/sidebar';
import {Summary} from './summary/summary';
import {classNames, FlowResultContext, useCurrentLhr, useHashParam} from './util';
import {classNames, FlowResultContext, useHashState} from './util';
import {Report} from './wrappers/report';
import {Topbar} from './topbar';
import {Header} from './header';
import {I18nProvider} from './i18n/i18n';

const Content: FunctionComponent = () => {
const currentLhr = useCurrentLhr();
const anchor = useHashParam('anchor');
const hashState = useHashState();
const ref = useRef<HTMLDivElement>(null);

useEffect(() => {
if (anchor) {
const el = document.getElementById(anchor);
useLayoutEffect(() => {
if (hashState && hashState.anchor) {
const el = document.getElementById(hashState.anchor);
if (el) {
el.scrollIntoView({behavior: 'smooth'});
return;
Expand All @@ -32,15 +31,15 @@ const Content: FunctionComponent = () => {

// Scroll to top no anchor is found.
if (ref.current) ref.current.scrollTop = 0;
}, [anchor, currentLhr]);
}, [hashState]);

return (
<div ref={ref} className="Content">
{
currentLhr ?
hashState ?
<>
<Header currentLhr={currentLhr}/>
<Report currentLhr={currentLhr}/>
<Header hashState={hashState}/>
<Report hashState={hashState}/>
</> :
<Summary/>
}
Expand Down
6 changes: 3 additions & 3 deletions flow-report/src/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@ const HeaderThumbnail: FunctionComponent<{
);
};

export const Header: FunctionComponent<{currentLhr: LH.FlowResult.LhrRef}> =
({currentLhr}) => {
export const Header: FunctionComponent<{hashState: LH.FlowResult.HashState}> =
({hashState}) => {
const flowResult = useFlowResult();
const {index} = currentLhr;
const {index} = hashState;

const step = flowResult.steps[index];
const prevStep = flowResult.steps[index - 1];
Expand Down
6 changes: 3 additions & 3 deletions flow-report/src/sidebar/flow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import {FunctionComponent} from 'preact';

import {FlowSegment} from '../common';
import {classNames, useCurrentLhr, useFlowResult} from '../util';
import {classNames, useHashState, useFlowResult} from '../util';
import {Separator} from '../common';

const SidebarFlowStep: FunctionComponent<{
Expand Down Expand Up @@ -41,7 +41,7 @@ const SidebarFlowSeparator: FunctionComponent = () => {

export const SidebarFlow: FunctionComponent = () => {
const flowResult = useFlowResult();
const currentLhr = useCurrentLhr();
const hashState = useHashState();

return (
<div className="SidebarFlow">
Expand All @@ -61,7 +61,7 @@ export const SidebarFlow: FunctionComponent = () => {
mode={lhr.gatherMode}
href={url.href}
label={name}
isCurrent={index === (currentLhr && currentLhr.index)}
isCurrent={index === (hashState && hashState.index)}
/>
</>;
})
Expand Down
6 changes: 3 additions & 3 deletions flow-report/src/sidebar/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,19 @@ import {FunctionComponent} from 'preact';
import {Separator} from '../common';
import {useI18n, useLocalizedStrings} from '../i18n/i18n';
import {CpuIcon, EnvIcon, SummaryIcon} from '../icons';
import {classNames, useCurrentLhr, useFlowResult} from '../util';
import {classNames, useHashState, useFlowResult} from '../util';
import {SidebarFlow} from './flow';

export const SidebarSummary: FunctionComponent = () => {
const currentLhr = useCurrentLhr();
const hashState = useHashState();
const strings = useLocalizedStrings();

const url = new URL(location.href);
url.hash = '#';
return (
<a
href={url.href}
className={classNames('SidebarSummary', {'Sidebar--current': currentLhr === null})}
className={classNames('SidebarSummary', {'Sidebar--current': hashState === null})}
data-testid="SidebarSummary"
>
<div className="SidebarSummary__icon">
Expand Down
24 changes: 12 additions & 12 deletions flow-report/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,27 +79,27 @@ export function useFlowResult(): LH.FlowResult {
return flowResult;
}

export function useHashParam(param: string) {
const [paramValue, setParamValue] = useState(getHashParam(param));
export function useHashParams(...params: string[]) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should have been designed this way initially, so changes to multiple hash params are handled in the same render.

const [paramValues, setParamValues] = useState(params.map(getHashParam));

// Use two-way-binding on the URL hash.
// Triggers a re-render if the param changes.
// Triggers a re-render if any param changes.
useEffect(() => {
function hashListener() {
const newIndexString = getHashParam(param);
if (newIndexString === paramValue) return;
setParamValue(newIndexString);
const newParamValues = params.map(getHashParam);
if (newParamValues.every((value, i) => value === paramValues[i])) return;
setParamValues(newParamValues);
}
window.addEventListener('hashchange', hashListener);
return () => window.removeEventListener('hashchange', hashListener);
}, [paramValue]);
}, [paramValues]);

return paramValue;
return paramValues;
}

export function useCurrentLhr(): LH.FlowResult.LhrRef|null {
export function useHashState(): LH.FlowResult.HashState|null {
const flowResult = useFlowResult();
const indexString = useHashParam('index');
const [indexString, anchor] = useHashParams('index', 'anchor');

// Memoize result so a new object is not created on every call.
return useMemo(() => {
Expand All @@ -117,6 +117,6 @@ export function useCurrentLhr(): LH.FlowResult.LhrRef|null {
return null;
}

return {value: step.lhr, index};
}, [indexString, flowResult]);
return {currentLhr: step.lhr, index, anchor};
}, [indexString, flowResult, anchor]);
}
15 changes: 10 additions & 5 deletions flow-report/src/wrappers/report.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,27 +28,32 @@ export function convertChildAnchors(element: HTMLElement, index: number) {

const nodeId = link.hash.substr(1);
link.hash = `#index=${index}&anchor=${nodeId}`;
link.onclick = e => {
Copy link
Member

@paulirish paulirish Oct 20, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i would expect the new-ish report click handler to end up doing the same thing..

gaugeWrapperEl.addEventListener('click', e => {
if (!gaugeWrapperEl.matches('[href^="#"]')) return;
const selector = gaugeWrapperEl.getAttribute('href');
const reportRoot = gaugeWrapperEl.closest('.lh-vars');
if (!selector || !reportRoot) return;
const destEl = this._dom.find(selector, reportRoot);
e.preventDefault();
destEl.scrollIntoView();
});

but it doesnt?

Copy link
Member Author

@adamraine adamraine Oct 20, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the flow report, the href of the link is now like #index=0&anchor=seo which isn't a selector anymore.

This would also work if we didn't convert all the link hrefs

e.preventDefault();
const el = document.getElementById(nodeId);
if (el) el.scrollIntoView();
};
}
}

export const Report: FunctionComponent<{currentLhr: LH.FlowResult.LhrRef}> =
({currentLhr}) => {
export const Report: FunctionComponent<{hashState: LH.FlowResult.HashState}> =
({hashState}) => {
const {dom, reportRenderer} = useReportRenderer();
const ref = useRef<HTMLDivElement>(null);

useLayoutEffect(() => {
if (ref.current) {
dom.clearComponentCache();
reportRenderer.renderReport(currentLhr.value, ref.current);
convertChildAnchors(ref.current, currentLhr.index);
reportRenderer.renderReport(hashState.currentLhr, ref.current);
convertChildAnchors(ref.current, hashState.index);
const topbar = ref.current.querySelector('.lh-topbar');
if (topbar) topbar.remove();
}

return () => {
if (ref.current) ref.current.textContent = '';
};
}, [reportRenderer, currentLhr]);
}, [reportRenderer, hashState]);

return (
<div ref={ref} className="lh-root" data-testid="Report"/>
Expand Down
12 changes: 6 additions & 6 deletions flow-report/test/header-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ beforeEach(() => {
});

it('renders all sections for a middle step', () => {
const currentLhr = {index: 1} as any;
const root = render(<Header currentLhr={currentLhr}/>, {wrapper});
const hashState = {index: 1} as any;
const root = render(<Header hashState={hashState}/>, {wrapper});

expect(root.baseElement.querySelector('.Header__prev-thumbnail')).toBeTruthy();
expect(root.baseElement.querySelector('.Header__prev-title')).toBeTruthy();
Expand All @@ -35,8 +35,8 @@ it('renders all sections for a middle step', () => {
});

it('renders only next section for first step', () => {
const currentLhr = {index: 0} as any;
const root = render(<Header currentLhr={currentLhr}/>, {wrapper});
const hashState = {index: 0} as any;
const root = render(<Header hashState={hashState}/>, {wrapper});

expect(root.baseElement.querySelector('.Header__prev-thumbnail')).toBeFalsy();
expect(root.baseElement.querySelector('.Header__prev-title')).toBeFalsy();
Expand All @@ -45,8 +45,8 @@ it('renders only next section for first step', () => {
});

it('renders only previous section for last step', () => {
const currentLhr = {index: 3} as any;
const root = render(<Header currentLhr={currentLhr}/>, {wrapper});
const hashState = {index: 3} as any;
const root = render(<Header hashState={hashState}/>, {wrapper});

expect(root.baseElement.querySelector('.Header__prev-thumbnail')).toBeTruthy();
expect(root.baseElement.querySelector('.Header__prev-title')).toBeTruthy();
Expand Down
43 changes: 32 additions & 11 deletions flow-report/test/util-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {renderHook} from '@testing-library/preact-hooks';
import {FunctionComponent} from 'preact';
import {act} from 'preact/test-utils';

import {FlowResultContext, useCurrentLhr} from '../src/util';
import {FlowResultContext, useHashState} from '../src/util';
import {flowResult} from './sample-flow';

let wrapper: FunctionComponent;
Expand All @@ -21,54 +21,75 @@ beforeEach(() => {
);
});

describe('useCurrentLhr', () => {
describe('useHashState', () => {
it('gets current lhr index from url hash', () => {
global.location.hash = '#index=1';
const {result} = renderHook(() => useCurrentLhr(), {wrapper});
const {result} = renderHook(() => useHashState(), {wrapper});
expect(console.warn).not.toHaveBeenCalled();
expect(result.current).toEqual({
index: 1,
value: flowResult.steps[1].lhr,
currentLhr: flowResult.steps[1].lhr,
anchor: null,
});
});

it('gets anchor id from url hash', () => {
global.location.hash = '#index=1&anchor=seo';
const {result} = renderHook(() => useHashState(), {wrapper});
expect(console.warn).not.toHaveBeenCalled();
expect(result.current).toEqual({
index: 1,
currentLhr: flowResult.steps[1].lhr,
anchor: 'seo',
});
});

it('changes on navigation', async () => {
global.location.hash = '#index=1';
const render = renderHook(() => useCurrentLhr(), {wrapper});
const render = renderHook(() => useHashState(), {wrapper});

expect(render.result.current).toEqual({
index: 1,
value: flowResult.steps[1].lhr,
currentLhr: flowResult.steps[1].lhr,
anchor: null,
});

await act(() => {
global.location.hash = '#index=2';
global.location.hash = '#index=2&anchor=seo';
});
await render.waitForNextUpdate();

expect(console.warn).not.toHaveBeenCalled();
expect(render.result.current).toEqual({
index: 2,
value: flowResult.steps[2].lhr,
currentLhr: flowResult.steps[2].lhr,
anchor: 'seo',
});
});

it('return null if lhr index is unset', () => {
const {result} = renderHook(() => useCurrentLhr(), {wrapper});
const {result} = renderHook(() => useHashState(), {wrapper});
expect(console.warn).not.toHaveBeenCalled();
expect(result.current).toBeNull();
});

it('return null if lhr index is out of bounds', () => {
global.location.hash = '#index=5';
const {result} = renderHook(() => useCurrentLhr(), {wrapper});
const {result} = renderHook(() => useHashState(), {wrapper});
expect(console.warn).toHaveBeenCalled();
expect(result.current).toBeNull();
});

it('returns null for invalid value', () => {
global.location.hash = '#index=OHNO';
const {result} = renderHook(() => useCurrentLhr(), {wrapper});
const {result} = renderHook(() => useHashState(), {wrapper});
expect(console.warn).toHaveBeenCalled();
expect(result.current).toBeNull();
});

it('returns null for invalid value with valid anchor', () => {
global.location.hash = '#index=OHNO&anchor=seo';
const {result} = renderHook(() => useHashState(), {wrapper});
expect(console.warn).toHaveBeenCalled();
expect(result.current).toBeNull();
});
Expand Down
5 changes: 3 additions & 2 deletions types/lhr/flow.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ declare module FlowResult {
name: string;
}

interface LhrRef {
value: Result;
interface HashState {
currentLhr: Result;
index: number;
anchor: string|null;
}
}

Expand Down