From e42c1bfa2db3daf3f2218705772a09eb5f7f5560 Mon Sep 17 00:00:00 2001 From: lita Date: Fri, 31 Dec 2021 15:33:51 +0100 Subject: [PATCH] feat: Add forwardRef for compatibility --- lib/click-outside/click-outside.tsx | 88 ++++++++++++++++++----------- 1 file changed, 56 insertions(+), 32 deletions(-) diff --git a/lib/click-outside/click-outside.tsx b/lib/click-outside/click-outside.tsx index 8e90a3af0..973345b07 100644 --- a/lib/click-outside/click-outside.tsx +++ b/lib/click-outside/click-outside.tsx @@ -1,59 +1,83 @@ -import React, { useRef, useEffect, MouseEvent, ReactElement } from 'react' +import React, { + useRef, + useEffect, + MouseEvent, + FC, + ReactElement, + forwardRef, + Ref, +} from 'react' import mergeRefs from 'react-merge-refs' import hasParent from './has-parent' interface ClickOutsideProps { active: boolean onClick: (e?: MouseEvent) => void - children: any + ref?: Ref } -const ClickOutside = ({ - active = true, - onClick, - children, -}: ClickOutsideProps) => { - const innerRef = useRef() - const child = children ? React.Children.only(children) : undefined - if (!child) { - throw new Error('A valid non Fragment React Children should be provided') - } +/** + * Use forward ref to allow this component to be used with other components like + * focus-trap-react, that rely on the same type of ref forwarding to direct children + */ +const ClickOutside: FC = forwardRef( + ({ active = true, onClick, children }, forwardedRef) => { + const innerRef = useRef() - useEffect(() => { - if (active) { - document.addEventListener('mousedown', handleClick) - document.addEventListener('touchstart', handleClick) + const child = children ? (React.Children.only(children) as any) : undefined + + if (!child || child.type === React.Fragment) { + /** + * React Fragments can't be used, as it would not be possible to pass the ref + * created here to them. + */ + throw new Error('A valid non Fragment React Children should be provided') } - return () => { + if (typeof onClick != 'function') { + throw new Error('onClick must be a valid function') + } + + useEffect(() => { if (active) { - document.removeEventListener('mousedown', handleClick) - document.removeEventListener('touchstart', handleClick) + document.addEventListener('mousedown', handleClick) + document.addEventListener('touchstart', handleClick) } - } - }) + return () => { + if (active) { + document.removeEventListener('mousedown', handleClick) + document.removeEventListener('touchstart', handleClick) + } + } + }) - const handleClick = (event: any) => { - if (!hasParent(event.target, innerRef?.current)) { - if (typeof onClick === 'function') { + const handleClick = (event: any) => { + /** + * Check if the clicked element is contained by the top level tag provided to the + * ClickOutside component, if not, Outside clicked! Fire onClick cb + */ + if (!hasParent(event.target, innerRef?.current)) { onClick(event) } } - } - const composedRefCallback = (element: ReactElement) => { - if (child) { + /** + * Preserve the child ref prop if exists and merge it with the one used here and the + * proxied by the forwardRef method + */ + const composedRefCallback = (element: ReactElement) => { if (typeof child.ref === 'function') { child.ref(element) } else if (child.ref) { child.ref.current = element } } + + return React.cloneElement(child, { + ref: mergeRefs([composedRefCallback, innerRef, forwardedRef]), + }) } +) - return React.cloneElement(child, { - ref: mergeRefs([composedRefCallback, innerRef]), - }) -} - +ClickOutside.displayName = 'ClickOutside' export default ClickOutside