Commit cef29a96 authored by Alejandro Celaya's avatar Alejandro Celaya Committed by Alejandro Celaya

Make ShareDialog receive the tabs to render

parent 3ea79419
......@@ -4,6 +4,7 @@ import { useEffect, useMemo } from 'preact/hooks';
import { confirm } from '../../shared/prompts';
import type { SidebarSettings } from '../../types/config';
import { serviceConfig } from '../config/service-config';
import { isThirdPartyService } from '../helpers/is-third-party-service';
import { shouldAutoDisplayTutorial } from '../helpers/session';
import { applyTheme } from '../helpers/theme';
import { withServices } from '../service-context';
......@@ -62,6 +63,12 @@ function HypothesisApp({
}
}, [isSidebar, profile, settings, store]);
const isThirdParty = isThirdPartyService(settings);
const exportAnnotations = store.isFeatureEnabled('export_annotations');
const importAnnotations = store.isFeatureEnabled('import_annotations');
const showShareButton =
!isThirdParty || exportAnnotations || importAnnotations;
const login = async () => {
if (serviceConfig(settings)) {
// Let the host page handle the login request
......@@ -155,12 +162,19 @@ function HypothesisApp({
onSignUp={signUp}
onLogout={logout}
isSidebar={isSidebar}
showShareButton={showShareButton}
/>
)}
<div className="container">
<ToastMessages />
<HelpPanel />
<ShareDialog />
{showShareButton && (
<ShareDialog
shareTab={!isThirdParty}
exportTab={exportAnnotations}
importTab={importAnnotations}
/>
)}
{route && (
<main>
......
......@@ -9,23 +9,34 @@ import ShareAnnotations from './ShareAnnotations';
import TabHeader from './TabHeader';
import TabPanel from './TabPanel';
export type ShareDialogProps = {
/** If true, the share tab will be rendered. Defaults to false */
shareTab?: boolean;
/** If true, the export tab will be rendered. Defaults to false */
exportTab?: boolean;
/** If true, the import tab will be rendered. Defaults to false */
importTab?: boolean;
};
/**
* Panel with sharing options.
* - If export feature flag is enabled, will show a tabbed interface with
* share and export tabs
* - If provided tabs include `export` or `import`, will show a tabbed interface
* - Else, shows a single "Share annotations" interface
*/
export default function ShareDialog() {
export default function ShareDialog({
shareTab,
exportTab,
importTab,
}: ShareDialogProps) {
const store = useSidebarStore();
const focusedGroup = store.focusedGroup();
const groupName = (focusedGroup && focusedGroup.name) || '...';
const panelTitle = `Share Annotations in ${groupName}`;
const showExportTab = store.isFeatureEnabled('export_annotations');
const showImportTab = store.isFeatureEnabled('import_annotations');
const tabbedDialog = showExportTab || showImportTab;
const tabbedDialog = exportTab || importTab;
const [selectedTab, setSelectedTab] = useState<'share' | 'export' | 'import'>(
'share',
// Determine initial selected tab, based on the first tab that will be displayed
shareTab ? 'share' : exportTab ? 'export' : 'import',
);
return (
......@@ -37,17 +48,19 @@ export default function ShareDialog() {
{tabbedDialog && (
<>
<TabHeader>
<Tab
id="share-panel-tab"
aria-controls="share-panel"
variant="tab"
selected={selectedTab === 'share'}
onClick={() => setSelectedTab('share')}
textContent={'Share'}
>
Share
</Tab>
{showExportTab && (
{shareTab && (
<Tab
id="share-panel-tab"
aria-controls="share-panel"
variant="tab"
selected={selectedTab === 'share'}
onClick={() => setSelectedTab('share')}
textContent="Share"
>
Share
</Tab>
)}
{exportTab && (
<Tab
id="export-panel-tab"
aria-controls="export-panel"
......@@ -59,7 +72,7 @@ export default function ShareDialog() {
Export
</Tab>
)}
{showImportTab && (
{importTab && (
<Tab
id="import-panel-tab"
aria-controls="import-panel"
......@@ -85,7 +98,7 @@ export default function ShareDialog() {
id="export-panel"
active={selectedTab === 'export'}
aria-labelledby="export-panel-tab"
title={`Export from ${focusedGroup?.name ?? '...'}`}
title={`Export from ${groupName}`}
>
<ExportAnnotations />
</TabPanel>
......@@ -93,14 +106,14 @@ export default function ShareDialog() {
id="import-panel"
active={selectedTab === 'import'}
aria-labelledby="import-panel-tab"
title={`Import into ${focusedGroup?.name ?? '...'}`}
title={`Import into ${groupName}`}
>
<ImportAnnotations />
</TabPanel>
</Card>
</>
)}
{!tabbedDialog && <ShareAnnotations />}
{shareTab && !tabbedDialog && <ShareAnnotations />}
</SidebarPanel>
);
}
......@@ -14,12 +14,12 @@ describe('ShareDialog', () => {
id: 'testprivate',
};
const createComponent = () => mount(<ShareDialog />);
const createComponent = (props = {}) =>
mount(<ShareDialog shareTab exportTab importTab {...props} />);
beforeEach(() => {
fakeStore = {
focusedGroup: sinon.stub().returns(fakePrivateGroup),
isFeatureEnabled: sinon.stub().returns(false),
};
$imports.$mock(mockImportedComponents());
......@@ -58,10 +58,6 @@ describe('ShareDialog', () => {
});
});
function enableFeature(feature) {
fakeStore.isFeatureEnabled.withArgs(feature).returns(true);
}
function selectTab(wrapper, name) {
wrapper
.find(`Tab[aria-controls="${name}-panel"]`)
......@@ -77,17 +73,20 @@ describe('ShareDialog', () => {
return wrapper.find('TabPanel').filter({ active: true });
}
it('does not render a tabbed dialog if import/export feature flags are not enabled', () => {
const wrapper = createComponent();
it('does not render a tabbed dialog if only share tab is provided', () => {
const wrapper = createComponent({ exportTab: false, importTab: false });
assert.isFalse(wrapper.find('TabHeader').exists());
});
['export_annotations', 'import_annotations'].forEach(feature => {
it(`renders a tabbed dialog when ${feature} feature is enabled`, () => {
enableFeature('export_annotations');
const wrapper = createComponent();
[
[{ shareTab: false }],
[{ importTab: false }],
[{ exportTab: false }],
[{}],
].forEach(props => {
it(`renders a tabbed dialog when more than one tab is provided`, () => {
const wrapper = createComponent(props);
assert.isTrue(wrapper.find('TabHeader').exists());
assert.isTrue(
......@@ -98,9 +97,6 @@ describe('ShareDialog', () => {
});
it('shows correct tab panel when each tab is clicked', () => {
enableFeature('export_annotations');
enableFeature('import_annotations');
const wrapper = createComponent();
selectTab(wrapper, 'export');
......@@ -119,24 +115,30 @@ describe('ShareDialog', () => {
assert.equal(activeTabPanel(wrapper).props().id, 'share-panel');
});
describe('a11y', () => {
beforeEach(() => {
enableFeature('export_annotations');
enableFeature('import_annotations');
it('renders empty if no tabs should be displayed', () => {
const wrapper = createComponent({
shareTab: false,
exportTab: false,
importTab: false,
});
assert.isFalse(wrapper.exists('TabHeader'));
assert.isFalse(wrapper.exists('ShareAnnotations'));
});
describe('a11y', () => {
it(
'should pass a11y checks',
checkAccessibility({
content: () =>
// ShareDialog renders a Fragment as its top-level component when
// `export_annotations` feature is enabled.
// it has import and/or export tabs.
// Wrapping it in a `div` ensures `checkAccessibility` internal logic
// does not discard all the Fragment children but the first one.
// See https://github.com/hypothesis/client/issues/5671
mount(
<div>
<ShareDialog />
<ShareDialog shareTab exportTab importTab />
</div>,
),
}),
......
......@@ -8,7 +8,6 @@ import classnames from 'classnames';
import type { SidebarSettings } from '../../types/config';
import { serviceConfig } from '../config/service-config';
import { isThirdPartyService } from '../helpers/is-third-party-service';
import { applyTheme } from '../helpers/theme';
import { withServices } from '../service-context';
import type { FrameSyncService } from '../services/frame-sync';
......@@ -21,6 +20,8 @@ import StreamSearchInput from './StreamSearchInput';
import UserMenu from './UserMenu';
export type TopBarProps = {
showShareButton: boolean;
/** Flag indicating whether the app is in a sidebar context */
isSidebar: boolean;
......@@ -49,8 +50,8 @@ function TopBar({
onSignUp,
frameSync,
settings,
showShareButton,
}: TopBarProps) {
const showSharePageButton = !isThirdPartyService(settings);
const loginLinkStyle = applyTheme(['accentColor'], settings);
const store = useSidebarStore();
......@@ -106,7 +107,7 @@ function TopBar({
onSearch={store.setFilterQuery}
/>
<SortMenu />
{showSharePageButton && (
{showShareButton && (
<IconButton
icon={ShareIcon}
expanded={isAnnotationsPanelOpen}
......
......@@ -14,6 +14,7 @@ describe('HypothesisApp', () => {
let fakeShouldAutoDisplayTutorial = null;
let fakeSettings = null;
let fakeToastMessenger = null;
let fakeIsThirdPartyService;
const createComponent = (props = {}) => {
return mount(
......@@ -53,6 +54,7 @@ describe('HypothesisApp', () => {
route: sinon.stub().returns('sidebar'),
getLink: sinon.stub(),
isFeatureEnabled: sinon.stub().returns(true),
};
fakeAuth = {};
......@@ -76,6 +78,8 @@ describe('HypothesisApp', () => {
fakeConfirm = sinon.stub().resolves(false);
fakeIsThirdPartyService = sinon.stub().returns(false);
$imports.$mock(mockImportedComponents());
$imports.$mock({
'../config/service-config': { serviceConfig: fakeServiceConfig },
......@@ -85,6 +89,9 @@ describe('HypothesisApp', () => {
},
'../helpers/theme': { applyTheme: fakeApplyTheme },
'../../shared/prompts': { confirm: fakeConfirm },
'../helpers/is-third-party-service': {
isThirdPartyService: fakeIsThirdPartyService,
},
});
});
......@@ -400,4 +407,21 @@ describe('HypothesisApp', () => {
assert.isFalse(container.hasClass('theme-clean'));
});
});
context('when there are no sharing tabs to show', () => {
beforeEach(() => {
fakeStore.isFeatureEnabled.returns(false);
fakeIsThirdPartyService.returns(true);
});
it('does not render ShareDialog', () => {
const wrapper = createComponent();
assert.isFalse(wrapper.exists('ShareDialog'));
});
it('disables share button in TopBar', () => {
const wrapper = createComponent();
assert.isFalse(wrapper.find('TopBar').prop('showShareButton'));
});
});
});
......@@ -9,12 +9,9 @@ describe('TopBar', () => {
let fakeFrameSync;
let fakeStore;
let fakeStreamer;
let fakeIsThirdPartyService;
let fakeServiceConfig;
beforeEach(() => {
fakeIsThirdPartyService = sinon.stub().returns(false);
fakeStore = {
filterQuery: sinon.stub().returns(null),
hasFetchedProfile: sinon.stub().returns(false),
......@@ -38,9 +35,6 @@ describe('TopBar', () => {
$imports.$mock({
'../store': { useSidebarStore: () => fakeStore },
'../helpers/is-third-party-service': {
isThirdPartyService: fakeIsThirdPartyService,
},
'../config/service-config': { serviceConfig: fakeServiceConfig },
});
});
......@@ -63,6 +57,7 @@ describe('TopBar', () => {
isSidebar={true}
settings={fakeSettings}
streamer={fakeStreamer}
showShareButton
{...props}
/>,
);
......@@ -149,13 +144,6 @@ describe('TopBar', () => {
});
});
it("checks whether we're using a third-party service", () => {
createTopBar();
assert.called(fakeIsThirdPartyService);
assert.alwaysCalledWithExactly(fakeIsThirdPartyService, fakeSettings);
});
context('when using a first-party service', () => {
it('shows the share annotations button', () => {
const wrapper = createTopBar();
......@@ -163,13 +151,9 @@ describe('TopBar', () => {
});
});
context('when using a third-party service', () => {
beforeEach(() => {
fakeIsThirdPartyService.returns(true);
});
context('when showShareButton is false', () => {
it("doesn't show the share annotations button", () => {
const wrapper = createTopBar();
const wrapper = createTopBar({ showShareButton: false });
assert.isFalse(
wrapper.exists('[title="Share annotations on this page"]'),
);
......
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