Unverified Commit 25d8df56 authored by Kyle Keating's avatar Kyle Keating Committed by GitHub

Merge pull request #1269 from hypothesis/react-selection-tabs

Add react selection-tabs component
parents 8813b8cf 1efb767b
'use strict'; 'use strict';
const classnames = require('classnames');
const propTypes = require('prop-types');
const { createElement } = require('preact');
const { Fragment } = require('preact');
const NewNoteBnt = require('./new-note-btn');
const sessionUtil = require('../util/session-util'); const sessionUtil = require('../util/session-util');
const uiConstants = require('../ui-constants'); const uiConstants = require('../ui-constants');
const useStore = require('../store/use-store');
const { withServices } = require('../util/service-context');
module.exports = { /**
controllerAs: 'vm', * Display name of the tab and annotation count.
//@ngInject */
controller: function($element, store, features, session, settings) { function Tab({
this.TAB_ANNOTATIONS = uiConstants.TAB_ANNOTATIONS; children,
this.TAB_NOTES = uiConstants.TAB_NOTES; count,
this.TAB_ORPHANS = uiConstants.TAB_ORPHANS; isWaitingToAnchor,
onChangeTab,
this.isThemeClean = settings.theme === 'clean'; selected,
type,
this.enableExperimentalNewNoteButton = }) {
settings.enableExperimentalNewNoteButton; return (
<a
this.selectTab = function(type) { className={classnames('selection-tabs__type', {
store.clearSelectedAnnotations(); 'is-selected': selected,
store.selectTab(type); })}
}; onMouseDown={onChangeTab.bind(this, type)}
onTouchStart={onChangeTab.bind(this, type)}
this.showAnnotationsUnavailableMessage = function() { >
return ( {children}
this.selectedTab === this.TAB_ANNOTATIONS && {count > 0 && !isWaitingToAnchor && (
this.totalAnnotations === 0 && <span className="selection-tabs__count"> {count}</span>
!this.isWaitingToAnchorAnnotations )}
); </a>
}; );
}
this.showNotesUnavailableMessage = function() { Tab.propTypes = {
return this.selectedTab === this.TAB_NOTES && this.totalNotes === 0; /**
}; * Child components.
*/
this.showSidebarTutorial = function() { children: propTypes.node.isRequired,
return sessionUtil.shouldShowSidebarTutorial(session.state); /**
}; * The total annotations for this tab.
}, */
bindings: { count: propTypes.number.isRequired,
isLoading: '<', /**
isWaitingToAnchorAnnotations: '<', * Are there any annotations still waiting to anchor?
selectedTab: '<', */
totalAnnotations: '<', isWaitingToAnchor: propTypes.bool.isRequired,
totalNotes: '<', /**
totalOrphans: '<', * Callback when this tab is active with type as a parameter.
}, */
template: require('../templates/selection-tabs.html'), onChangeTab: propTypes.func.isRequired,
/**
* Is this tab currently selected?
*/
selected: propTypes.bool.isRequired,
/**
* The type value for this tab. One of
* 'annotation', 'note', or 'orphan'.
*/
type: propTypes.oneOf(['annotation', 'note', 'orphan']).isRequired,
}; };
/**
* Tabbed display of annotations and notes.
*/
function SelectionTabs({
isWaitingToAnchorAnnotations,
isLoading,
selectedTab,
totalAnnotations,
totalNotes,
totalOrphans,
settings,
session,
}) {
// actions
const store = useStore(store => ({
clearSelectedAnnotations: store.clearSelectedAnnotations,
selectTab: store.selectTab,
}));
const isThemeClean = settings.theme === 'clean';
const selectTab = function(type) {
store.clearSelectedAnnotations();
store.selectTab(type);
};
const showAnnotationsUnavailableMessage =
selectedTab === uiConstants.TAB_ANNOTATIONS &&
totalAnnotations === 0 &&
!isWaitingToAnchorAnnotations;
const showNotesUnavailableMessage =
selectedTab === uiConstants.TAB_NOTES && totalNotes === 0;
const showSidebarTutorial = sessionUtil.shouldShowSidebarTutorial(
session.state
);
return (
<Fragment>
<div
className={classnames('selection-tabs', {
'selection-tabs--theme-clean': isThemeClean,
})}
>
<Tab
count={totalAnnotations}
isWaitingToAnchor={isWaitingToAnchorAnnotations}
selected={selectedTab === uiConstants.TAB_ANNOTATIONS}
type={uiConstants.TAB_ANNOTATIONS}
onChangeTab={selectTab}
>
Annotations
</Tab>
<Tab
count={totalNotes}
isWaitingToAnchor={isWaitingToAnchorAnnotations}
selected={selectedTab === uiConstants.TAB_NOTES}
type={uiConstants.TAB_NOTES}
onChangeTab={selectTab}
>
Page Notes
</Tab>
{totalOrphans > 0 && (
<Tab
count={totalOrphans}
isWaitingToAnchor={isWaitingToAnchorAnnotations}
selected={selectedTab === uiConstants.TAB_ORPHANS}
type={uiConstants.TAB_ORPHANS}
onChangeTab={selectTab}
>
Orphans
</Tab>
)}
</div>
{selectedTab === uiConstants.TAB_NOTES &&
settings.enableExperimentalNewNoteButton && <NewNoteBnt />}
{!isLoading && (
<div className="selection-tabs__empty-message">
{showNotesUnavailableMessage && (
<div className="annotation-unavailable-message">
<p className="annotation-unavailable-message__label">
There are no page notes in this group.
{settings.enableExperimentalNewNoteButton &&
!showSidebarTutorial && (
<div className="annotation-unavailable-message__tutorial">
Create one by clicking the{' '}
<i className="help-icon h-icon-note" /> button.
</div>
)}
</p>
</div>
)}
{showAnnotationsUnavailableMessage && (
<div className="annotation-unavailable-message">
<p className="annotation-unavailable-message__label">
There are no annotations in this group.
{!showSidebarTutorial && (
<div className="annotation-unavailable-message__tutorial">
Create one by selecting some text and clicking the{' '}
<i className="help-icon h-icon-annotate" /> button.
</div>
)}
</p>
</div>
)}
</div>
)}
</Fragment>
);
}
SelectionTabs.propTypes = {
/**
* Are we waiting on any annotations from the server?
*/
isLoading: propTypes.bool.isRequired,
/**
* Are there any annotations still waiting to anchor?
*/
isWaitingToAnchorAnnotations: propTypes.bool.isRequired,
/**
* The currently selected tab (annotations, notes or orphans).
*/
selectedTab: propTypes.oneOf(['annotation', 'note', 'orphan']).isRequired,
/**
* The totals for each respect tab.
*/
totalAnnotations: propTypes.number.isRequired,
totalNotes: propTypes.number.isRequired,
totalOrphans: propTypes.number.isRequired,
// Injected services.
settings: propTypes.object.isRequired,
session: propTypes.object.isRequired,
};
SelectionTabs.injectedProps = ['session', 'settings'];
module.exports = withServices(SelectionTabs);
'use strict'; 'use strict';
const angular = require('angular'); const { shallow, mount } = require('enzyme');
const { createElement } = require('preact');
const util = require('../../directive/test/util');
const NewNoteBtn = require('../new-note-btn');
describe('selectionTabs', function() { const uiConstants = require('../../ui-constants');
const fakeSession = { const SelectionTabs = require('../selection-tabs');
state: {
preferences: { describe('SelectionTabs', function() {
show_sidebar_tutorial: false, // mock services
}, let fakeSession;
}, let fakeSettings;
};
const fakeSettings = { // default props
enableExperimentalNewNoteButton: false, const defaultProps = {
isLoading: false,
isWaitingToAnchorAnnotations: false,
selectedTab: uiConstants.TAB_ANNOTATIONS,
totalAnnotations: 123,
totalNotes: 456,
totalOrphans: 0,
}; };
before(function() { SelectionTabs.$imports.$mock({
angular '../store/use-store': callback =>
.module('app', []) callback({
.component('selectionTabs', require('../selection-tabs')); clearSelectedAnnotations: sinon.stub(),
selectTab: sinon.stub(),
}),
}); });
beforeEach(function() { function createComponent(props) {
const fakeStore = {}; return shallow(
const fakeFeatures = { <SelectionTabs
flagEnabled: sinon.stub().returns(true), session={fakeSession}
settings={fakeSettings}
{...defaultProps}
{...props}
/>
).dive();
}
// required for <Tab> rendering
function createDeepComponent(props) {
return mount(
<SelectionTabs
session={fakeSession}
settings={fakeSettings}
{...defaultProps}
{...props}
/>
);
}
beforeEach(() => {
fakeSession = {
state: {
preferences: {
show_sidebar_tutorial: false,
},
},
};
fakeSettings = {
enableExperimentalNewNoteButton: false,
}; };
angular.mock.module('app', {
store: fakeStore,
features: fakeFeatures,
session: fakeSession,
settings: fakeSettings,
});
}); });
context('displays selection tabs, counts and a selection', function() { const unavailableMessage = wrapper =>
it('should display the tabs and counts of annotations and notes', function() { wrapper.find('.annotation-unavailable-message__label').text();
const elem = util.createDirective(document, 'selectionTabs', {
selectedTab: 'annotation',
totalAnnotations: '123',
totalNotes: '456',
});
const tabs = elem[0].querySelectorAll('a');
assert.include(tabs[0].textContent, 'Annotations'); context('displays selection tabs and counts', function() {
assert.include(tabs[1].textContent, 'Notes'); it('should display the tabs and counts of annotations and notes', function() {
assert.include(tabs[0].textContent, '123'); const wrapper = createDeepComponent();
assert.include(tabs[1].textContent, '456'); const tabs = wrapper.find('a');
assert.isTrue(tabs.at(0).contains('Annotations'));
assert.equal(
tabs
.at(0)
.find('.selection-tabs__count')
.text(),
123
);
assert.isTrue(tabs.at(1).contains('Page Notes'));
assert.equal(
tabs
.at(1)
.find('.selection-tabs__count')
.text(),
456
);
}); });
it('should display annotations tab as selected', function() { it('should display annotations tab as selected', function() {
const elem = util.createDirective(document, 'selectionTabs', { const wrapper = createDeepComponent();
selectedTab: 'annotation', const aTags = wrapper.find('a');
totalAnnotations: '123', assert.isTrue(aTags.at(0).hasClass('is-selected'));
totalNotes: '456',
});
const tabs = elem[0].querySelectorAll('a');
assert.isTrue(tabs[0].classList.contains('is-selected'));
}); });
it('should display notes tab as selected', function() { it('should display notes tab as selected', function() {
const elem = util.createDirective(document, 'selectionTabs', { const wrapper = createDeepComponent({
selectedTab: 'note', selectedTab: uiConstants.TAB_NOTES,
totalAnnotations: '123',
totalNotes: '456',
}); });
const tabs = elem[0].querySelectorAll('a'); const tabs = wrapper.find('a');
assert.isTrue(tabs.at(1).hasClass('is-selected'));
assert.isTrue(tabs[1].classList.contains('is-selected'));
}); });
it('should not show the clean theme when settings does not contain the clean theme option', function() { it('should display orphans tab as selected if there is 1 or more orphans', function() {
const elem = util.createDirective(document, 'selectionTabs', { const wrapper = createDeepComponent({
selectedTab: 'annotation', selectedTab: uiConstants.TAB_ORPHANS,
totalAnnotations: '123', totalOrphans: 1,
totalNotes: '456',
}); });
const tabs = wrapper.find('a');
assert.isTrue(tabs.at(2).hasClass('is-selected'));
});
assert.isFalse( it('should not display orphans tab if there are 0 orphans', function() {
elem[0] const wrapper = createDeepComponent({
.querySelectorAll('.selection-tabs')[0] selectedTab: uiConstants.TAB_ORPHANS,
.classList.contains('selection-tabs--theme-clean') });
); const tabs = wrapper.find('a');
assert.equal(tabs.length, 2);
}); });
it('should show the clean theme when settings contains the clean theme option', function() { it('should show the clean theme when settings contains the clean theme option', function() {
angular.mock.module('app', { fakeSettings.theme = 'clean';
store: {}, const wrapper = createComponent();
features: { assert.isTrue(wrapper.exists('.selection-tabs--theme-clean'));
flagEnabled: sinon.stub().returns(true), });
},
settings: { theme: 'clean' },
});
const elem = util.createDirective(document, 'selectionTabs', { it('should not show the clean theme when settings does not contain the clean theme option', function() {
selectedTab: 'annotation', const wrapper = createComponent();
totalAnnotations: '123', assert.isFalse(wrapper.exists('.selection-tabs--theme-clean'));
totalNotes: '456', });
});
assert.isTrue( it('should not display the new-note-bnt when the annotations tab is active', function() {
elem[0] const wrapper = createComponent();
.querySelectorAll('.selection-tabs')[0] assert.equal(wrapper.find(NewNoteBtn).length, 0);
.classList.contains('selection-tabs--theme-clean')
);
}); });
it('should not display the new new note button when the annotations tab is active', function() { it('should not display the new-note-btn when the notes tab is active and the new-note-btn is disabled', function() {
const elem = util.createDirective(document, 'selectionTabs', { const wrapper = createComponent({
selectedTab: 'annotation', selectedTab: uiConstants.TAB_NOTES,
totalAnnotations: '123',
totalNotes: '456',
}); });
const newNoteElem = elem[0].querySelectorAll('new-note-btn'); assert.equal(wrapper.find(NewNoteBtn).length, 0);
assert.equal(newNoteElem.length, 0);
}); });
it('should not display the new note button when the notes tab is active and the new note button is disabled', function() { it('should display the new-note-btn when the notes tab is active and the new-note-btn is enabled', function() {
const elem = util.createDirective(document, 'selectionTabs', { fakeSettings.enableExperimentalNewNoteButton = true;
selectedTab: 'note', const wrapper = createComponent({
totalAnnotations: '123', selectedTab: uiConstants.TAB_NOTES,
totalNotes: '456',
}); });
const newNoteElem = elem[0].querySelectorAll('new-note-btn'); assert.equal(wrapper.find(NewNoteBtn).length, 1);
});
assert.equal(newNoteElem.length, 0); it('should not display a message when its loading annotation count is 0', function() {
const wrapper = createComponent({
totalAnnotations: 0,
isLoading: true,
});
assert.isFalse(wrapper.exists('.annotation-unavailable-message__label'));
}); });
it('should display the new note button when the notes tab is active and the new note button is enabled', function() { it('should not display a message when its loading notes count is 0', function() {
fakeSettings.enableExperimentalNewNoteButton = true; const wrapper = createComponent({
const elem = util.createDirective(document, 'selectionTabs', { selectedTab: uiConstants.TAB_NOTES,
selectedTab: 'note', totalNotes: 0,
totalAnnotations: '123', isLoading: true,
totalNotes: '456',
}); });
const newNoteElem = elem[0].querySelectorAll('new-note-btn'); assert.isFalse(wrapper.exists('.annotation-unavailable-message__label'));
});
assert.equal(newNoteElem.length, 1); it('should not display the longer version of the no annotations message when there are no annotations and isWaitingToAnchorAnnotations is true', function() {
const wrapper = createComponent({
totalAnnotations: 0,
isWaitingToAnchorAnnotations: true,
isLoading: false,
});
assert.isFalse(wrapper.exists('.annotation-unavailable-message__label'));
}); });
it('should display the longer version of the no notes message when there are no notes', function() { it('should display the longer version of the no notes message when there are no notes', function() {
fakeSession.state.preferences.show_sidebar_tutorial = false; const wrapper = createComponent({
fakeSettings.enableExperimentalNewNoteButton = false; selectedTab: uiConstants.TAB_NOTES,
const elem = util.createDirective(document, 'selectionTabs', {
selectedTab: 'note',
totalAnnotations: '10',
totalNotes: 0, totalNotes: 0,
}); });
const unavailableMsg = elem[0].querySelector(
'.annotation-unavailable-message__label'
);
const unavailableTutorial = elem[0].querySelector(
'.annotation-unavailable-message__tutorial'
);
const noteIcon = unavailableTutorial.querySelector('i');
assert.include( assert.include(
unavailableMsg.textContent, unavailableMessage(wrapper),
'There are no page notes in this group.' 'There are no page notes in this group.'
); );
});
it('should display the prompt to create a note when there are no notes and enableExperimentalNewNoteButton is true', function() {
fakeSettings.enableExperimentalNewNoteButton = true;
const wrapper = createComponent({
selectedTab: uiConstants.TAB_NOTES,
totalNotes: 0,
});
assert.include( assert.include(
unavailableTutorial.textContent, wrapper.find('.annotation-unavailable-message__tutorial').text(),
'Create one by clicking the' 'Create one by clicking the'
); );
assert.isTrue(noteIcon.classList.contains('h-icon-note')); assert.isTrue(
wrapper
.find('.annotation-unavailable-message__tutorial i')
.hasClass('h-icon-note')
);
}); });
it('should display the longer version of the no annotations message when there are no annotations', function() { it('should display the longer version of the no annotations message when there are no annotations', function() {
fakeSession.state.preferences.show_sidebar_tutorial = false; const wrapper = createComponent({
fakeSettings.enableExperimentalNewNoteButton = false;
const elem = util.createDirective(document, 'selectionTabs', {
selectedTab: 'annotation',
totalAnnotations: 0, totalAnnotations: 0,
totalNotes: '10',
}); });
const unavailableMsg = elem[0].querySelector(
'.annotation-unavailable-message__label'
);
const unavailableTutorial = elem[0].querySelector(
'.annotation-unavailable-message__tutorial'
);
const noteIcon = unavailableTutorial.querySelector('i');
assert.include( assert.include(
unavailableMsg.textContent, unavailableMessage(wrapper),
'There are no annotations in this group.' 'There are no annotations in this group.'
); );
assert.include( assert.include(
unavailableTutorial.textContent, wrapper.find('.annotation-unavailable-message__tutorial').text(),
'Create one by selecting some text and clicking the' 'Create one by selecting some text and clicking the'
); );
assert.isTrue(noteIcon.classList.contains('h-icon-annotate')); assert.isTrue(
wrapper
.find('.annotation-unavailable-message__tutorial i')
.hasClass('h-icon-annotate')
);
}); });
context('when the sidebar tutorial is displayed', function() { context('when the sidebar tutorial is displayed', function() {
fakeSession.state.preferences.show_sidebar_tutorial = true;
it('should display the shorter version of the no notes message when there are no notes', function() { it('should display the shorter version of the no notes message when there are no notes', function() {
const elem = util.createDirective(document, 'selectionTabs', { fakeSession.state.preferences.show_sidebar_tutorial = true;
selectedTab: 'note', const wrapper = createComponent({
totalAnnotations: '10',
totalNotes: 0, totalNotes: 0,
selectedTab: uiConstants.TAB_NOTES,
}); });
const msg = elem[0].querySelector(
'.annotation-unavailable-message__label'
);
assert.include( const msg = unavailableMessage(wrapper);
msg.textContent,
'There are no page notes in this group.' assert.include(msg, 'There are no page notes in this group.');
); assert.notInclude(msg, 'Create one by clicking the');
assert.notInclude(msg.textContent, 'Create one by clicking the');
assert.notInclude( assert.notInclude(
msg.textContent, msg,
'Create one by selecting some text and clicking the' 'Create one by selecting some text and clicking the'
); );
}); });
it('should display the shorter version of the no annotations message when there are no annotations', function() { it('should display the shorter version of the no annotations message when there are no annotations', function() {
const elem = util.createDirective(document, 'selectionTabs', { fakeSession.state.preferences.show_sidebar_tutorial = true;
selectedTab: 'annotation', const wrapper = createComponent({
totalAnnotations: 0, totalAnnotations: 0,
totalNotes: '10',
}); });
const msg = elem[0].querySelector(
'.annotation-unavailable-message__label'
);
assert.include( const msg = unavailableMessage(wrapper);
msg.textContent,
'There are no annotations in this group.' assert.include(msg, 'There are no annotations in this group.');
); assert.notInclude(msg, 'Create one by clicking the');
assert.notInclude(msg.textContent, 'Create one by clicking the');
assert.notInclude( assert.notInclude(
msg.textContent, msg,
'Create one by selecting some text and clicking the' 'Create one by selecting some text and clicking the'
); );
}); });
......
...@@ -188,7 +188,10 @@ function startAngularApp(config) { ...@@ -188,7 +188,10 @@ function startAngularApp(config) {
'newNoteBtn', 'newNoteBtn',
wrapReactComponent(require('./components/new-note-btn')) wrapReactComponent(require('./components/new-note-btn'))
) )
.component('selectionTabs', require('./components/selection-tabs')) .component(
'selectionTabs',
wrapReactComponent(require('./components/selection-tabs'))
)
.component('sidebarContent', require('./components/sidebar-content')) .component('sidebarContent', require('./components/sidebar-content'))
.component( .component(
'sidebarContentError', 'sidebarContentError',
......
<!-- Tabbed display of annotations and notes. -->
<div class="selection-tabs"
ng-class="{'selection-tabs--theme-clean' : vm.isThemeClean }">
<a class="selection-tabs__type"
href="#"
ng-class="{'is-selected': vm.selectedTab === vm.TAB_ANNOTATIONS}"
h-on-touch="vm.selectTab(vm.TAB_ANNOTATIONS)">
Annotations
<span class="selection-tabs__count"
ng-if="vm.totalAnnotations > 0 && !vm.isWaitingToAnchorAnnotations">
{{ vm.totalAnnotations }}
</span>
</a>
<a class="selection-tabs__type"
href="#"
ng-class="{'is-selected': vm.selectedTab === vm.TAB_NOTES}"
h-on-touch="vm.selectTab(vm.TAB_NOTES)">
Page Notes
<span class="selection-tabs__count"
ng-if="vm.totalNotes > 0 && !vm.isWaitingToAnchorAnnotations">
{{ vm.totalNotes }}
</span>
</a>
<a class="selection-tabs__type selection-tabs__type--orphan"
ng-if="vm.totalOrphans > 0"
href="#"
ng-class="{'is-selected': vm.selectedTab === vm.TAB_ORPHANS}"
h-on-touch="vm.selectTab(vm.TAB_ORPHANS)">
Orphans
<span class="selection-tabs__count"
ng-if="vm.totalOrphans > 0 && !vm.isWaitingToAnchorAnnotations">
{{ vm.totalOrphans }}
</span>
</a>
</div>
<new-note-btn
ng-if="vm.selectedTab === vm.TAB_NOTES && vm.enableExperimentalNewNoteButton">
</new-note-btn>
<div ng-if="!vm.isLoading()" class="selection-tabs__empty-message">
<div ng-if="vm.showNotesUnavailableMessage()" class="annotation-unavailable-message">
<p class="annotation-unavailable-message__label">
There are no page notes in this group.
<br />
<div ng-if="!vm.enableExperimentalNewNoteButton && !vm.showSidebarTutorial()" class="annotation-unavailable-message__tutorial">
Create one by clicking the
<i class="help-icon h-icon-note"></i>
button.
</div>
</p>
</div>
<div ng-if="vm.showAnnotationsUnavailableMessage()" class="annotation-unavailable-message">
<p class="annotation-unavailable-message__label">
There are no annotations in this group.
<br />
<div ng-if="!vm.showSidebarTutorial()" class="annotation-unavailable-message__tutorial">
Create one by selecting some text and clicking the
<i class="help-icon h-icon-annotate"></i> button.
</div>
</p>
</div>
</div>
<selection-tabs <selection-tabs
ng-if="vm.showSelectedTabs()" ng-if="vm.showSelectedTabs()"
is-waiting-to-anchor-annotations="vm.waitingToAnchorAnnotations" is-waiting-to-anchor-annotations="vm.waitingToAnchorAnnotations"
is-loading="vm.isLoading" is-loading="vm.isLoading()"
selected-tab="vm.selectedTab" selected-tab="vm.selectedTab"
total-annotations="vm.totalAnnotations" total-annotations="vm.totalAnnotations"
total-notes="vm.totalNotes" total-notes="vm.totalNotes"
......
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