From c206bb4e3161eaf9c6320ea8b9bd28d68e082288 Mon Sep 17 00:00:00 2001 From: Alexey Sudilovskiy Date: Thu, 12 Sep 2024 15:13:24 +0200 Subject: [PATCH 1/5] feat: add native copy method --- .../ClipboardButton/ClipboardButton.tsx | 9 ++++- src/components/ClipboardButton/README.md | 1 + .../CopyToClipboard/CopyToClipboard.tsx | 29 +++++++++++++- src/components/CopyToClipboard/README.md | 13 ++++--- src/components/CopyToClipboard/types.ts | 3 ++ src/components/Label/Label.tsx | 5 ++- src/components/Label/README.md | 39 ++++++++++--------- src/utils/copyText.ts | 7 ++++ 8 files changed, 77 insertions(+), 29 deletions(-) create mode 100644 src/utils/copyText.ts diff --git a/src/components/ClipboardButton/ClipboardButton.tsx b/src/components/ClipboardButton/ClipboardButton.tsx index c30d42065e..b8227ae862 100644 --- a/src/components/ClipboardButton/ClipboardButton.tsx +++ b/src/components/ClipboardButton/ClipboardButton.tsx @@ -106,6 +106,7 @@ export function ClipboardButton(props: ClipboardButtonProps) { hasTooltip = true, onMouseEnter, onFocus, + nativeCopy, ...buttonProps } = props; @@ -154,7 +155,13 @@ export function ClipboardButton(props: ClipboardButtonProps) { ); return ( - + {(status) => ( | 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` | | diff --git a/src/components/CopyToClipboard/CopyToClipboard.tsx b/src/components/CopyToClipboard/CopyToClipboard.tsx index 6265056c99..b08bcc5058 100644 --- a/src/components/CopyToClipboard/CopyToClipboard.tsx +++ b/src/components/CopyToClipboard/CopyToClipboard.tsx @@ -4,18 +4,23 @@ 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, options, timeout, nativeCopy, onCopy} = props; const [status, setStatus] = React.useState(INITIAL_STATUS); const timerIdRef = React.useRef(); - const content = React.useMemo(() => children(status), [children, status]); + const content = React.useMemo>>( + () => children(status), + [children, status], + ); const handleCopy = React.useCallback['onCopy']>( (copyText, result) => { @@ -27,12 +32,32 @@ export function CopyToClipboard(props: CopyToClipboardProps) { [onCopy, timeout], ); + const onClickWithCopy: React.MouseEventHandler = React.useCallback( + (event) => { + copyText(text).then( + () => handleCopy(text, true), + () => handleCopy(text, false), + ); + + if (typeof content.props?.onClick === 'function') { + content.props.onClick(event); + } + }, + [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 ( {content} diff --git a/src/components/CopyToClipboard/README.md b/src/components/CopyToClipboard/README.md index 8141e47215..ffe28c445d 100644 --- a/src/components/CopyToClipboard/README.md +++ b/src/components/CopyToClipboard/README.md @@ -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` | | diff --git a/src/components/CopyToClipboard/types.ts b/src/components/CopyToClipboard/types.ts index b70a733b7f..9cd3e07c5c 100644 --- a/src/components/CopyToClipboard/types.ts +++ b/src/components/CopyToClipboard/types.ts @@ -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; } diff --git a/src/components/Label/Label.tsx b/src/components/Label/Label.tsx index f217390805..8dbea7ca70 100644 --- a/src/components/Label/Label.tsx +++ b/src/components/Label/Label.tsx @@ -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 */ @@ -73,6 +75,7 @@ export const Label = React.forwardRef(function Label( className, disabled, copyText, + nativeCopy, closeButtonLabel, copyButtonLabel, interactive = false, @@ -178,7 +181,7 @@ export const Label = React.forwardRef(function Label( if (hasCopy && copyText && !hasOnClick) { return ( - + {(status) => renderLabel(status)} ); diff --git a/src/components/Label/README.md b/src/components/Label/README.md index f1d68832c7..c80d2902a5 100644 --- a/src/components/Label/README.md +++ b/src/components/Label/README.md @@ -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` | | diff --git a/src/utils/copyText.ts b/src/utils/copyText.ts new file mode 100644 index 0000000000..88b5906206 --- /dev/null +++ b/src/utils/copyText.ts @@ -0,0 +1,7 @@ +export function copyText(text: string) { + if (navigator.clipboard.writeText) { + return navigator.clipboard.writeText(text); + } + + return Promise.reject(new Error('Native copy is not available')); +} From 0a33baa817eaf29dea464b2e49820ce4bb01cbae Mon Sep 17 00:00:00 2001 From: Alexey Sudilovskiy Date: Thu, 12 Sep 2024 15:47:18 +0200 Subject: [PATCH 2/5] docs: use uikit component in storybook --- src/demo/colors/ColorPanel.tsx | 23 ++++++++++----------- src/demo/colors/ColorTable.tsx | 9 ++++----- src/demo/typography/TextPanel.tsx | 33 +++++++++++++++++-------------- 3 files changed, 34 insertions(+), 31 deletions(-) diff --git a/src/demo/colors/ColorPanel.tsx b/src/demo/colors/ColorPanel.tsx index b6a70c430e..48cac697d7 100644 --- a/src/demo/colors/ColorPanel.tsx +++ b/src/demo/colors/ColorPanel.tsx @@ -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'; @@ -39,18 +38,20 @@ export function ColorPanel(props: ColorPanelProps) { const copyText = `var(${varName})`; return (
- -
- + + {() => ( +
+ )} +
{color.title}
- -
{varName}
-
+ + {() =>
{varName}
} +
{color.description}
diff --git a/src/demo/colors/ColorTable.tsx b/src/demo/colors/ColorTable.tsx index b31e434883..2a9ba87a70 100644 --- a/src/demo/colors/ColorTable.tsx +++ b/src/demo/colors/ColorTable.tsx @@ -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'; @@ -83,9 +82,9 @@ export function ColorTable({theme}: ColorTableProps) { ); return varExist ? ( - - {content} - + + {() => content} + ) : ( content ); diff --git a/src/demo/typography/TextPanel.tsx b/src/demo/typography/TextPanel.tsx index 11f68ab487..985202fffd 100644 --- a/src/demo/typography/TextPanel.tsx +++ b/src/demo/typography/TextPanel.tsx @@ -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'; @@ -32,24 +31,28 @@ export function TextPanel(props: TextPanelProps) {
{item.title}
- -
{varName}
-
+ + {() =>
{varName}
} +
{item.description && (
{item.description}
)} {props.variant && ( - -
- {SAMPLE_TEXT} -
-
+ + {() => ( +
+ {SAMPLE_TEXT} +
+ )} +
)}
From ad70361e0f88733066fcb6681d85c0c8d37f7654 Mon Sep 17 00:00:00 2001 From: Alexey Sudilovskiy Date: Thu, 12 Sep 2024 15:50:08 +0200 Subject: [PATCH 3/5] chore: add text for copy --- .../ClipboardButton/__stories__/ClipboardButton.stories.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/ClipboardButton/__stories__/ClipboardButton.stories.tsx b/src/components/ClipboardButton/__stories__/ClipboardButton.stories.tsx index ad6d56996d..35304c8756 100644 --- a/src/components/ClipboardButton/__stories__/ClipboardButton.stories.tsx +++ b/src/components/ClipboardButton/__stories__/ClipboardButton.stories.tsx @@ -9,6 +9,9 @@ import {ClipboardButton} from '../ClipboardButton'; export default { title: 'Components/Utils/ClipboardButton', component: ClipboardButton, + args: { + text: 'Clipboard text from ``', + }, } as Meta; type Story = StoryObj; From d6bc68b9e68077929f3e333850180b6bbfb522f4 Mon Sep 17 00:00:00 2001 From: Alexey Sudilovskiy Date: Thu, 12 Sep 2024 16:19:07 +0200 Subject: [PATCH 4/5] fix: check current text in promise --- .../CopyToClipboard/CopyToClipboard.tsx | 27 ++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/src/components/CopyToClipboard/CopyToClipboard.tsx b/src/components/CopyToClipboard/CopyToClipboard.tsx index b08bcc5058..6db8936a4d 100644 --- a/src/components/CopyToClipboard/CopyToClipboard.tsx +++ b/src/components/CopyToClipboard/CopyToClipboard.tsx @@ -13,6 +13,7 @@ const INITIAL_STATUS: CopyToClipboardStatus = 'pending'; export function CopyToClipboard(props: CopyToClipboardProps) { const {children, text, options, timeout, nativeCopy, onCopy} = props; + const textRef = React.useRef(text); const [status, setStatus] = React.useState(INITIAL_STATUS); const timerIdRef = React.useRef(); @@ -34,14 +35,28 @@ export function CopyToClipboard(props: CopyToClipboardProps) { const onClickWithCopy: React.MouseEventHandler = React.useCallback( (event) => { + textRef.current = text; + copyText(text).then( - () => handleCopy(text, true), - () => handleCopy(text, false), + () => { + if (text === textRef.current) { + handleCopy(text, true); + + if (typeof content.props?.onClick === 'function') { + content.props.onClick(event); + } + } + }, + () => { + if (text === textRef.current) { + handleCopy(text, false); + + if (typeof content.props?.onClick === 'function') { + content.props.onClick(event); + } + } + }, ); - - if (typeof content.props?.onClick === 'function') { - content.props.onClick(event); - } }, [content.props, handleCopy, text], ); From 3ded26613476dbb34f8e93ec65d08e773196a6cc Mon Sep 17 00:00:00 2001 From: Alexey Date: Fri, 11 Oct 2024 21:37:23 +0200 Subject: [PATCH 5/5] refactor: extract copy helper --- .../CopyToClipboard/CopyToClipboard.tsx | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/src/components/CopyToClipboard/CopyToClipboard.tsx b/src/components/CopyToClipboard/CopyToClipboard.tsx index 6db8936a4d..09c7bb99c7 100644 --- a/src/components/CopyToClipboard/CopyToClipboard.tsx +++ b/src/components/CopyToClipboard/CopyToClipboard.tsx @@ -37,24 +37,20 @@ export function CopyToClipboard(props: CopyToClipboardProps) { (event) => { textRef.current = text; + function copy(result: boolean) { + if (text === textRef.current) { + handleCopy(text, result); + + content.props?.onClick?.(event); + } + } + copyText(text).then( () => { - if (text === textRef.current) { - handleCopy(text, true); - - if (typeof content.props?.onClick === 'function') { - content.props.onClick(event); - } - } + copy(true); }, () => { - if (text === textRef.current) { - handleCopy(text, false); - - if (typeof content.props?.onClick === 'function') { - content.props.onClick(event); - } - } + copy(false); }, ); },