Commit 5f0e7b76 authored by Alejandro Celaya's avatar Alejandro Celaya Committed by Alejandro Celaya

Migrate Help sidebar panel to tabs

parent b34060fa
import { Link, LinkButton } from '@hypothesis/frontend-shared'; import { Card, Link, Tab } from '@hypothesis/frontend-shared';
import { ArrowRightIcon, ExternalIcon } from '@hypothesis/frontend-shared'; import { ExternalIcon } from '@hypothesis/frontend-shared';
import type { ComponentChildren as Children } from 'preact'; import classnames from 'classnames';
import { useCallback, useMemo, useState } from 'preact/hooks'; import { useCallback, useId, useMemo, useState } from 'preact/hooks';
import { username } from '../helpers/account-id'; import { username } from '../helpers/account-id';
import { VersionData } from '../helpers/version-data'; import { VersionData } from '../helpers/version-data';
import { withServices } from '../service-context'; import { withServices } from '../service-context';
import type { SessionService } from '../services/session'; import type { SessionService } from '../services/session';
import { useSidebarStore } from '../store'; import { useSidebarStore } from '../store';
import TabHeader from './ShareDialog/TabHeader';
import TabPanel from './ShareDialog/TabPanel';
import SidebarPanel from './SidebarPanel'; import SidebarPanel from './SidebarPanel';
import Tutorial from './Tutorial'; import Tutorial from './Tutorial';
import VersionInfo from './VersionInfo'; import VersionInfo from './VersionInfo';
type HelpPanelNavigationButtonProps = {
children: Children;
onClick: (e: Event) => void;
};
/**
* Navigation link-button to swap between sub-panels in the help panel
*/
function HelpPanelNavigationButton({
children,
onClick,
}: HelpPanelNavigationButtonProps) {
return (
<LinkButton variant="brand" onClick={onClick} underline="hover">
<div className="flex items-center gap-x-1">
{children}
<ArrowRightIcon className="w-em h-em" />
</div>
</LinkButton>
);
}
type HelpPanelTabProps = { type HelpPanelTabProps = {
/** What the tab's link should say. */ /** What the tab's link should say. */
linkText: string; linkText: string;
...@@ -42,7 +22,7 @@ type HelpPanelTabProps = { ...@@ -42,7 +22,7 @@ type HelpPanelTabProps = {
}; };
/** /**
* External link "tabs" inside of the help panel. * External link "tabs" at the bottom of the help panel.
*/ */
function HelpPanelTab({ linkText, url }: HelpPanelTabProps) { function HelpPanelTab({ linkText, url }: HelpPanelTabProps) {
return ( return (
...@@ -64,6 +44,8 @@ type HelpPanelProps = { ...@@ -64,6 +44,8 @@ type HelpPanelProps = {
session: SessionService; session: SessionService;
}; };
type PanelKey = 'tutorial' | 'versionInfo';
/** /**
* A help sidebar panel with two sub-panels: tutorial and version info. * A help sidebar panel with two sub-panels: tutorial and version info.
*/ */
...@@ -74,6 +56,10 @@ function HelpPanel({ session }: HelpPanelProps) { ...@@ -74,6 +56,10 @@ function HelpPanel({ session }: HelpPanelProps) {
const profile = store.profile(); const profile = store.profile();
const displayName = const displayName =
profile.user_info?.display_name ?? username(profile.userid); profile.user_info?.display_name ?? username(profile.userid);
const tutorialTabId = useId();
const tutorialPanelId = useId();
const versionTabId = useId();
const versionPanelId = useId();
// Should this panel be auto-opened at app launch? Note that the actual // Should this panel be auto-opened at app launch? Note that the actual
// auto-open triggering of this panel is owned by the `HypothesisApp` component. // auto-open triggering of this panel is owned by the `HypothesisApp` component.
...@@ -82,12 +68,6 @@ function HelpPanel({ session }: HelpPanelProps) { ...@@ -82,12 +68,6 @@ function HelpPanel({ session }: HelpPanelProps) {
const hasAutoDisplayPreference = const hasAutoDisplayPreference =
!!store.profile().preferences.show_sidebar_tutorial; !!store.profile().preferences.show_sidebar_tutorial;
const subPanelTitles = {
tutorial: 'Getting started',
versionInfo: 'About this version',
};
type PanelKey = keyof typeof subPanelTitles;
// The "Tutorial" (getting started) subpanel is the default panel shown // The "Tutorial" (getting started) subpanel is the default panel shown
const [activeSubPanel, setActiveSubPanel] = useState<PanelKey>('tutorial'); const [activeSubPanel, setActiveSubPanel] = useState<PanelKey>('tutorial');
...@@ -115,11 +95,6 @@ function HelpPanel({ session }: HelpPanelProps) { ...@@ -115,11 +95,6 @@ function HelpPanel({ session }: HelpPanelProps) {
// create-new-ticket form // create-new-ticket form
const supportTicketURL = `https://web.hypothes.is/get-help/?sys_info=${versionData.asEncodedURLString()}`; const supportTicketURL = `https://web.hypothes.is/get-help/?sys_info=${versionData.asEncodedURLString()}`;
const openSubPanel = (e: Event, panelName: PanelKey) => {
e.preventDefault();
setActiveSubPanel(panelName);
};
const onActiveChanged = useCallback( const onActiveChanged = useCallback(
(active: boolean) => { (active: boolean) => {
if (!active && hasAutoDisplayPreference) { if (!active && hasAutoDisplayPreference) {
...@@ -137,41 +112,63 @@ function HelpPanel({ session }: HelpPanelProps) { ...@@ -137,41 +112,63 @@ function HelpPanel({ session }: HelpPanelProps) {
title="Help" title="Help"
panelName="help" panelName="help"
onActiveChanged={onActiveChanged} onActiveChanged={onActiveChanged}
variant="custom"
> >
<div className="space-y-4"> <TabHeader>
<div className="flex items-center"> <Tab
<h3 className="grow text-md font-medium" data-testid="subpanel-title"> id={tutorialTabId}
{subPanelTitles[activeSubPanel]} aria-controls={tutorialPanelId}
</h3> variant="tab"
{activeSubPanel === 'versionInfo' && ( textContent="Help"
<HelpPanelNavigationButton selected={activeSubPanel === 'tutorial'}
onClick={e => openSubPanel(e, 'tutorial')} onClick={() => setActiveSubPanel('tutorial')}
data-testid="tutorial-tab"
> >
Getting started Help
</HelpPanelNavigationButton> </Tab>
)} <Tab
{activeSubPanel === 'tutorial' && ( id={versionTabId}
<HelpPanelNavigationButton aria-controls={versionPanelId}
onClick={e => openSubPanel(e, 'versionInfo')} variant="tab"
textContent="Version"
selected={activeSubPanel === 'versionInfo'}
onClick={() => setActiveSubPanel('versionInfo')}
data-testid="version-info-tab"
>
Version
</Tab>
</TabHeader>
<Card
classes={classnames({
'rounded-tl-none': activeSubPanel === 'tutorial',
})}
>
<div className="border-b">
<TabPanel
id={tutorialPanelId}
aria-labelledby={tutorialTabId}
active={activeSubPanel === 'tutorial'}
title="Getting started"
>
<Tutorial />
</TabPanel>
<TabPanel
id={versionPanelId}
aria-labelledby={versionTabId}
active={activeSubPanel === 'versionInfo'}
title="Version details"
> >
About this version
</HelpPanelNavigationButton>
)}
</div>
<div className="border-y py-4">
{activeSubPanel === 'tutorial' && <Tutorial />}
{activeSubPanel === 'versionInfo' && (
<VersionInfo versionData={versionData} /> <VersionInfo versionData={versionData} />
)} </TabPanel>
</div> </div>
<div className="flex items-center"> <div className="flex items-center p-3">
<HelpPanelTab <HelpPanelTab
linkText="Help topics" linkText="Help topics"
url="https://web.hypothes.is/help/" url="https://web.hypothes.is/help/"
/> />
<HelpPanelTab linkText="New support ticket" url={supportTicketURL} /> <HelpPanelTab linkText="New support ticket" url={supportTicketURL} />
</div> </div>
</div> </Card>
</SidebarPanel> </SidebarPanel>
); );
} }
......
import { Tab } from '@hypothesis/frontend-shared';
import { import {
checkAccessibility, checkAccessibility,
mockImportedComponents, mockImportedComponents,
...@@ -45,45 +46,46 @@ describe('HelpPanel', () => { ...@@ -45,45 +46,46 @@ describe('HelpPanel', () => {
'../store': { useSidebarStore: () => fakeStore }, '../store': { useSidebarStore: () => fakeStore },
'../helpers/version-data': { VersionData: FakeVersionData }, '../helpers/version-data': { VersionData: FakeVersionData },
}); });
$imports.$restore({
// Rendering TabHeader and TabPanel is needed for a11y tests
'./ShareDialog/TabHeader': true,
'./ShareDialog/TabPanel': true,
});
}); });
afterEach(() => { afterEach(() => {
$imports.$restore(); $imports.$restore();
}); });
const getActivePanel = wrapper => wrapper.find('TabPanel[active=true]');
const clickTab = (wrapper, tabId) =>
wrapper.find(`button[data-testid="${tabId}"]`).simulate('click');
context('when viewing tutorial sub-panel', () => { context('when viewing tutorial sub-panel', () => {
it('should show tutorial by default', () => { it('should show tutorial by default', () => {
const wrapper = createComponent(); const wrapper = createComponent();
const subHeader = wrapper.find('[data-testid="subpanel-title"]'); const selectedTab = wrapper
.find(Tab)
.findWhere(tab => tab.prop('selected'));
const activePanel = getActivePanel(wrapper);
assert.include(subHeader.text(), 'Getting started'); assert.include(selectedTab.text(), 'Help');
assert.isTrue(wrapper.find('Tutorial').exists()); assert.isTrue(activePanel.find('Tutorial').exists());
assert.isFalse(wrapper.find('VersionInfo').exists()); assert.isFalse(activePanel.find('VersionInfo').exists());
}); });
it('should show navigation link to versionInfo sub-panel', () => { it('should switch to versionInfo sub-panel when second tab is clicked', async () => {
const wrapper = createComponent(); const wrapper = createComponent();
const button = wrapper.find('HelpPanelNavigationButton'); clickTab(wrapper, 'version-info-tab');
assert.include(button.text(), 'About this version');
});
it('should switch to versionInfo sub-panel when navigation button clicked', async () => { const activePanel = getActivePanel(wrapper);
const wrapper = createComponent();
act(() => {
wrapper
.find('LinkButton')
.getDOMNode()
.dispatchEvent(new Event('click'));
});
wrapper.update();
assert.isTrue(wrapper.find('VersionInfo').exists()); assert.isTrue(activePanel.find('VersionInfo').exists());
assert.equal( assert.equal(
wrapper.find('VersionInfo').prop('versionData'), activePanel.find('VersionInfo').prop('versionData'),
fakeVersionData, fakeVersionData,
); );
assert.isFalse(wrapper.find('Tutorial').exists()); assert.isFalse(activePanel.find('Tutorial').exists());
}); });
}); });
...@@ -137,46 +139,18 @@ describe('HelpPanel', () => { ...@@ -137,46 +139,18 @@ describe('HelpPanel', () => {
]); ]);
}); });
it('should show navigation link back to tutorial sub-panel', () => { it('should switch to tutorial sub-panel when first tab is clicked', () => {
const wrapper = createComponent();
act(() => {
wrapper
.find('LinkButton')
.getDOMNode()
.dispatchEvent(new Event('click'));
});
wrapper.update();
const link = wrapper.find('LinkButton');
assert.isTrue(wrapper.find('VersionInfo').exists());
assert.isFalse(wrapper.find('Tutorial').exists());
assert.include(link.text(), 'Getting started');
});
it('should switch to tutorial sub-panel when link clicked', () => {
const wrapper = createComponent(); const wrapper = createComponent();
// Click to get to VersionInfo sub-panel... // Click to get to VersionInfo sub-panel...
act(() => { clickTab(wrapper, 'version-info-tab');
wrapper
.find('LinkButton')
.getDOMNode()
.dispatchEvent(new Event('click'));
});
wrapper.update();
// Click again to get back to tutorial sub-panel // Click again to get back to tutorial sub-panel
act(() => { clickTab(wrapper, 'tutorial-tab');
wrapper
.find('LinkButton') const activePanel = getActivePanel(wrapper);
.getDOMNode()
.dispatchEvent(new Event('click'));
});
wrapper.update();
assert.isFalse(wrapper.find('VersionInfo').exists()); assert.isFalse(activePanel.find('VersionInfo').exists());
assert.isTrue(wrapper.find('Tutorial').exists()); assert.isTrue(activePanel.find('Tutorial').exists());
}); });
}); });
...@@ -272,7 +246,7 @@ describe('HelpPanel', () => { ...@@ -272,7 +246,7 @@ describe('HelpPanel', () => {
it( it(
'should pass a11y checks', 'should pass a11y checks',
checkAccessibility({ checkAccessibility({
content: () => createComponent(), content: () => <HelpPanel session={fakeSessionService} />,
}), }),
); );
}); });
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment