Skip to content

Instantly share code, notes, and snippets.

@akozhemiakin
Last active February 4, 2020 11:00
Show Gist options
  • Save akozhemiakin/e91c23b8ebc2ceb9c59edab947b2de92 to your computer and use it in GitHub Desktop.
Save akozhemiakin/e91c23b8ebc2ceb9c59edab947b2de92 to your computer and use it in GitHub Desktop.
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