Skip to content

Commit

Permalink
refactor(CopyToClipboard): replaced class component by functional com…
Browse files Browse the repository at this point in the history
…ponent (#1216)
  • Loading branch information
atroynikov authored and amje committed Feb 1, 2024
1 parent 1f5a161 commit 61d14fa
Show file tree
Hide file tree
Showing 4 changed files with 50 additions and 75 deletions.
9 changes: 3 additions & 6 deletions src/components/ClipboardButton/ClipboardButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,14 @@ import {Button} from '../Button';
import type {ButtonProps, ButtonSize} from '../Button';
import {ClipboardIcon} from '../ClipboardIcon';
import {CopyToClipboard} from '../CopyToClipboard';
import type {CopyToClipboardBaseProps, CopyToClipboardStatus} from '../CopyToClipboard/types';
import type {CopyToClipboardProps, CopyToClipboardStatus} from '../CopyToClipboard/types';
import {Tooltip} from '../Tooltip';

import i18n from './i18n';

export interface ClipboardButtonProps
extends CopyToClipboardBaseProps,
Omit<ClipboardButtonComponentProps, 'status' | 'onClick'> {
/** Time to restore initial state, ms */
timeout?: number;
}
extends Omit<CopyToClipboardProps, 'children'>,
Omit<ClipboardButtonComponentProps, 'status' | 'onClick'> {}

interface ClipboardButtonComponentProps
extends Omit<ButtonProps, 'href' | 'component' | 'target' | 'rel' | 'loading' | 'children'> {
Expand Down
87 changes: 24 additions & 63 deletions src/components/CopyToClipboard/CopyToClipboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,77 +2,38 @@ import React from 'react';

import ReactCopyToClipboard from 'react-copy-to-clipboard';

import type {
CopyToClipboardBaseProps,
CopyToClipboardContent,
CopyToClipboardStatus,
} from './types';
import type {CopyToClipboardProps, CopyToClipboardStatus} from './types';

interface CopyToClipboardGeneralProps extends CopyToClipboardBaseProps {
children: CopyToClipboardContent;
}

interface CopyToClipboardDefaultProps {
timeout: number;
}
const INITIAL_STATUS: CopyToClipboardStatus = 'pending';

interface CopyToClipboardInnerProps
extends CopyToClipboardGeneralProps,
CopyToClipboardDefaultProps {}
export function CopyToClipboard(props: CopyToClipboardProps) {
const {children, text, options, timeout, onCopy} = props;

export interface CopyToClipboardProps
extends CopyToClipboardGeneralProps,
Partial<CopyToClipboardDefaultProps> {}
const [status, setStatus] = React.useState<CopyToClipboardStatus>(INITIAL_STATUS);

interface CopyToClipboardState {
status: CopyToClipboardStatus;
}
const timerIdRef = React.useRef<number>();

export class CopyToClipboard extends React.Component<
CopyToClipboardInnerProps,
CopyToClipboardState
> {
static INITIAL_STATUS: CopyToClipboardStatus = 'pending';
const content = React.useMemo(() => children(status), [children, status]);

state: CopyToClipboardState = {
status: CopyToClipboard.INITIAL_STATUS,
};
const handleCopy = React.useCallback<Required<ReactCopyToClipboard.Props>['onCopy']>(
(copyText, result) => {
setStatus(result ? 'success' : 'error');
window.clearTimeout(timerIdRef.current);
timerIdRef.current = window.setTimeout(() => setStatus(INITIAL_STATUS), timeout);
onCopy?.(copyText, result);
},
[onCopy, timeout],
);

private timerId?: number;
React.useEffect(() => () => window.clearTimeout(timerIdRef.current), []);

componentWillUnmount() {
clearTimeout(this.timerId);
if (!React.isValidElement(content)) {
throw new Error('Content must be a valid react element');
}

render() {
const {children, text, options} = this.props;
const {status} = this.state;
const content = children(status);

if (!React.isValidElement(content)) {
throw new Error('Content must be a valid react element');
}

return (
<ReactCopyToClipboard text={String(text)} onCopy={this.handleCopy} options={options}>
{content}
</ReactCopyToClipboard>
);
}

private handleCopy = (text: string, result: boolean) => {
const {timeout, onCopy} = this.props;

this.setState({
status: result ? 'success' : 'error',
});

clearTimeout(this.timerId);
this.timerId = window.setTimeout(() => {
this.setState({status: CopyToClipboard.INITIAL_STATUS});
this.timerId = undefined;
}, timeout);

onCopy?.(text, result);
};
return (
<ReactCopyToClipboard text={text} onCopy={handleCopy} options={options}>
{content}
</ReactCopyToClipboard>
);
}
18 changes: 18 additions & 0 deletions src/components/CopyToClipboard/__tests__/CopyToClipboard.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from 'react';

import {render} from '../../../../test-utils/utils';
import {CopyToClipboard} from '../CopyToClipboard';
import type {CopyToClipboardContent} from '../types';

describe('CopyToClipboard', () => {
test('content must be a valid react element', () => {
const onCopy = jest.fn();
expect(() =>
render(
<CopyToClipboard text="Text to copy" onCopy={onCopy}>
{(() => 123) as any as CopyToClipboardContent}
</CopyToClipboard>,
),
).toThrow('Content must be a valid react element');
});
});
11 changes: 5 additions & 6 deletions src/components/CopyToClipboard/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type React from 'react';

import type ReactCopyToClipboard from 'react-copy-to-clipboard';

export type CopyToClipboardStatus = 'pending' | 'success' | 'error';
Expand All @@ -6,13 +8,10 @@ export type OnCopyHandler = (text: string, result: boolean) => void;

export type CopyToClipboardContent = (status: CopyToClipboardStatus) => React.ReactElement;

export interface CopyToClipboardBaseProps {
/** Text to copy */
export interface CopyToClipboardProps {
text: string;

/** Handler that would be invoked after copy to clipboard */
timeout?: number;
children: CopyToClipboardContent;
onCopy?: OnCopyHandler;

/** copy-to-clipboard options */
options?: ReactCopyToClipboard.Options;
}

0 comments on commit 61d14fa

Please sign in to comment.