Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: use native copy method #1852

Open
wants to merge 8 commits into
base: next
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 0 additions & 36 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 0 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,6 @@
"lodash": "^4.17.21",
"rc-slider": "^11.1.7",
"react-beautiful-dnd": "^13.1.1",
"react-copy-to-clipboard": "^5.1.0",
"react-transition-group": "^4.4.5",
"react-virtualized-auto-sizer": "^1.0.24",
"react-window": "^1.8.10",
Expand Down Expand Up @@ -184,7 +183,6 @@
"@types/lodash": "^4.17.13",
"@types/react": "^18.3.12",
"@types/react-beautiful-dnd": "^13.1.8",
"@types/react-copy-to-clipboard": "^5.0.7",
"@types/react-dom": "^18.3.1",
"@types/react-transition-group": "^4.4.11",
"@types/react-virtualized-auto-sizer": "^1.0.4",
Expand Down
3 changes: 1 addition & 2 deletions src/components/ClipboardButton/ClipboardButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,6 @@
text,
timeout = DEFAULT_TIMEOUT,
onCopy,
options,
hasTooltip = true,
onMouseEnter,
onFocus,
Expand All @@ -116,7 +115,7 @@
React.useEffect(() => window.clearTimeout(timerIdRef.current), []);

const handleCopy: OnCopyHandler = React.useCallback(
(text, result) => {

Check warning on line 118 in src/components/ClipboardButton/ClipboardButton.tsx

View workflow job for this annotation

GitHub Actions / Verify Files

'text' is already declared in the upper scope on line 102 column 9
onCopy?.(text, result);
setTooltipDisabled(false);
setTooltipCloseDelay(timeout);
Expand Down Expand Up @@ -154,7 +153,7 @@
);

return (
<CopyToClipboard text={text} timeout={timeout} onCopy={handleCopy} options={options}>
<CopyToClipboard text={text} timeout={timeout} onCopy={handleCopy}>
{(status) => (
<ClipboardButtonComponent
{...buttonProps}
Expand Down
17 changes: 8 additions & 9 deletions src/components/ClipboardButton/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,14 @@ LANDING_BLOCK-->

`ClipboardButton` properties are inherited from `Button` [properties](../Button/README.md#properties) except `href`, `component`, `target`, `rel`, `loading`, `children`.

| Name | Description | Type | Default |
| :----------------- | :----------------------------------------------------------------------- | :-----------------------------------------------: | :---------: |
| hasTooltip | Disable tooltip. Tooltip won't be shown | `boolean` | `true` |
| onCopy | Callback after copy `(text: string, result: boolean) => void` | `Function` | |
| options | Copy to clipboard options | [CopyToClipboardOptions](#copytoclipboardoptions) | |
| text | Text to copy | `string` | |
| timeout | Time before state bounces back to its normal after the button is clicked | `number` | `1000` |
| tooltipInitialText | Text shown before copy | `string` | `"Copy"` |
| tooltipSuccessText | Text shown after copy | `string` | `"Copied!"` |
| Name | Description | Type | Default |
| :----------------- | :----------------------------------------------------------------------- | :--------: | :---------: |
| hasTooltip | Disable tooltip. Tooltip won't be shown | `boolean` | `true` |
| onCopy | Callback after copy `(text: string, result: boolean) => void` | `Function` | |
| text | Text to copy | `string` | |
| timeout | Time before state bounces back to its normal after the button is clicked | `number` | `1000` |
| tooltipInitialText | Text shown before copy | `string` | `"Copy"` |
| tooltipSuccessText | Text shown after copy | `string` | `"Copied!"` |

### CopyToClipboardOptions

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import {ClipboardButton} from '../ClipboardButton';
export default {
title: 'Components/Utils/ClipboardButton',
component: ClipboardButton,
args: {
text: 'Clipboard text from `<ClipboardButton/>`',
},
} as Meta;

type Story = StoryObj<typeof ClipboardButton>;
Expand Down
46 changes: 36 additions & 10 deletions src/components/CopyToClipboard/CopyToClipboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,27 @@

import React from 'react';

import ReactCopyToClipboard from 'react-copy-to-clipboard';
import {copyText} from '../../utils/copyText';

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

const INITIAL_STATUS: CopyToClipboardStatus = 'pending';

export function CopyToClipboard(props: CopyToClipboardProps) {
const {children, text, options, timeout, onCopy} = props;
const {children, text, timeout, onCopy} = props;

const textRef = React.useRef(text);
const [status, setStatus] = React.useState<CopyToClipboardStatus>(INITIAL_STATUS);

const timerIdRef = React.useRef<number>();

const content = React.useMemo(() => children(status), [children, status]);
const content = React.useMemo<React.ReactElement<React.HTMLAttributes<HTMLElement>>>(
() => children(status),
[children, status],
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's no benefit of useMemo at all, children as function typically inline in jsx, so every render is new instance


const handleCopy = React.useCallback<Required<ReactCopyToClipboard.Props>['onCopy']>(
(copyText, result) => {
const handleCopy = React.useCallback(
(copyText: string, result: boolean) => {

Check warning on line 25 in src/components/CopyToClipboard/CopyToClipboard.tsx

View workflow job for this annotation

GitHub Actions / Verify Files

'copyText' is already declared in the upper scope on line 5 column 9
setStatus(result ? 'success' : 'error');
window.clearTimeout(timerIdRef.current);
timerIdRef.current = window.setTimeout(() => setStatus(INITIAL_STATUS), timeout);
Expand All @@ -27,15 +31,37 @@
[onCopy, timeout],
);

const onClickWithCopy: React.MouseEventHandler<HTMLElement> = React.useCallback(
(event) => {
textRef.current = text;

function copy(result: boolean) {
if (text === textRef.current) {
handleCopy(text, result);

content.props?.onClick?.(event);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we call this onClick unconditionally?

}
}

copyText(text).then(
() => {
copy(true);
},
() => {
copy(false);
},
);
},
[content.props, handleCopy, text],
);

React.useEffect(() => () => window.clearTimeout(timerIdRef.current), []);

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

return (
<ReactCopyToClipboard text={text} onCopy={handleCopy} options={options}>
{content}
</ReactCopyToClipboard>
);
return React.cloneElement(content, {
onClick: onClickWithCopy,
});
}
4 changes: 1 addition & 3 deletions src/components/CopyToClipboard/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import type React from 'react';

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

export type CopyToClipboardStatus = 'pending' | 'success' | 'error';

export type OnCopyHandler = (text: string, result: boolean) => void;
Expand All @@ -11,7 +9,7 @@ export type CopyToClipboardContent = (status: CopyToClipboardStatus) => React.Re
export interface CopyToClipboardProps {
text: string;
timeout?: number;
/** Child element should have `onClick` handler to work properly */
children: CopyToClipboardContent;
onCopy?: OnCopyHandler;
options?: ReactCopyToClipboard.Options;
}
23 changes: 12 additions & 11 deletions src/demo/colors/ColorPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import React from 'react';

import {Bulb} from '@gravity-ui/icons';
import ReactCopyToClipboard from 'react-copy-to-clipboard';

import {ActionTooltip, Button, Icon} from '../../components';
import {ActionTooltip, Button, CopyToClipboard, Icon} from '../../components';
import {useUniqId} from '../../hooks';

import './ColorPanel.scss';
Expand Down Expand Up @@ -39,18 +38,20 @@ export function ColorPanel(props: ColorPanelProps) {
const copyText = `var(${varName})`;
return (
<div className="color-panel__card" key={color.name}>
<ReactCopyToClipboard text={copyText}>
<div
className={`color-panel__card-box ${boxBorders}`}
style={{background: `var(${varName})`}}
/>
</ReactCopyToClipboard>
<CopyToClipboard text={copyText}>
{() => (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's support normal children type as well as it was in ReactCopyToClipboard ? I find it usefull, if I don't need status feedback

<div
className={`color-panel__card-box ${boxBorders}`}
style={{background: `var(${varName})`}}
/>
)}
</CopyToClipboard>
<div className="color-panel__card-texts">
<div className="color-panel__card-headline">
<div className="color-panel__card-title">{color.title}</div>
<ReactCopyToClipboard text={copyText}>
<div className="color-panel__card-var">{varName}</div>
</ReactCopyToClipboard>
<CopyToClipboard text={copyText}>
{() => <div className="color-panel__card-var">{varName}</div>}
</CopyToClipboard>
</div>
<div className="color-panel__card-description">{color.description}</div>
</div>
Expand Down
9 changes: 4 additions & 5 deletions src/demo/colors/ColorTable.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import React from 'react';

import {Ban} from '@gravity-ui/icons';
import ReactCopyToClipboard from 'react-copy-to-clipboard';

import {Icon} from '../../components';
import {CopyToClipboard, Icon} from '../../components';
import {cn} from '../../components/utils/cn';

import './ColorTable.scss';
Expand Down Expand Up @@ -83,9 +82,9 @@ export function ColorTable({theme}: ColorTableProps) {
);

return varExist ? (
<ReactCopyToClipboard text={`var(${varName})`} key={step}>
{content}
</ReactCopyToClipboard>
<CopyToClipboard text={`var(${varName})`} key={step}>
{() => content}
</CopyToClipboard>
) : (
content
);
Expand Down
33 changes: 18 additions & 15 deletions src/demo/typography/TextPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import React from 'react';

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

import {CopyToClipboard} from '../../components';
import {cn} from '../../components/utils/cn';

import './TextPanel.scss';
Expand Down Expand Up @@ -32,24 +31,28 @@ export function TextPanel(props: TextPanelProps) {
<div className={b('card-texts')}>
<div className={b('card-headline')}>
<div className={b('card-title')}>{item.title}</div>
<ReactCopyToClipboard text={copyText}>
<div className={b('card-var')}>{varName}</div>
</ReactCopyToClipboard>
<CopyToClipboard text={copyText}>
{() => <div className={b('card-var')}>{varName}</div>}
</CopyToClipboard>
</div>
{item.description && (
<div className={b('card-description')}>{item.description}</div>
)}
{props.variant && (
<ReactCopyToClipboard text={copyText}>
<div
className={b('card-sample', {variant: varName})}
style={
props.variant ? undefined : {fontFamily: `var(${varName})`}
}
>
{SAMPLE_TEXT}
</div>
</ReactCopyToClipboard>
<CopyToClipboard text={copyText}>
{() => (
<div
className={b('card-sample', {variant: varName})}
style={
props.variant
? undefined
: {fontFamily: `var(${varName})`}
}
>
{SAMPLE_TEXT}
</div>
)}
</CopyToClipboard>
)}
</div>
</div>
Expand Down
7 changes: 7 additions & 0 deletions src/utils/copyText.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export function copyText(text: string) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file is not needed to be at the root utils, place it near the component please

if (navigator?.clipboard?.writeText) {
return navigator.clipboard.writeText(text);
}

return Promise.reject(new Error('Native copy is not available'));
}
Loading