Commit 88a79cb7 authored by Lyza Danger Gardner's avatar Lyza Danger Gardner

Push some logic from `annotation` -> `AnnotationBody`

Move some excerpt control from `Annotation` into `AnnotationBody` for
simplification.
parent e77d9fa9
import { createElement } from 'preact'; import { createElement } from 'preact';
import { useState } from 'preact/hooks';
import propTypes from 'prop-types'; import propTypes from 'prop-types';
import { isHidden } from '../util/annotation-metadata';
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';
...@@ -9,82 +13,76 @@ import MarkdownView from './markdown-view'; ...@@ -9,82 +13,76 @@ import MarkdownView from './markdown-view';
* Display the rendered content of an annotation. * Display the rendered content of an annotation.
*/ */
export default function AnnotationBody({ export default function AnnotationBody({
collapse, annotation,
isEditing, isEditing,
isHiddenByModerator,
onCollapsibleChanged,
onEditText, onEditText,
onToggleCollapsed,
text, text,
}) { }) {
// Should the text content of `Excerpt` be rendered in a collapsed state,
// assuming it is collapsible (exceeds allotted collapsed space)?
const [isCollapsed, setIsCollapsed] = useState(true);
// Does the text content of `Excerpt` take up enough vertical space that
// collapsing/expanding is relevant?
const [isCollapsible, setIsCollapsible] = useState(false);
const toggleText = isCollapsed ? 'More' : 'Less';
const toggleTitle = isCollapsed
? 'Show full annotation text'
: 'Show the first few lines only';
return ( return (
<section className="annotation-body"> <section className="annotation-body">
{!isEditing && ( {!isEditing && (
<Excerpt <Excerpt
collapse={collapse} collapse={isCollapsed}
collapsedHeight={400} collapsedHeight={400}
inlineControls={false} inlineControls={false}
onCollapsibleChanged={onCollapsibleChanged} onCollapsibleChanged={setIsCollapsible}
onToggleCollapsed={collapsed => onToggleCollapsed({ collapsed })} onToggleCollapsed={setIsCollapsed}
overflowThreshold={20} overflowThreshold={20}
> >
<MarkdownView <MarkdownView
markdown={text} markdown={text}
textClass={{ textClass={{
'annotation-body__text': true, 'annotation-body__text': true,
'is-hidden': isHiddenByModerator, 'is-hidden': isHidden(annotation),
'has-content': text.length > 0, 'has-content': text.length > 0,
}} }}
/> />
</Excerpt> </Excerpt>
)} )}
{isEditing && <MarkdownEditor text={text} onEditText={onEditText} />} {isEditing && <MarkdownEditor text={text} onEditText={onEditText} />}
{isCollapsible && !isEditing && (
<div className="annotation-body__collapse-toggle">
<Button
className="annotation-body__collapse-toggle-button"
onClick={() => setIsCollapsed(!isCollapsed)}
buttonText={toggleText}
title={toggleTitle}
/>
</div>
)}
</section> </section>
); );
} }
AnnotationBody.propTypes = { AnnotationBody.propTypes = {
/** /**
* Whether to limit the height of the annotation body. * The annotation in question
*
* If this is true and the intrinsic height exceeds a fixed threshold, the
* body is truncated. See `onCollapsibleChanged` and `onToggleCollapsed`.
*/ */
collapse: propTypes.bool, annotation: propTypes.object.isRequired,
/** /**
* Whether to display the body in edit mode (if true) or view mode. * Whether to display the body in edit mode (if true) or view mode.
*/ */
isEditing: propTypes.bool, isEditing: propTypes.bool,
/**
* `true` if the contents of this annotation body have been redacted by
* a moderator.
*
* For redacted annotations, the text is shown struck-through (if available)
* or replaced by a placeholder indicating redacted content (if `text` is
* empty).
*/
isHiddenByModerator: propTypes.bool,
/**
* Callback invoked when the height of the rendered annotation body increases
* above or falls below the threshold at which the `collapse` prop will affect
* it.
*/
onCollapsibleChanged: 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,
/**
* Callback invoked when the user clicks a shaded area at the bottom of a
* truncated body to indicate that they want to see the rest of the content.
*/
onToggleCollapsed: propTypes.func,
/** /**
* 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.
......
...@@ -71,14 +71,6 @@ function AnnotationController( ...@@ -71,14 +71,6 @@ function AnnotationController(
* methods goes here. * methods goes here.
*/ */
this.$onInit = () => { this.$onInit = () => {
/** Determines whether controls to expand/collapse the annotation body
* are displayed adjacent to the tags field.
*/
self.canCollapseBody = false;
/** Determines whether the annotation body should be collapsed. */
self.collapseBody = true;
/** True if the annotation is currently being saved. */ /** True if the annotation is currently being saved. */
self.isSaving = false; self.isSaving = false;
...@@ -242,11 +234,6 @@ function AnnotationController( ...@@ -242,11 +234,6 @@ function AnnotationController(
} }
}; };
this.toggleCollapseBody = function(event) {
event.stopPropagation();
self.collapseBody = !self.collapseBody;
};
/** /**
* @ngdoc method * @ngdoc method
* @name annotation.AnnotationController#reply * @name annotation.AnnotationController#reply
...@@ -364,25 +351,10 @@ function AnnotationController( ...@@ -364,25 +351,10 @@ function AnnotationController(
return store.hasPendingDeletion(self.annotation.id); return store.hasPendingDeletion(self.annotation.id);
}; };
this.isHiddenByModerator = function() {
return self.annotation.hidden;
};
this.isReply = function() { this.isReply = function() {
return isReply(self.annotation); return isReply(self.annotation);
}; };
/**
* Sets whether or not the controls for expanding/collapsing the body of
* lengthy annotations should be shown.
*/
this.setBodyCollapsible = function(canCollapse) {
if (canCollapse === self.canCollapseBody) {
return;
}
self.canCollapseBody = canCollapse;
};
this.setText = function(text) { this.setText = function(text) {
store.createDraft(self.annotation, { store.createDraft(self.annotation, {
isPrivate: self.state().isPrivate, isPrivate: self.state().isPrivate,
......
...@@ -77,7 +77,7 @@ function Excerpt({ ...@@ -77,7 +77,7 @@ function Excerpt({
// prettier-ignore // prettier-ignore
const isCollapsible = const isCollapsible =
newContentHeight > (collapsedHeight + overflowThreshold); newContentHeight > (collapsedHeight + overflowThreshold);
onCollapsibleChanged({ collapsible: isCollapsible }); onCollapsibleChanged(isCollapsible);
}, [collapsedHeight, onCollapsibleChanged, overflowThreshold]); }, [collapsedHeight, onCollapsibleChanged, overflowThreshold]);
useLayoutEffect(() => { useLayoutEffect(() => {
......
import { mount } from 'enzyme'; import { mount } from 'enzyme';
import { createElement } from 'preact'; import { createElement } from 'preact';
import { act } from 'preact/test-utils';
import * as fixtures from '../../test/annotation-fixtures';
import AnnotationBody from '../annotation-body'; import AnnotationBody from '../annotation-body';
import { $imports } from '../annotation-body'; import { $imports } from '../annotation-body';
...@@ -8,7 +11,14 @@ import mockImportedComponents from './mock-imported-components'; ...@@ -8,7 +11,14 @@ import mockImportedComponents from './mock-imported-components';
describe('AnnotationBody', () => { describe('AnnotationBody', () => {
function createBody(props = {}) { function createBody(props = {}) {
return mount(<AnnotationBody text="test comment" {...props} />); return mount(
<AnnotationBody
annotation={fixtures.defaultAnnotation()}
isEditing={false}
text="test comment"
{...props}
/>
);
} }
beforeEach(() => { beforeEach(() => {
...@@ -30,4 +40,53 @@ describe('AnnotationBody', () => { ...@@ -30,4 +40,53 @@ describe('AnnotationBody', () => {
assert.isTrue(wrapper.exists('MarkdownEditor')); assert.isTrue(wrapper.exists('MarkdownEditor'));
assert.isFalse(wrapper.exists('MarkdownView')); assert.isFalse(wrapper.exists('MarkdownView'));
}); });
it('does not render controls to expand/collapse the excerpt if it is not collapsible', () => {
const wrapper = createBody();
// By default, `isCollapsible` is `false` until changed by `Excerpt`,
// so the expand/collapse button will not render
assert.notOk(wrapper.find('Button').exists());
});
it('renders controls to expand/collapse the excerpt if it is collapsible', () => {
const wrapper = createBody();
const excerpt = wrapper.find('Excerpt');
act(() => {
// change the `isCollapsible` state to `true` via the `Excerpt`
excerpt.props().onCollapsibleChanged(true);
});
wrapper.update();
const button = wrapper.find('Button');
assert.isOk(button.exists());
assert.equal(button.props().buttonText, 'More');
assert.equal(button.props().title, 'Show full annotation text');
});
it('shows appropriate button text to collapse the Excerpt if expanded', () => {
const wrapper = createBody();
const excerpt = wrapper.find('Excerpt');
act(() => {
// Get the `isCollapsible` state to `true`
excerpt.props().onCollapsibleChanged(true);
// Force a re-render so the button shows up
});
wrapper.update();
act(() => {
wrapper
.find('Button')
.props()
.onClick();
});
wrapper.update();
const buttonProps = wrapper.find('Button').props();
assert.equal(buttonProps.buttonText, 'Less');
assert.equal(buttonProps.title, 'Show the first few lines only');
});
}); });
...@@ -123,12 +123,8 @@ describe('annotation', function() { ...@@ -123,12 +123,8 @@ describe('annotation', function() {
.component('annotation', annotationComponent) .component('annotation', annotationComponent)
.component('annotationBody', { .component('annotationBody', {
bindings: { bindings: {
collapse: '<',
isEditing: '<', isEditing: '<',
isHiddenByModerator: '<',
onCollapsibleChanged: '&',
onEditText: '&', onEditText: '&',
onToggleCollapsed: '&',
text: '<', text: '<',
}, },
}) })
...@@ -807,35 +803,5 @@ describe('annotation', function() { ...@@ -807,35 +803,5 @@ describe('annotation', function() {
assert.calledWith($rootScope.$broadcast, events.ANNOTATION_DELETED); assert.calledWith($rootScope.$broadcast, events.ANNOTATION_DELETED);
}); });
}); });
[
{
context: 'for moderators',
ann: Object.assign(fixtures.moderatedAnnotation({ hidden: true }), {
// Content still present.
text: 'Some offensive content',
}),
isHiddenByModerator: true,
},
{
context: 'for non-moderators',
ann: Object.assign(fixtures.moderatedAnnotation({ hidden: true }), {
// Content filtered out by service.
tags: [],
text: '',
}),
isHiddenByModerator: true,
},
].forEach(({ ann, context, isHiddenByModerator }) => {
it(`passes moderation status to annotation body (${context})`, () => {
const el = createDirective(ann).element;
assert.match(
el.find('annotation-body').controller('annotationBody'),
sinon.match({
isHiddenByModerator,
})
);
});
});
}); });
}); });
...@@ -101,7 +101,7 @@ describe('Excerpt', () => { ...@@ -101,7 +101,7 @@ describe('Excerpt', () => {
sizeChangedCallback(); sizeChangedCallback();
}); });
assert.calledWith(onCollapsibleChanged, { collapsible: true }); assert.calledWith(onCollapsibleChanged, true);
}); });
it('calls `onToggleCollapsed` when user clicks in bottom area to expand excerpt', () => { it('calls `onToggleCollapsed` when user clicks in bottom area to expand excerpt', () => {
......
...@@ -15,22 +15,12 @@ ...@@ -15,22 +15,12 @@
</annotation-quote> </annotation-quote>
<annotation-body <annotation-body
collapse="vm.collapseBody" annotation="vm.annotation"
is-editing="vm.editing()" is-editing="vm.editing()"
is-hidden-by-moderator="vm.isHiddenByModerator()"
on-collapsible-changed="vm.setBodyCollapsible(collapsible)"
on-edit-text="vm.setText(text)" on-edit-text="vm.setText(text)"
on-toggle-collapsed="vm.collapseBody = collapsed"
text="vm.state().text"> text="vm.state().text">
</annotation-body> </annotation-body>
<div class="annotation-link-more">
<a class="annotation-link u-strong" ng-show="vm.canCollapseBody && !vm.editing()"
ng-click="vm.toggleCollapseBody($event)"
ng-title="vm.collapseBody ? 'Show the full annotation text' : 'Show the first few lines only'"
ng-bind="vm.collapseBody ? 'More' : 'Less'"
h-branding="accentColor">more</a>
</div>
<!-- Tags --> <!-- Tags -->
<tag-editor <tag-editor
ng-if="vm.editing()" ng-if="vm.editing()"
......
...@@ -6,9 +6,11 @@ ...@@ -6,9 +6,11 @@
// Margin between top of ascent of annotation body and // Margin between top of ascent of annotation body and
// bottom of ascent of annotation-quote should be ~15px. // bottom of ascent of annotation-quote should be ~15px.
margin-top: var.$layout-h-margin - 5px; margin-top: var.$layout-h-margin - 5px;
margin-bottom: var.$layout-h-margin;
margin-right: 0px; margin-right: 0px;
margin-left: 0px; margin-left: 0px;
.annotation-body__collapse-toggle-button {
background-color: transparent;
}
} }
.annotation-body__text { .annotation-body__text {
...@@ -36,3 +38,9 @@ ...@@ -36,3 +38,9 @@
); );
} }
} }
.annotation-body__collapse-toggle {
display: flex;
justify-content: flex-end;
padding: 0.5em 0;
}
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