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: add native copy method #1852

Open
wants to merge 5 commits into
base: main
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
9 changes: 8 additions & 1 deletion src/components/ClipboardButton/ClipboardButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@
hasTooltip = true,
onMouseEnter,
onFocus,
nativeCopy,
...buttonProps
} = props;

Expand All @@ -116,7 +117,7 @@
React.useEffect(() => window.clearTimeout(timerIdRef.current), []);

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

Check warning on line 120 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 +155,13 @@
);

return (
<CopyToClipboard text={text} timeout={timeout} onCopy={handleCopy} options={options}>
<CopyToClipboard
text={text}
timeout={timeout}
onCopy={handleCopy}
options={options}
nativeCopy={nativeCopy}
>
{(status) => (
<ClipboardButtonComponent
{...buttonProps}
Expand Down
1 change: 1 addition & 0 deletions src/components/ClipboardButton/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ LANDING_BLOCK-->
| Name | Description | Type | Default |
| :----------------- | :----------------------------------------------------------------------- | :-----------------------------------------------: | :---------: |
| hasTooltip | Disable tooltip. Tooltip won't be shown | `boolean` | `true` |
| nativeCopy | Use native clipboard methods instead of `copy-to-clipboard` lib | `Function` | |
| onCopy | Callback after copy `(text: string, result: boolean) => void` | `Function` | |
| options | Copy to clipboard options | [CopyToClipboardOptions](#copytoclipboardoptions) | |
| text | Text to copy | `string` | |
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
40 changes: 38 additions & 2 deletions src/components/CopyToClipboard/CopyToClipboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,27 @@

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, options, timeout, nativeCopy, 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],
);

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

Check warning on line 27 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 7 column 9
setStatus(result ? 'success' : 'error');
window.clearTimeout(timerIdRef.current);
timerIdRef.current = window.setTimeout(() => setStatus(INITIAL_STATUS), timeout);
Expand All @@ -27,12 +33,42 @@
[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);
}
}

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');
}

if (nativeCopy) {
return React.cloneElement(content, {
onClick: onClickWithCopy,
});
}

return (
<ReactCopyToClipboard text={text} onCopy={handleCopy} options={options}>
{content}
Expand Down
13 changes: 7 additions & 6 deletions src/components/CopyToClipboard/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,10 @@ const buttonText = {

## Properties

| Name | Description | Type | Default |
| :------- | :---------------------------------------------------------------------- | :--------: | :-----: |
| children | Render function `(status: CopyToClipboardStatus) => React.ReactElement` | `Function` | |
| onCopy | `copy` event handler | `Function` | |
| text | Text to copy | `string` | |
| timeout | Time in ms to restore initial state | `number` | |
| Name | Description | Type | Default |
| :--------- | :---------------------------------------------------------------------- | :--------: | :-----: |
| children | Render function `(status: CopyToClipboardStatus) => React.ReactElement` | `Function` | |
| onCopy | `copy` event handler | `Function` | |
| text | Text to copy | `string` | |
| timeout | Time in ms to restore initial state | `number` | |
| nativeCopy | Use native clipboard methods instead of `copy-to-clipboard` lib | `number` | |
3 changes: 3 additions & 0 deletions src/components/CopyToClipboard/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ 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;
/** Use native copy instead of `copy-to-clipboard` */
nativeCopy?: boolean;
}
5 changes: 4 additions & 1 deletion src/components/Label/Label.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ export interface LabelProps extends QAProps {
closeButtonLabel?: string;
/** `aria-label` of copy button */
copyButtonLabel?: string;
/** Use native clipboard methods */
nativeCopy?: boolean;
/** Handler for copy event */
onCopy?(text: string, result: boolean): void;
/** Handler for click on label itself */
Expand Down Expand Up @@ -73,6 +75,7 @@ export const Label = React.forwardRef(function Label(
className,
disabled,
copyText,
nativeCopy,
closeButtonLabel,
copyButtonLabel,
interactive = false,
Expand Down Expand Up @@ -178,7 +181,7 @@ export const Label = React.forwardRef(function Label(

if (hasCopy && copyText && !hasOnClick) {
return (
<CopyToClipboard text={copyText} onCopy={onCopy} timeout={1000}>
<CopyToClipboard text={copyText} onCopy={onCopy} timeout={1000} nativeCopy={nativeCopy}>
{(status) => renderLabel(status)}
</CopyToClipboard>
);
Expand Down
39 changes: 20 additions & 19 deletions src/components/Label/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,22 +251,23 @@ LANDING_BLOCK-->

## Properties

| Name | Description | Type | Default |
| :--------------- | :-------------------------------------------- | :----------------------------: | :---------: |
| children | Content | `React.ReactNode` | |
| className | HTML `class` attribute | `string` | |
| closeButtonLabel | `aria-label` of the close button | `string` | |
| copyButtonLabel | `aria-label` of the copy button | `string` | |
| copyText | Text to copy | `string` | |
| disabled | Disabled state | `boolean` | |
| icon | Label icon (on the left) | `React.ReactNode` | |
| interactive | Enable hover effect | `boolean` | |
| onClick | `click` event handler | `Function` | |
| onCloseClick | Close button `click` event handler | `Function` | |
| onCopy | `copy` event handler | `Function` | |
| size | Label size | `"xs"` `"s"` `"m"` | `"s"` |
| theme | Label theme | `string` | `"normal"` |
| type | Label type | `"default"` `"copy"` `"close"` | `"default"` |
| value | Label value (displayed as "children : value") | `string` | |
| title | HTML `title` attribute | `string` | |
| qa | HTML `data-qa` attribute, used in tests | `string` | |
| Name | Description | Type | Default |
| :--------------- | :-------------------------------------------------------------- | :----------------------------: | :---------: |
| children | Content | `React.ReactNode` | |
| className | HTML `class` attribute | `string` | |
| closeButtonLabel | `aria-label` of the close button | `string` | |
| copyButtonLabel | `aria-label` of the copy button | `string` | |
| copyText | Text to copy | `string` | |
| nativeCopy | Use native clipboard methods instead of `copy-to-clipboard` lib | `string` | |
| disabled | Disabled state | `boolean` | |
| icon | Label icon (on the left) | `React.ReactNode` | |
| interactive | Enable hover effect | `boolean` | |
| onClick | `click` event handler | `Function` | |
| onCloseClick | Close button `click` event handler | `Function` | |
| onCopy | `copy` event handler | `Function` | |
| size | Label size | `"xs"` `"s"` `"m"` | `"s"` |
| theme | Label theme | `string` | `"normal"` |
| type | Label type | `"default"` `"copy"` `"close"` | `"default"` |
| value | Label value (displayed as "children : value") | `string` | |
| title | HTML `title` attribute | `string` | |
| qa | HTML `data-qa` attribute, used in tests | `string` | |
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} nativeCopy>
{() => (
<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} nativeCopy>
{() => <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} nativeCopy>
{() => 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} nativeCopy>
{() => <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} nativeCopy>
{() => (
<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) {
if (navigator.clipboard.writeText) {
Copy link
Contributor

Choose a reason for hiding this comment

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

navigator?.clipboard?.writeText ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think if is better for possible stackatrace investigation

Copy link
Contributor

Choose a reason for hiding this comment

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

but it is possible that application will fail, if there's no clipboard in navigator proto. We don't use try ... catch when using copyText.
I suggested not removing if, but add checks like if (navigator?.clipboard?.writeText) { ... }

return navigator.clipboard.writeText(text);
}

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