Commit eed9efb0 authored by Lyza Danger Gardner's avatar Lyza Danger Gardner Committed by Lyza Gardner

Implement content banner layout

parent dfb7db12
......@@ -7,8 +7,8 @@
"title": "Document provided by JSTOR"
},
"item": {
"title": "Chapter 2: A chapter",
"containerTitle": "Book Title Here"
"title": "Populations of Thrips tabaci, with Special Reference to Virus Transmission",
"containerTitle": "Journal of Animal Ecology"
},
"links": {
"previousItem": "https://jstor.org/stable/book123.1",
......
import { LabeledButton, Link } from '@hypothesis/frontend-shared';
import classnames from 'classnames';
import {
Link,
CaretLeftIcon,
CaretRightIcon,
} from '@hypothesis/frontend-shared/lib/next';
/**
* @typedef {import('../../types/annotator').ContentInfoConfig} ContentInfoConfig
......@@ -8,40 +14,111 @@ import { LabeledButton, Link } from '@hypothesis/frontend-shared';
* A banner that displays information about the current document and the entity
* that is providing access to it (eg. JSTOR).
*
* Layout columns:
* - Logo
* - Container title (only shown on screens at `2xl` breakpoint and wider)
* - Item title with previous and next links
*
* @param {object} props
* @param {ContentInfoConfig} props.info
* @param {() => void} props.onClose
*/
export default function ContentInfoBanner({ info, onClose }) {
export default function ContentInfoBanner({ info }) {
return (
<div className="flex items-center border-b gap-x-4 px-2 py-1 bg-white text-annotator-lg">
<Link href={info.logo.link} target="_blank" data-testid="logo-link">
<img src={info.logo.logo} alt={info.logo.title} />
</Link>
<div className="grow">
{info.links.previousItem && (
<Link href={info.links.previousItem} target="_blank">
Previous article
<div
className={classnames(
'h-10 bg-white px-4 text-slate-7 text-annotator-base border-b',
'grid items-center',
// Two columns in narrower viewports; three in wider
'grid-cols-[100px_minmax(0,auto)] gap-x-4',
'2xl:grid-cols-[100px_minmax(0,auto)_minmax(0,auto)] 2xl:gap-x-3'
)}
>
<div data-testid="content-logo">
{info.logo && (
<Link href={info.logo.link} target="_blank" data-testid="logo-link">
<img
alt={info.logo.title}
src={info.logo.logo}
data-testid="logo-image"
/>
</Link>
)}
{info.item.title}
{info.item.containerTitle && (
<span>
{' '}
from <i>{info.item.containerTitle}</i>
</span>
</div>
<div
className={classnames(
// Container title (this element) is not shown on narrow screens
'hidden',
'2xl:block 2xl:whitespace-nowrap 2xl:overflow-hidden 2xl:text-ellipsis',
'font-semibold'
)}
data-testid="content-container-info"
title={info.item.containerTitle}
>
{info.item.containerTitle}
</div>
<div
className={classnames(
// Flex layout for item title, next and previous links
'flex justify-center items-center gap-x-2'
)}
data-testid="content-item-info"
>
<div
className={classnames(
// Narrower viewports center this flex content:
// this element is not needed for alignment
'hidden',
// Wider viewports align this flex content to the right:
// This empty element is needed to fill extra space at left
'2xl:block 2xl:grow'
)}
/>
{info.links.previousItem && (
<>
<Link
classes="flex gap-x-1 items-center text-annotator-sm whitespace-nowrap"
title="Open previous item"
href={info.links.previousItem}
underline="always"
target="_blank"
data-testid="content-previous-link"
>
<CaretLeftIcon className="w-em h-em" />
<span>Previous</span>
</Link>
<div className="text-annotator-sm">|</div>
</>
)}
<div
className={classnames(
// This element will shrink and truncate fluidly.
// Overriding min-width `auto` prevents the content from overflowing
// See https://stackoverflow.com/a/66689926/434243.
'min-w-0 whitespace-nowrap overflow-hidden text-ellipsis shrink font-medium'
)}
>
<span title={info.item.title} data-testid="content-item-title">
{info.item.title}
</span>
</div>
{info.links.nextItem && (
<Link href={info.links.nextItem} target="_blank">
Next article
</Link>
<>
<div className="text-annotator-sm">|</div>
<Link
title="Open next item"
classes="flex gap-x-1 items-center text-annotator-sm whitespace-nowrap"
href={info.links.nextItem}
underline="always"
target="_blank"
data-testid="content-next-link"
>
<span>Next</span>
<CaretRightIcon className="w-em h-em" />
</Link>
</>
)}
</div>
<div className="text-annotator-base">
<LabeledButton onClick={onClose} data-testid="close-button">
Close
</LabeledButton>
</div>
</div>
);
}
......@@ -2,43 +2,113 @@ import { mount } from 'enzyme';
import ContentInfoBanner from '../ContentInfoBanner';
const contentInfo = {
logo: {
link: 'https://www.jstor.org',
logo: 'https://www.jstorg.org/logo.svg',
title: 'JSTOR homepage',
},
item: {
title: 'Chapter 2: Some book chapter',
},
links: {
previousItem: 'https://www.jstor.org/stable/book.123.1',
nextItem: 'https://www.jstor.org/stable/book.123.3',
},
};
let contentInfo;
const createComponent = props =>
mount(<ContentInfoBanner info={contentInfo} {...props} />);
describe('ContentInfoBanner', () => {
it('renders banner', () => {
const wrapper = mount(<ContentInfoBanner info={contentInfo} />);
assert.include(wrapper.text(), contentInfo.item.title);
beforeEach(() => {
contentInfo = {
logo: {
link: 'https://www.jstor.org',
logo: 'https://www.jstor.org/logo.svg',
title: 'JSTOR homepage',
},
item: {
title: 'Chapter 2: Some book chapter',
containerTitle: 'Expansive Book',
},
links: {
previousItem: 'https://www.jstor.org/stable/book.123.1',
nextItem: 'https://www.jstor.org/stable/book.123.3',
},
};
});
it('shows linked partner logo', () => {
const wrapper = createComponent();
const logo = wrapper.find('Link[data-testid="logo-link"]');
assert.equal(logo.prop('href'), contentInfo.logo.link);
const logoLink = wrapper.find('Link[data-testid="logo-link"]');
const logoImg = wrapper.find('img[data-testid="logo-image"]');
assert.equal(logoLink.prop('href'), contentInfo.logo.link);
assert.equal(logoImg.prop('src'), 'https://www.jstor.org/logo.svg');
});
it('closes when "Close" button is clicked', () => {
const onClose = sinon.stub();
const wrapper = mount(
<ContentInfoBanner info={contentInfo} onClose={onClose} />
it('shows item title', () => {
const wrapper = createComponent();
// TODO: This should be a link once the item link is available in
// content-info metadata
const title = wrapper.find('span[data-testid="content-item-title"]');
assert.equal(title.text(), 'Chapter 2: Some book chapter');
});
it('provides disclosure of long titles through title attributes', () => {
const wrapper = createComponent();
// Element text could be partially obscured (CSS truncation), so these
// title attributes provide access to the full titles
assert.equal(
wrapper.find('div[data-testid="content-container-info"]').prop('title'),
'Expansive Book'
);
const closeButton = wrapper.find(
'LabeledButton[data-testid="close-button"]'
assert.equal(
wrapper.find('span[data-testid="content-item-title"]').prop('title'),
'Chapter 2: Some book chapter'
);
});
describe('next and previous links', () => {
it('displays next and previous links when available', () => {
const wrapper = createComponent();
const prevLink = wrapper.find(
'Link[data-testid="content-previous-link"]'
);
const nextLink = wrapper.find('Link[data-testid="content-next-link"]');
assert.equal(
prevLink.prop('href'),
'https://www.jstor.org/stable/book.123.1'
);
assert.equal(
nextLink.prop('href'),
'https://www.jstor.org/stable/book.123.3'
);
});
it('does not display previous link if unavailable', () => {
const noLinks = { ...contentInfo };
delete noLinks.links.previousItem;
const wrapper = createComponent({ contentInfo: noLinks });
const prevLink = wrapper.find(
'Link[data-testid="content-previous-link"]'
);
const nextLink = wrapper.find('Link[data-testid="content-next-link"]');
assert.isFalse(prevLink.exists());
assert.isTrue(nextLink.exists());
});
it('does not display next link if unavailable', () => {
const noLinks = { ...contentInfo };
delete noLinks.links.nextItem;
const wrapper = createComponent({ contentInfo: noLinks });
closeButton.prop('onClick')();
const prevLink = wrapper.find(
'Link[data-testid="content-previous-link"]'
);
const nextLink = wrapper.find('Link[data-testid="content-next-link"]');
assert.calledOnce(onClose);
assert.isTrue(prevLink.exists());
assert.isFalse(nextLink.exists());
});
});
});
......@@ -278,10 +278,7 @@ export class PDFIntegration extends TinyEmitter {
render(
<Banners>
{this._bannerState.contentInfo && (
<ContentInfoBanner
info={this._bannerState.contentInfo}
onClose={() => this._updateBannerState({ contentInfo: null })}
/>
<ContentInfoBanner info={this._bannerState.contentInfo} />
)}
{this._bannerState.noTextWarning && <WarningBanner />}
</Banners>,
......
......@@ -249,19 +249,6 @@ describe('annotator/integrations/pdf', () => {
assert.isNotNull(banner);
assert.include(banner.shadowRoot.textContent, contentInfo.item.title);
});
it('closes content info banner when "Close" button is clicked', () => {
pdfIntegration = createPDFIntegration();
pdfIntegration.showContentInfo(contentInfo);
const banner = getBanner();
const closeButton = banner.shadowRoot.querySelector(
'button[data-testid=close-button]'
);
closeButton.click();
assert.isNull(getBanner());
});
});
it('does not show a warning when PDF has selectable text', async () => {
......
......@@ -170,6 +170,8 @@ export default {
sm: '360px',
md: '480px',
lg: '768px',
xl: '1024px',
'2xl': '1220px',
// Narrow mobile screens
'annotator-sm': '240px',
// Wider mobile screens/small tablets
......
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