Commit 3366264d authored by Lyza Danger Gardner's avatar Lyza Danger Gardner

Use `IconButton` in several components

- Eliminate custom sub-components that fulfill the same task
- Replace `AnnotationActionButton` with `IconButton` where used
- Consolidate and adjust styling
parent a1b6559f
...@@ -6,11 +6,11 @@ const { createElement } = require('preact'); ...@@ -6,11 +6,11 @@ const { createElement } = require('preact');
const { withServices } = require('../util/service-context'); const { withServices } = require('../util/service-context');
const { isShareable, shareURI } = require('../util/annotation-sharing'); const { isShareable, shareURI } = require('../util/annotation-sharing');
const AnnotationActionButton = require('./annotation-action-button');
const AnnotationShareControl = require('./annotation-share-control'); const AnnotationShareControl = require('./annotation-share-control');
const IconButton = require('./icon-button');
/** /**
* A collection of `AnnotationActionButton`s in the footer area of an annotation. * A collection of `IconButton`s in the footer area of an annotation.
*/ */
function AnnotationActionBar({ function AnnotationActionBar({
annotation, annotation,
...@@ -45,16 +45,12 @@ function AnnotationActionBar({ ...@@ -45,16 +45,12 @@ function AnnotationActionBar({
return ( return (
<div className="annotation-action-bar"> <div className="annotation-action-bar">
{showEditAction && ( {showEditAction && (
<AnnotationActionButton icon="edit" label="Edit" onClick={onEdit} /> <IconButton icon="edit" title="Edit" onClick={onEdit} />
)} )}
{showDeleteAction && ( {showDeleteAction && (
<AnnotationActionButton <IconButton icon="trash" title="Delete" onClick={onDelete} />
icon="trash"
label="Delete"
onClick={onDelete}
/>
)} )}
<AnnotationActionButton icon="reply" label="Reply" onClick={onReply} /> <IconButton icon="reply" title="Reply" onClick={onReply} />
{showShareAction && ( {showShareAction && (
<AnnotationShareControl <AnnotationShareControl
group={annotationGroup} group={annotationGroup}
...@@ -63,17 +59,17 @@ function AnnotationActionBar({ ...@@ -63,17 +59,17 @@ function AnnotationActionBar({
/> />
)} )}
{showFlagAction && !annotation.flagged && ( {showFlagAction && !annotation.flagged && (
<AnnotationActionButton <IconButton
icon="flag" icon="flag"
label="Report this annotation to moderators" title="Report this annotation to moderators"
onClick={onFlag} onClick={onFlag}
/> />
)} )}
{showFlagAction && annotation.flagged && ( {showFlagAction && annotation.flagged && (
<AnnotationActionButton <IconButton
isActive={true} isActive={true}
icon="flag--active" icon="flag--active"
label="Annotation has been reported to the moderators" title="Annotation has been reported to the moderators"
/> />
)} )}
</div> </div>
......
...@@ -8,7 +8,7 @@ const useElementShouldClose = require('./hooks/use-element-should-close'); ...@@ -8,7 +8,7 @@ const useElementShouldClose = require('./hooks/use-element-should-close');
const { copyText } = require('../util/copy-to-clipboard'); const { copyText } = require('../util/copy-to-clipboard');
const { withServices } = require('../util/service-context'); const { withServices } = require('../util/service-context');
const AnnotationActionButton = require('./annotation-action-button'); const IconButton = require('./icon-button');
const ShareLinks = require('./share-links'); const ShareLinks = require('./share-links');
const SvgIcon = require('./svg-icon'); const SvgIcon = require('./svg-icon');
...@@ -84,12 +84,7 @@ function AnnotationShareControl({ ...@@ -84,12 +84,7 @@ function AnnotationShareControl({
return ( return (
<div className="annotation-share-control" ref={shareRef}> <div className="annotation-share-control" ref={shareRef}>
<AnnotationActionButton <IconButton icon="share" title="Share" onClick={toggleSharePanel} />
icon="share"
isDisabled={false}
label="Share"
onClick={toggleSharePanel}
/>
{isOpen && ( {isOpen && (
<div className="annotation-share-panel"> <div className="annotation-share-panel">
<div className="annotation-share-panel__header"> <div className="annotation-share-panel__header">
......
...@@ -15,11 +15,13 @@ function IconButton({ ...@@ -15,11 +15,13 @@ function IconButton({
isActive = false, isActive = false,
title, title,
onClick = () => null, onClick = () => null,
useCompactStyle = false,
}) { }) {
return ( return (
<button <button
className={classnames('icon-button', className, { className={classnames('icon-button', className, {
'is-active': isActive, 'is-active': isActive,
'icon-button--compact': useCompactStyle,
})} })}
onClick={onClick} onClick={onClick}
aria-pressed={isActive} aria-pressed={isActive}
...@@ -31,7 +33,10 @@ function IconButton({ ...@@ -31,7 +33,10 @@ function IconButton({
} }
IconButton.propTypes = { IconButton.propTypes = {
/** Optional additional class(es) to apply to the component element */ /** Optional additional class(es) to apply to the component element
* NB: Padding is controlled by the component's styles. Use
* `useCompactStyle` for tighter padding.
*/
className: propTypes.string, className: propTypes.string,
/** The name of the SVGIcon to render */ /** The name of the SVGIcon to render */
icon: propTypes.string.isRequired, icon: propTypes.string.isRequired,
...@@ -41,6 +46,8 @@ IconButton.propTypes = { ...@@ -41,6 +46,8 @@ IconButton.propTypes = {
title: propTypes.string.isRequired, title: propTypes.string.isRequired,
/** optional callback for clicks */ /** optional callback for clicks */
onClick: propTypes.func, onClick: propTypes.func,
/** tighten padding and make icon button fit in smaller space */
useCompactStyle: propTypes.bool,
}; };
module.exports = IconButton; module.exports = IconButton;
...@@ -7,6 +7,7 @@ const propTypes = require('prop-types'); ...@@ -7,6 +7,7 @@ const propTypes = require('prop-types');
const useStore = require('../store/use-store'); const useStore = require('../store/use-store');
const IconButton = require('./icon-button');
const Spinner = require('./spinner'); const Spinner = require('./spinner');
/** /**
...@@ -60,14 +61,13 @@ function SearchInput({ alwaysExpanded, query, onSearch }) { ...@@ -60,14 +61,13 @@ function SearchInput({ alwaysExpanded, query, onSearch }) {
onInput={e => setPendingQuery(e.target.value)} onInput={e => setPendingQuery(e.target.value)}
/> />
{!isLoading && ( {!isLoading && (
<button <IconButton
type="button" className="search-input__icon-button top-bar__icon-button"
className="search-input__icon top-bar__btn" icon="search"
title="Search"
onClick={() => input.current.focus()} onClick={() => input.current.focus()}
> title="Search annotations"
<i className="h-icon-search" /> useCompactStyle
</button> />
)} )}
{isLoading && <Spinner className="top-bar__btn" title="Loading…" />} {isLoading && <Spinner className="top-bar__btn" title="Loading…" />}
</form> </form>
......
...@@ -4,6 +4,7 @@ const { createElement } = require('preact'); ...@@ -4,6 +4,7 @@ const { createElement } = require('preact');
const useStore = require('../store/use-store'); const useStore = require('../store/use-store');
const IconButton = require('./icon-button');
const Menu = require('./menu'); const Menu = require('./menu');
const MenuItem = require('./menu-item'); const MenuItem = require('./menu-item');
...@@ -35,7 +36,14 @@ function SortMenu() { ...@@ -35,7 +36,14 @@ function SortMenu() {
); );
}); });
const menuLabel = <i className="h-icon-sort top-bar__btn" />; const menuLabel = (
<IconButton
className="top-bar__icon-button"
icon="sort"
title="Sort annotations"
useCompactStyle
/>
);
return ( return (
<div className="sort-menu"> <div className="sort-menu">
......
...@@ -52,7 +52,7 @@ describe('AnnotationActionBar', () => { ...@@ -52,7 +52,7 @@ describe('AnnotationActionBar', () => {
}; };
const getButton = (wrapper, iconName) => { const getButton = (wrapper, iconName) => {
return wrapper.find('AnnotationActionButton').filter({ icon: iconName }); return wrapper.find('IconButton').filter({ icon: iconName });
}; };
beforeEach(() => { beforeEach(() => {
......
...@@ -33,7 +33,7 @@ describe('AnnotationShareControl', () => { ...@@ -33,7 +33,7 @@ describe('AnnotationShareControl', () => {
function openElement(wrapper) { function openElement(wrapper) {
act(() => { act(() => {
wrapper wrapper
.find('AnnotationActionButton') .find('IconButton')
.props() .props()
.onClick(); .onClick();
}); });
...@@ -92,7 +92,7 @@ describe('AnnotationShareControl', () => { ...@@ -92,7 +92,7 @@ describe('AnnotationShareControl', () => {
act(() => { act(() => {
wrapper wrapper
.find('AnnotationActionButton') .find('IconButton')
.props() .props()
.onClick(); .onClick();
}); });
......
...@@ -54,12 +54,9 @@ describe('TopBar', () => { ...@@ -54,12 +54,9 @@ describe('TopBar', () => {
TopBar.$imports.$restore(); TopBar.$imports.$restore();
}); });
function applyUpdateBtn(wrapper) { // Helper to retrieve an `IconButton` by icon name, for convenience
return wrapper.find('.top-bar__btn--refresh'); function getButton(wrapper, iconName) {
} return wrapper.find('IconButton').filter({ icon: iconName });
function helpBtn(wrapper) {
return wrapper.find('.top-bar__help-btn');
} }
function createTopBar(props = {}) { function createTopBar(props = {}) {
...@@ -79,21 +76,23 @@ describe('TopBar', () => { ...@@ -79,21 +76,23 @@ describe('TopBar', () => {
it('shows the pending update count', () => { it('shows the pending update count', () => {
fakeStore.pendingUpdateCount.returns(1); fakeStore.pendingUpdateCount.returns(1);
const wrapper = createTopBar(); const wrapper = createTopBar();
const applyBtn = applyUpdateBtn(wrapper); const applyBtn = getButton(wrapper, 'refresh');
assert.isTrue(applyBtn.exists()); assert.isTrue(applyBtn.exists());
}); });
it('does not show the pending update count when there are no updates', () => { it('does not show the pending update count when there are no updates', () => {
const wrapper = createTopBar(); const wrapper = createTopBar();
const applyBtn = applyUpdateBtn(wrapper); const applyBtn = getButton(wrapper, 'refresh');
assert.isFalse(applyBtn.exists()); assert.isFalse(applyBtn.exists());
}); });
it('applies updates when clicked', () => { it('applies updates when clicked', () => {
fakeStore.pendingUpdateCount.returns(1); fakeStore.pendingUpdateCount.returns(1);
const wrapper = createTopBar(); const wrapper = createTopBar();
const applyBtn = applyUpdateBtn(wrapper); const applyBtn = getButton(wrapper, 'refresh');
applyBtn.simulate('click');
applyBtn.props().onClick();
assert.called(fakeStreamer.applyPendingUpdates); assert.called(fakeStreamer.applyPendingUpdates);
}); });
...@@ -101,8 +100,10 @@ describe('TopBar', () => { ...@@ -101,8 +100,10 @@ describe('TopBar', () => {
context('no help service handler configured in services (default)', () => { context('no help service handler configured in services (default)', () => {
it('toggles Help Panel on click', () => { it('toggles Help Panel on click', () => {
const wrapper = createTopBar(); const wrapper = createTopBar();
const help = helpBtn(wrapper); const helpButton = getButton(wrapper, 'help');
help.simulate('click');
helpButton.props().onClick();
assert.calledWith(fakeStore.toggleSidebarPanel, uiConstants.PANEL_HELP); assert.calledWith(fakeStore.toggleSidebarPanel, uiConstants.PANEL_HELP);
}); });
...@@ -114,20 +115,22 @@ describe('TopBar', () => { ...@@ -114,20 +115,22 @@ describe('TopBar', () => {
}, },
}); });
const wrapper = createTopBar(); const wrapper = createTopBar();
const help = helpBtn(wrapper); const helpButton = getButton(wrapper, 'help');
wrapper.update(); wrapper.update();
assert.isTrue(help.hasClass('is-active')); assert.isTrue(helpButton.props().isActive);
assert.isOk(help.prop('aria-pressed'));
}); });
context('help service handler configured in services', () => { context('help service handler configured in services', () => {
it('fires a bridge event if help clicked and service is configured', () => { it('fires a bridge event if help clicked and service is configured', () => {
fakeServiceConfig.returns({ onHelpRequestProvided: true }); fakeServiceConfig.returns({ onHelpRequestProvided: true });
const wrapper = createTopBar(); const wrapper = createTopBar();
const help = helpBtn(wrapper);
help.simulate('click'); const helpButton = getButton(wrapper, 'help');
helpButton.props().onClick();
assert.equal(fakeStore.toggleSidebarPanel.callCount, 0); assert.equal(fakeStore.toggleSidebarPanel.callCount, 0);
assert.calledWith(fakeBridge.call, bridgeEvents.HELP_REQUESTED); assert.calledWith(fakeBridge.call, bridgeEvents.HELP_REQUESTED);
}); });
...@@ -208,7 +211,10 @@ describe('TopBar', () => { ...@@ -208,7 +211,10 @@ describe('TopBar', () => {
it('toggles the share annotations panel when "Share" is clicked', () => { it('toggles the share annotations panel when "Share" is clicked', () => {
const wrapper = createTopBar(); const wrapper = createTopBar();
wrapper.find('.top-bar__share-btn').simulate('click'); const shareButton = getButton(wrapper, 'share');
shareButton.props().onClick();
assert.calledWith( assert.calledWith(
fakeStore.toggleSidebarPanel, fakeStore.toggleSidebarPanel,
uiConstants.PANEL_SHARE_ANNOTATIONS uiConstants.PANEL_SHARE_ANNOTATIONS
...@@ -221,11 +227,10 @@ describe('TopBar', () => { ...@@ -221,11 +227,10 @@ describe('TopBar', () => {
activePanelName: uiConstants.PANEL_SHARE_ANNOTATIONS, activePanelName: uiConstants.PANEL_SHARE_ANNOTATIONS,
}, },
}); });
const wrapper = createTopBar(); const wrapper = createTopBar();
const shareEl = wrapper.find('.top-bar__share-btn'); const shareButton = getButton(wrapper, 'share');
assert.include(shareEl.prop('className'), 'is-active'); assert.isTrue(shareButton.prop('isActive'));
}); });
it('displays search input in the sidebar', () => { it('displays search input in the sidebar', () => {
......
...@@ -13,48 +13,12 @@ const { withServices } = require('../util/service-context'); ...@@ -13,48 +13,12 @@ const { withServices } = require('../util/service-context');
const uiConstants = require('../ui-constants'); const uiConstants = require('../ui-constants');
const GroupList = require('./group-list'); const GroupList = require('./group-list');
const IconButton = require('./icon-button');
const SearchInput = require('./search-input'); const SearchInput = require('./search-input');
const StreamSearchInput = require('./stream-search-input'); const StreamSearchInput = require('./stream-search-input');
const SortMenu = require('./sort-menu'); const SortMenu = require('./sort-menu');
const SvgIcon = require('./svg-icon');
const UserMenu = require('./user-menu'); const UserMenu = require('./user-menu');
/**
* Reusable component to render a button for toggling a sidebar panel.
* Takes an `onClick` callback, as panel toggles are sometimes more complex than
* just opening and closing a panel.
*/
function TogglePanelButton({ panelName, iconName, title, onClick }) {
const currentActivePanel = useStore(
store => store.getState().sidebarPanels.activePanelName
);
const isActive = currentActivePanel === panelName;
return (
<button
className={classnames('top-bar__btn', `top-bar__${iconName}-btn`, {
'is-active': isActive,
})}
onClick={onClick}
title={title}
aria-pressed={isActive ? 'true' : 'false'}
>
<SvgIcon
name={iconName}
className={classnames(`top-bar__{$iconName}-icon`)}
/>
</button>
);
}
TogglePanelButton.propTypes = {
/** The panel's string name as defined in `uiConstants` */
panelName: propTypes.string.isRequired,
iconName: propTypes.string.isRequired,
title: propTypes.string.isRequired,
/** callback */
onClick: propTypes.func.isRequired,
};
/** /**
* The toolbar which appears at the top of the sidebar providing actions * The toolbar which appears at the top of the sidebar providing actions
* to switch groups, view account information, sort/filter annotations etc. * to switch groups, view account information, sort/filter annotations etc.
...@@ -86,6 +50,10 @@ function TopBar({ ...@@ -86,6 +50,10 @@ function TopBar({
togglePanelFn(uiConstants.PANEL_SHARE_ANNOTATIONS); togglePanelFn(uiConstants.PANEL_SHARE_ANNOTATIONS);
}; };
const currentActivePanel = useStore(
store => store.getState().sidebarPanels.activePanelName
);
/** /**
* Open the help panel, or, if a service callback is configured to handle * Open the help panel, or, if a service callback is configured to handle
* help requests, fire a relevant event instead * help requests, fire a relevant event instead
...@@ -130,11 +98,13 @@ function TopBar({ ...@@ -130,11 +98,13 @@ function TopBar({
<div className="top-bar__inner content"> <div className="top-bar__inner content">
<StreamSearchInput /> <StreamSearchInput />
<div className="top-bar__expander" /> <div className="top-bar__expander" />
<TogglePanelButton <IconButton
panelName={uiConstants.PANEL_HELP} className="top-bar__icon-button"
iconName="help" icon="help"
title="Help" isActive={currentActivePanel === uiConstants.PANEL_HELP}
onClick={requestHelp} onClick={requestHelp}
title="Help"
useCompactStyle
/> />
{loginControl} {loginControl}
</div> </div>
...@@ -145,31 +115,37 @@ function TopBar({ ...@@ -145,31 +115,37 @@ function TopBar({
<GroupList className="GroupList" auth={auth} /> <GroupList className="GroupList" auth={auth} />
<div className="top-bar__expander" /> <div className="top-bar__expander" />
{pendingUpdateCount > 0 && ( {pendingUpdateCount > 0 && (
<button <IconButton
className="top-bar__btn top-bar__btn--refresh" className="top-bar__icon-button top-bar__icon-button--refresh"
icon="refresh"
onClick={applyPendingUpdates} onClick={applyPendingUpdates}
title={`Show ${pendingUpdateCount} new/updated ${ title={`Show ${pendingUpdateCount} new/updated ${
pendingUpdateCount === 1 ? 'annotation' : 'annotations' pendingUpdateCount === 1 ? 'annotation' : 'annotations'
}`} }`}
> useCompactStyle
<SvgIcon className="top-bar__apply-icon" name="refresh" /> />
</button>
)} )}
<SearchInput query={filterQuery} onSearch={setFilterQuery} /> <SearchInput query={filterQuery} onSearch={setFilterQuery} />
<SortMenu /> <SortMenu />
{showSharePageButton && ( {showSharePageButton && (
<TogglePanelButton <IconButton
panelName={uiConstants.PANEL_SHARE_ANNOTATIONS} className="top-bar__icon-button"
iconName="share" icon="share"
title="Share annotations on this page" isActive={
currentActivePanel === uiConstants.PANEL_SHARE_ANNOTATIONS
}
onClick={toggleSharePanel} onClick={toggleSharePanel}
title="Share annotations on this page"
useCompactStyle
/> />
)} )}
<TogglePanelButton <IconButton
panelName={uiConstants.PANEL_HELP} className="top-bar__icon-button"
iconName="help" icon="help"
title="Help" isActive={currentActivePanel === uiConstants.PANEL_HELP}
onClick={requestHelp} onClick={requestHelp}
title="Help"
useCompactStyle
/> />
{loginControl} {loginControl}
</div> </div>
......
...@@ -8,6 +8,7 @@ const { isThirdPartyUser } = require('../util/account-id'); ...@@ -8,6 +8,7 @@ const { isThirdPartyUser } = require('../util/account-id');
const serviceConfig = require('../service-config'); const serviceConfig = require('../service-config');
const { withServices } = require('../util/service-context'); const { withServices } = require('../util/service-context');
const IconButton = require('./icon-button');
const Menu = require('./menu'); const Menu = require('./menu');
const MenuSection = require('./menu-section'); const MenuSection = require('./menu-section');
const MenuItem = require('./menu-item'); const MenuItem = require('./menu-item');
...@@ -44,7 +45,14 @@ function UserMenu({ auth, bridge, onLogout, serviceUrl, settings }) { ...@@ -44,7 +45,14 @@ function UserMenu({ auth, bridge, onLogout, serviceUrl, settings }) {
return props; return props;
})(); })();
const menuLabel = <i className="h-icon-account top-bar__btn" />; const menuLabel = (
<IconButton
className="top-bar__icon-button"
icon="profile"
title="User menu"
useCompactStyle
/>
);
return ( return (
<div className="user-menu"> <div className="user-menu">
<Menu label={menuLabel} title={auth.displayName} align="right"> <Menu label={menuLabel} title={auth.displayName} align="right">
......
...@@ -11,6 +11,10 @@ ...@@ -11,6 +11,10 @@
color: var.$brand; color: var.$brand;
} }
} }
&--compact {
padding: 0.25em;
}
} }
@media (pointer: coarse) { @media (pointer: coarse) {
...@@ -18,4 +22,12 @@ ...@@ -18,4 +22,12 @@
min-width: var.$touch-target-size; min-width: var.$touch-target-size;
min-height: var.$touch-target-size; min-height: var.$touch-target-size;
} }
// Until the top bar can be refactored to allow for breathing room around
// the search interface, we can't spare the room for comfortable tap targets
// on touchscreen devices. This overrides `IconButton`'s larger tap targets.
.icon-button--compact {
min-width: auto;
min-height: auto;
}
} }
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
color: var.$grey-mid; color: var.$grey-mid;
} }
.search-input__icon { .search-input__icon-button {
order: 0; order: 0;
} }
...@@ -27,11 +27,8 @@ ...@@ -27,11 +27,8 @@
width: 100%; width: 100%;
// The search box expands when focused, via a change in the // The search box expands when focused, via a change in the
// `max-width` property. In Safari, the <input> will not accept // `max-width` property.
// focus if `max-width` is set to 0px so we set it to max-width: 0px;
// a near-zero positive value instead.
// See https://github.com/hypothesis/h/issues/2654
max-width: 0.1px;
transition: max-width 0.3s ease-out, padding-left 0.3s ease-out; transition: max-width 0.3s ease-out, padding-left 0.3s ease-out;
......
...@@ -16,6 +16,24 @@ ...@@ -16,6 +16,24 @@
// Force top-bar onto a new compositor layer so that it does not judder when // Force top-bar onto a new compositor layer so that it does not judder when
// the window is scrolled. // the window is scrolled.
transform: translate3d(0, 0, 0); transform: translate3d(0, 0, 0);
&__icon-button {
&.is-active {
color: var.$grey-6;
&:hover {
color: var.$grey-6;
}
}
}
&__icon-button--refresh {
color: var.$brand;
&:hover {
color: var.$brand;
}
}
} }
.top-bar--theme-clean { .top-bar--theme-clean {
...@@ -55,29 +73,3 @@ ...@@ -55,29 +73,3 @@
.top-bar__expander { .top-bar__expander {
flex-grow: 1; flex-grow: 1;
} }
.top-bar__btn {
@include buttons.reset-native-btn-styles;
height: 100%;
color: var.$grey-semi;
display: inline-block;
cursor: pointer;
padding: 1px 3px 0 3px;
&:hover {
color: var.$grey-mid;
}
&.is-active {
color: var.$grey-mid;
}
&--refresh {
color: var.$brand;
&:hover {
color: var.$brand;
}
}
}
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