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');
const { isShareable, shareURI } = require('../util/annotation-sharing');
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({
annotation,
......@@ -44,13 +44,11 @@ function AnnotationActionBar({
return (
<div className="annotation-action-bar">
{showEditAction && (
<IconButton icon="edit" title="Edit" onClick={onEdit} />
)}
{showEditAction && <Button icon="edit" title="Edit" onClick={onEdit} />}
{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 && (
<AnnotationShareControl
group={annotationGroup}
......@@ -59,14 +57,14 @@ function AnnotationActionBar({
/>
)}
{showFlagAction && !annotation.flagged && (
<IconButton
<Button
icon="flag"
title="Report this annotation to moderators"
onClick={onFlag}
/>
)}
{showFlagAction && annotation.flagged && (
<IconButton
<Button
isActive={true}
icon="flag--active"
title="Annotation has been reported to the moderators"
......
......@@ -6,7 +6,7 @@ const { createElement } = require('preact');
const { applyTheme } = require('../util/theme');
const { withServices } = require('../util/service-context');
const ActionButton = require('./action-button');
const Button = require('./button');
const Menu = require('./menu');
const MenuItem = require('./menu-item');
......@@ -75,9 +75,9 @@ function AnnotationPublishControl({
/>
</Menu>
</div>
<ActionButton
<Button
icon="cancel"
label="Cancel"
buttonText="Cancel"
onClick={onCancel}
useCompactStyle
/>
......
......@@ -8,7 +8,7 @@ const useElementShouldClose = require('./hooks/use-element-should-close');
const { copyText } = require('../util/copy-to-clipboard');
const { withServices } = require('../util/service-context');
const IconButton = require('./icon-button');
const Button = require('./button');
const ShareLinks = require('./share-links');
const SvgIcon = require('./svg-icon');
......@@ -84,7 +84,7 @@ function AnnotationShareControl({
return (
<div className="annotation-share-control" ref={shareRef}>
<IconButton icon="share" title="Share" onClick={toggleSharePanel} />
<Button icon="share" title="Share" onClick={toggleSharePanel} />
{isOpen && (
<div className="annotation-share-panel">
<div className="annotation-share-panel__header">
......@@ -102,13 +102,13 @@ function AnnotationShareControl({
readOnly
ref={inputRef}
/>
<button
className="annotation-share-panel__copy-btn"
aria-label="Copy share link to clipboard"
<Button
icon="copy"
title="Copy share link to clipboard"
onClick={copyShareLink}
>
<SvgIcon name="copy" />
</button>
useInputStyle
useCompactStyle
/>
</div>
<div className="annotation-share-panel__permissions">
{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');
const useStore = require('../store/use-store');
const IconButton = require('./icon-button');
const Button = require('./button');
const Spinner = require('./spinner');
/**
......@@ -61,7 +61,7 @@ function SearchInput({ alwaysExpanded, query, onSearch }) {
onInput={e => setPendingQuery(e.target.value)}
/>
{!isLoading && (
<IconButton
<Button
className="search-input__icon-button top-bar__icon-button"
icon="search"
onClick={() => input.current.focus()}
......
......@@ -8,7 +8,7 @@ const { withServices } = require('../util/service-context');
const uiConstants = require('../ui-constants');
const useStore = require('../store/use-store');
const ActionButton = require('./action-button');
const Button = require('./button');
/**
* Of the annotations in the thread `annThread`, how many
......@@ -149,12 +149,12 @@ function SearchStatusBar({ rootThread }) {
<div>
{modes.filtered && (
<div className="search-status-bar">
<ActionButton
<Button
icon="cancel"
label="Clear search"
buttonText="Clear search"
onClick={actions.clearSelection}
isPrimary
useCompactStyle
usePrimaryStyle
/>
<span className="search-status-bar__filtered-text">
{modeText.filtered}
......@@ -170,11 +170,11 @@ function SearchStatusBar({ rootThread }) {
)}
{modes.selected && (
<div className="search-status-bar">
<ActionButton
label={modeText.selected}
<Button
buttonText={modeText.selected}
onClick={actions.clearSelection}
isPrimary
useCompactStyle
usePrimaryStyle
/>
</div>
)}
......
......@@ -7,6 +7,7 @@ const { copyText } = require('../util/copy-to-clipboard');
const { withServices } = require('../util/service-context');
const uiConstants = require('../ui-constants');
const Button = require('./button');
const ShareLinks = require('./share-links');
const SidebarPanel = require('./sidebar-panel');
const SvgIcon = require('./svg-icon');
......@@ -76,14 +77,12 @@ function ShareAnnotationsPanel({ analytics, flash }) {
value={shareURI}
readOnly
/>
<button
<Button
icon="copy"
onClick={copyShareLink}
title="copy share link"
aria-label="Copy share link"
className="share-annotations-panel__copy-btn"
>
<SvgIcon name="copy" />
</button>
title="Copy share link"
useInputStyle
/>
</div>
<p>
{focusedGroup.type === 'private' ? (
......
......@@ -7,7 +7,7 @@ const scrollIntoView = require('scroll-into-view');
const useStore = require('../store/use-store');
const ActionButton = require('./action-button');
const Button = require('./button');
const Slider = require('./slider');
/**
......@@ -47,9 +47,9 @@ function SidebarPanel({ children, panelName, title, onActiveChanged }) {
<div className="sidebar-panel__header">
<div className="sidebar-panel__title u-stretch">{title}</div>
<div>
<ActionButton
<Button
icon="cancel"
label="Close"
buttonText="Close"
onClick={closePanel}
useCompactStyle
/>
......
......@@ -4,7 +4,7 @@ const { createElement } = require('preact');
const useStore = require('../store/use-store');
const IconButton = require('./icon-button');
const Button = require('./button');
const Menu = require('./menu');
const MenuItem = require('./menu-item');
......@@ -37,7 +37,7 @@ function SortMenu() {
});
const menuLabel = (
<IconButton
<Button
className="top-bar__icon-button"
icon="sort"
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', () => {
};
const getButton = (wrapper, iconName) => {
return wrapper.find('IconButton').filter({ icon: iconName });
return wrapper.find('Button').filter({ icon: iconName });
};
beforeEach(() => {
......
......@@ -191,7 +191,7 @@ describe('AnnotationPublishControl', () => {
const wrapper = createAnnotationPublishControl({
onCancel: fakeOnCancel,
});
const cancelBtn = wrapper.find('ActionButton');
const cancelBtn = wrapper.find('Button');
cancelBtn.props().onClick();
......
......@@ -16,6 +16,10 @@ describe('AnnotationShareControl', () => {
let container;
const getButton = (wrapper, iconName) => {
return wrapper.find('Button').filter({ icon: iconName });
};
function createComponent(props = {}) {
return mount(
<AnnotationShareControl
......@@ -33,7 +37,7 @@ describe('AnnotationShareControl', () => {
function openElement(wrapper) {
act(() => {
wrapper
.find('IconButton')
.find('Button')
.props()
.onClick();
});
......@@ -89,12 +93,10 @@ describe('AnnotationShareControl', () => {
it('toggles the share control element when the button is clicked', () => {
const wrapper = createComponent();
const button = getButton(wrapper, 'share');
act(() => {
wrapper
.find('IconButton')
.props()
.onClick();
button.props().onClick();
});
wrapper.update();
......@@ -115,7 +117,9 @@ describe('AnnotationShareControl', () => {
const wrapper = createComponent();
openElement(wrapper);
wrapper.find('.annotation-share-panel__copy-btn').simulate('click');
getButton(wrapper, 'copy')
.props()
.onClick();
assert.calledWith(
fakeCopyToClipboard.copyText,
......@@ -127,7 +131,9 @@ describe('AnnotationShareControl', () => {
const wrapper = createComponent();
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');
});
......@@ -137,7 +143,9 @@ describe('AnnotationShareControl', () => {
const wrapper = createComponent();
openElement(wrapper);
wrapper.find('.annotation-share-panel__copy-btn').simulate('click');
getButton(wrapper, 'copy')
.props()
.onClick();
assert.calledWith(fakeFlash.error, 'Unable to copy link');
});
......
......@@ -129,14 +129,6 @@ describe('annotation', function() {
angular
.module('h', [])
.component('annotation', annotationComponent)
.component('annotationActionButton', {
bindings: {
icon: '<',
isDisabled: '<',
label: '<',
onClick: '&',
},
})
.component('annotationBody', {
bindings: {
collapse: '<',
......
......@@ -3,15 +3,15 @@
const { createElement } = require('preact');
const { mount } = require('enzyme');
const IconButton = require('../icon-button');
const Button = require('../button');
const mockImportedComponents = require('./mock-imported-components');
describe('IconButton', () => {
describe('Button', () => {
let fakeOnClick;
function createComponent(props = {}) {
return mount(
<IconButton
<Button
icon="fakeIcon"
isActive={false}
title="My Action"
......@@ -23,11 +23,11 @@ describe('IconButton', () => {
beforeEach(() => {
fakeOnClick = sinon.stub();
IconButton.$imports.$mock(mockImportedComponents());
Button.$imports.$mock(mockImportedComponents());
});
afterEach(() => {
IconButton.$imports.$restore();
Button.$imports.$restore();
});
it('adds active className if `isActive` is `true`', () => {
......@@ -46,6 +46,20 @@ describe('IconButton', () => {
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', () => {
const wrapper = createComponent();
wrapper.find('button').simulate('click');
......@@ -61,6 +75,18 @@ describe('IconButton', () => {
it('sets compact style if `useCompactStyle` is set`', () => {
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', () => {
const wrapper = createComponent({});
const button = wrapper.find('ActionButton');
assert.equal(button.props().label, 'Clear search');
const button = wrapper.find('Button');
assert.equal(button.props().buttonText, 'Clear search');
const searchResultsText = wrapper.find('span').text();
assert.equal(searchResultsText, test.expectedText);
......@@ -251,10 +251,10 @@ describe('SearchStatusBar', () => {
const wrapper = createComponent({});
const button = wrapper.find('ActionButton');
const button = wrapper.find('Button');
assert.isTrue(button.exists());
assert.equal(button.props().label, test.buttonText);
assert.equal(button.props().buttonText, test.buttonText);
});
});
});
......@@ -281,10 +281,10 @@ describe('SearchStatusBar', () => {
const wrapper = createComponent({});
const button = wrapper.find('ActionButton');
const button = wrapper.find('Button');
assert.isTrue(button.exists());
assert.equal(button.props().label, test.buttonText);
assert.equal(button.props().buttonText, test.buttonText);
});
});
});
......@@ -307,10 +307,10 @@ describe('SearchStatusBar', () => {
const wrapper = createComponent({});
const button = wrapper.find('ActionButton');
const button = wrapper.find('Button');
assert.isTrue(button.exists());
assert.equal(button.props().label, 'Clear search');
assert.equal(button.props().buttonText, 'Clear search');
});
});
});
......
......@@ -149,7 +149,10 @@ describe('ShareAnnotationsPanel', () => {
it('copies link to clipboard when copy button clicked', () => {
const wrapper = createShareAnnotationsPanel();
wrapper.find('button').simulate('click');
wrapper
.find('Button')
.props()
.onClick();
assert.calledWith(
fakeCopyToClipboard.copyText,
......@@ -160,7 +163,10 @@ describe('ShareAnnotationsPanel', () => {
it('confirms link copy when successful', () => {
const wrapper = createShareAnnotationsPanel();
wrapper.find('button').simulate('click');
wrapper
.find('Button')
.props()
.onClick();
assert.calledWith(fakeFlash.info, 'Copied share link to clipboard');
});
......@@ -168,7 +174,10 @@ describe('ShareAnnotationsPanel', () => {
fakeCopyToClipboard.copyText.throws();
const wrapper = createShareAnnotationsPanel();
wrapper.find('button').simulate('click');
wrapper
.find('Button')
.props()
.onClick();
assert.calledWith(fakeFlash.error, 'Unable to copy link');
});
......
......@@ -47,7 +47,7 @@ describe('SidebarPanel', () => {
const wrapper = createSidebarPanel({ panelName: 'flibberty' });
wrapper
.find('ActionButton')
.find('Button')
.props()
.onClick();
......
......@@ -54,9 +54,9 @@ describe('TopBar', () => {
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) {
return wrapper.find('IconButton').filter({ icon: iconName });
return wrapper.find('Button').filter({ icon: iconName });
}
function createTopBar(props = {}) {
......
......@@ -12,8 +12,8 @@ const serviceConfig = require('../service-config');
const { withServices } = require('../util/service-context');
const uiConstants = require('../ui-constants');
const Button = require('./button');
const GroupList = require('./group-list');
const IconButton = require('./icon-button');
const SearchInput = require('./search-input');
const StreamSearchInput = require('./stream-search-input');
const SortMenu = require('./sort-menu');
......@@ -98,7 +98,7 @@ function TopBar({
<div className="top-bar__inner content">
<StreamSearchInput />
<div className="top-bar__expander" />
<IconButton
<Button
className="top-bar__icon-button"
icon="help"
isActive={currentActivePanel === uiConstants.PANEL_HELP}
......@@ -115,7 +115,7 @@ function TopBar({
<GroupList className="GroupList" auth={auth} />
<div className="top-bar__expander" />
{pendingUpdateCount > 0 && (
<IconButton
<Button
className="top-bar__icon-button top-bar__icon-button--refresh"
icon="refresh"
onClick={applyPendingUpdates}
......@@ -128,7 +128,7 @@ function TopBar({
<SearchInput query={filterQuery} onSearch={setFilterQuery} />
<SortMenu />
{showSharePageButton && (
<IconButton
<Button
className="top-bar__icon-button"
icon="share"
isActive={
......@@ -139,7 +139,7 @@ function TopBar({
useCompactStyle
/>
)}
<IconButton
<Button
className="top-bar__icon-button"
icon="help"
isActive={currentActivePanel === uiConstants.PANEL_HELP}
......
......@@ -8,7 +8,7 @@ const { isThirdPartyUser } = require('../util/account-id');
const serviceConfig = require('../service-config');
const { withServices } = require('../util/service-context');
const IconButton = require('./icon-button');
const Button = require('./button');
const Menu = require('./menu');
const MenuSection = require('./menu-section');
const MenuItem = require('./menu-item');
......@@ -46,7 +46,7 @@ function UserMenu({ auth, bridge, onLogout, serviceUrl, settings }) {
})();
const menuLabel = (
<IconButton
<Button
className="top-bar__icon-button"
icon="profile"
title="User menu"
......
......@@ -6,7 +6,7 @@ const { createElement } = require('preact');
const { copyText } = require('../util/copy-to-clipboard');
const { withServices } = require('../util/service-context');
const ActionButton = require('./action-button');
const Button = require('./button');
/**
* Display current client version info
......@@ -38,8 +38,8 @@ function VersionInfo({ flash, versionData }) {
<dd className="version-info__value">{versionData.timestamp}</dd>
</dl>
<div className="version-info__actions">
<ActionButton
label="Copy version details"
<Button
buttonText="Copy version details"
onClick={copyVersionData}
icon="copy"
/>
......
......@@ -27,69 +27,43 @@
transition: 0.2s ease-out;
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
* hover transition effects
* A button with background color, layout rules to allow an icon, and text settings
*/
@mixin action-button($icon-margin: 0 5px) {
@include button-base;
@mixin button-basic {
display: flex;
align-items: center;
border-radius: 2px;
border: none;
background-color: var.$grey-1;
color: var.$grey-5;
font-weight: 700;
color: var.$grey-5;
padding-right: 0.75em;
padding-left: 0.75em;
&:hover {
background-color: var.$grey-2;
}
&__label {
padding-left: 0.25em;
padding-right: 0.25em;
}
&__icon {
color: var.$grey-5;
margin: $icon-margin;
&--compact {
margin: 0;
}
&--primary {
color: var.$grey-1;
}
}
&--primary {
color: var.$grey-1;
background-color: var.$grey-mid;
&:hover {
color: var.$grey-1;
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;
/* colors for a "primary" button */
@mixin button-primary {
color: var.$grey-1;
background-color: var.$grey-mid;
&:hover {
background-color: var.$grey-2;
color: var.$grey-5;
color: var.$grey-1;
background-color: var.$grey-6;
}
}
......@@ -36,10 +36,6 @@
font-weight: 700;
}
&__close-btn {
@include buttons.action-button(0px);
}
&__content {
margin: 1em;
margin-top: 0;
......
@use "../../mixins/buttons";
.action-button {
@include buttons.action-button;
}
@use '../../mixins/buttons';
@use '../../mixins/links';
@use '../../mixins/panel';
@use "../../variables" as var;
......@@ -26,11 +25,6 @@
font-size: var.$small-font-size;
}
&__copy-btn {
@include buttons.input-icon-button;
padding: 5px;
}
&__permissions {
margin: 0.5em 0;
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 "../../mixins/buttons";
@use "../../mixins/links";
.share-annotations-panel {
......@@ -19,10 +18,6 @@
border-radius: 0;
}
&__copy-btn {
@include buttons.input-icon-button;
}
&__icon--inline {
width: 1em;
height: 1em;
......
......@@ -24,7 +24,6 @@
// Components
// ----------
@use './components/action-button';
@use './components/annotation-action-bar';
@use './components/annotation';
@use './components/annotation-body';
......@@ -37,12 +36,12 @@
@use './components/annotation-share-info';
@use './components/annotation-thread';
@use './components/annotation-user';
@use './components/button';
@use './components/excerpt';
@use './components/focused-mode-header';
@use './components/group-list';
@use './components/group-list-item';
@use './components/help-panel';
@use './components/icon-button';
@use './components/logged-out-message';
@use './components/markdown-editor';
@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