Commit 36d6ce94 authored by Lyza Danger Gardner's avatar Lyza Danger Gardner

Add `ThreadCard` component

This component renders top-level threads as “cards” in sidebar
parent c07d796e
import { mount } from 'enzyme';
import { createElement } from 'preact';
import ThreadCard from '../thread-card';
import { $imports } from '../thread-card';
import { checkAccessibility } from '../../../test-util/accessibility';
import mockImportedComponents from '../../../test-util/mock-imported-components';
describe('ThreadCard', () => {
let fakeDebounce;
let fakeFrameSync;
let fakeStore;
let fakeThread;
function createComponent(props) {
return mount(
<ThreadCard
frameSync={fakeFrameSync}
settings={{}}
thread={fakeThread}
{...props}
/>
);
}
beforeEach(() => {
fakeDebounce = sinon.stub().returnsArg(0);
fakeFrameSync = {
focusAnnotations: sinon.stub(),
scrollToAnnotation: sinon.stub(),
};
fakeStore = {
isAnnotationFocused: sinon.stub().returns(false),
};
fakeThread = {
id: 't1',
annotation: { $tag: 'myTag' },
};
$imports.$mock(mockImportedComponents());
$imports.$mock({
'lodash.debounce': fakeDebounce,
'../store/use-store': callback => callback(fakeStore),
});
});
afterEach(() => {
$imports.$restore();
});
it('renders a `Thread` for the passed `thread`', () => {
const wrapper = createComponent();
assert(wrapper.find('Thread').props().thread === fakeThread);
});
it('applies a focused CSS class if the annotation thread is focused', () => {
fakeStore.isAnnotationFocused.returns(true);
const wrapper = createComponent();
assert(wrapper.find('.thread-card').hasClass('is-focused'));
});
it('applies a CSS class if settings indicate a `clean` theme', () => {
const wrapper = createComponent({ settings: { theme: 'clean' } });
assert(wrapper.find('.thread-card').hasClass('thread-card--theme-clean'));
});
describe('mouse and click events', () => {
it('scrolls to the annotation when the `ThreadCard` is clicked', () => {
const wrapper = createComponent();
wrapper.find('.thread-card').simulate('click');
assert.calledWith(fakeFrameSync.scrollToAnnotation, 'myTag');
});
it('focuses the annotation thread when mouse enters', () => {
const wrapper = createComponent();
wrapper.find('.thread-card').simulate('mouseenter');
assert.calledWith(fakeFrameSync.focusAnnotations, sinon.match(['myTag']));
});
it('unfocuses the annotation thread when mouse exits', () => {
const wrapper = createComponent();
wrapper.find('.thread-card').simulate('mouseleave');
assert.calledWith(fakeFrameSync.focusAnnotations, sinon.match([]));
});
});
it(
'should pass a11y checks',
checkAccessibility({
content: () => createComponent(),
})
);
});
import classnames from 'classnames';
import { createElement } from 'preact';
import { useCallback } from 'preact/hooks';
import debounce from 'lodash.debounce';
import propTypes from 'prop-types';
import useStore from '../store/use-store';
import { withServices } from '../util/service-context';
import Thread from './thread';
/**
* A "top-level" `Thread`, rendered as a "card" in the sidebar. A `Thread`
* renders its own child `Thread`s within itself.
*/
function ThreadCard({ frameSync, settings = {}, thread }) {
const threadTag = thread.annotation && thread.annotation.$tag;
const isFocused = useStore(store => store.isAnnotationFocused(threadTag));
const focusThreadAnnotation = useCallback(
debounce(tag => {
const focusTags = tag ? [tag] : [];
frameSync.focusAnnotations(focusTags);
}, 10),
[frameSync]
);
const scrollToAnnotation = useCallback(
tag => {
frameSync.scrollToAnnotation(tag);
},
[frameSync]
);
return (
/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */
<div
onClick={() => scrollToAnnotation(threadTag)}
onMouseEnter={() => focusThreadAnnotation(threadTag)}
onMouseLeave={() => focusThreadAnnotation(null)}
key={thread.id}
className={classnames('thread-card', {
'is-focused': isFocused,
'thread-card--theme-clean': settings.theme === 'clean',
})}
>
<Thread thread={thread} showDocumentInfo={false} />
</div>
);
}
ThreadCard.propTypes = {
thread: propTypes.object.isRequired,
/** injected */
frameSync: propTypes.object.isRequired,
settings: propTypes.object,
};
ThreadCard.injectedProps = ['frameSync', 'settings'];
export default withServices(ThreadCard);
@use "../../variables" as var;
.thread-card {
margin-bottom: 1em;
padding: 1em;
border-radius: 2px;
box-shadow: 0px 1px 1px 0px rgba(0, 0, 0, 0.1);
cursor: pointer;
background-color: var.$white;
&.is-focused {
box-shadow: 0px 2px 3px 0px rgba(0, 0, 0, 0.15);
}
&--theme-clean {
// Give a little more space so that the border appears centered
// between cards
padding-bottom: 1.5em;
border-bottom: 1px solid var.$grey-2;
box-shadow: none;
&:hover {
box-shadow: none;
}
}
}
......@@ -62,6 +62,7 @@
@use './components/tag-editor';
@use './components/tag-list';
@use './components/thread';
@use './components/thread-card';
@use './components/thread-list';
@use './components/toast-messages';
@use './components/tooltip';
......
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