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 { useState } from 'preact/hooks';
import propTypes from 'prop-types';
import { isHidden } from '../util/annotation-metadata';
import Button from './button';
import Excerpt from './excerpt';
import MarkdownEditor from './markdown-editor';
import MarkdownView from './markdown-view';
......@@ -9,82 +13,76 @@ import MarkdownView from './markdown-view';
* Display the rendered content of an annotation.
*/
export default function AnnotationBody({
collapse,
annotation,
isEditing,
isHiddenByModerator,
onCollapsibleChanged,
onEditText,
onToggleCollapsed,
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 (
<section className="annotation-body">
{!isEditing && (
<Excerpt
collapse={collapse}
collapse={isCollapsed}
collapsedHeight={400}
inlineControls={false}
onCollapsibleChanged={onCollapsibleChanged}
onToggleCollapsed={collapsed => onToggleCollapsed({ collapsed })}
onCollapsibleChanged={setIsCollapsible}
onToggleCollapsed={setIsCollapsed}
overflowThreshold={20}
>
<MarkdownView
markdown={text}
textClass={{
'annotation-body__text': true,
'is-hidden': isHiddenByModerator,
'is-hidden': isHidden(annotation),
'has-content': text.length > 0,
}}
/>
</Excerpt>
)}
{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>
);
}
AnnotationBody.propTypes = {
/**
* Whether to limit the height of the annotation body.
*
* If this is true and the intrinsic height exceeds a fixed threshold, the
* body is truncated. See `onCollapsibleChanged` and `onToggleCollapsed`.
* The annotation in question
*/
collapse: propTypes.bool,
annotation: propTypes.object.isRequired,
/**
* Whether to display the body in edit mode (if true) or view mode.
*/
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.
*/
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`
* is false) or displayed in a text area otherwise.
......
......@@ -71,14 +71,6 @@ function AnnotationController(
* methods goes here.
*/
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. */
self.isSaving = false;
......@@ -242,11 +234,6 @@ function AnnotationController(
}
};
this.toggleCollapseBody = function(event) {
event.stopPropagation();
self.collapseBody = !self.collapseBody;
};
/**
* @ngdoc method
* @name annotation.AnnotationController#reply
......@@ -364,25 +351,10 @@ function AnnotationController(
return store.hasPendingDeletion(self.annotation.id);
};
this.isHiddenByModerator = function() {
return self.annotation.hidden;
};
this.isReply = function() {
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) {
store.createDraft(self.annotation, {
isPrivate: self.state().isPrivate,
......
......@@ -77,7 +77,7 @@ function Excerpt({
// prettier-ignore
const isCollapsible =
newContentHeight > (collapsedHeight + overflowThreshold);
onCollapsibleChanged({ collapsible: isCollapsible });
onCollapsibleChanged(isCollapsible);
}, [collapsedHeight, onCollapsibleChanged, overflowThreshold]);
useLayoutEffect(() => {
......
import { mount } from 'enzyme';
import { createElement } from 'preact';
import { act } from 'preact/test-utils';
import * as fixtures from '../../test/annotation-fixtures';
import AnnotationBody from '../annotation-body';
import { $imports } from '../annotation-body';
......@@ -8,7 +11,14 @@ import mockImportedComponents from './mock-imported-components';
describe('AnnotationBody', () => {
function createBody(props = {}) {
return mount(<AnnotationBody text="test comment" {...props} />);
return mount(
<AnnotationBody
annotation={fixtures.defaultAnnotation()}
isEditing={false}
text="test comment"
{...props}
/>
);
}
beforeEach(() => {
......@@ -30,4 +40,53 @@ describe('AnnotationBody', () => {
assert.isTrue(wrapper.exists('MarkdownEditor'));
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() {
.component('annotation', annotationComponent)
.component('annotationBody', {
bindings: {
collapse: '<',
isEditing: '<',
isHiddenByModerator: '<',
onCollapsibleChanged: '&',
onEditText: '&',
onToggleCollapsed: '&',
text: '<',
},
})
......@@ -807,35 +803,5 @@ describe('annotation', function() {
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', () => {
sizeChangedCallback();
});
assert.calledWith(onCollapsibleChanged, { collapsible: true });
assert.calledWith(onCollapsibleChanged, true);
});
it('calls `onToggleCollapsed` when user clicks in bottom area to expand excerpt', () => {
......
......@@ -15,22 +15,12 @@
</annotation-quote>
<annotation-body
collapse="vm.collapseBody"
annotation="vm.annotation"
is-editing="vm.editing()"
is-hidden-by-moderator="vm.isHiddenByModerator()"
on-collapsible-changed="vm.setBodyCollapsible(collapsible)"
on-edit-text="vm.setText(text)"
on-toggle-collapsed="vm.collapseBody = collapsed"
text="vm.state().text">
</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 -->
<tag-editor
ng-if="vm.editing()"
......
......@@ -6,9 +6,11 @@
// Margin between top of ascent of annotation body and
// 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__collapse-toggle-button {
background-color: transparent;
}
}
.annotation-body__text {
......@@ -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