Unverified Commit 35ab50a8 authored by Douglas Fabris's avatar Douglas Fabris Committed by GitHub

feat: useClipboard hook (#337)

* wip hook

* fix types

* initial test

* wip

* Full test suite completed

* server test added and fix prettier

* hook implementation

* matchMedia's mock improvement

* fix test

* Fix matchMediaMock usage

* Docs

* server.spec

* server test fix

* hook implementation

* usePrefersColorScheme hook implementation

* wip hook

* fix types

* initial test

* wip

* Full test suite completed

* server test added and fix prettier

* docs added

* fix review
Co-authored-by: default avatarGabriel Henriques <gabriel.henriques@rocket.chat>
Co-authored-by: default avatarGuilherme Gazzo <guilherme@gazzo.xyz>
Co-authored-by: default avatarTasso Evangelista <tasso.evangelista@rocket.chat>
parent f54a69d2
......@@ -43,45 +43,47 @@ yarn add @rocket.chat/fuselage-hooks
- [useAutoFocus](#useautofocus)
- [Parameters](#parameters)
- [useBreakpoints](#usebreakpoints)
- [useDebouncedCallback](#usedebouncedcallback)
- [useClipboard](#useclipboard)
- [Parameters](#parameters-1)
- [useDebouncedReducer](#usedebouncedreducer)
- [useDebouncedCallback](#usedebouncedcallback)
- [Parameters](#parameters-2)
- [useDebouncedState](#usedebouncedstate)
- [useDebouncedReducer](#usedebouncedreducer)
- [Parameters](#parameters-3)
- [useDebouncedUpdates](#usedebouncedupdates)
- [useDebouncedState](#usedebouncedstate)
- [Parameters](#parameters-4)
- [useDebouncedValue](#usedebouncedvalue)
- [useDebouncedUpdates](#usedebouncedupdates)
- [Parameters](#parameters-5)
- [useDebouncedValue](#usedebouncedvalue)
- [Parameters](#parameters-6)
- [useIsomorphicLayoutEffect](#useisomorphiclayouteffect)
- [useLazyRef](#uselazyref)
- [Parameters](#parameters-6)
- [useMediaQueries](#usemediaqueries)
- [Parameters](#parameters-7)
- [useMediaQuery](#usemediaquery)
- [useMediaQueries](#usemediaqueries)
- [Parameters](#parameters-8)
- [useMergedRefs](#usemergedrefs)
- [useMediaQuery](#usemediaquery)
- [Parameters](#parameters-9)
- [useMutableCallback](#usemutablecallback)
- [useMergedRefs](#usemergedrefs)
- [Parameters](#parameters-10)
- [usePosition](#useposition)
- [useMutableCallback](#usemutablecallback)
- [Parameters](#parameters-11)
- [usePrefersColorScheme](#usepreferscolorscheme)
- [usePosition](#useposition)
- [Parameters](#parameters-12)
- [usePrefersColorScheme](#usepreferscolorscheme)
- [Parameters](#parameters-13)
- [usePrefersReducedData](#useprefersreduceddata)
- [usePrefersReducedMotion](#useprefersreducedmotion)
- [useResizeObserver](#useresizeobserver)
- [Parameters](#parameters-13)
- [useSafely](#usesafely)
- [Parameters](#parameters-14)
- [useStableArray](#usestablearray)
- [useSafely](#usesafely)
- [Parameters](#parameters-15)
- [useLocalStorage](#uselocalstorage)
- [useStableArray](#usestablearray)
- [Parameters](#parameters-16)
- [useSessionStorage](#usesessionstorage)
- [useLocalStorage](#uselocalstorage)
- [Parameters](#parameters-17)
- [useToggle](#usetoggle)
- [useSessionStorage](#usesessionstorage)
- [Parameters](#parameters-18)
- [useToggle](#usetoggle)
- [Parameters](#parameters-19)
- [useUniqueId](#useuniqueid)
### useAutoFocus
......@@ -101,6 +103,20 @@ Hook to catch which responsive design' breakpoints are active.
Returns **[Array](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array)&lt;[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)>** an array of the active breakpoint names
### useClipboard
Hook to copy the passed content to the clipboard.
#### Parameters
- `text` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)**
- `$1` **UseClipboardParams** (optional, default `{}`)
- `$1.clearTime` (optional, default `2000`)
- `$1.onCopySuccess` (optional, default `():void=>undefined`)
- `$1.onCopyError` (optional, default `():void=>undefined`)
Returns **UseClipboardReturn** an object with the copy function and the hasCopied state
### useDebouncedCallback
Hook to memoize a debounced version of a callback.
......@@ -326,104 +342,3 @@ Returns **\[[boolean](https://developer.mozilla.org/docs/Web/JavaScript/Referenc
Hook to keep a unique ID string.
Returns **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** the unique ID string
# &lt;&lt;&lt;&lt;&lt;&lt;&lt; HEAD
### usePrefersColorScheme
Hook to get the prefers-color-scheme value.
#### Parameters
- `scheme` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)?**
Returns **[boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean)** `true` if the prefers-color-scheme matches
=======
### usePrefersReducedData
Hook to get the prefers-reduce-data value.
Returns **[boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean)** `true` if the prefers-reduce-data is set reduce in the media queries that matches
### usePrefersReducedMotion
Hook to get the prefers-reduce-motion value.
Returns **[boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean)** `true` if the prefers-reduce-motion is set reduce in the media queries that matches
### useResizeObserver
Hook to track dimension changes in a DOM element using the ResizeObserver API.
#### Parameters
- `options` **UseResizeObserverOptions** (optional, default `{}`)
- `options.debounceDelay`
Returns **{ref: RefObject&lt;[Element](https://developer.mozilla.org/docs/Web/API/Element)>, contentBoxSize: ResizeObserverSize, borderBoxSize: ResizeObserverSize}** a triple containing the ref and the size information
### useSafely
Hook that wraps pairs of state and dispatcher to provide a new dispatcher
which can be safe and asynchronically called even after the component unmounted.
#### Parameters
- `pair` **\[S, (Dispatch&lt;A> | DispatchWithoutAction)]** the state and dispatcher pair which will be patched
- `pair.0`
- `pair.1`
Returns **\[S, D]** a state value and safe dispatcher pair
### useStableArray
Hook to create an array with stable identity if its elements are equal.
#### Parameters
- `array` **T** the array
- `compare` **function (a: T, b: T): [boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean)** the equality function that checks if two array elements are
equal (optional, default `Object.is`)
Returns **T** the passed array if the elements are NOT equals; the previously
stored array otherwise
### useLocalStorage
Hook to deal with localStorage
#### Parameters
- `key` the key associated to the value in the storage
- `initialValue` the value returned when the key is not found at the storage
Returns **any** a state and a setter function
### useSessionStorage
Hook to deal with sessionStorage
#### Parameters
- `key` the key associated to the value in the storage
- `initialValue` the value returned when the key is not found at the storage
Returns **any** a state and a setter function
### useToggle
Hook to create a toggleable boolean state.
#### Parameters
- `initialValue` **([boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean) | function (): [boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean))?** the initial value or the initial state generator function
Returns **\[[boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean), D]** a state boolean value and a state toggler function
### useUniqueId
Hook to keep a unique ID string.
Returns **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** the unique ID string
......@@ -24,6 +24,7 @@
| [getVariantBoundaries](./fuselage-hooks.getvariantboundaries.md) | |
| [useAutoFocus](./fuselage-hooks.useautofocus.md) | Hook to automatically request focus for an DOM element. |
| [useBreakpoints](./fuselage-hooks.usebreakpoints.md) | Hook to catch which responsive design' breakpoints are active. |
| [useClipboard](./fuselage-hooks.useclipboard.md) | Hook to copy the passed content to the clipboard. |
| [useDebouncedCallback](./fuselage-hooks.usedebouncedcallback.md) | Hook to memoize a debounced version of a callback. |
| [useDebouncedValue](./fuselage-hooks.usedebouncedvalue.md) | Hook to keep a debounced reference of a value. |
| [useIsomorphicLayoutEffect](./fuselage-hooks.useisomorphiclayouteffect.md) | Replacement for the <code>useEffect</code> hook that is safely ignored on SSR. |
......@@ -52,4 +53,5 @@
| [PositionFlipOrder](./fuselage-hooks.positionfliporder.md) | |
| [PositionOptions\_2](./fuselage-hooks.positionoptions_2.md) | |
| [Positions](./fuselage-hooks.positions.md) | |
| [UseClipboardReturn](./fuselage-hooks.useclipboardreturn.md) | |
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [@rocket.chat/fuselage-hooks](./fuselage-hooks.md) &gt; [useClipboard](./fuselage-hooks.useclipboard.md)
## useClipboard variable
Hook to copy the passed content to the clipboard.
<b>Signature:</b>
```typescript
useClipboard: (text: string, { clearTime, onCopySuccess, onCopyError, }?: UseClipboardParams) => UseClipboardReturn
```
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [@rocket.chat/fuselage-hooks](./fuselage-hooks.md) &gt; [UseClipboardReturn](./fuselage-hooks.useclipboardreturn.md)
## UseClipboardReturn type
<b>Signature:</b>
```typescript
export declare type UseClipboardReturn = {
copy: (e?: Event) => Promise<void>;
hasCopied: boolean;
};
```
......@@ -9,5 +9,5 @@ Hook to create a toggleable boolean state.
<b>Signature:</b>
```typescript
useToggle: <D extends DispatchWithoutAction | Dispatch<SetStateAction<boolean>>>(initialValue?: boolean | (() => boolean)) => [boolean, D]
useToggle: <D extends Dispatch<SetStateAction<boolean>> | DispatchWithoutAction>(initialValue?: boolean | (() => boolean)) => [boolean, D]
```
export * from './useAutoFocus';
export * from './useBreakpoints';
export * from './useClipboard';
export * from './useDebouncedCallback';
export * from './useDebouncedReducer';
export * from './useDebouncedState';
......
/**
* @jest-environment node
*/
import { FunctionComponent, createElement, StrictMode } from 'react';
import { renderToString } from 'react-dom/server';
import { useClipboard, UseClipboardReturn } from './useClipboard';
describe('useClipboard hook on server', () => {
it('has hasCopied and copy properties', () => {
let hookObject: UseClipboardReturn;
const TestComponent: FunctionComponent = () => {
hookObject = useClipboard('Lorem Ipsum Indolor Dolor');
return null;
};
renderToString(createElement(StrictMode, {}, createElement(TestComponent)));
expect(hookObject).toHaveProperty('copy');
expect(hookObject).toHaveProperty('hasCopied');
});
});
import { createElement, FunctionComponent, StrictMode } from 'react';
import { render } from 'react-dom';
import { act } from 'react-dom/test-utils';
import { useClipboard, UseClipboardReturn } from './useClipboard';
describe('useClipboard hook', () => {
let container;
beforeEach(() => {
container = document.createElement('div');
document.body.appendChild(container);
});
afterEach(() => {
container.remove();
container = null;
});
it('has hasCopied and copy properties', () => {
let hookObject: UseClipboardReturn;
const TestComponent: FunctionComponent = () => {
hookObject = useClipboard('Lorem Ipsum Indolor Dolor');
return null;
};
act(() => {
render(
createElement(StrictMode, {}, createElement(TestComponent)),
container
);
});
expect(hookObject).toHaveProperty('copy');
expect(hookObject).toHaveProperty('hasCopied');
});
it('updates hasCopied to true', async () => {
Object.assign(navigator, {
clipboard: {
writeText: () => Promise.resolve(),
},
});
let hookObject: UseClipboardReturn;
const TestComponent: FunctionComponent = () => {
hookObject = useClipboard('Lorem Ipsum Indolor Dolor');
return null;
};
act(() => {
render(
createElement(StrictMode, {}, createElement(TestComponent)),
container
);
});
await act(async () => {
hookObject.copy();
});
expect(hookObject.hasCopied).toBe(true);
});
it('reverts hasCopied to false', async () => {
jest.useFakeTimers();
const delay = 100 + Math.round(100 * Math.random());
const delayBeforeUpdate = Math.round(delay * 0.75);
Object.assign(navigator, {
clipboard: {
writeText: () =>
new Promise((resolve) => {
return resolve();
}),
},
});
let hookObject: UseClipboardReturn;
const TestComponent: FunctionComponent = () => {
hookObject = useClipboard('Lorem Ipsum Indolor Dolor', {
clearTime: delay,
});
return null;
};
act(() => {
render(
createElement(StrictMode, {}, createElement(TestComponent)),
container
);
});
await act(async () => {
hookObject.copy();
});
expect(hookObject.hasCopied).toBe(true);
act(() => {
jest.advanceTimersByTime(delayBeforeUpdate);
});
expect(hookObject.hasCopied).toBe(true);
act(() => {
jest.advanceTimersByTime(delay - delayBeforeUpdate);
});
expect(hookObject.hasCopied).toBe(false);
});
it('runs only success function receiving event object', async () => {
Object.assign(navigator, {
clipboard: {
writeText: () => Promise.resolve(),
},
});
const onCopySuccess = jest.fn();
const onCopyError = jest.fn();
let hookObject: UseClipboardReturn;
const TestComponent: FunctionComponent = () => {
hookObject = useClipboard('Lorem Ipsum Indolor Dolor', {
onCopySuccess,
onCopyError,
});
return null;
};
act(() => {
render(
createElement(StrictMode, {}, createElement(TestComponent)),
container
);
});
const event = new MouseEvent('click');
await act(async () => {
hookObject.copy(event);
});
expect(onCopySuccess).toBeCalledWith(
expect.objectContaining({ type: 'click' })
);
expect(onCopyError).toBeCalledTimes(0);
});
it('runs only error function receiving error object', async () => {
Object.assign(navigator, {
clipboard: {
writeText: () => Promise.reject(new Error('rejected')),
},
});
const onCopyError = jest.fn();
const onCopySuccess = jest.fn();
let hookObject: UseClipboardReturn;
const TestComponent: FunctionComponent = () => {
hookObject = useClipboard('Lorem Ipsum Indolor Dolor', {
onCopySuccess,
onCopyError,
});
return null;
};
act(() => {
render(
createElement(StrictMode, {}, createElement(TestComponent)),
container
);
});
const event = new MouseEvent('click');
await act(async () => {
hookObject.copy(event);
});
expect(onCopyError).toBeCalledWith(
expect.objectContaining({ message: 'rejected' })
);
expect(onCopySuccess).toBeCalledTimes(0);
});
});
import { useEffect, useState } from 'react';
import { useMutableCallback } from './useMutableCallback';
type UseClipboardParams = {
clearTime?: number;
onCopySuccess?: (e?: Event) => void;
onCopyError?: (e?: Event) => void;
};
export type UseClipboardReturn = {
copy: (e?: Event) => Promise<void>;
hasCopied: boolean;
};
/**
* Hook to copy the passed content to the clipboard.
*
* @returns an object with the copy function and the hasCopied state
* @public
*/
export const useClipboard = (
text: string,
{
clearTime = 2000,
onCopySuccess = (): void => undefined,
onCopyError = (): void => undefined,
}: UseClipboardParams = {}
): UseClipboardReturn => {
const [hasCopied, setHasCopied] = useState(false);
const copy = useMutableCallback(async (e?: Event) => {
e?.preventDefault();
try {
await navigator.clipboard.writeText(text);
onCopySuccess(e);
setHasCopied(true);
} catch (e) {
onCopyError(e);
}
});
useEffect(() => {
if (!hasCopied) {
return;
}
const timeout = setTimeout(() => {
setHasCopied(false);
}, clearTime);
return () => clearTimeout(timeout);
}, [hasCopied, clearTime]);
return { copy, hasCopied };
};
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment