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