packages/core/components/CopyTextButton: refactor to functional component + provide errorApi in storybook

This commit is contained in:
Patrik Oldsberg
2020-04-18 18:20:11 +02:00
parent 09411b0661
commit 8b429c03a7
3 changed files with 89 additions and 61 deletions
@@ -14,20 +14,20 @@
* limitations under the License.
*/
import React from 'react';
import React, { useState, useRef } from 'react';
import PropTypes from 'prop-types';
import { IconButton, Tooltip, withStyles } from '@material-ui/core';
import { IconButton, Tooltip, makeStyles } from '@material-ui/core';
import CopyIcon from '@material-ui/icons/FileCopy';
import { errorApiRef, useApi } from '../../api';
import { errorApiRef, useApi } from 'api';
const buttonStyles = theme => ({
const useStyles = makeStyles(theme => ({
button: {
'&:hover': {
backgroundColor: theme.palette.highlight,
cursor: 'pointer',
},
},
});
}));
/**
* Copy text button with visual feedback in the form of
@@ -44,68 +44,56 @@ const buttonStyles = theme => ({
* Example:
* <CopyTextButton text="My text that I want to be copied to the clipboard" />
*/
class CopyTextButton extends React.Component {
static propTypes = {
text: PropTypes.string.isRequired,
tooltipDelay: PropTypes.number,
tooltipText: PropTypes.string,
};
const CopyTextButton = ({
text,
tooltipDelay = 1000,
tooltipText = 'Text copied to clipboard',
}) => {
const classes = useStyles();
const errorApi = useApi(errorApiRef);
const inputRef = useRef();
const [open, setOpen] = useState(false);
static defaultProps = {
tooltipDelay: 1000,
tooltipText: 'Text copied to clipboard',
};
state = {
open: false,
};
handleTooltipClose = () => {
this.setState({ open: false });
};
handleTooltipOpen = () => {
this.setState({ open: true });
};
handleCopyClick = e => {
const handleCopyClick = e => {
e.stopPropagation();
this.handleTooltipOpen();
setOpen(true);
try {
this.clipboardInput.select();
inputRef.current.select();
document.execCommand('copy');
} catch (error) {
const errorApi = useApi(errorApiRef);
errorApi.post(error);
}
};
render() {
const { classes, text, tooltipDelay, tooltipText } = this.props;
return (
<>
<input
ref={inputRef}
type="text"
style={{ position: 'absolute', top: -9999, left: 9999 }}
defaultValue={text}
/>
<Tooltip
id="copy-test-tooltip"
title={tooltipText}
placement="top"
leaveDelay={tooltipDelay}
onClose={() => setOpen(false)}
open={open}
>
<IconButton onClick={handleCopyClick} className={classes.button}>
<CopyIcon />
</IconButton>
</Tooltip>
</>
);
};
return (
<>
<input
ref={el => (this.clipboardInput = el)}
type="text"
style={{ position: 'absolute', top: -9999, left: 9999 }}
defaultValue={text}
/>
<Tooltip
id="copy-test-tooltip"
title={tooltipText}
placement="top"
leaveDelay={tooltipDelay}
onClose={this.handleTooltipClose}
open={this.state.open}
>
<IconButton onClick={this.handleCopyClick} className={classes.button}>
<CopyIcon />
</IconButton>
</Tooltip>
</>
);
}
}
CopyTextButton.propTypes = {
text: PropTypes.string.isRequired,
tooltipDelay: PropTypes.number,
tooltipText: PropTypes.string,
};
export default withStyles(buttonStyles)(CopyTextButton);
export default CopyTextButton;
@@ -16,10 +16,28 @@
import React from 'react';
import CopyTextButton from '.';
import { ApiProvider, errorApiRef, ApiRegistry } from 'api';
export default {
title: 'CopyTextButton',
component: CopyTextButton,
decorators: [
storyFn => {
// TODO: move this to common storybook config, requires core package to be separate from components
const registry = ApiRegistry.from([
[
errorApiRef,
{
post(error) {
// eslint-disable-next-line no-alert
window.alert(`Component posted error, ${error}`);
},
},
],
]);
return <ApiProvider apis={registry} children={storyFn()} />;
},
],
};
export const Default = () => (
@@ -18,6 +18,7 @@ import React from 'react';
import { render } from '@testing-library/react';
import { wrapInThemedTestApp } from '@backstage/test-utils';
import CopyTextButton from './CopyTextButton';
import { ApiRegistry, errorApiRef, ApiProvider } from 'api';
const props = {
text: 'mockText',
@@ -25,10 +26,25 @@ const props = {
tooltipText: 'mockTooltip',
};
const apiRegistry = ApiRegistry.from([
[
errorApiRef,
{
post(error) {
throw error;
},
},
],
]);
describe('<CopyTextButton />', () => {
it('renders without exploding', () => {
const { getByDisplayValue } = render(
wrapInThemedTestApp(<CopyTextButton {...props} />),
wrapInThemedTestApp(
<ApiProvider apis={apiRegistry}>
<CopyTextButton {...props} />
</ApiProvider>,
),
);
getByDisplayValue('mockText');
});
@@ -38,7 +54,13 @@ describe('<CopyTextButton />', () => {
it.skip('displays tooltip on click', () => {
const spy = jest.fn();
Object.defineProperty(document, 'execCommand', { value: spy });
const rendered = render(wrapInThemedTestApp(<CopyTextButton {...props} />));
const rendered = render(
wrapInThemedTestApp(
<ApiProvider apis={apiRegistry}>
<CopyTextButton {...props} />
</ApiProvider>,
),
);
const button = rendered.getByTitle('mockTooltip');
button.click();
expect(spy).toHaveBeenCalled();