-
Notifications
You must be signed in to change notification settings - Fork 184
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: create utilities functions (#6137)
- `useGlobalOnClickOutside()` – функция для отслеживания клика на обалсть вне целевых элементов. - `useStableCallback()` – подобие `React.useCallback()`, но без второго аргумента с `deps`. - `createPortal()` – внутри сразу фолбечит на `document.body`. Использую в `useFloatingWithIteractions()`. > **Note** > > Из-за особенности работы `iframe` в Styleguide, `document.body`, полученный даже через `document` из `getDOM()`, соответствует корневому элементу, а не тому, что в `iframe`. Создал функцию `getDocumentBody()`, с помощью которой нужно получать `document.body` и передавать в `createPortal()`.
- Loading branch information
Showing
8 changed files
with
226 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
import * as React from 'react'; | ||
import { fireEvent, render } from '@testing-library/react'; | ||
import { useGlobalOnClickOutside } from './useGlobalOnClickOutside'; | ||
|
||
interface WrapperUseGlobalOnClickOutsideProps { | ||
disableTarget?: 'target-1' | 'target-2'; | ||
disableAllTarget?: boolean; | ||
globalClickHandler(): void; | ||
} | ||
|
||
const WrapperUseGlobalOnClickOutside = ({ | ||
disableTarget, | ||
disableAllTarget = false, | ||
globalClickHandler, | ||
}: WrapperUseGlobalOnClickOutsideProps) => { | ||
const target1Ref = React.createRef<HTMLDivElement>(); | ||
const target2Ref = React.createRef<HTMLDivElement>(); | ||
|
||
useGlobalOnClickOutside( | ||
globalClickHandler, | ||
disableAllTarget || disableTarget === 'target-1' ? null : target1Ref, | ||
disableAllTarget || disableTarget === 'target-2' ? null : target2Ref, | ||
); | ||
|
||
return ( | ||
<div data-testid="root"> | ||
<div data-testid="outside" /> | ||
<div data-testid="target-1" ref={target1Ref}></div> | ||
<div data-testid="target-2" ref={target2Ref}> | ||
<div data-testid="target-2-child" /> | ||
</div> | ||
</div> | ||
); | ||
}; | ||
|
||
describe(useGlobalOnClickOutside, () => { | ||
it('should works with multiple refs provided', () => { | ||
const globalClickHandler = jest.fn(); | ||
const result = render( | ||
<WrapperUseGlobalOnClickOutside globalClickHandler={globalClickHandler} />, | ||
); | ||
|
||
fireEvent.click(result.getByTestId('target-1')); | ||
fireEvent.click(result.getByTestId('target-2')); | ||
fireEvent.click(result.getByTestId('target-2-child')); | ||
expect(globalClickHandler).not.toHaveBeenCalled(); | ||
|
||
fireEvent.click(document.documentElement); | ||
fireEvent.click(result.getByTestId('root')); | ||
fireEvent.click(result.getByTestId('outside')); | ||
expect(globalClickHandler).toHaveBeenCalledTimes(3); | ||
}); | ||
|
||
it('should work with one ref provided', () => { | ||
const globalClickHandler = jest.fn(); | ||
const result = render( | ||
<WrapperUseGlobalOnClickOutside | ||
globalClickHandler={globalClickHandler} | ||
disableTarget="target-1" | ||
/>, | ||
); | ||
|
||
fireEvent.click(result.getByTestId('target-1')); | ||
fireEvent.click(result.getByTestId('target-2')); | ||
expect(globalClickHandler).toHaveBeenCalledTimes(1); | ||
|
||
result.rerender(<WrapperUseGlobalOnClickOutside globalClickHandler={globalClickHandler} />); | ||
|
||
fireEvent.click(result.getByTestId('target-1')); | ||
fireEvent.click(result.getByTestId('target-2')); | ||
fireEvent.click(document.documentElement); | ||
fireEvent.click(result.getByTestId('root')); | ||
fireEvent.click(result.getByTestId('outside')); | ||
expect(globalClickHandler).toHaveBeenCalledTimes(4); | ||
}); | ||
|
||
it('should clear events if no refs provided', () => { | ||
const globalClickHandler = jest.fn(); | ||
|
||
const result = render( | ||
<WrapperUseGlobalOnClickOutside globalClickHandler={globalClickHandler} />, | ||
); | ||
|
||
fireEvent.click(result.getByTestId('target-2')); | ||
expect(globalClickHandler).not.toHaveBeenCalled(); | ||
|
||
fireEvent.click(result.getByTestId('outside')); | ||
expect(globalClickHandler).toHaveBeenCalledTimes(1); | ||
|
||
result.rerender( | ||
<WrapperUseGlobalOnClickOutside globalClickHandler={globalClickHandler} disableAllTarget />, | ||
); | ||
|
||
fireEvent.click(result.getByTestId('target-2')); | ||
fireEvent.click(result.getByTestId('outside')); | ||
expect(globalClickHandler).toHaveBeenCalledTimes(1); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import * as React from 'react'; | ||
import { isElement, useDOM } from '../lib/dom'; | ||
import { useIsomorphicLayoutEffect } from '../lib/useIsomorphicLayoutEffect'; | ||
|
||
/** | ||
* Завязывается на document. | ||
* | ||
* @private | ||
*/ | ||
export const useGlobalOnClickOutside = < | ||
T extends React.RefObject<ElementType> | undefined | null, | ||
ElementType extends Element = Element, | ||
>( | ||
callback: (event: MouseEvent) => void, | ||
...refs: T[] | ||
) => { | ||
const { document } = useDOM(); | ||
useIsomorphicLayoutEffect(() => { | ||
const someRefNotNull = refs.some((ref) => ref && ref.current !== null); | ||
if (!document || !someRefNotNull) { | ||
return; | ||
} | ||
const handleClick = (event: MouseEvent) => { | ||
const targetEl = event.target; | ||
const someRefHasTargetEl = | ||
isElement(targetEl) && | ||
refs.some((ref) => ref && ref.current && ref.current.contains(targetEl)); | ||
if (!someRefHasTargetEl) { | ||
callback(event); | ||
} | ||
}; | ||
document.addEventListener('click', handleClick, { | ||
passive: true, | ||
capture: true, | ||
}); | ||
return () => { | ||
document.removeEventListener('click', handleClick, true); | ||
}; | ||
}, [document, callback, ...refs]); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import * as React from 'react'; | ||
import { renderHook } from '@testing-library/react-hooks'; | ||
import { useStableCallback } from './useStableCallback'; | ||
|
||
describe(useStableCallback, () => { | ||
it('should save first provided fn', () => { | ||
const fn1 = jest.fn(); | ||
const stableCallback = renderHook((fn) => useStableCallback(fn), { | ||
initialProps: fn1, | ||
}); | ||
const memo = renderHook((fn) => React.useMemo(() => fn(), [fn]), { | ||
initialProps: stableCallback.result.current, | ||
}); | ||
|
||
expect(fn1).toHaveBeenCalledTimes(1); | ||
|
||
const fn2 = jest.fn(); | ||
stableCallback.rerender(fn2); | ||
memo.rerender(stableCallback.result.current); | ||
|
||
expect(fn2).toHaveBeenCalledTimes(0); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import * as React from 'react'; | ||
import { useIsomorphicLayoutEffect } from '../lib/useIsomorphicLayoutEffect'; | ||
|
||
/** | ||
* Inspired by https://github.com/facebook/react/issues/14099#issuecomment-440013892 | ||
*/ | ||
export function useStableCallback< | ||
Fn extends (...args: any[]) => any = (...args: unknown[]) => unknown, | ||
>(fn: Fn): Fn; | ||
export function useStableCallback<Args extends unknown[], Return>( | ||
fn: (...args: Args) => Return, | ||
): (...args: Args) => Return; | ||
export function useStableCallback<Args extends unknown[], Return>( | ||
fn: (...args: Args) => Return, | ||
): (...args: Args) => Return { | ||
const ref = React.useRef(fn); | ||
useIsomorphicLayoutEffect(() => { | ||
ref.current = fn; | ||
}); | ||
return React.useRef((...args: Args) => (0, ref.current)(...args)).current; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import * as React from 'react'; | ||
import { render } from '@testing-library/react'; | ||
import { createPortal } from './createPortal'; | ||
|
||
describe(createPortal, () => { | ||
it.each([ | ||
{ id: undefined, container: undefined }, | ||
{ id: 'document.body', container: document.body }, | ||
])('should in create portal to body if container is $id)', ({ container }) => { | ||
const rendered = render(<div>{createPortal(<div data-testid="portal"></div>, container)}</div>); | ||
expect(document.body).toContainElement(rendered.getByTestId('portal')); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import * as ReactDOM from 'react-dom'; | ||
import { getDocumentBody } from './dom'; | ||
|
||
export const createPortal = ( | ||
children: React.ReactNode, | ||
container?: Element | DocumentFragment, | ||
key?: null | string, | ||
) => { | ||
const resolvedContainer = container ? container : getDocumentBody(); | ||
return resolvedContainer && ReactDOM.createPortal(children, resolvedContainer, key); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters