Unverified Commit 7032092c authored by Lyza Gardner's avatar Lyza Gardner Committed by GitHub

Merge pull request #1988 from hypothesis/annotation-layout

Fix annotation and thread vertical rhythm and layout
parents c5642ed2 96d9ff5e
import { Fragment, createElement } from 'preact'; import { createElement } from 'preact';
import { useState } from 'preact/hooks'; import { useState } from 'preact/hooks';
import propTypes from 'prop-types'; import propTypes from 'prop-types';
...@@ -8,6 +8,8 @@ import Button from './button'; ...@@ -8,6 +8,8 @@ import Button from './button';
import Excerpt from './excerpt'; import Excerpt from './excerpt';
import MarkdownEditor from './markdown-editor'; import MarkdownEditor from './markdown-editor';
import MarkdownView from './markdown-view'; import MarkdownView from './markdown-view';
import TagEditor from './tag-editor';
import TagList from './tag-list';
/** /**
* Display the rendered content of an annotation. * Display the rendered content of an annotation.
...@@ -15,7 +17,9 @@ import MarkdownView from './markdown-view'; ...@@ -15,7 +17,9 @@ import MarkdownView from './markdown-view';
export default function AnnotationBody({ export default function AnnotationBody({
annotation, annotation,
isEditing, isEditing,
onEditTags,
onEditText, onEditText,
tags,
text, text,
}) { }) {
// Should the text content of `Excerpt` be rendered in a collapsed state, // Should the text content of `Excerpt` be rendered in a collapsed state,
...@@ -31,36 +35,37 @@ export default function AnnotationBody({ ...@@ -31,36 +35,37 @@ export default function AnnotationBody({
? 'Show full annotation text' ? 'Show full annotation text'
: 'Show the first few lines only'; : 'Show the first few lines only';
const showExcerpt = !isEditing && text.length > 0;
const showTagList = !isEditing && tags.length > 0;
return ( return (
<Fragment> <section className="annotation-body">
<section className="annotation-body"> {showExcerpt && (
{!isEditing && ( <Excerpt
<Excerpt collapse={isCollapsed}
collapse={isCollapsed} collapsedHeight={400}
collapsedHeight={400} inlineControls={false}
inlineControls={false} onCollapsibleChanged={setIsCollapsible}
onCollapsibleChanged={setIsCollapsible} onToggleCollapsed={setIsCollapsed}
onToggleCollapsed={setIsCollapsed} overflowThreshold={20}
overflowThreshold={20} >
> <MarkdownView
<MarkdownView markdown={text}
markdown={text} textClass={{
textClass={{ 'annotation-body__text': true,
'annotation-body__text': true, 'is-hidden': isHidden(annotation),
'is-hidden': isHidden(annotation), 'has-content': text.length > 0,
'has-content': text.length > 0, }}
}}
/>
</Excerpt>
)}
{isEditing && (
<MarkdownEditor
label="Annotation body"
text={text}
onEditText={onEditText}
/> />
)} </Excerpt>
</section> )}
{isEditing && (
<MarkdownEditor
label="Annotation body"
text={text}
onEditText={onEditText}
/>
)}
{isCollapsible && !isEditing && ( {isCollapsible && !isEditing && (
<div className="annotation-body__collapse-toggle"> <div className="annotation-body__collapse-toggle">
<Button <Button
...@@ -71,7 +76,9 @@ export default function AnnotationBody({ ...@@ -71,7 +76,9 @@ export default function AnnotationBody({
/> />
</div> </div>
)} )}
</Fragment> {showTagList && <TagList annotation={annotation} tags={tags} />}
{isEditing && <TagEditor onEditTags={onEditTags} tagList={tags} />}
</section>
); );
} }
...@@ -86,11 +93,18 @@ AnnotationBody.propTypes = { ...@@ -86,11 +93,18 @@ AnnotationBody.propTypes = {
*/ */
isEditing: propTypes.bool, isEditing: propTypes.bool,
/**
* Callback invoked when the user edits tags.
*/
onEditTags: propTypes.func,
/** /**
* Callback invoked when the user edits the content of the annotation body. * Callback invoked when the user edits the content of the annotation body.
*/ */
onEditText: propTypes.func, onEditText: propTypes.func,
tags: propTypes.array.isRequired,
/** /**
* The markdown annotation body, which is either rendered as HTML (if `isEditing` * The markdown annotation body, which is either rendered as HTML (if `isEditing`
* is false) or displayed in a text area otherwise. * is false) or displayed in a text area otherwise.
......
...@@ -15,8 +15,6 @@ import AnnotationLicense from './annotation-license'; ...@@ -15,8 +15,6 @@ import AnnotationLicense from './annotation-license';
import AnnotationPublishControl from './annotation-publish-control'; import AnnotationPublishControl from './annotation-publish-control';
import AnnotationQuote from './annotation-quote'; import AnnotationQuote from './annotation-quote';
import Button from './button'; import Button from './button';
import TagEditor from './tag-editor';
import TagList from './tag-list';
/** /**
* A single annotation. * A single annotation.
...@@ -52,7 +50,7 @@ function Annotation({ ...@@ -52,7 +50,7 @@ function Annotation({
const toggleAction = threadIsCollapsed ? 'Show replies' : 'Hide replies'; const toggleAction = threadIsCollapsed ? 'Show replies' : 'Hide replies';
const toggleText = `${toggleAction} (${replyCount})`; const toggleText = `${toggleAction} (${replyCount})`;
const shouldShowActions = !isSaving && !isEditing && !isCollapsedReply; const shouldShowActions = !isSaving && !isEditing;
const shouldShowLicense = isEditing && !isPrivate && group.type !== 'private'; const shouldShowLicense = isEditing && !isPrivate && group.type !== 'private';
const shouldShowReplyToggle = replyCount > 0 && !isReply(annotation); const shouldShowReplyToggle = replyCount > 0 && !isReply(annotation);
...@@ -108,42 +106,52 @@ function Annotation({ ...@@ -108,42 +106,52 @@ function Annotation({
showDocumentInfo={showDocumentInfo} showDocumentInfo={showDocumentInfo}
threadIsCollapsed={threadIsCollapsed} threadIsCollapsed={threadIsCollapsed}
/> />
{hasQuote && <AnnotationQuote annotation={annotation} />} {hasQuote && <AnnotationQuote annotation={annotation} />}
<AnnotationBody
annotation={annotation} {!isCollapsedReply && (
isEditing={isEditing} <AnnotationBody
onEditText={onEditText} annotation={annotation}
text={text} isEditing={isEditing}
/> onEditTags={onEditTags}
{isEditing && <TagEditor onEditTags={onEditTags} tagList={tags} />} onEditText={onEditText}
{!isEditing && <TagList annotation={annotation} tags={tags} />} tags={tags}
<footer className="annotation__footer"> text={text}
{isEditing && ( />
<div className="annotation__form-actions"> )}
<AnnotationPublishControl
annotation={annotation} {!isCollapsedReply && (
isDisabled={isEmpty} <footer className="annotation__footer">
onSave={onSave} {isEditing && (
/> <div className="annotation__form-actions">
</div> <AnnotationPublishControl
)} annotation={annotation}
{shouldShowLicense && <AnnotationLicense />} isDisabled={isEmpty}
<div className="annotation__controls"> onSave={onSave}
{shouldShowReplyToggle && ( />
<Button
className="annotation__reply-toggle"
onClick={onToggleReplies}
buttonText={toggleText}
/>
)}
{isSaving && <div className="annotation__actions">Saving...</div>}
{shouldShowActions && (
<div className="annotation__actions">
<AnnotationActionBar annotation={annotation} onReply={onReply} />
</div> </div>
)} )}
</div> {shouldShowLicense && <AnnotationLicense />}
</footer> <div className="annotation__controls">
{shouldShowReplyToggle && (
<Button
className="annotation__reply-toggle"
onClick={onToggleReplies}
buttonText={toggleText}
/>
)}
{isSaving && <div className="annotation__actions">Saving...</div>}
{shouldShowActions && (
<div className="annotation__actions">
<AnnotationActionBar
annotation={annotation}
onReply={onReply}
/>
</div>
)}
</div>
</footer>
)}
</article> </article>
); );
} }
......
...@@ -16,6 +16,8 @@ describe('AnnotationBody', () => { ...@@ -16,6 +16,8 @@ describe('AnnotationBody', () => {
<AnnotationBody <AnnotationBody
annotation={fixtures.defaultAnnotation()} annotation={fixtures.defaultAnnotation()}
isEditing={false} isEditing={false}
onEditTags={() => null}
tags={[]}
text="test comment" text="test comment"
{...props} {...props}
/> />
...@@ -88,10 +90,41 @@ describe('AnnotationBody', () => { ...@@ -88,10 +90,41 @@ describe('AnnotationBody', () => {
assert.equal(buttonProps.title, 'Show the first few lines only'); assert.equal(buttonProps.title, 'Show the first few lines only');
}); });
describe('tag list and editor', () => {
it('renders a list of tags if not editing and annotation has tags', () => {
const wrapper = createBody({ isEditing: false, tags: ['foo', 'bar'] });
assert.isTrue(wrapper.find('TagList').exists());
});
it('does not render a tag list if annotation has no tags', () => {
const wrapper = createBody({ isEditing: false, tags: [] });
assert.isFalse(wrapper.find('TagList').exists());
});
it('renders a tag editor if annotation is being edited', () => {
const wrapper = createBody({ isEditing: true, tags: ['foo', 'bar'] });
assert.isTrue(wrapper.find('TagEditor').exists());
assert.isFalse(wrapper.find('TagList').exists());
});
});
it( it(
'should pass a11y checks', 'should pass a11y checks',
checkAccessibility({ checkAccessibility([
content: () => createBody(), {
}) content: () => createBody(),
},
{
name: 'when annotation has tags (tag list)',
content: () => createBody({ isEditing: false, tags: ['foo', 'bar'] }),
},
{
name: 'when annotation is being edited and has tags',
content: () => createBody({ isEditing: true, tags: ['foo', 'bar'] }),
},
])
); );
}); });
...@@ -8,8 +8,6 @@ import { checkAccessibility } from '../../../test-util/accessibility'; ...@@ -8,8 +8,6 @@ import { checkAccessibility } from '../../../test-util/accessibility';
import mockImportedComponents from '../../../test-util/mock-imported-components'; import mockImportedComponents from '../../../test-util/mock-imported-components';
import { waitFor } from '../../../test-util/wait'; import { waitFor } from '../../../test-util/wait';
// @TODO Note this import as `Annotation` for easier updating later
import Annotation from '../annotation'; import Annotation from '../annotation';
import { $imports } from '../annotation'; import { $imports } from '../annotation';
...@@ -145,38 +143,6 @@ describe('Annotation', () => { ...@@ -145,38 +143,6 @@ describe('Annotation', () => {
}); });
}); });
describe('tags', () => {
it('renders tag editor if `isEditing', () => {
setEditingMode(true);
const wrapper = createComponent();
assert.isTrue(wrapper.find('TagEditor').exists());
assert.isFalse(wrapper.find('TagList').exists());
});
it('updates annotation draft if tags changed', () => {
setEditingMode(true);
const wrapper = createComponent();
wrapper
.find('TagEditor')
.props()
.onEditTags({ tags: ['uno', 'dos'] });
const call = fakeStore.createDraft.getCall(0);
assert.calledOnce(fakeStore.createDraft);
assert.sameMembers(call.args[1].tags, ['uno', 'dos']);
});
it('renders tag list if not `isEditing', () => {
const wrapper = createComponent();
assert.isTrue(wrapper.find('TagList').exists());
assert.isFalse(wrapper.find('TagEditor').exists());
});
});
describe('publish control', () => { describe('publish control', () => {
it('should show the publish control if in edit mode', () => { it('should show the publish control if in edit mode', () => {
setEditingMode(true); setEditingMode(true);
...@@ -461,28 +427,66 @@ describe('Annotation', () => { ...@@ -461,28 +427,66 @@ describe('Annotation', () => {
assert.isFalse(wrapper.find('AnnotationActionBar').exists()); assert.isFalse(wrapper.find('AnnotationActionBar').exists());
}); });
});
it('should not show annotations if the annotation is a collapsed reply', () => { context('annotation thread is collapsed', () => {
fakeMetadata.isReply.returns(true); context('collapsed reply', () => {
const wrapper = createComponent({ threadIsCollapsed: true }); beforeEach(() => {
fakeMetadata.isReply.returns(true);
});
assert.isFalse(wrapper.find('AnnotationActionBar').exists()); it('should not render body or footer', () => {
const wrapper = createComponent({ threadIsCollapsed: true });
assert.isFalse(wrapper.find('AnnotationBody').exists());
assert.isFalse(wrapper.find('footer').exists());
});
it('should not show actions', () => {
const wrapper = createComponent({ threadIsCollapsed: true });
assert.isFalse(wrapper.find('AnnotationActionBar').exists());
});
}); });
it( context('collapsed top-level annotation', () => {
'should pass a11y checks', it('should render body and footer', () => {
checkAccessibility([ fakeMetadata.isReply.returns(false);
{ const wrapper = createComponent({ threadIsCollapsed: true });
content: () => createComponent(),
assert.isTrue(wrapper.find('AnnotationBody').exists());
assert.isTrue(wrapper.find('footer').exists());
});
});
});
it(
'should pass a11y checks',
checkAccessibility([
{
content: () => createComponent(),
},
{
name: 'When editing',
content: () => {
setEditingMode(true);
return createComponent();
}, },
{ },
name: 'When editing', {
content: () => { name: 'when a collapsed top-level thread',
setEditingMode(true); content: () => {
return createComponent(); fakeMetadata.isReply.returns(false);
}, return createComponent({ threadIsCollapsed: true });
}, },
]) },
); {
}); name: 'when a collapsed reply',
content: () => {
fakeMetadata.isReply.returns(true);
return createComponent({ threadIsCollapsed: true });
},
},
])
);
}); });
...@@ -3,12 +3,7 @@ ...@@ -3,12 +3,7 @@
.annotation-body { .annotation-body {
@include var.font-normal; @include var.font-normal;
color: var.$grey-7; color: var.$grey-7;
// Margin between top of ascent of annotation body and margin: 1em 0;
// bottom of ascent of annotation-quote should be ~15px.
margin-top: var.$layout-h-margin - 5px;
margin-bottom: var.$layout-h-margin;
margin-right: 0px;
margin-left: 0px;
} }
.annotation-body__text { .annotation-body__text {
...@@ -38,12 +33,12 @@ ...@@ -38,12 +33,12 @@
} }
.annotation-body__collapse-toggle { .annotation-body__collapse-toggle {
// Negative top margin to bring this up tight under `.annotation-body` margin: 0.5em 0;
margin-top: -(var.$layout-h-margin - 5px);
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
.annotation-body__collapse-toggle-button { .annotation-body__collapse-toggle-button {
padding: 0.5em;
background-color: transparent; background-color: transparent;
} }
} }
...@@ -3,9 +3,6 @@ ...@@ -3,9 +3,6 @@
.annotation-header { .annotation-header {
@include forms.pie-clearfix; @include forms.pie-clearfix;
// Margin between top of x-height of username and
// top of the annotation card should be ~15px
margin-top: -(var.$layout-h-margin) + 10px;
color: var.$grey-6; color: var.$grey-6;
&__row { &__row {
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
@include var.font-small; @include var.font-small;
border-top: 1px solid var.$grey-3; border-top: 1px solid var.$grey-3;
padding-top: 0.5em; padding-top: 0.5em;
margin: 0.5em 0; margin: 1em 0;
&__link { &__link {
display: flex; display: flex;
......
@use '../../variables' as var; @use '../../variables' as var;
.annotation-quote { .annotation-quote {
// Margin between top of ascent of annotation quote and margin: 1em 0;
// bottom of ascent of username should be ~15px
margin-top: var.$layout-h-margin - 5px;
// Margin between bottom of ascent of annotation quote and
// top of ascent of annotation-body should be ~15px
margin-bottom: var.$layout-h-margin - 3px;
} }
.annotation-quote.is-orphan { .annotation-quote.is-orphan {
......
...@@ -66,42 +66,7 @@ ...@@ -66,42 +66,7 @@
&__footer { &__footer {
@include var.font-normal; @include var.font-normal;
color: var.$grey-5; color: var.$grey-5;
margin-top: var.$layout-h-margin; margin-top: 1em;
}
&--reply &__footer {
margin-top: var.$layout-h-margin - 8px;
}
}
// FIXME vertical rhythm here should be refactored
.annotation--reply {
.annotation-header {
// Margin between bottom of ascent of annotation card footer labels
// and top of ascent of username should be ~20px
margin-top: 0px;
}
.annotation-body {
// Margin between top of ascent of annotation body and
// bottom of ascent of username should be ~15px
margin-top: var.$layout-h-margin - 8px;
// Margin between bottom of ascent of annotation body and
// top of annotation footer labels should be ~15px
margin-bottom: var.$layout-h-margin - 3px;
}
}
.annotation--reply.is-collapsed {
margin-bottom: 0;
.annotation-header {
margin: 0;
}
.annotation-body,
.annotation-footer {
display: none;
} }
} }
......
...@@ -3,6 +3,8 @@ ...@@ -3,6 +3,8 @@
@use "../../variables" as var; @use "../../variables" as var;
.tag-editor { .tag-editor {
margin: 0.5em 0;
&__input { &__input {
@include forms.form-input; @include forms.form-input;
width: 100%; width: 100%;
...@@ -11,12 +13,13 @@ ...@@ -11,12 +13,13 @@
&__tags { &__tags {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
margin: 0.5em 0;
} }
&__item { &__item {
display: flex; display: flex;
margin-right: 0.5em; margin: 0.25em 0.5em 0.25em 0;
margin-bottom: 0.5em; line-height: var.$base-line-height;
} }
&__edit { &__edit {
......
@use "../../variables" as var; @use "../../variables" as var;
.tag-list { .tag-list {
margin: 1em 0;
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
&__item { &__item {
display: flex; margin: 0.25em 0.5em 0.25em 0;
margin-right: 0.5em; padding: 0 0.5em;
margin-bottom: 0.5em; line-height: var.$base-line-height;
background: var.$grey-1;
border: 1px solid var.$grey-3;
border-radius: 2px;
} }
&__link, &__link,
&__text { &__text {
text-decoration: none; text-decoration: none;
color: var.$grey-6; color: var.$grey-6;
background: var.$grey-1;
border: 1px solid var.$grey-3;
border-radius: 2px;
padding: 0 0.5em;
} }
&__link { &__link {
......
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
box-shadow: 0px 1px 1px 0px rgba(0, 0, 0, 0.1); box-shadow: 0px 1px 1px 0px rgba(0, 0, 0, 0.1);
border-radius: 2px; border-radius: 2px;
cursor: pointer; cursor: pointer;
padding: var.$layout-h-margin; padding: 1em;
background-color: var.$white; background-color: var.$white;
&:hover { &:hover {
......
...@@ -5,28 +5,35 @@ ...@@ -5,28 +5,35 @@
&--reply { &--reply {
margin-top: 0.5em; margin-top: 0.5em;
padding-top: 0.5em;
} }
// Conserve space for deeper usable nesting
&__children {
margin-left: -0.75em;
}
// Left "channel" of thread
&__collapse { &__collapse {
margin: 0.25em; margin-right: 1em;
margin-top: 0; border-right: 1px dashed var.$grey-3;
// The entire channel is NOT clickable so don't make it look like it is
// (overrides `pointer` cursor applied to entire card)
cursor: auto; cursor: auto;
border-left: 1px dashed var.$grey-3;
// Darken thread line on hover as a visual cue to show related thread items
&:hover { &:hover {
border-left: 1px dashed var.$grey-4; border-right: 1px dashed var.$grey-4;
} }
.is-collapsed & { .is-collapsed & {
border-left: none; border-right: none;
} }
} }
// TODO These styles should be consolidated with other `Button` styles // TODO These styles should be consolidated with other `Button` styles
&__collapse-button { &__collapse-button {
margin-left: -1.25em; margin-right: -1.25em;
padding: 0.25em 0.75em 1em 0.75em; padding: 0.25em 0.75em 0.75em 0.75em;
// Need a non-transparent background so that the dashed border line // Need a non-transparent background so that the dashed border line
// does not show through the button // does not show through the button
background-color: var.$white; background-color: var.$white;
......
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