Commit 226f6155 authored by Lyza Danger Gardner's avatar Lyza Danger Gardner Committed by Lyza Gardner

Remove button components

These shared button components have been moved to the `frontend-shared`
package.
parent 56b6769e
import classnames from 'classnames';
import { SvgIcon } from '@hypothesis/frontend-shared';
/**
* @typedef ButtonProps
* @prop {import("preact").ComponentChildren} [children]
* @prop {string} [className]
* @prop {string} [icon] - Name of `SvgIcon` to render in the button
* @prop {'left'|'right'} [iconPosition] - Icon positioned to left or to
* right of button text
* @prop {boolean} [disabled]
* @prop {boolean} [expanded] - Is the element associated with this button
* expanded (set `aria-expanded`)
* @prop {boolean} [pressed] - Is this button currently "active?" (set
* `aria-pressed`)
* @prop {() => any} [onClick]
* @prop {'small'|'medium'|'large'} [size='medium'] - Relative button size:
* affects padding
* @prop {Object} [style] - Optional inline styles
* @prop {string} [title] - Button title; used for `aria-label` attribute
* @prop {'normal'|'primary'|'light'|'dark'} [variant='normal'] - For styling: element variant
*/
/**
* @typedef IconButtonBaseProps
* @prop {string} icon - Icon is required for icon buttons
* @prop {string} title - Title is required for icon buttons
*/
/**
* @typedef {ButtonProps & IconButtonBaseProps} IconButtonProps
*/
/**
* @param {ButtonProps} props
*/
function ButtonBase({
children,
className,
icon,
iconPosition = 'left',
disabled,
expanded,
pressed,
onClick = () => {},
size = 'medium',
style = {},
title,
variant = 'normal',
}) {
const otherAttributes = {};
if (typeof disabled === 'boolean') {
otherAttributes.disabled = disabled;
}
if (typeof title !== 'undefined') {
otherAttributes.title = title;
otherAttributes['aria-label'] = title;
}
if (typeof expanded === 'boolean') {
otherAttributes['aria-expanded'] = expanded;
}
if (typeof pressed === 'boolean') {
otherAttributes['aria-pressed'] = pressed;
}
return (
<button
className={classnames(
className,
`${className}--${size}`,
`${className}--${variant}`,
{
[`${className}--icon-${iconPosition}`]: icon,
}
)}
onClick={onClick}
{...otherAttributes}
style={style}
>
{children}
</button>
);
}
/**
* An icon-only button
*
* @param {IconButtonProps} props
*/
export function IconButton(props) {
const { className = 'IconButton', ...restProps } = props;
const { icon } = props;
return (
<ButtonBase className={className} {...restProps}>
<SvgIcon name={icon} />
</ButtonBase>
);
}
/**
* A labeled button, with or without an icon
*
* @param {ButtonProps} props
*/
export function LabeledButton(props) {
const { icon, iconPosition = 'left' } = props;
const { children, className = 'LabeledButton', ...restProps } = props;
return (
<ButtonBase className={className} {...restProps}>
{icon && iconPosition === 'left' && <SvgIcon name={icon} />}
{children}
{icon && iconPosition === 'right' && <SvgIcon name={icon} />}
</ButtonBase>
);
}
/**
* A button styled to appear as an HTML link (<a>)
*
* @param {ButtonProps} props
*/
export function LinkButton(props) {
const { children } = props;
return (
<ButtonBase className="LinkButton" {...props}>
{children}
</ButtonBase>
);
}
import { mount } from 'enzyme';
import { IconButton, LabeledButton, LinkButton } from '../buttons.js';
import { $imports } from '../buttons.js';
import { checkAccessibility } from '../../../test-util/accessibility';
import mockImportedComponents from '../../../test-util/mock-imported-components';
// Add common tests for a button component for stuff provided by `ButtonBase`
function addCommonTests({ componentName, createComponentFn, withIcon = true }) {
describe(`${componentName} common support`, () => {
if (withIcon) {
it('renders the indicated icon', () => {
const wrapper = createComponentFn({ icon: 'fakeIcon' });
const button = wrapper.find('button');
const icon = wrapper.find('SvgIcon');
assert.equal(icon.prop('name'), 'fakeIcon');
// Icon is positioned "left" even if it is the only element in the <button>
assert.isTrue(button.hasClass(`${componentName}--icon-left`));
});
}
it('invokes callback on click', () => {
const onClick = sinon.stub();
const wrapper = createComponentFn({ onClick });
wrapper.find('button').simulate('click');
assert.calledOnce(onClick);
});
it('uses an internal no-op callback if no `onClick` is provided', () => {
// This test merely exercises the `onClick` prop default-value branch
// in the code
const wrapper = createComponentFn({ onClick: undefined });
wrapper.find('button').simulate('click');
});
it('uses a default className', () => {
const wrapper = createComponentFn();
assert.isTrue(wrapper.find('button').hasClass(componentName));
});
['primary', 'light', 'dark'].forEach(variant => {
it('renders a valid variant', () => {
const wrapper = createComponentFn({ variant });
assert.isTrue(
wrapper.find('button').hasClass(`${componentName}--${variant}`)
);
});
});
it('sets a `normal` variant modifier class by default', () => {
const wrapper = createComponentFn();
assert.isTrue(
wrapper.find('button').hasClass(`${componentName}--normal`)
);
});
['small', 'medium', 'large'].forEach(size => {
it('renders a valid size', () => {
const wrapper = createComponentFn({ size });
assert.isTrue(
wrapper.find('button').hasClass(`${componentName}--${size}`)
);
});
});
it('sets a `medium` size modifier class by default', () => {
const wrapper = createComponentFn();
assert.isTrue(
wrapper.find('button').hasClass(`${componentName}--medium`)
);
});
it('overrides className when provided', () => {
const wrapper = createComponentFn({
className: 'CustomClassName',
variant: 'primary',
});
assert.isTrue(wrapper.find('button').hasClass('CustomClassName'));
assert.isTrue(
wrapper.find('button').hasClass('CustomClassName--primary')
);
assert.isFalse(wrapper.find('button').hasClass(componentName));
});
it('adds inline styles when provided', () => {
const wrapper = createComponentFn({ style: { backgroundColor: 'pink' } });
assert.equal(
wrapper.getDOMNode().getAttribute('style'),
'background-color: pink;'
);
});
[
{
propName: 'expanded',
propValue: true,
attributeName: 'aria-expanded',
attributeValue: 'true',
},
{
propName: 'pressed',
propValue: true,
attributeName: 'aria-pressed',
attributeValue: 'true',
},
{
propName: 'title',
propValue: 'Click here',
attributeName: 'aria-label',
attributeValue: 'Click here',
},
{
propName: 'disabled',
propValue: true,
attributeName: 'disabled',
attributeValue: '',
},
].forEach(testCase => {
it('sets attributes on the button element', () => {
const wrapper = createComponentFn({
[testCase.propName]: testCase.propValue,
});
const element = wrapper.find('button').getDOMNode();
assert.equal(
element.getAttribute(testCase.attributeName),
testCase.attributeValue
);
});
});
});
}
describe('buttons', () => {
let fakeOnClick;
beforeEach(() => {
fakeOnClick = sinon.stub();
$imports.$mock(mockImportedComponents());
});
afterEach(() => {
$imports.$restore();
});
describe('IconButton', () => {
function createComponent(props = {}) {
return mount(
<IconButton
icon="fakeIcon"
title="My Action"
onClick={fakeOnClick}
{...props}
/>
);
}
addCommonTests({
componentName: 'IconButton',
createComponentFn: createComponent,
});
it(
'should pass a11y checks',
checkAccessibility({
content: () => createComponent(),
})
);
});
describe('LabeledButton', () => {
function createComponent(props = {}) {
return mount(
<LabeledButton title="My Action" onClick={fakeOnClick} {...props}>
Do this
</LabeledButton>
);
}
addCommonTests({
componentName: 'LabeledButton',
createComponentFn: createComponent,
});
it('renders the indicated icon on the right if `iconPosition` is `right`', () => {
const wrapper = createComponent({
icon: 'fakeIcon',
iconPosition: 'right',
});
const icon = wrapper.find('SvgIcon');
const button = wrapper.find('button');
assert.equal(icon.prop('name'), 'fakeIcon');
assert.isTrue(button.hasClass('LabeledButton--icon-right'));
});
it('does not render an icon if none indicated', () => {
// Icon not required for `LabeledButton`
const wrapper = createComponent();
const icon = wrapper.find('SvgIcon');
assert.isFalse(icon.exists());
});
it('renders children', () => {
const wrapper = createComponent();
assert.equal(wrapper.text(), 'Do this');
});
it(
'should pass a11y checks',
checkAccessibility({
content: () => createComponent(),
})
);
});
describe('LinkButton', () => {
function createComponent(props = {}) {
return mount(
<LinkButton title="My Action" onClick={fakeOnClick} {...props}>
Do this
</LinkButton>
);
}
addCommonTests({
componentName: 'LinkButton',
createComponentFn: createComponent,
withIcon: false,
});
it('renders children', () => {
const wrapper = createComponent();
assert.equal(wrapper.text(), 'Do this');
});
it(
'should pass a11y checks',
checkAccessibility({
content: () => createComponent(),
})
);
});
});
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