Commit 89bc4044 authored by Lyza Danger Gardner's avatar Lyza Danger Gardner

Reimplement Help/Tutorial panel

parent 7953c6a4
'use strict';
const { createElement } = require('preact');
const { useCallback, useMemo, useState } = require('preact/hooks');
const propTypes = require('prop-types');
const uiConstants = require('../ui-constants');
const useStore = require('../store/use-store');
const VersionData = require('../util/version-data');
const { withServices } = require('../util/service-context');
const SidebarPanel = require('./sidebar-panel');
const SvgIcon = require('./svg-icon');
const Tutorial = require('./tutorial');
const VersionInfo = require('./version-info');
/**
* @ngdoc directive
* @name helpPanel
* @description Displays product version and environment info
* External link "tabs" inside of the help panel.
*/
// @ngInject
module.exports = {
controllerAs: 'vm',
// @ngInject
controller: function($scope, $window, store, serviceUrl) {
this.userAgent = $window.navigator.userAgent;
this.version = '__VERSION__'; // replaced by versionify
this.dateTime = new Date();
this.serviceUrl = serviceUrl;
$scope.$watch(
function() {
return store.frames();
},
function(frames) {
if (frames.length === 0) {
return;
}
this.url = frames[0].uri;
this.documentFingerprint = frames[0].metadata.documentFingerprint;
}.bind(this)
);
},
template: require('../templates/help-panel.html'),
bindings: {
auth: '<',
onClose: '&',
},
function HelpPanelTab({ linkText, url }) {
return (
<div className="help-panel-tabs__tab">
<a
href={url}
className="help-panel-tabs__link"
target="_blank"
rel="noopener noreferrer"
>
{linkText}{' '}
<SvgIcon
name="external"
className="help-panel-tabs__icon"
inline={true}
/>
</a>
</div>
);
}
HelpPanelTab.propTypes = {
/* What the tab's link should say */
linkText: propTypes.string.isRequired,
/* Where the tab's link should go */
url: propTypes.string.isRequired,
};
/**
* A help sidebar panel with two sub-panels: tutorial and version info.
*/
function HelpPanel({ auth, session }) {
const mainFrame = useStore(store => store.mainFrame());
// Should this panel be auto-opened at app launch? Note that the actual
// auto-open triggering of this panel is owned by the `hypothesis-app` component.
// This reference is such that we know whether we should "dismiss" the tutorial
// (permanently for this user) when it is closed.
const hasAutoDisplayPreference = useStore(
store => !!store.getState().session.preferences.show_sidebar_tutorial
);
// The "Tutorial" (getting started) subpanel is the default panel shown
const [activeSubPanel, setActiveSubPanel] = useState('tutorial');
// Build version details about this session/app
const versionData = useMemo(() => {
const userInfo = auth || {};
const documentInfo = mainFrame || {};
return new VersionData(userInfo, documentInfo);
}, [auth, mainFrame]);
// The support ticket URL encodes some version info in it to pre-fill in the
// create-new-ticket form
const supportTicketURL = `https://web.hypothes.is/get-help/?sys_info=${versionData.asEncodedURLString()}`;
const subPanelTitles = {
tutorial: 'Getting started',
versionInfo: 'About this version',
};
const openSubPanel = (e, panelName) => {
e.preventDefault();
setActiveSubPanel(panelName);
};
const dismissFn = session.dismissSidebarTutorial; // Reference for useCallback dependency
const onActiveChanged = useCallback(
active => {
if (!active && hasAutoDisplayPreference) {
// If the tutorial is currently being auto-displayed, update the user
// preference to disable the auto-display from happening on subsequent
// app launches
dismissFn();
}
},
[dismissFn, hasAutoDisplayPreference]
);
return (
<SidebarPanel
title="Need some help?"
panelName={uiConstants.PANEL_HELP}
onActiveChanged={onActiveChanged}
>
<h3 className="help-panel__sub-panel-title">
{subPanelTitles[activeSubPanel]}
</h3>
<div className="help-panel__content">
{activeSubPanel === 'tutorial' && <Tutorial />}
{activeSubPanel === 'versionInfo' && (
<VersionInfo versionData={versionData} />
)}
<div className="help-panel__footer">
{activeSubPanel === 'versionInfo' && (
<a
href="#"
className="help-panel__sub-panel-link help-panel__sub-panel-link--left"
onClick={e => openSubPanel(e, 'tutorial')}
>
<SvgIcon name="arrow-left" className="help-panel__icon" />
<div>Getting started</div>
</a>
)}
{activeSubPanel === 'tutorial' && (
<a
href="#"
className="help-panel__sub-panel-link help-panel__sub-panel-link--right"
onClick={e => openSubPanel(e, 'versionInfo')}
>
<div>About this version</div>
<SvgIcon name="arrow-right" className="help-panel__icon" />
</a>
)}
</div>
</div>
<div className="help-panel-tabs">
<HelpPanelTab
linkText="Help topics"
url="https://web.hypothes.is/help/"
/>
<HelpPanelTab linkText="New support ticket" url={supportTicketURL} />
</div>
</SidebarPanel>
);
}
HelpPanel.propTypes = {
/* Object with auth and user information */
auth: propTypes.object.isRequired,
session: propTypes.object.isRequired,
};
HelpPanel.injectedProps = ['session'];
module.exports = withServices(HelpPanel);
......@@ -4,6 +4,9 @@ const events = require('../events');
const { parseAccountID } = require('../util/account-id');
const serviceConfig = require('../service-config');
const bridgeEvents = require('../../shared/bridge-events');
const uiConstants = require('../ui-constants');
const isSidebar = require('../util/is-sidebar');
const { shouldAutoDisplayTutorial } = require('../util/session-util');
/**
* Return the user's authentication status from their profile.
......@@ -56,23 +59,34 @@ function HypothesisAppController(
// used by templates to show an intermediate or loading state.
this.auth = { status: 'unknown' };
// App dialogs
this.helpPanel = { visible: false };
// Check to see if we're in the sidebar, or on a standalone page such as
// the stream page or an individual annotation page.
this.isSidebar = $window.top !== $window;
this.isSidebar = isSidebar();
if (this.isSidebar) {
frameSync.connect();
}
// Reload the view when the user switches accounts
this.onUserChange = profile => {
self.auth = authStateFromProfile(profile);
if (
shouldAutoDisplayTutorial(
this.isSidebar,
store.getState().session,
settings
)
) {
// Auto-open the tutorial (help) panel
store.openSidebarPanel(uiConstants.PANEL_HELP);
}
};
$scope.$on(events.USER_CHANGED, function(event, data) {
self.auth = authStateFromProfile(data.profile);
self.onUserChange(data.profile);
});
session.load().then(profile => {
self.auth = authStateFromProfile(profile);
self.onUserChange(profile);
});
/**
......@@ -110,17 +124,6 @@ function HypothesisAppController(
$window.open(serviceUrl('signup'));
};
this.showHelpPanel = function() {
const service = serviceConfig(settings) || {};
if (service.onHelpRequestProvided) {
// Let the host page handle the help request.
bridge.call(bridgeEvents.HELP_REQUESTED);
return;
}
this.helpPanel.visible = true;
};
// Prompt to discard any unsaved drafts.
const promptToLogout = function() {
// TODO - Replace this with a UI which doesn't look terrible.
......
'use strict';
const angular = require('angular');
const { mount } = require('enzyme');
const { createElement } = require('preact');
const { act } = require('preact/test-utils');
describe('helpPanel', function() {
const HelpPanel = require('../help-panel');
const mockImportedComponents = require('./mock-imported-components');
describe('HelpPanel', function() {
let fakeAuth;
let fakeSessionService;
let fakeStore;
let $componentController;
let $rootScope;
let fakeVersionData;
let fakeVersionDataObject;
function createComponent(props) {
return mount(
<HelpPanel auth={fakeAuth} session={fakeSessionService} {...props} />
);
}
beforeEach(function() {
beforeEach(() => {
fakeAuth = {};
fakeSessionService = { dismissSidebarTutorial: sinon.stub() };
fakeStore = {
frames: sinon.stub().returns([]),
getState: sinon
.stub()
.returns({ session: { preferences: { show_sidebar_tutorial: true } } }),
mainFrame: sinon.stub().returns(null),
};
fakeVersionDataObject = {
asEncodedURLString: sinon.stub().returns('fakeURLString'),
};
fakeVersionData = sinon.stub().returns(fakeVersionDataObject);
HelpPanel.$imports.$mock(mockImportedComponents());
HelpPanel.$imports.$mock({
'../store/use-store': callback => callback(fakeStore),
'../util/version-data': fakeVersionData,
});
});
afterEach(() => {
HelpPanel.$imports.$restore();
});
context('when viewing tutorial sub-panel', () => {
it('should show tutorial by default', () => {
const wrapper = createComponent();
const subHeader = wrapper.find('.help-panel__sub-panel-title');
assert.equal(subHeader.text(), 'Getting started');
assert.isTrue(wrapper.find('Tutorial').exists());
assert.isFalse(wrapper.find('VersionInfo').exists());
});
it('should show navigation link to versionInfo sub-panel', () => {
const wrapper = createComponent();
const link = wrapper.find('.help-panel__sub-panel-link');
assert.equal(link.text(), 'About this version');
});
it('should switch to versionInfo sub-panel when footer link clicked', () => {
const wrapper = createComponent();
wrapper.find('.help-panel__sub-panel-link').simulate('click');
assert.equal(
wrapper.find('.help-panel__sub-panel-title').text(),
'About this version'
);
assert.isTrue(wrapper.find('VersionInfo').exists());
assert.equal(
wrapper.find('VersionInfo').prop('versionData'),
fakeVersionDataObject
);
assert.isFalse(wrapper.find('Tutorial').exists());
});
});
context('when viewing versionInfo sub-panel', () => {
it('should show navigation link back to tutorial sub-panel', () => {
const wrapper = createComponent();
wrapper.find('.help-panel__sub-panel-link').simulate('click');
const link = wrapper.find('.help-panel__sub-panel-link');
angular.module('h', []).component('helpPanel', require('../help-panel'));
assert.isTrue(wrapper.find('VersionInfo').exists());
assert.isFalse(wrapper.find('Tutorial').exists());
assert.equal(link.text(), 'Getting started');
});
it('should switch to tutorial sub-panel when link clicked', () => {
const wrapper = createComponent();
// Click to get to version-info sub-panel...
wrapper.find('.help-panel__sub-panel-link').simulate('click');
const link = wrapper.find('.help-panel__sub-panel-link');
// Click again to get back to tutorial sub-panel
link.simulate('click');
assert.isFalse(wrapper.find('VersionInfo').exists());
assert.isTrue(wrapper.find('Tutorial').exists());
});
});
describe('`HelpPanelTab`s', () => {
it('should render static link to knowledge base', () => {
const wrapper = createComponent();
angular.mock.module('h', {
store: fakeStore,
serviceUrl: sinon.stub(),
assert.isTrue(
wrapper
.find('HelpPanelTab')
.filter({ linkText: 'Help topics' })
.exists()
);
});
angular.mock.inject(function(_$componentController_, _$rootScope_) {
$componentController = _$componentController_;
$rootScope = _$rootScope_;
it('should render dynamic link to create a new help ticket', () => {
const wrapper = createComponent();
const helpTab = wrapper
.find('HelpPanelTab')
.filter({ linkText: 'New support ticket' });
assert.isTrue(helpTab.exists());
assert.include(helpTab.prop('url'), 'fakeURLString');
});
});
it('displays the URL and fingerprint of the first connected frame', function() {
fakeStore.frames.returns([
{
uri: 'https://publisher.org/article.pdf',
metadata: {
documentFingerprint: '12345',
},
},
]);
const $scope = $rootScope.$new();
const ctrl = $componentController('helpPanel', { $scope: $scope });
$scope.$digest();
assert.equal(ctrl.url, 'https://publisher.org/article.pdf');
assert.equal(ctrl.documentFingerprint, '12345');
context('dismissing the tutorial and clearing profile setting', () => {
context('profile preference to auto-show tutorial is truthy', () => {
beforeEach(() => {
fakeStore.getState.returns({
session: { preferences: { show_sidebar_tutorial: true } },
});
});
it('should not dismiss the panel when it is initially opened', () => {
const wrapper = createComponent();
const onActiveChanged = wrapper
.find('SidebarPanel')
.prop('onActiveChanged');
act(() => {
// "Activate" the panel (simulate the `SidebarPanel` communicating
// an active state via callback prop)
onActiveChanged(true);
});
assert.notOk(fakeSessionService.dismissSidebarTutorial.callCount);
});
it('should invoke dismiss service method when panel is first closed', () => {
const wrapper = createComponent();
const onActiveChanged = wrapper
.find('SidebarPanel')
.prop('onActiveChanged');
act(() => {
// "Activate" the panel (simulate the `SidebarPanel` communicating
// an active state via callback prop)
onActiveChanged(true);
// Now "close" the panel
onActiveChanged(false);
});
assert.calledOnce(fakeSessionService.dismissSidebarTutorial);
});
});
context('profile preference to auto-show tutorial is falsy', () => {
beforeEach(() => {
fakeStore.getState.returns({
session: { preferences: { show_sidebar_tutorial: false } },
});
});
it('should not invoke dismiss service method when panel is closed', () => {
const wrapper = createComponent();
const onActiveChanged = wrapper
.find('SidebarPanel')
.prop('onActiveChanged');
act(() => {
// "Activate" the panel (simulate the `SidebarPanel` communicating
// an active state via callback prop)
onActiveChanged(true);
// Now "close" the panel
onActiveChanged(false);
});
assert.notOk(fakeSessionService.dismissSidebarTutorial.callCount);
});
});
});
});
......@@ -18,9 +18,11 @@ describe('sidebar.components.hypothesis-app', function() {
let fakeFeatures = null;
let fakeFlash = null;
let fakeFrameSync = null;
let fakeIsSidebar = null;
let fakeParams = null;
let fakeServiceConfig = null;
let fakeSession = null;
let fakeShouldAutoDisplayTutorial = null;
let fakeGroups = null;
let fakeRoute = null;
let fakeServiceUrl = null;
......@@ -40,10 +42,16 @@ describe('sidebar.components.hypothesis-app', function() {
});
beforeEach(function() {
fakeIsSidebar = sandbox.stub().returns(true);
fakeServiceConfig = sandbox.stub();
fakeShouldAutoDisplayTutorial = sinon.stub().returns(false);
hypothesisApp.$imports.$mock({
'../util/is-sidebar': fakeIsSidebar,
'../service-config': fakeServiceConfig,
'../util/session-util': {
shouldAutoDisplayTutorial: fakeShouldAutoDisplayTutorial,
},
});
angular.module('h', []).component('hypothesisApp', hypothesisApp);
......@@ -60,8 +68,15 @@ describe('sidebar.components.hypothesis-app', function() {
fakeStore = {
tool: 'comment',
clearSelectedAnnotations: sandbox.spy(),
getState: sinon.stub(),
getState: sinon.stub().returns({
session: {
preferences: {
show_sidebar_tutorial: false,
},
},
}),
clearGroups: sinon.stub(),
openSidebarPanel: sinon.stub(),
// draft store
countDrafts: sandbox.stub().returns(0),
discardAllDrafts: sandbox.stub(),
......@@ -143,32 +158,36 @@ describe('sidebar.components.hypothesis-app', function() {
sandbox.restore();
});
describe('#isSidebar', function() {
it('is false if the window is the top window', function() {
fakeWindow.top = fakeWindow;
const ctrl = createController();
assert.isFalse(ctrl.isSidebar);
});
it('is true if the window is not the top window', function() {
fakeWindow.top = {};
const ctrl = createController();
assert.isTrue(ctrl.isSidebar);
});
});
it('connects to host frame in the sidebar app', function() {
fakeWindow.top = {};
fakeIsSidebar.returns(true);
createController();
assert.called(fakeFrameSync.connect);
});
it('does not connect to the host frame in the stream', function() {
fakeWindow.top = fakeWindow;
fakeIsSidebar.returns(false);
createController();
assert.notCalled(fakeFrameSync.connect);
});
describe('auto-opening tutorial', () => {
it('should open tutorial on profile load when criteria are met', () => {
fakeShouldAutoDisplayTutorial.returns(true);
createController();
return fakeSession.load().then(() => {
assert.calledOnce(fakeStore.openSidebarPanel);
});
});
it('should not open tutorial on profile load when criteria are not met', () => {
fakeShouldAutoDisplayTutorial.returns(false);
createController();
return fakeSession.load().then(() => {
assert.equal(fakeStore.openSidebarPanel.callCount, 0);
});
});
});
it('auth.status is "unknown" on startup', function() {
const ctrl = createController();
assert.equal(ctrl.auth.status, 'unknown');
......@@ -290,66 +309,6 @@ describe('sidebar.components.hypothesis-app', function() {
});
});
describe('#showHelpPanel', function() {
context('when using a third-party service', function() {
context("when there's no onHelpRequest callback function", function() {
beforeEach('configure a service with no onHelpRequest', function() {
fakeServiceConfig.returns({});
});
it('does not send an event', function() {
createController().showHelpPanel();
assert.notCalled(fakeBridge.call);
});
it('shows the help panel', function() {
const ctrl = createController();
ctrl.showHelpPanel();
assert.isTrue(ctrl.helpPanel.visible);
});
});
context("when there's an onHelpRequest callback function", function() {
beforeEach('provide an onHelpRequest callback', function() {
fakeServiceConfig.returns({ onHelpRequestProvided: true });
});
it('sends the HELP_REQUESTED event', function() {
createController().showHelpPanel();
assert.calledWith(fakeBridge.call, bridgeEvents.HELP_REQUESTED);
});
it('does not show the help panel', function() {
const ctrl = createController();
ctrl.showHelpPanel();
assert.isFalse(ctrl.helpPanel.visible);
});
});
});
context('when not using a third-party service', function() {
it('does not send an event', function() {
createController().showHelpPanel();
assert.notCalled(fakeBridge.call);
});
it('shows the help panel', function() {
const ctrl = createController();
ctrl.showHelpPanel();
assert.isTrue(ctrl.helpPanel.visible);
});
});
});
describe('#login()', function() {
beforeEach(() => {
fakeAuth.login = sinon.stub().returns(Promise.resolve());
......
......@@ -4,15 +4,18 @@ const { createElement } = require('preact');
const { mount } = require('enzyme');
const uiConstants = require('../../ui-constants');
const bridgeEvents = require('../../../shared/bridge-events');
const TopBar = require('../top-bar');
const mockImportedComponents = require('./mock-imported-components');
describe('TopBar', () => {
const fakeSettings = {};
let fakeBridge;
let fakeStore;
let fakeStreamer;
let fakeIsThirdPartyService;
let fakeServiceConfig;
beforeEach(() => {
fakeIsThirdPartyService = sinon.stub().returns(false);
......@@ -29,6 +32,12 @@ describe('TopBar', () => {
toggleSidebarPanel: sinon.stub(),
};
fakeBridge = {
call: sinon.stub(),
};
fakeServiceConfig = sinon.stub().returns({});
fakeStreamer = {
applyPendingUpdates: sinon.stub(),
};
......@@ -37,6 +46,7 @@ describe('TopBar', () => {
TopBar.$imports.$mock({
'../store/use-store': callback => callback(fakeStore),
'../util/is-third-party-service': fakeIsThirdPartyService,
'../service-config': fakeServiceConfig,
});
});
......@@ -57,6 +67,7 @@ describe('TopBar', () => {
return mount(
<TopBar
auth={auth}
bridge={fakeBridge}
isSidebar={true}
settings={fakeSettings}
streamer={fakeStreamer}
......@@ -86,14 +97,41 @@ describe('TopBar', () => {
assert.called(fakeStreamer.applyPendingUpdates);
});
it('shows Help Panel when help icon is clicked', () => {
const onShowHelpPanel = sinon.stub();
const wrapper = createTopBar({
onShowHelpPanel: onShowHelpPanel,
describe('`HelpButton` and help requests', () => {
context('no help service handler configured in services (default)', () => {
it('toggles Help Panel on click', () => {
const wrapper = createTopBar();
const help = helpBtn(wrapper);
help.simulate('click');
assert.calledWith(fakeStore.toggleSidebarPanel, uiConstants.PANEL_HELP);
});
it('displays a help icon active state when help panel active', () => {
// state returning active sidebar panel as `PANEL_HELP` triggers active class
fakeStore.getState = sinon.stub().returns({
sidebarPanels: {
activePanelName: uiConstants.PANEL_HELP,
},
});
const wrapper = createTopBar();
const help = helpBtn(wrapper);
wrapper.update();
assert.isTrue(help.hasClass('top-bar__btn--active'));
assert.isOk(help.prop('aria-expanded'));
});
context('help service handler configured in services', () => {
it('fires a bridge event if help clicked and service is configured', () => {
fakeServiceConfig.returns({ onHelpRequestProvided: true });
const wrapper = createTopBar();
const help = helpBtn(wrapper);
help.simulate('click');
assert.equal(fakeStore.toggleSidebarPanel.callCount, 0);
assert.calledWith(fakeBridge.call, bridgeEvents.HELP_REQUESTED);
});
});
});
const help = helpBtn(wrapper);
help.simulate('click');
assert.called(onShowHelpPanel);
});
describe('login/account actions', () => {
......@@ -170,7 +208,10 @@ describe('TopBar', () => {
it('toggles the share annotations panel when "Share" is clicked', () => {
const wrapper = createTopBar();
wrapper.find('[title="Share annotations on this page"]').simulate('click');
assert.called(fakeStore.toggleSidebarPanel);
assert.calledWith(
fakeStore.toggleSidebarPanel,
uiConstants.PANEL_SHARE_ANNOTATIONS
);
});
it('adds an active-state class to the "Share" icon when the panel is open', () => {
......@@ -217,14 +258,5 @@ describe('TopBar', () => {
assert.isFalse(wrapper.exists('SortMenu'));
assert.isFalse(wrapper.exists('button[title="Share this page"]'));
});
it('does show the Help menu and user menu', () => {
const wrapper = createTopBar({
isSidebar: false,
auth: { status: 'logged-in' },
});
assert.isTrue(wrapper.exists('button[title="Help"]'));
assert.isTrue(wrapper.exists('UserMenu'));
});
});
});
......@@ -4,9 +4,11 @@ const { Fragment, createElement } = require('preact');
const classnames = require('classnames');
const propTypes = require('prop-types');
const bridgeEvents = require('../../shared/bridge-events');
const useStore = require('../store/use-store');
const { applyTheme } = require('../util/theme');
const isThirdPartyService = require('../util/is-third-party-service');
const serviceConfig = require('../service-config');
const { withServices } = require('../util/service-context');
const uiConstants = require('../ui-constants');
......@@ -17,16 +19,44 @@ const SortMenu = require('./sort-menu');
const SvgIcon = require('./svg-icon');
const UserMenu = require('./user-menu');
/**
* Button for opening/closing the help panel
*/
function HelpButton({ onClick }) {
const isActive = useStore(
store =>
store.getState().sidebarPanels.activePanelName === uiConstants.PANEL_HELP
);
return (
<button
className={classnames('top-bar__btn top-bar__help-btn', {
'top-bar__btn--active': isActive,
})}
onClick={onClick}
title="Help"
aria-expanded={isActive}
aria-pressed={isActive}
>
<SvgIcon name="help" className="top-bar__help-icon" />
</button>
);
}
HelpButton.propTypes = {
/* callback */
onClick: propTypes.func.isRequired,
};
/**
* The toolbar which appears at the top of the sidebar providing actions
* to switch groups, view account information, sort/filter annotations etc.
*/
function TopBar({
auth,
bridge,
isSidebar,
onLogin,
onLogout,
onShowHelpPanel,
onSignUp,
settings,
streamer,
......@@ -51,6 +81,19 @@ function TopBar({
togglePanelFn(uiConstants.PANEL_SHARE_ANNOTATIONS);
};
/**
* Open the help panel, or, if a service callback is configured to handle
* help requests, fire a relevant event instead
*/
const requestHelp = () => {
const service = serviceConfig(settings) || {};
if (service.onHelpRequestProvided) {
bridge.call(bridgeEvents.HELP_REQUESTED);
} else {
togglePanelFn(uiConstants.PANEL_HELP);
}
};
const loginControl = (
<Fragment>
{auth.status === 'unknown' && (
......@@ -82,14 +125,7 @@ function TopBar({
<div className="top-bar__inner content">
<StreamSearchInput />
<div className="top-bar__expander" />
<button
className="top-bar__btn top-bar__help-btn"
onClick={onShowHelpPanel}
title="Help"
aria-label="Help"
>
<SvgIcon name="help" className="top-bar__help-icon" />
</button>
<HelpButton onClick={requestHelp} />
{loginControl}
</div>
)}
......@@ -124,14 +160,7 @@ function TopBar({
<SvgIcon name="share" />
</button>
)}
<button
className="top-bar__btn top-bar__help-btn"
onClick={onShowHelpPanel}
title="Help"
aria-label="Help"
>
<SvgIcon name="help" className="top-bar__help-icon" />
</button>
<HelpButton onClick={requestHelp} />
{loginControl}
</div>
)}
......@@ -152,16 +181,13 @@ TopBar.propTypes = {
username: propTypes.string,
}),
bridge: propTypes.object.isRequired,
/**
* Flag indicating whether the app is the sidebar or a top-level page.
*/
isSidebar: propTypes.bool,
/**
* Callback invoked when user clicks "Help" button.
*/
onShowHelpPanel: propTypes.func,
/**
* Callback invoked when user clicks "Login" button.
*/
......@@ -178,6 +204,6 @@ TopBar.propTypes = {
streamer: propTypes.object,
};
TopBar.injectedProps = ['settings', 'streamer'];
TopBar.injectedProps = ['bridge', 'settings', 'streamer'];
module.exports = withServices(TopBar);
......@@ -155,6 +155,8 @@ function startAngularApp(config) {
.component(
'helpLink',
wrapReactComponent(require('./components/help-link'))
'helpPanel',
wrapReactComponent(require('./components/help-panel'))
)
.component('helpPanel', require('./components/help-panel'))
.component(
......
......@@ -4,16 +4,11 @@
on-login="vm.login()"
on-sign-up="vm.signUp()"
on-logout="vm.logout()"
on-show-help-panel="vm.showHelpPanel()"
is-sidebar="::vm.isSidebar">
</top-bar>
<div class="content">
<sidebar-tutorial ng-if="vm.isSidebar"></sidebar-tutorial>
<help-panel ng-if="vm.helpPanel.visible"
on-close="vm.helpPanel.visible = false"
auth="vm.auth">
</help-panel>
<help-panel auth="vm.auth"></help-panel>
<share-annotations-panel></share-annotations-panel>
<main ng-view=""></main>
</div>
......
......@@ -5,6 +5,7 @@
*/
module.exports = {
PANEL_HELP: 'help',
PANEL_SHARE_ANNOTATIONS: 'shareGroupAnnotations',
TAB_ANNOTATIONS: 'annotation',
TAB_NOTES: 'note',
......
.help-panel {
@include font-normal;
background: $grey-3;
margin-bottom: 0.72em;
padding: $layout-h-margin;
border-radius: 2px;
}
&__sub-panel-title {
margin: 0;
padding: 0.5em;
text-align: center;
font-size: 1.25em;
font-weight: 600;
}
.help-panel-title {
color: $grey-6;
font-weight: bold;
// Margin between top of the dialog and
// top of x-height of title should be ~15px.
margin-top: -5px;
}
&__content {
padding: 0.5em;
border-top: 1px solid $grey-3;
border-bottom: 1px solid $grey-3;
line-height: $normal-line-height;
font-size: $normal-font-size;
}
.help-panel-content {
// Margin between bottom of ascent of title and
// top of x-height of content should be 20px.
margin-top: 11px;
}
&__icon {
width: 12px;
height: 12px;
}
.help-panel-content__key {
width: 100px;
float: left;
color: $grey-4;
}
&__footer {
padding: 0.5em 0;
display: flex;
align-items: center;
}
.help-panel-content__val {
word-wrap: break-word;
margin-left: 100px;
}
&__sub-panel-link {
display: flex;
align-items: center;
color: $brand;
.help-panel-content {
margin-top: 10px;
margin-bottom: 15px;
}
&--right {
margin-left: auto;
}
&-icon {
margin: 5px;
width: 12px;
height: 12px;
}
}
&-tabs {
display: flex;
align-items: center;
&__tab {
flex: 1 1 0px;
margin-top: 0.5em;
border-right: 1px solid $grey-3;
text-align: center;
font-size: 1.25em;
color: $grey-5;
&:last-of-type {
border-right: none;
}
}
&__link {
color: $grey-5;
}
.help-panel-content__link {
color: $grey-6;
text-decoration: underline;
&:hover {
text-decoration: underline;
&__icon {
width: 12px;
height: 12px;
}
}
}
......@@ -15,6 +15,16 @@
border-bottom-style: solid;
}
&__subheader {
width: 100%;
text-align: center;
padding: 1em 0.5em;
border-bottom: 1px solid $grey-3;
font-size: 1.25em;
font-weight: 500;
color: $grey-5;
}
&__title {
color: $brand;
font-size: $body2-font-size;
......@@ -44,7 +54,7 @@
}
&__content {
padding: 0.75em;
padding-top: 0;
margin: 1em;
margin-top: 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