2022-07-26 14:19:30 +02:00
|
|
|
import { css } from '@emotion/css';
|
|
|
|
import { FocusScope } from '@react-aria/focus';
|
2023-02-02 17:53:18 +00:00
|
|
|
import React, { useEffect, useRef, useState } from 'react';
|
2022-07-26 14:19:30 +02:00
|
|
|
import { usePopperTooltip } from 'react-popper-tooltip';
|
|
|
|
import { CSSTransition } from 'react-transition-group';
|
|
|
|
|
|
|
|
import { ReactUtils } from '../../utils';
|
|
|
|
import { Portal } from '../Portal/Portal';
|
|
|
|
import { TooltipPlacement } from '../Tooltip/types';
|
|
|
|
|
|
|
|
export interface Props {
|
|
|
|
overlay: React.ReactElement | (() => React.ReactElement);
|
|
|
|
placement?: TooltipPlacement;
|
2022-11-17 09:03:12 +00:00
|
|
|
children: React.ReactElement | ((isOpen: boolean) => React.ReactElement);
|
2023-02-02 17:53:18 +00:00
|
|
|
/** Amount in pixels to nudge the dropdown vertically and horizontally, respectively. */
|
|
|
|
offset?: [number, number];
|
|
|
|
onVisibleChange?: (state: boolean) => void;
|
2022-07-26 14:19:30 +02:00
|
|
|
}
|
|
|
|
|
2023-02-02 17:53:18 +00:00
|
|
|
export const Dropdown = React.memo(({ children, overlay, placement, offset, onVisibleChange }: Props) => {
|
2022-07-26 14:19:30 +02:00
|
|
|
const [show, setShow] = useState(false);
|
2022-09-20 11:47:31 +02:00
|
|
|
const transitionRef = useRef(null);
|
2022-07-26 14:19:30 +02:00
|
|
|
|
2023-02-02 17:53:18 +00:00
|
|
|
useEffect(() => {
|
|
|
|
onVisibleChange?.(show);
|
|
|
|
}, [onVisibleChange, show]);
|
|
|
|
|
2022-07-26 14:19:30 +02:00
|
|
|
const { getArrowProps, getTooltipProps, setTooltipRef, setTriggerRef, visible } = usePopperTooltip({
|
|
|
|
visible: show,
|
|
|
|
placement: placement,
|
|
|
|
onVisibleChange: setShow,
|
|
|
|
interactive: true,
|
|
|
|
delayHide: 0,
|
|
|
|
delayShow: 0,
|
2023-02-02 17:53:18 +00:00
|
|
|
offset: offset ?? [0, 8],
|
2022-07-26 14:19:30 +02:00
|
|
|
trigger: ['click'],
|
|
|
|
});
|
|
|
|
|
|
|
|
const animationDuration = 150;
|
|
|
|
const animationStyles = getStyles(animationDuration);
|
|
|
|
|
|
|
|
const onOverlayClicked = () => {
|
|
|
|
setShow(false);
|
|
|
|
};
|
|
|
|
|
2023-01-26 09:23:53 +00:00
|
|
|
const handleKeys = (event: React.KeyboardEvent) => {
|
|
|
|
if (event.key === 'Escape' || event.key === 'Tab') {
|
|
|
|
setShow(false);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2022-07-26 14:19:30 +02:00
|
|
|
return (
|
|
|
|
<>
|
2022-11-17 09:03:12 +00:00
|
|
|
{React.cloneElement(typeof children === 'function' ? children(visible) : children, {
|
2022-07-26 14:19:30 +02:00
|
|
|
ref: setTriggerRef,
|
|
|
|
})}
|
|
|
|
{visible && (
|
|
|
|
<Portal>
|
2023-01-26 09:23:53 +00:00
|
|
|
<FocusScope autoFocus restoreFocus contain>
|
2022-11-29 11:13:21 +00:00
|
|
|
{/*
|
|
|
|
this is handling bubbled events from the inner overlay
|
|
|
|
see https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/no-static-element-interactions.md#case-the-event-handler-is-only-being-used-to-capture-bubbled-events
|
|
|
|
*/}
|
|
|
|
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */}
|
2023-01-26 09:23:53 +00:00
|
|
|
<div ref={setTooltipRef} {...getTooltipProps()} onClick={onOverlayClicked} onKeyDown={handleKeys}>
|
2022-07-26 14:19:30 +02:00
|
|
|
<div {...getArrowProps({ className: 'tooltip-arrow' })} />
|
|
|
|
<CSSTransition
|
2022-09-20 11:47:31 +02:00
|
|
|
nodeRef={transitionRef}
|
2022-07-26 14:19:30 +02:00
|
|
|
appear={true}
|
|
|
|
in={true}
|
|
|
|
timeout={{ appear: animationDuration, exit: 0, enter: 0 }}
|
|
|
|
classNames={animationStyles}
|
|
|
|
>
|
2022-09-20 11:47:31 +02:00
|
|
|
<div ref={transitionRef}>{ReactUtils.renderOrCallToRender(overlay)}</div>
|
2022-07-26 14:19:30 +02:00
|
|
|
</CSSTransition>
|
|
|
|
</div>
|
|
|
|
</FocusScope>
|
|
|
|
</Portal>
|
|
|
|
)}
|
|
|
|
</>
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
|
|
|
Dropdown.displayName = 'Dropdown';
|
|
|
|
|
|
|
|
const getStyles = (duration: number) => {
|
|
|
|
return {
|
2023-01-12 11:10:09 +02:00
|
|
|
appear: css({
|
|
|
|
opacity: '0',
|
|
|
|
position: 'relative',
|
|
|
|
transform: 'scaleY(0.5)',
|
|
|
|
transformOrigin: 'top',
|
|
|
|
}),
|
|
|
|
appearActive: css({
|
|
|
|
opacity: '1',
|
|
|
|
transform: 'scaleY(1)',
|
|
|
|
transition: `transform ${duration}ms cubic-bezier(0.2, 0, 0.2, 1), opacity ${duration}ms cubic-bezier(0.2, 0, 0.2, 1)`,
|
|
|
|
}),
|
2022-07-26 14:19:30 +02:00
|
|
|
};
|
|
|
|
};
|