From 30620a60e665758cee868d484188d415c352f9b2 Mon Sep 17 00:00:00 2001 From: Tilman Vatteroth Date: Sun, 27 Mar 2022 21:05:44 +0200 Subject: [PATCH] Refactor copy overlay Signed-off-by: Tilman Vatteroth --- locales/en.json | 6 +- .../common/copyable/copy-overlay.tsx | 77 ------------------- .../copy-to-clipboard-button.tsx | 15 +++- .../copyable-field/copyable-field.tsx | 56 +++++++------- .../copyable/hooks/use-copy-overlay.tsx | 75 ++++++++++++++++++ .../document-bar/share/share-modal.tsx | 10 +-- .../renderer-pane/render-iframe.tsx | 1 + 7 files changed, 125 insertions(+), 115 deletions(-) delete mode 100644 src/components/common/copyable/copy-overlay.tsx create mode 100644 src/components/common/copyable/hooks/use-copy-overlay.tsx diff --git a/locales/en.json b/locales/en.json index 4050c3145..feb9e070e 100644 --- a/locales/en.json +++ b/locales/en.json @@ -496,12 +496,14 @@ "avatarOf": "avatar of '{{name}}'", "why": "Why?", "loading": "Loading ...", - "successfullyCopied": "Copied!", - "copyError": "Error while copying!", "errorOccurred": "An error occurred", "errorWhileLoadingLibrary": "An unexpected error occurred while loading '{{name}}'.\nCheck the browser console for more information.\nReport this error only if it comes up again.", "readForMoreInfo": "Read here for more information" }, + "copyOverlay": { + "error": "Error while copying!", + "success": "Copied!" + }, "login": { "chooseMethod": "Choose method", "signInVia": "Sign in via {{service}}", diff --git a/src/components/common/copyable/copy-overlay.tsx b/src/components/common/copyable/copy-overlay.tsx deleted file mode 100644 index 10d65397d..000000000 --- a/src/components/common/copyable/copy-overlay.tsx +++ /dev/null @@ -1,77 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import type { RefObject } from 'react' -import React, { useCallback, useEffect, useState } from 'react' -import { Overlay, Tooltip } from 'react-bootstrap' -import { Trans, useTranslation } from 'react-i18next' -import { v4 as uuid } from 'uuid' -import { ShowIf } from '../show-if/show-if' -import { Logger } from '../../../utils/logger' -import { isClientSideRendering } from '../../../utils/is-client-side-rendering' - -export interface CopyOverlayProps { - content: string - clickComponent: RefObject -} - -const log = new Logger('CopyOverlay') - -export const CopyOverlay: React.FC = ({ content, clickComponent }) => { - useTranslation() - const [showCopiedTooltip, setShowCopiedTooltip] = useState(false) - const [error, setError] = useState(false) - const [tooltipId] = useState(() => uuid()) - - const copyToClipboard = useCallback((content: string) => { - if (!isClientSideRendering()) { - log.error('Clipboard not available in server side rendering') - return - } - navigator.clipboard - .writeText(content) - .then(() => { - setError(false) - }) - .catch((error: Error) => { - setError(true) - log.error('Copy failed', error) - }) - .finally(() => { - setShowCopiedTooltip(true) - setTimeout(() => { - setShowCopiedTooltip(false) - }, 2000) - }) - }, []) - - useEffect(() => { - if (clickComponent && clickComponent.current) { - clickComponent.current.addEventListener('click', () => copyToClipboard(content)) - const clickComponentSaved = clickComponent.current - return () => { - if (clickComponentSaved) { - clickComponentSaved.removeEventListener('click', () => copyToClipboard(content)) - } - } - } - }, [clickComponent, copyToClipboard, content]) - - return ( - - {(props) => ( - - - - - - - - - )} - - ) -} diff --git a/src/components/common/copyable/copy-to-clipboard-button/copy-to-clipboard-button.tsx b/src/components/common/copyable/copy-to-clipboard-button/copy-to-clipboard-button.tsx index e8e280ea3..bd8c3a3c5 100644 --- a/src/components/common/copyable/copy-to-clipboard-button/copy-to-clipboard-button.tsx +++ b/src/components/common/copyable/copy-to-clipboard-button/copy-to-clipboard-button.tsx @@ -9,7 +9,7 @@ import { Button } from 'react-bootstrap' import type { Variant } from 'react-bootstrap/types' import { useTranslation } from 'react-i18next' import { ForkAwesomeIcon } from '../../fork-awesome/fork-awesome-icon' -import { CopyOverlay } from '../copy-overlay' +import { useCopyOverlay } from '../hooks/use-copy-overlay' import type { PropsWithDataCypressId } from '../../../../utils/cypress-attribute' import { cypressId } from '../../../../utils/cypress-attribute' @@ -19,6 +19,14 @@ export interface CopyToClipboardButtonProps extends PropsWithDataCypressId { variant?: Variant } +/** + * Shows a button that copies the given content on click. + * + * @param content The content to copy + * @param size The size of the button + * @param variant The bootstrap variant of the button + * @param props Other props that are forwarded to the bootstrap button + */ export const CopyToClipboardButton: React.FC = ({ content, size = 'sm', @@ -28,6 +36,8 @@ export const CopyToClipboardButton: React.FC = ({ const { t } = useTranslation() const button = useRef(null) + const [copyToClipboard, overlayElement] = useCopyOverlay(button, content) + return ( - + {overlayElement} ) } diff --git a/src/components/common/copyable/copyable-field/copyable-field.tsx b/src/components/common/copyable/copyable-field/copyable-field.tsx index d11942985..49d4a6281 100644 --- a/src/components/common/copyable/copyable-field/copyable-field.tsx +++ b/src/components/common/copyable/copyable-field/copyable-field.tsx @@ -4,62 +4,64 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import React, { Fragment, useCallback, useMemo, useRef } from 'react' +import React, { useCallback, useMemo } from 'react' import { Button, FormControl, InputGroup } from 'react-bootstrap' import { useTranslation } from 'react-i18next' import { ForkAwesomeIcon } from '../../fork-awesome/fork-awesome-icon' import { ShowIf } from '../../show-if/show-if' -import { CopyOverlay } from '../copy-overlay' import { Logger } from '../../../../utils/logger' import { isClientSideRendering } from '../../../../utils/is-client-side-rendering' +import { CopyToClipboardButton } from '../copy-to-clipboard-button/copy-to-clipboard-button' export interface CopyableFieldProps { content: string - nativeShareButton?: boolean - url?: string + shareOriginUrl?: string } const log = new Logger('CopyableField') -export const CopyableField: React.FC = ({ content, nativeShareButton, url }) => { +/** + * Provides an input field with an attached copy button and a share button (if supported by the browser) + * + * @param content The content to present + * @param shareOriginUrl The URL of the page to which the shared content should be linked. If this value is omitted then the share button won't be shown. + */ +export const CopyableField: React.FC = ({ content, shareOriginUrl }) => { useTranslation() - const copyButton = useRef(null) + + const sharingSupported = useMemo( + () => shareOriginUrl !== undefined && isClientSideRendering() && typeof navigator.share === 'function', + [shareOriginUrl] + ) const doShareAction = useCallback(() => { - if (!isClientSideRendering()) { - log.error('Native sharing not available in server side rendering') + if (!sharingSupported) { + log.error('Native sharing not available') return } navigator .share({ text: content, - url: url + url: shareOriginUrl }) .catch((error: Error) => { log.error('Native sharing failed', error) }) - }, [content, url]) - - const sharingSupported = useMemo(() => isClientSideRendering() && typeof navigator.share === 'function', []) + }, [content, shareOriginUrl, sharingSupported]) return ( - - - + + + + + + - - - - - - - - - + + ) } diff --git a/src/components/common/copyable/hooks/use-copy-overlay.tsx b/src/components/common/copyable/hooks/use-copy-overlay.tsx new file mode 100644 index 000000000..0e9ff7490 --- /dev/null +++ b/src/components/common/copyable/hooks/use-copy-overlay.tsx @@ -0,0 +1,75 @@ +/* + * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import type { ReactElement, RefObject } from 'react' +import React, { useCallback, useMemo, useState } from 'react' +import { Overlay, Tooltip } from 'react-bootstrap' +import { Trans, useTranslation } from 'react-i18next' +import { v4 as uuid } from 'uuid' +import { ShowIf } from '../../show-if/show-if' +import { Logger } from '../../../../utils/logger' +import { isClientSideRendering } from '../../../../utils/is-client-side-rendering' + +const log = new Logger('useCopyOverlay') + +/** + * Provides a function that writes the given text into the browser clipboard and an {@link Overlay overlay} that is shown when the copy action was successful. + * + * @param clickComponent The component to which the overlay should be attached + * @param content The content that should be copied + * @return the copy function and the overlay + */ +export const useCopyOverlay = ( + clickComponent: RefObject, + content: string +): [copyToCliphoard: () => void, overlayElement: ReactElement] => { + useTranslation() + const [showCopiedTooltip, setShowCopiedTooltip] = useState(false) + const [error, setError] = useState(false) + const [tooltipId] = useState(() => uuid()) + + const copyToClipboard = useCallback(() => { + if (!isClientSideRendering()) { + log.error('Clipboard not available in server side rendering') + return + } + navigator.clipboard + .writeText(content) + .then(() => { + setError(false) + }) + .catch((error: Error) => { + setError(true) + log.error('Copy failed', error) + }) + .finally(() => { + setShowCopiedTooltip(true) + setTimeout(() => { + setShowCopiedTooltip(false) + }, 2000) + }) + }, [content]) + + const overlayElement = useMemo( + () => ( + + {(props) => ( + + + + + + + + + )} + + ), + [clickComponent, error, showCopiedTooltip, tooltipId] + ) + + return [copyToClipboard, overlayElement] +} diff --git a/src/components/editor-page/document-bar/share/share-modal.tsx b/src/components/editor-page/document-bar/share/share-modal.tsx index 5263fbb2c..d40c93a20 100644 --- a/src/components/editor-page/document-bar/share/share-modal.tsx +++ b/src/components/editor-page/document-bar/share/share-modal.tsx @@ -26,18 +26,14 @@ export const ShareModal: React.FC = ({ show, onHide }) => - + - + - + diff --git a/src/components/editor-page/renderer-pane/render-iframe.tsx b/src/components/editor-page/renderer-pane/render-iframe.tsx index 3de12e0f8..1c012149b 100644 --- a/src/components/editor-page/renderer-pane/render-iframe.tsx +++ b/src/components/editor-page/renderer-pane/render-iframe.tsx @@ -142,6 +142,7 @@ export const RenderIframe: React.FC = ({ ref={frameReference} referrerPolicy={'no-referrer'} className={`border-0 ${frameClasses ?? ''}`} + allow={'clipboard-write'} {...cypressAttribute('renderer-ready', rendererReady ? 'true' : 'false')} {...cypressAttribute('renderer-type', rendererType)} />