From b58acc3f341c474b67f536ec49ea142fbbd27673 Mon Sep 17 00:00:00 2001 From: Konasov Dmitriy Date: Wed, 1 May 2024 16:45:21 +0200 Subject: [PATCH] click-to-copy feature --- .../docs/docs/click-to-copy.md | 41 +++++++++++++++++ react-ux-semantics/src/click-to-copy/index.ts | 16 +++++++ react-ux-semantics/src/index.ts | 1 + .../tests/click-to-copy.test.ts | 44 +++++++++++++++++++ 4 files changed, 102 insertions(+) create mode 100644 react-ux-semantics-docs/docs/docs/click-to-copy.md create mode 100644 react-ux-semantics/src/click-to-copy/index.ts create mode 100644 react-ux-semantics/tests/click-to-copy.test.ts diff --git a/react-ux-semantics-docs/docs/docs/click-to-copy.md b/react-ux-semantics-docs/docs/docs/click-to-copy.md new file mode 100644 index 0000000..63a0303 --- /dev/null +++ b/react-ux-semantics-docs/docs/docs/click-to-copy.md @@ -0,0 +1,41 @@ +--- +sidebar_position: 3 +title: Click to copy +--- + +## General description + +Hook that allows you to implement copy-by-click semantic – when user clicks on the interface element and gets some predefined text copied to clipboard. + +## Usage example + +```jsx +export const ComponentWithCopyableText = () => { + const text = 'This text will be copied!'; + const [ clickHandler, copyPromise ] = useClickToCopy(text); + + return ( + <> +
{text}
+ + + ) +}; +``` + +## API + +### useClickToCopy + +```ts +declare function useClickToCopy(text: string): [() => void, Promise]; +``` + +#### Arguments + +|arg|type|description| +|:--|:---|:----------| +|`text`|`string`|Text to copy| + + +Returns a tuple with handler for a trigger and promise, which will be resolve when the text will be copied. \ No newline at end of file diff --git a/react-ux-semantics/src/click-to-copy/index.ts b/react-ux-semantics/src/click-to-copy/index.ts new file mode 100644 index 0000000..cf41ed8 --- /dev/null +++ b/react-ux-semantics/src/click-to-copy/index.ts @@ -0,0 +1,16 @@ +import { useCallback } from "react"; + +export function useClickToCopy(text: string): [() => void, Promise] { + let resolver: () => void; + const promise = new Promise((resolve) => { + resolver = resolve; + }); + + const handler = useCallback(async () => { + await navigator.clipboard.writeText(text); + + resolver(); + }, [text, resolver]); + + return [handler, promise]; +} diff --git a/react-ux-semantics/src/index.ts b/react-ux-semantics/src/index.ts index 9e37f35..854c701 100644 --- a/react-ux-semantics/src/index.ts +++ b/react-ux-semantics/src/index.ts @@ -1,2 +1,3 @@ export { useAutohide } from "./autohide"; +export { useClickToCopy } from "./click-to-copy"; export { useShowable } from "./showable"; diff --git a/react-ux-semantics/tests/click-to-copy.test.ts b/react-ux-semantics/tests/click-to-copy.test.ts new file mode 100644 index 0000000..c33c24f --- /dev/null +++ b/react-ux-semantics/tests/click-to-copy.test.ts @@ -0,0 +1,44 @@ +import { describe, it, vi, expect, afterEach } from "vitest"; +import { renderHook, act } from "@testing-library/react"; + +import { useClickToCopy } from "../src/click-to-copy"; + +type DeepPartial = T extends object + ? { + [P in keyof T]?: DeepPartial; + } + : T; + +const navigatorMock: DeepPartial = { + clipboard: { + writeText: vi.fn(() => Promise.resolve()), + }, +}; + +vi.stubGlobal("navigator", navigatorMock); + +describe("click to copy", () => { + afterEach(() => { + vi.resetAllMocks(); + }); + + it("should call clipboard method of we API when handler is called", async () => { + const { result } = renderHook(() => useClickToCopy("henlo, fren!")); + const [handler] = result.current; + + await act(() => handler()); + + expect(navigatorMock.clipboard?.writeText).toHaveBeenCalledWith( + "henlo, fren!", + ); + }); + + it("should resolve promise when text was copied to clipboard", async () => { + const { result } = renderHook(() => useClickToCopy("henlo, fren!")); + const [handler, promise] = result.current; + + await act(() => handler()); + + await expect(promise).resolves.toBeFalsy(); + }); +});