Last active
February 4, 2020 11:00
-
-
Save akozhemiakin/e91c23b8ebc2ceb9c59edab947b2de92 to your computer and use it in GitHub Desktop.
This file contains hidden or 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
import React, { FC, useLayoutEffect, CSSProperties, useEffect, useRef, useMemo, useCallback, useState } from 'react'; | |
import { createPopper, VirtualElement, Options, Instance } from '@popperjs/core'; | |
/** | |
* Helper hooks | |
*/ | |
function usePrevious<T>(value: T): T | undefined { | |
const ref = useRef<T>(); | |
useEffect(() => { | |
ref.current = value; | |
}); | |
return ref.current; | |
} | |
export type RefCallback<T> = { bivarianceHack(instance: T | null): void }['bivarianceHack']; | |
export function useStateRef<T>(v: T): [T | null, RefCallback<T>]; | |
export function useStateRef<T>(): [T | null | undefined, RefCallback<T | undefined>]; | |
export function useStateRef<T>(v?: T): [T | null | undefined, RefCallback<T | undefined>] { | |
const [state, setState] = useState<T | undefined | null>(v); | |
const callbackRef: RefCallback<T> = useCallback(v => setState(v), []); | |
return [state, callbackRef]; | |
} | |
/** | |
* usePopper hook | |
*/ | |
export interface UsePopperResult { | |
/** Triggers update on popper instance */ | |
update: () => Promise<void>; | |
} | |
export const usePopper = ( | |
reference: Element | VirtualElement | undefined | null, | |
popper: HTMLElement | undefined | null, | |
customOptions: Partial<Options> = {}, | |
): UsePopperResult => { | |
const instanceRef = useRef<Instance>(); | |
const clear = useCallback(() => { | |
if (instanceRef.current) instanceRef.current.destroy(); | |
instanceRef.current = undefined; | |
}, []); | |
const prevReference = usePrevious(reference); | |
const prevPopper = usePrevious(popper); | |
const prevOptions = usePrevious(customOptions); | |
const update = useCallback(async () => { | |
if (instanceRef.current) { | |
await instanceRef.current.update(); | |
return; | |
} | |
}, []); | |
useEffect(() => { | |
if (!instanceRef.current && reference && popper) { | |
instanceRef.current = createPopper(reference, popper, customOptions); | |
} else if (!(reference && popper)) { | |
if (instanceRef.current) clear(); | |
} else if (instanceRef.current && (prevReference !== reference || prevPopper !== popper)) { | |
clear(); | |
instanceRef.current = createPopper(reference, popper, customOptions); | |
} else if (instanceRef.current && prevOptions !== customOptions) { | |
instanceRef.current.setOptions(customOptions); | |
} | |
}, [reference, popper, prevReference, prevPopper, prevOptions, clear, customOptions]); | |
useEffect( | |
() => () => { | |
if (instanceRef.current) { | |
instanceRef.current.destroy(); | |
} | |
}, | |
[], | |
); | |
const actions = useMemo( | |
() => ({ | |
update, | |
}), | |
[update], | |
); | |
return actions; | |
}; | |
/** | |
* Declarative wrapper around usePopper hook | |
*/ | |
export interface PopperProps { | |
/** Controls if this popper is open */ | |
isOpen: boolean; | |
/** Reference to the anchor element */ | |
reference?: Element | VirtualElement | null; | |
/** If true, children will be unmounted when popper is closed */ | |
unmountHidden?: boolean; | |
/** Additional options to pass to the Popper library. Consult Popper docs for additional info */ | |
options?: Partial<Options>; | |
} | |
export const Popper: FC<PopperProps> = ({ reference, children, isOpen, unmountHidden, options }) => { | |
const [popper, setPopper] = useStateRef<HTMLDivElement>(); | |
const actions = usePopper(reference, popper, options); | |
const showChildren = isOpen || !unmountHidden; | |
useLayoutEffect(() => { | |
if (isOpen || showChildren) { | |
actions.update(); | |
} | |
}, [isOpen, showChildren, actions, popper]); | |
const style = useMemo( | |
() => ({ | |
zIndex: 20, | |
display: isOpen ? undefined : 'none', | |
}), | |
[isOpen], | |
); | |
return ( | |
<div ref={setPopper} style={style}> | |
{showChildren ? children : undefined} | |
</div> | |
); | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment