Commit d85d01d6 authored by Lyza Danger Gardner's avatar Lyza Danger Gardner

Replace `ActionButton`, `IconButton` components with `Button`

Converge button components into a single `Button` component and adjust
across various components accordingly. Refactor SASS for sanity.
parent 481aa74c
'use strict';
const classnames = require('classnames');
const propTypes = require('prop-types');
const { createElement } = require('preact');
const SvgIcon = require('./svg-icon');
/**
* A button, with (required) text label and an (optional) icon
*/
function ActionButton({
icon = '',
isActive = false,
isPrimary = false,
label,
onClick = () => null,
useCompactStyle = false,
}) {
return (
<button
className={classnames('action-button', {
'action-button--compact': useCompactStyle,
'action-button--primary': isPrimary,
'is-active': isActive,
})}
onClick={onClick}
aria-pressed={isActive}
title={label}
>
{icon && (
<SvgIcon
name={icon}
className={classnames('action-button__icon', {
'action-button__icon--compact': useCompactStyle,
'action-button__icon--primary': isPrimary,
})}
/>
)}
<span className="action-button__label">{label}</span>
</button>
);
}
ActionButton.propTypes = {
/** The name of the SVGIcon to render */
icon: propTypes.string,
/** Is this button currently in an "active" or in an "on" state? */
isActive: propTypes.bool,
/**
* Does this button represent the "primary" action available? If so,
* differentiating styles will be applied.
*/
isPrimary: propTypes.bool,
/** a label used for the `title` and `aria-label` attributes */
label: propTypes.string.isRequired,
/** callback for button clicks */
onClick: propTypes.func,
/** Allows a variant of this type of button that takes up less space */
useCompactStyle: propTypes.bool,
};
module.exports = ActionButton;
...@@ -7,10 +7,10 @@ const { withServices } = require('../util/service-context'); ...@@ -7,10 +7,10 @@ const { withServices } = require('../util/service-context');
const { isShareable, shareURI } = require('../util/annotation-sharing'); const { isShareable, shareURI } = require('../util/annotation-sharing');
const AnnotationShareControl = require('./annotation-share-control'); const AnnotationShareControl = require('./annotation-share-control');
const IconButton = require('./icon-button'); const Button = require('./button');
/** /**
* A collection of `IconButton`s in the footer area of an annotation. * A collection of `Button`s in the footer area of an annotation.
*/ */
function AnnotationActionBar({ function AnnotationActionBar({
annotation, annotation,
...@@ -44,13 +44,11 @@ function AnnotationActionBar({ ...@@ -44,13 +44,11 @@ function AnnotationActionBar({
return ( return (
<div className="annotation-action-bar"> <div className="annotation-action-bar">
{showEditAction && ( {showEditAction && <Button icon="edit" title="Edit" onClick={onEdit} />}
<IconButton icon="edit" title="Edit" onClick={onEdit} />
)}
{showDeleteAction && ( {showDeleteAction && (
<IconButton icon="trash" title="Delete" onClick={onDelete} /> <Button icon="trash" title="Delete" onClick={onDelete} />
)} )}
<IconButton icon="reply" title="Reply" onClick={onReply} /> <Button icon="reply" title="Reply" onClick={onReply} />
{showShareAction && ( {showShareAction && (
<AnnotationShareControl <AnnotationShareControl
group={annotationGroup} group={annotationGroup}
...@@ -59,14 +57,14 @@ function AnnotationActionBar({ ...@@ -59,14 +57,14 @@ function AnnotationActionBar({
/> />
)} )}
{showFlagAction && !annotation.flagged && ( {showFlagAction && !annotation.flagged && (
<IconButton <Button
icon="flag" icon="flag"
title="Report this annotation to moderators" title="Report this annotation to moderators"
onClick={onFlag} onClick={onFlag}
/> />
)} )}
{showFlagAction && annotation.flagged && ( {showFlagAction && annotation.flagged && (
<IconButton <Button
isActive={true} isActive={true}
icon="flag--active" icon="flag--active"
title="Annotation has been reported to the moderators" title="Annotation has been reported to the moderators"
......
...@@ -6,7 +6,7 @@ const { createElement } = require('preact'); ...@@ -6,7 +6,7 @@ const { createElement } = require('preact');
const { applyTheme } = require('../util/theme'); const { applyTheme } = require('../util/theme');
const { withServices } = require('../util/service-context'); const { withServices } = require('../util/service-context');
const ActionButton = require('./action-button'); const Button = require('./button');
const Menu = require('./menu'); const Menu = require('./menu');
const MenuItem = require('./menu-item'); const MenuItem = require('./menu-item');
...@@ -75,9 +75,9 @@ function AnnotationPublishControl({ ...@@ -75,9 +75,9 @@ function AnnotationPublishControl({
/> />
</Menu> </Menu>
</div> </div>
<ActionButton <Button
icon="cancel" icon="cancel"
label="Cancel" buttonText="Cancel"
onClick={onCancel} onClick={onCancel}
useCompactStyle useCompactStyle
/> />
......
...@@ -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 IconButton = require('./icon-button'); const Button = require('./button');
const ShareLinks = require('./share-links'); const ShareLinks = require('./share-links');
const SvgIcon = require('./svg-icon'); const SvgIcon = require('./svg-icon');
...@@ -84,7 +84,7 @@ function AnnotationShareControl({ ...@@ -84,7 +84,7 @@ function AnnotationShareControl({
return ( return (
<div className="annotation-share-control" ref={shareRef}> <div className="annotation-share-control" ref={shareRef}>
<IconButton icon="share" title="Share" onClick={toggleSharePanel} /> <Button icon="share" title="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">
...@@ -102,13 +102,13 @@ function AnnotationShareControl({ ...@@ -102,13 +102,13 @@ function AnnotationShareControl({
readOnly readOnly
ref={inputRef} ref={inputRef}
/> />
<button <Button
className="annotation-share-panel__copy-btn" icon="copy"
aria-label="Copy share link to clipboard" title="Copy share link to clipboard"
onClick={copyShareLink} onClick={copyShareLink}
> useInputStyle
<SvgIcon name="copy" /> useCompactStyle
</button> />
</div> </div>
<div className="annotation-share-panel__permissions"> <div className="annotation-share-panel__permissions">
{annotationSharingInfo} {annotationSharingInfo}
......
'use strict';
const classnames = require('classnames');
const propTypes = require('prop-types');
const { createElement } = require('preact');
const SvgIcon = require('./svg-icon');
/**
* A button, one of three base types depending on provided props:
* - Icon-only button: `icon` present, `buttonText` missing
* - Text-only button: `buttonText` present, `icon` missing
* - Icon and text: both `icon` and `buttonText` present
*
* Buttons may be additionally styled with 0 to n of (none are mutually exclusive):
* - `useCompactStyle`: for fitting into tighter spaces
* - `useInputStyle`: for placing an icon-only button next to an input field
* - `usePrimaryStyle`: for applying "primary action" styling
* - `className`: arbitrary additional class name(s) to apply
*/
function Button({
buttonText = '',
className = '',
icon = '',
isActive = false,
onClick = () => null,
title,
useCompactStyle = false,
useInputStyle = false,
usePrimaryStyle = false,
}) {
// If `buttonText` is provided, the `title` prop is optional and the `button`'s
// `title` attribute will be set from the `buttonText`
title = title || buttonText;
const baseClassName = buttonText ? 'button--labeled' : 'button--icon-only';
return (
<button
className={classnames(
'button',
baseClassName,
{
'button--compact': useCompactStyle,
'button--input': useInputStyle,
'button--primary': usePrimaryStyle,
'is-active': isActive,
},
className
)}
onClick={onClick}
aria-pressed={isActive}
title={title}
>
{icon && <SvgIcon name={icon} className="button__icon" />}
{buttonText}
</button>
);
}
/**
* Validation callback for `propTypes`. The given `propName` is conditionally
* required depending on the presence or absence of the `buttonText` property. Return
* an `Error` on validation failure, per propTypes API.
*
* @return {Error|undefined}
*/
function requiredStringIfButtonTextMissing(props, propName, componentName) {
if (
typeof props.buttonText !== 'string' &&
typeof props[propName] !== 'string'
) {
return new Error(
`string property '${propName}' must be supplied to component '${componentName}'\
if string property 'buttonText' omitted`
);
}
return undefined;
}
Button.propTypes = {
/**
* The presence of this property indicates that the button will have a
* visibly-rendered label (with this prop's value). For brevity, when providing
* `buttonText`, the `title` prop is optional—if `title` is not present,
* the value of `buttonText` will be used for the button's `title` attribute,
* as they are typically identical. When this prop is missing, an icon-only
* button will be rendered.
*/
buttonText: propTypes.string,
/**
* optional CSS classes to add to the `button` element. These classes may
* control color, etc., but should not define padding or layout, which are
* owned by this component
*/
className: propTypes.string,
/**
* The name of the SVGIcon to render. This is optional if a `buttonText` is
* provided.
*/
icon: requiredStringIfButtonTextMissing,
/** Is this button currently in an "active"/"on" state? */
isActive: propTypes.bool,
/** callback for button clicks */
onClick: propTypes.func,
/**
* `title`, used for button `title`, is required unless `buttonText` is present
*/
title: requiredStringIfButtonTextMissing,
/** Allows a variant of button that takes up less space */
useCompactStyle: propTypes.bool,
/** Allows a variant of button that can sit right next to an input field */
useInputStyle: propTypes.bool,
/**
* Does this button represent the "primary" action available? If so,
* differentiating styles will be applied.
*/
usePrimaryStyle: propTypes.bool,
};
module.exports = Button;
'use strict';
const classnames = require('classnames');
const propTypes = require('prop-types');
const { createElement } = require('preact');
const SvgIcon = require('./svg-icon');
/**
* A simple icon-only button
*/
function IconButton({
className = '',
icon,
isActive = false,
title,
onClick = () => null,
useCompactStyle = false,
}) {
return (
<button
className={classnames('icon-button', className, {
'is-active': isActive,
'icon-button--compact': useCompactStyle,
})}
onClick={onClick}
aria-pressed={isActive}
title={title}
>
<SvgIcon name={icon} className="icon-button__icon" />
</button>
);
}
IconButton.propTypes = {
/** 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,
/** The name of the SVGIcon to render */
icon: propTypes.string.isRequired,
/** Is this button currently in an "active" or "on" state? */
isActive: propTypes.bool,
/** a value used for the `title` and `aria-label` attributes */
title: propTypes.string.isRequired,
/** optional callback for clicks */
onClick: propTypes.func,
/** tighten padding and make icon button fit in smaller space */
useCompactStyle: propTypes.bool,
};
module.exports = IconButton;
...@@ -7,7 +7,7 @@ const propTypes = require('prop-types'); ...@@ -7,7 +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 Button = require('./button');
const Spinner = require('./spinner'); const Spinner = require('./spinner');
/** /**
...@@ -61,7 +61,7 @@ function SearchInput({ alwaysExpanded, query, onSearch }) { ...@@ -61,7 +61,7 @@ function SearchInput({ alwaysExpanded, query, onSearch }) {
onInput={e => setPendingQuery(e.target.value)} onInput={e => setPendingQuery(e.target.value)}
/> />
{!isLoading && ( {!isLoading && (
<IconButton <Button
className="search-input__icon-button top-bar__icon-button" className="search-input__icon-button top-bar__icon-button"
icon="search" icon="search"
onClick={() => input.current.focus()} onClick={() => input.current.focus()}
......
...@@ -8,7 +8,7 @@ const { withServices } = require('../util/service-context'); ...@@ -8,7 +8,7 @@ const { withServices } = require('../util/service-context');
const uiConstants = require('../ui-constants'); const uiConstants = require('../ui-constants');
const useStore = require('../store/use-store'); const useStore = require('../store/use-store');
const ActionButton = require('./action-button'); const Button = require('./button');
/** /**
* Of the annotations in the thread `annThread`, how many * Of the annotations in the thread `annThread`, how many
...@@ -149,12 +149,12 @@ function SearchStatusBar({ rootThread }) { ...@@ -149,12 +149,12 @@ function SearchStatusBar({ rootThread }) {
<div> <div>
{modes.filtered && ( {modes.filtered && (
<div className="search-status-bar"> <div className="search-status-bar">
<ActionButton <Button
icon="cancel" icon="cancel"
label="Clear search" buttonText="Clear search"
onClick={actions.clearSelection} onClick={actions.clearSelection}
isPrimary
useCompactStyle useCompactStyle
usePrimaryStyle
/> />
<span className="search-status-bar__filtered-text"> <span className="search-status-bar__filtered-text">
{modeText.filtered} {modeText.filtered}
...@@ -170,11 +170,11 @@ function SearchStatusBar({ rootThread }) { ...@@ -170,11 +170,11 @@ function SearchStatusBar({ rootThread }) {
)} )}
{modes.selected && ( {modes.selected && (
<div className="search-status-bar"> <div className="search-status-bar">
<ActionButton <Button
label={modeText.selected} buttonText={modeText.selected}
onClick={actions.clearSelection} onClick={actions.clearSelection}
isPrimary
useCompactStyle useCompactStyle
usePrimaryStyle
/> />
</div> </div>
)} )}
......
...@@ -7,6 +7,7 @@ const { copyText } = require('../util/copy-to-clipboard'); ...@@ -7,6 +7,7 @@ const { copyText } = require('../util/copy-to-clipboard');
const { withServices } = require('../util/service-context'); const { withServices } = require('../util/service-context');
const uiConstants = require('../ui-constants'); const uiConstants = require('../ui-constants');
const Button = require('./button');
const ShareLinks = require('./share-links'); const ShareLinks = require('./share-links');
const SidebarPanel = require('./sidebar-panel'); const SidebarPanel = require('./sidebar-panel');
const SvgIcon = require('./svg-icon'); const SvgIcon = require('./svg-icon');
...@@ -76,14 +77,12 @@ function ShareAnnotationsPanel({ analytics, flash }) { ...@@ -76,14 +77,12 @@ function ShareAnnotationsPanel({ analytics, flash }) {
value={shareURI} value={shareURI}
readOnly readOnly
/> />
<button <Button
icon="copy"
onClick={copyShareLink} onClick={copyShareLink}
title="copy share link" title="Copy share link"
aria-label="Copy share link" useInputStyle
className="share-annotations-panel__copy-btn" />
>
<SvgIcon name="copy" />
</button>
</div> </div>
<p> <p>
{focusedGroup.type === 'private' ? ( {focusedGroup.type === 'private' ? (
......
...@@ -7,7 +7,7 @@ const scrollIntoView = require('scroll-into-view'); ...@@ -7,7 +7,7 @@ const scrollIntoView = require('scroll-into-view');
const useStore = require('../store/use-store'); const useStore = require('../store/use-store');
const ActionButton = require('./action-button'); const Button = require('./button');
const Slider = require('./slider'); const Slider = require('./slider');
/** /**
...@@ -47,9 +47,9 @@ function SidebarPanel({ children, panelName, title, onActiveChanged }) { ...@@ -47,9 +47,9 @@ function SidebarPanel({ children, panelName, title, onActiveChanged }) {
<div className="sidebar-panel__header"> <div className="sidebar-panel__header">
<div className="sidebar-panel__title u-stretch">{title}</div> <div className="sidebar-panel__title u-stretch">{title}</div>
<div> <div>
<ActionButton <Button
icon="cancel" icon="cancel"
label="Close" buttonText="Close"
onClick={closePanel} onClick={closePanel}
useCompactStyle useCompactStyle
/> />
......
...@@ -4,7 +4,7 @@ const { createElement } = require('preact'); ...@@ -4,7 +4,7 @@ const { createElement } = require('preact');
const useStore = require('../store/use-store'); const useStore = require('../store/use-store');
const IconButton = require('./icon-button'); const Button = require('./button');
const Menu = require('./menu'); const Menu = require('./menu');
const MenuItem = require('./menu-item'); const MenuItem = require('./menu-item');
...@@ -37,7 +37,7 @@ function SortMenu() { ...@@ -37,7 +37,7 @@ function SortMenu() {
}); });
const menuLabel = ( const menuLabel = (
<IconButton <Button
className="top-bar__icon-button" className="top-bar__icon-button"
icon="sort" icon="sort"
title="Sort annotations" title="Sort annotations"
......
'use strict';
const { createElement } = require('preact');
const { mount } = require('enzyme');
const ActionButton = require('../action-button');
const mockImportedComponents = require('./mock-imported-components');
describe('ActionButton', () => {
let fakeOnClick;
function createComponent(props = {}) {
return mount(
<ActionButton
icon="fakeIcon"
label="My Action"
onClick={fakeOnClick}
{...props}
/>
);
}
beforeEach(() => {
fakeOnClick = sinon.stub();
ActionButton.$imports.$mock(mockImportedComponents());
});
afterEach(() => {
ActionButton.$imports.$restore();
});
it('renders `SvgIcon` if icon property set', () => {
const wrapper = createComponent();
assert.equal(wrapper.find('SvgIcon').prop('name'), 'fakeIcon');
});
it('does not render `SvgIcon` if no icon property set', () => {
const wrapper = createComponent({ icon: undefined });
assert.isFalse(wrapper.find('SvgIcon').exists());
});
it('invokes `onClick` callback when pressed', () => {
const wrapper = createComponent();
wrapper.find('button').simulate('click');
assert.calledOnce(fakeOnClick);
});
it('adds compact styles if `useCompactStyle` is `true`', () => {
const wrapper = createComponent({ useCompactStyle: true });
assert.isTrue(wrapper.find('button').hasClass('action-button--compact'));
});
context('in active state (`isActive` is `true`)', () => {
it('adds `is-active` className', () => {
const wrapper = createComponent({ isActive: true });
assert.isTrue(wrapper.find('button').hasClass('is-active'));
});
it('sets `aria-pressed` attribute on button', () => {
const wrapper = createComponent({ isActive: true });
assert.isTrue(wrapper.find('button').prop('aria-pressed'));
});
});
});
...@@ -52,7 +52,7 @@ describe('AnnotationActionBar', () => { ...@@ -52,7 +52,7 @@ describe('AnnotationActionBar', () => {
}; };
const getButton = (wrapper, iconName) => { const getButton = (wrapper, iconName) => {
return wrapper.find('IconButton').filter({ icon: iconName }); return wrapper.find('Button').filter({ icon: iconName });
}; };
beforeEach(() => { beforeEach(() => {
......
...@@ -191,7 +191,7 @@ describe('AnnotationPublishControl', () => { ...@@ -191,7 +191,7 @@ describe('AnnotationPublishControl', () => {
const wrapper = createAnnotationPublishControl({ const wrapper = createAnnotationPublishControl({
onCancel: fakeOnCancel, onCancel: fakeOnCancel,
}); });
const cancelBtn = wrapper.find('ActionButton'); const cancelBtn = wrapper.find('Button');
cancelBtn.props().onClick(); cancelBtn.props().onClick();
......
...@@ -16,6 +16,10 @@ describe('AnnotationShareControl', () => { ...@@ -16,6 +16,10 @@ describe('AnnotationShareControl', () => {
let container; let container;
const getButton = (wrapper, iconName) => {
return wrapper.find('Button').filter({ icon: iconName });
};
function createComponent(props = {}) { function createComponent(props = {}) {
return mount( return mount(
<AnnotationShareControl <AnnotationShareControl
...@@ -33,7 +37,7 @@ describe('AnnotationShareControl', () => { ...@@ -33,7 +37,7 @@ describe('AnnotationShareControl', () => {
function openElement(wrapper) { function openElement(wrapper) {
act(() => { act(() => {
wrapper wrapper
.find('IconButton') .find('Button')
.props() .props()
.onClick(); .onClick();
}); });
...@@ -89,12 +93,10 @@ describe('AnnotationShareControl', () => { ...@@ -89,12 +93,10 @@ describe('AnnotationShareControl', () => {
it('toggles the share control element when the button is clicked', () => { it('toggles the share control element when the button is clicked', () => {
const wrapper = createComponent(); const wrapper = createComponent();
const button = getButton(wrapper, 'share');
act(() => { act(() => {
wrapper button.props().onClick();
.find('IconButton')
.props()
.onClick();
}); });
wrapper.update(); wrapper.update();
...@@ -115,7 +117,9 @@ describe('AnnotationShareControl', () => { ...@@ -115,7 +117,9 @@ describe('AnnotationShareControl', () => {
const wrapper = createComponent(); const wrapper = createComponent();
openElement(wrapper); openElement(wrapper);
wrapper.find('.annotation-share-panel__copy-btn').simulate('click'); getButton(wrapper, 'copy')
.props()
.onClick();
assert.calledWith( assert.calledWith(
fakeCopyToClipboard.copyText, fakeCopyToClipboard.copyText,
...@@ -127,7 +131,9 @@ describe('AnnotationShareControl', () => { ...@@ -127,7 +131,9 @@ describe('AnnotationShareControl', () => {
const wrapper = createComponent(); const wrapper = createComponent();
openElement(wrapper); openElement(wrapper);
wrapper.find('.annotation-share-panel__copy-btn').simulate('click'); getButton(wrapper, 'copy')
.props()
.onClick();
assert.calledWith(fakeFlash.info, 'Copied share link to clipboard'); assert.calledWith(fakeFlash.info, 'Copied share link to clipboard');
}); });
...@@ -137,7 +143,9 @@ describe('AnnotationShareControl', () => { ...@@ -137,7 +143,9 @@ describe('AnnotationShareControl', () => {
const wrapper = createComponent(); const wrapper = createComponent();
openElement(wrapper); openElement(wrapper);
wrapper.find('.annotation-share-panel__copy-btn').simulate('click'); getButton(wrapper, 'copy')
.props()
.onClick();
assert.calledWith(fakeFlash.error, 'Unable to copy link'); assert.calledWith(fakeFlash.error, 'Unable to copy link');
}); });
......
...@@ -129,14 +129,6 @@ describe('annotation', function() { ...@@ -129,14 +129,6 @@ describe('annotation', function() {
angular angular
.module('h', []) .module('h', [])
.component('annotation', annotationComponent) .component('annotation', annotationComponent)
.component('annotationActionButton', {
bindings: {
icon: '<',
isDisabled: '<',
label: '<',
onClick: '&',
},
})
.component('annotationBody', { .component('annotationBody', {
bindings: { bindings: {
collapse: '<', collapse: '<',
......
...@@ -3,15 +3,15 @@ ...@@ -3,15 +3,15 @@
const { createElement } = require('preact'); const { createElement } = require('preact');
const { mount } = require('enzyme'); const { mount } = require('enzyme');
const IconButton = require('../icon-button'); const Button = require('../button');
const mockImportedComponents = require('./mock-imported-components'); const mockImportedComponents = require('./mock-imported-components');
describe('IconButton', () => { describe('Button', () => {
let fakeOnClick; let fakeOnClick;
function createComponent(props = {}) { function createComponent(props = {}) {
return mount( return mount(
<IconButton <Button
icon="fakeIcon" icon="fakeIcon"
isActive={false} isActive={false}
title="My Action" title="My Action"
...@@ -23,11 +23,11 @@ describe('IconButton', () => { ...@@ -23,11 +23,11 @@ describe('IconButton', () => {
beforeEach(() => { beforeEach(() => {
fakeOnClick = sinon.stub(); fakeOnClick = sinon.stub();
IconButton.$imports.$mock(mockImportedComponents()); Button.$imports.$mock(mockImportedComponents());
}); });
afterEach(() => { afterEach(() => {
IconButton.$imports.$restore(); Button.$imports.$restore();
}); });
it('adds active className if `isActive` is `true`', () => { it('adds active className if `isActive` is `true`', () => {
...@@ -46,6 +46,20 @@ describe('IconButton', () => { ...@@ -46,6 +46,20 @@ describe('IconButton', () => {
assert.isTrue(wrapper.find('button').prop('aria-pressed')); assert.isTrue(wrapper.find('button').prop('aria-pressed'));
}); });
it('sets `title` to provided `title` prop', () => {
const wrapper = createComponent({});
assert.equal(wrapper.find('button').prop('title'), 'My Action');
});
it('uses `buttonText` to set `title` attr if `title` missing', () => {
const wrapper = createComponent({
buttonText: 'My Label',
title: undefined,
});
assert.equal(wrapper.find('button').prop('title'), 'My Label');
});
it('invokes `onClick` callback when pressed', () => { it('invokes `onClick` callback when pressed', () => {
const wrapper = createComponent(); const wrapper = createComponent();
wrapper.find('button').simulate('click'); wrapper.find('button').simulate('click');
...@@ -61,6 +75,18 @@ describe('IconButton', () => { ...@@ -61,6 +75,18 @@ describe('IconButton', () => {
it('sets compact style if `useCompactStyle` is set`', () => { it('sets compact style if `useCompactStyle` is set`', () => {
const wrapper = createComponent({ useCompactStyle: true }); const wrapper = createComponent({ useCompactStyle: true });
assert.isTrue(wrapper.find('button').hasClass('icon-button--compact')); assert.isTrue(wrapper.find('button').hasClass('button--compact'));
});
it('sets input style if `useInputStyle` is set', () => {
const wrapper = createComponent({ useInputStyle: true });
assert.isTrue(wrapper.find('button').hasClass('button--input'));
});
it('sets primary style if `usePrimaryStyle` is set`', () => {
const wrapper = createComponent({ usePrimaryStyle: true });
assert.isTrue(wrapper.find('button').hasClass('button--primary'));
}); });
}); });
...@@ -113,8 +113,8 @@ describe('SearchStatusBar', () => { ...@@ -113,8 +113,8 @@ describe('SearchStatusBar', () => {
const wrapper = createComponent({}); const wrapper = createComponent({});
const button = wrapper.find('ActionButton'); const button = wrapper.find('Button');
assert.equal(button.props().label, 'Clear search'); assert.equal(button.props().buttonText, 'Clear search');
const searchResultsText = wrapper.find('span').text(); const searchResultsText = wrapper.find('span').text();
assert.equal(searchResultsText, test.expectedText); assert.equal(searchResultsText, test.expectedText);
...@@ -251,10 +251,10 @@ describe('SearchStatusBar', () => { ...@@ -251,10 +251,10 @@ describe('SearchStatusBar', () => {
const wrapper = createComponent({}); const wrapper = createComponent({});
const button = wrapper.find('ActionButton'); const button = wrapper.find('Button');
assert.isTrue(button.exists()); assert.isTrue(button.exists());
assert.equal(button.props().label, test.buttonText); assert.equal(button.props().buttonText, test.buttonText);
}); });
}); });
}); });
...@@ -281,10 +281,10 @@ describe('SearchStatusBar', () => { ...@@ -281,10 +281,10 @@ describe('SearchStatusBar', () => {
const wrapper = createComponent({}); const wrapper = createComponent({});
const button = wrapper.find('ActionButton'); const button = wrapper.find('Button');
assert.isTrue(button.exists()); assert.isTrue(button.exists());
assert.equal(button.props().label, test.buttonText); assert.equal(button.props().buttonText, test.buttonText);
}); });
}); });
}); });
...@@ -307,10 +307,10 @@ describe('SearchStatusBar', () => { ...@@ -307,10 +307,10 @@ describe('SearchStatusBar', () => {
const wrapper = createComponent({}); const wrapper = createComponent({});
const button = wrapper.find('ActionButton'); const button = wrapper.find('Button');
assert.isTrue(button.exists()); assert.isTrue(button.exists());
assert.equal(button.props().label, 'Clear search'); assert.equal(button.props().buttonText, 'Clear search');
}); });
}); });
}); });
......
...@@ -149,7 +149,10 @@ describe('ShareAnnotationsPanel', () => { ...@@ -149,7 +149,10 @@ describe('ShareAnnotationsPanel', () => {
it('copies link to clipboard when copy button clicked', () => { it('copies link to clipboard when copy button clicked', () => {
const wrapper = createShareAnnotationsPanel(); const wrapper = createShareAnnotationsPanel();
wrapper.find('button').simulate('click'); wrapper
.find('Button')
.props()
.onClick();
assert.calledWith( assert.calledWith(
fakeCopyToClipboard.copyText, fakeCopyToClipboard.copyText,
...@@ -160,7 +163,10 @@ describe('ShareAnnotationsPanel', () => { ...@@ -160,7 +163,10 @@ describe('ShareAnnotationsPanel', () => {
it('confirms link copy when successful', () => { it('confirms link copy when successful', () => {
const wrapper = createShareAnnotationsPanel(); const wrapper = createShareAnnotationsPanel();
wrapper.find('button').simulate('click'); wrapper
.find('Button')
.props()
.onClick();
assert.calledWith(fakeFlash.info, 'Copied share link to clipboard'); assert.calledWith(fakeFlash.info, 'Copied share link to clipboard');
}); });
...@@ -168,7 +174,10 @@ describe('ShareAnnotationsPanel', () => { ...@@ -168,7 +174,10 @@ describe('ShareAnnotationsPanel', () => {
fakeCopyToClipboard.copyText.throws(); fakeCopyToClipboard.copyText.throws();
const wrapper = createShareAnnotationsPanel(); const wrapper = createShareAnnotationsPanel();
wrapper.find('button').simulate('click'); wrapper
.find('Button')
.props()
.onClick();
assert.calledWith(fakeFlash.error, 'Unable to copy link'); assert.calledWith(fakeFlash.error, 'Unable to copy link');
}); });
......
...@@ -47,7 +47,7 @@ describe('SidebarPanel', () => { ...@@ -47,7 +47,7 @@ describe('SidebarPanel', () => {
const wrapper = createSidebarPanel({ panelName: 'flibberty' }); const wrapper = createSidebarPanel({ panelName: 'flibberty' });
wrapper wrapper
.find('ActionButton') .find('Button')
.props() .props()
.onClick(); .onClick();
......
...@@ -54,9 +54,9 @@ describe('TopBar', () => { ...@@ -54,9 +54,9 @@ describe('TopBar', () => {
TopBar.$imports.$restore(); TopBar.$imports.$restore();
}); });
// Helper to retrieve an `IconButton` by icon name, for convenience // Helper to retrieve an `Button` by icon name, for convenience
function getButton(wrapper, iconName) { function getButton(wrapper, iconName) {
return wrapper.find('IconButton').filter({ icon: iconName }); return wrapper.find('Button').filter({ icon: iconName });
} }
function createTopBar(props = {}) { function createTopBar(props = {}) {
......
...@@ -12,8 +12,8 @@ const serviceConfig = require('../service-config'); ...@@ -12,8 +12,8 @@ const serviceConfig = require('../service-config');
const { withServices } = require('../util/service-context'); const { withServices } = require('../util/service-context');
const uiConstants = require('../ui-constants'); const uiConstants = require('../ui-constants');
const Button = require('./button');
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');
...@@ -98,7 +98,7 @@ function TopBar({ ...@@ -98,7 +98,7 @@ 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" />
<IconButton <Button
className="top-bar__icon-button" className="top-bar__icon-button"
icon="help" icon="help"
isActive={currentActivePanel === uiConstants.PANEL_HELP} isActive={currentActivePanel === uiConstants.PANEL_HELP}
...@@ -115,7 +115,7 @@ function TopBar({ ...@@ -115,7 +115,7 @@ 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 && (
<IconButton <Button
className="top-bar__icon-button top-bar__icon-button--refresh" className="top-bar__icon-button top-bar__icon-button--refresh"
icon="refresh" icon="refresh"
onClick={applyPendingUpdates} onClick={applyPendingUpdates}
...@@ -128,7 +128,7 @@ function TopBar({ ...@@ -128,7 +128,7 @@ function TopBar({
<SearchInput query={filterQuery} onSearch={setFilterQuery} /> <SearchInput query={filterQuery} onSearch={setFilterQuery} />
<SortMenu /> <SortMenu />
{showSharePageButton && ( {showSharePageButton && (
<IconButton <Button
className="top-bar__icon-button" className="top-bar__icon-button"
icon="share" icon="share"
isActive={ isActive={
...@@ -139,7 +139,7 @@ function TopBar({ ...@@ -139,7 +139,7 @@ function TopBar({
useCompactStyle useCompactStyle
/> />
)} )}
<IconButton <Button
className="top-bar__icon-button" className="top-bar__icon-button"
icon="help" icon="help"
isActive={currentActivePanel === uiConstants.PANEL_HELP} isActive={currentActivePanel === uiConstants.PANEL_HELP}
......
...@@ -8,7 +8,7 @@ const { isThirdPartyUser } = require('../util/account-id'); ...@@ -8,7 +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 Button = require('./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');
...@@ -46,7 +46,7 @@ function UserMenu({ auth, bridge, onLogout, serviceUrl, settings }) { ...@@ -46,7 +46,7 @@ function UserMenu({ auth, bridge, onLogout, serviceUrl, settings }) {
})(); })();
const menuLabel = ( const menuLabel = (
<IconButton <Button
className="top-bar__icon-button" className="top-bar__icon-button"
icon="profile" icon="profile"
title="User menu" title="User menu"
......
...@@ -6,7 +6,7 @@ const { createElement } = require('preact'); ...@@ -6,7 +6,7 @@ const { createElement } = require('preact');
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 ActionButton = require('./action-button'); const Button = require('./button');
/** /**
* Display current client version info * Display current client version info
...@@ -38,8 +38,8 @@ function VersionInfo({ flash, versionData }) { ...@@ -38,8 +38,8 @@ function VersionInfo({ flash, versionData }) {
<dd className="version-info__value">{versionData.timestamp}</dd> <dd className="version-info__value">{versionData.timestamp}</dd>
</dl> </dl>
<div className="version-info__actions"> <div className="version-info__actions">
<ActionButton <Button
label="Copy version details" buttonText="Copy version details"
onClick={copyVersionData} onClick={copyVersionData}
icon="copy" icon="copy"
/> />
......
...@@ -27,45 +27,38 @@ ...@@ -27,45 +27,38 @@
transition: 0.2s ease-out; transition: 0.2s ease-out;
color: var.$grey-6; color: var.$grey-6;
} }
&.is-active {
color: var.$brand;
&:hover {
color: var.$brand;
}
}
} }
/** /**
* A <button> composed of an SVG icon (left) and text (right) with some * A button with background color, layout rules to allow an icon, and text settings
* hover transition effects
*/ */
@mixin action-button($icon-margin: 0 5px) { @mixin button-basic {
@include button-base;
display: flex; display: flex;
align-items: center; align-items: center;
border-radius: 2px; border-radius: 2px;
border: none; border: none;
background-color: var.$grey-1; background-color: var.$grey-1;
color: var.$grey-5;
font-weight: 700; font-weight: 700;
&:hover {
background-color: var.$grey-2;
}
&__label {
padding-left: 0.25em;
padding-right: 0.25em;
}
&__icon {
color: var.$grey-5; color: var.$grey-5;
margin: $icon-margin;
&--compact { padding-right: 0.75em;
margin: 0; padding-left: 0.75em;
}
&--primary { &:hover {
color: var.$grey-1; background-color: var.$grey-2;
}
} }
}
&--primary { /* colors for a "primary" button */
@mixin button-primary {
color: var.$grey-1; color: var.$grey-1;
background-color: var.$grey-mid; background-color: var.$grey-mid;
...@@ -73,23 +66,4 @@ ...@@ -73,23 +66,4 @@
color: var.$grey-1; color: var.$grey-1;
background-color: var.$grey-6; background-color: var.$grey-6;
} }
}
}
/**
* An action button that is icon-only and displayed to the right of an
* <input> element. Colors are one tick more subtle than `action-button`.
*/
@mixin input-icon-button {
@include action-button;
padding: 10px;
border-radius: 0; // Turn off border-radius to align with <input> edges
border: 1px solid var.$grey-3;
border-left: 0px; // Avoid double border with the <input>
color: var.$grey-4;
&:hover {
background-color: var.$grey-2;
color: var.$grey-5;
}
} }
...@@ -36,10 +36,6 @@ ...@@ -36,10 +36,6 @@
font-weight: 700; font-weight: 700;
} }
&__close-btn {
@include buttons.action-button(0px);
}
&__content { &__content {
margin: 1em; margin: 1em;
margin-top: 0; margin-top: 0;
......
@use "../../mixins/buttons";
.action-button {
@include buttons.action-button;
}
@use '../../mixins/buttons';
@use '../../mixins/links'; @use '../../mixins/links';
@use '../../mixins/panel'; @use '../../mixins/panel';
@use "../../variables" as var; @use "../../variables" as var;
...@@ -26,11 +25,6 @@ ...@@ -26,11 +25,6 @@
font-size: var.$small-font-size; font-size: var.$small-font-size;
} }
&__copy-btn {
@include buttons.input-icon-button;
padding: 5px;
}
&__permissions { &__permissions {
margin: 0.5em 0; margin: 0.5em 0;
font-size: var.$small-font-size; font-size: var.$small-font-size;
......
@use "../../mixins/buttons";
@use "../../variables" as var;
/**
* Expected button markup structure:
*
* <button class="button <button--icon-only|button--labeled> [button--compact] [button--primary] [is-active]">
* [<SvgIcon class="button__icon" />]
* [<span>label</span>]
* </button>
*/
.button {
@include buttons.button-base;
}
.button--labeled {
@include buttons.button-basic;
// Need a little space around the icon if a label is next to it
.button__icon {
margin: 0 5px;
}
}
/* A button that sits right next to a (text) input and feels "at one" with it */
.button--input {
@include buttons.button-basic;
padding: 10px;
border-radius: 0; // Turn off border-radius to align with <input> edges
border: 1px solid var.$grey-3;
border-left: 0px; // Avoid double border with the <input>
color: var.$grey-4;
&:hover {
background-color: var.$grey-2;
color: var.$grey-5;
}
}
// In a few cases, it's necessary to squeeze buttons into tight spots
.button--compact {
// e.g. in the top bar, need to have icons right next to each other
&.button--icon-only {
padding: 0.25em;
}
// e.g. when next to an input field in a small/tight component
&.button--input {
padding: 0.25em 0.5em;
}
// e.g. the "close" button for sidebar panels, other tighter spaces
&.button--labeled {
padding-right: 0.5em;
padding-left: 0.5em;
.button__icon {
margin: 0;
}
}
}
.button--primary {
@include buttons.button-primary;
}
@media (pointer: coarse) {
.button {
min-width: 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 `Button`'s larger tap targets.
.button--compact {
min-width: auto;
min-height: auto;
}
}
@use "../../mixins/buttons";
@use "../../variables" as var;
.icon-button {
@include buttons.button-base;
&.is-active {
color: var.$brand;
&:hover {
color: var.$brand;
}
}
&--compact {
padding: 0.25em;
}
}
@media (pointer: coarse) {
.icon-button {
min-width: 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;
}
}
@use "../../variables" as var; @use "../../variables" as var;
@use "../../mixins/buttons";
@use "../../mixins/links"; @use "../../mixins/links";
.share-annotations-panel { .share-annotations-panel {
...@@ -19,10 +18,6 @@ ...@@ -19,10 +18,6 @@
border-radius: 0; border-radius: 0;
} }
&__copy-btn {
@include buttons.input-icon-button;
}
&__icon--inline { &__icon--inline {
width: 1em; width: 1em;
height: 1em; height: 1em;
......
...@@ -24,7 +24,6 @@ ...@@ -24,7 +24,6 @@
// Components // Components
// ---------- // ----------
@use './components/action-button';
@use './components/annotation-action-bar'; @use './components/annotation-action-bar';
@use './components/annotation'; @use './components/annotation';
@use './components/annotation-body'; @use './components/annotation-body';
...@@ -37,12 +36,12 @@ ...@@ -37,12 +36,12 @@
@use './components/annotation-share-info'; @use './components/annotation-share-info';
@use './components/annotation-thread'; @use './components/annotation-thread';
@use './components/annotation-user'; @use './components/annotation-user';
@use './components/button';
@use './components/excerpt'; @use './components/excerpt';
@use './components/focused-mode-header'; @use './components/focused-mode-header';
@use './components/group-list'; @use './components/group-list';
@use './components/group-list-item'; @use './components/group-list-item';
@use './components/help-panel'; @use './components/help-panel';
@use './components/icon-button';
@use './components/logged-out-message'; @use './components/logged-out-message';
@use './components/markdown-editor'; @use './components/markdown-editor';
@use './components/markdown-view'; @use './components/markdown-view';
......
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