feat: Add forwardRef for compatibility

This commit is contained in:
lita 2021-12-31 15:33:51 +01:00
parent 1ccf5b3870
commit e42c1bfa2d

View File

@ -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 mergeRefs from 'react-merge-refs'
import hasParent from './has-parent' import hasParent from './has-parent'
interface ClickOutsideProps { interface ClickOutsideProps {
active: boolean active: boolean
onClick: (e?: MouseEvent) => void onClick: (e?: MouseEvent) => void
children: any ref?: Ref<any>
} }
const ClickOutside = ({ /**
active = true, * Use forward ref to allow this component to be used with other components like
onClick, * focus-trap-react, that rely on the same type of ref forwarding to direct children
children, */
}: ClickOutsideProps) => { const ClickOutside: FC<ClickOutsideProps> = forwardRef(
const innerRef = useRef() ({ active = true, onClick, children }, forwardedRef) => {
const child = children ? React.Children.only(children) : undefined const innerRef = useRef()
if (!child) {
throw new Error('A valid non Fragment React Children should be provided')
}
useEffect(() => { const child = children ? (React.Children.only(children) as any) : undefined
if (active) {
document.addEventListener('mousedown', handleClick) if (!child || child.type === React.Fragment) {
document.addEventListener('touchstart', handleClick) /**
* 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) { if (active) {
document.removeEventListener('mousedown', handleClick) document.addEventListener('mousedown', handleClick)
document.removeEventListener('touchstart', handleClick) document.addEventListener('touchstart', handleClick)
} }
} return () => {
}) if (active) {
document.removeEventListener('mousedown', handleClick)
document.removeEventListener('touchstart', handleClick)
}
}
})
const handleClick = (event: any) => { const handleClick = (event: any) => {
if (!hasParent(event.target, innerRef?.current)) { /**
if (typeof onClick === 'function') { * 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) 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') { if (typeof child.ref === 'function') {
child.ref(element) child.ref(element)
} else if (child.ref) { } else if (child.ref) {
child.ref.current = element child.ref.current = element
} }
} }
return React.cloneElement(child, {
ref: mergeRefs([composedRefCallback, innerRef, forwardedRef]),
})
} }
)
return React.cloneElement(child, { ClickOutside.displayName = 'ClickOutside'
ref: mergeRefs([composedRefCallback, innerRef]),
})
}
export default ClickOutside export default ClickOutside