2022-07-26 14:19:30 +02:00
|
|
|
import { css } from '@emotion/css';
|
2024-01-03 12:42:26 +00:00
|
|
|
import {
|
2024-03-19 10:22:17 +00:00
|
|
|
FloatingFocusManager,
|
2024-01-03 12:42:26 +00:00
|
|
|
autoUpdate,
|
|
|
|
flip,
|
|
|
|
offset as floatingUIOffset,
|
|
|
|
shift,
|
|
|
|
useClick,
|
|
|
|
useDismiss,
|
|
|
|
useFloating,
|
|
|
|
useInteractions,
|
|
|
|
} from '@floating-ui/react';
|
2023-02-02 17:53:18 +00:00
|
|
|
import React, { useEffect, useRef, useState } from 'react';
|
2022-07-26 14:19:30 +02:00
|
|
|
import { CSSTransition } from 'react-transition-group';
|
|
|
|
|
|
|
|
import { ReactUtils } from '../../utils';
|
2024-01-03 12:42:26 +00:00
|
|
|
import { getPlacement } from '../../utils/tooltipUtils';
|
2022-07-26 14:19:30 +02:00
|
|
|
import { Portal } from '../Portal/Portal';
|
|
|
|
import { TooltipPlacement } from '../Tooltip/types';
|
|
|
|
|
|
|
|
export interface Props {
|
|
|
|
overlay: React.ReactElement | (() => React.ReactElement);
|
|
|
|
placement?: TooltipPlacement;
|
2023-04-04 14:40:44 +01:00
|
|
|
children: 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]);
|
|
|
|
|
2024-01-03 12:42:26 +00:00
|
|
|
// the order of middleware is important!
|
|
|
|
const middleware = [
|
|
|
|
floatingUIOffset({
|
|
|
|
mainAxis: offset?.[0] ?? 8,
|
|
|
|
crossAxis: offset?.[1] ?? 0,
|
|
|
|
}),
|
|
|
|
flip({
|
|
|
|
fallbackAxisSideDirection: 'end',
|
|
|
|
// see https://floating-ui.com/docs/flip#combining-with-shift
|
|
|
|
crossAxis: false,
|
|
|
|
boundary: document.body,
|
|
|
|
}),
|
|
|
|
shift(),
|
|
|
|
];
|
|
|
|
|
|
|
|
const { context, refs, floatingStyles } = useFloating({
|
|
|
|
open: show,
|
|
|
|
placement: getPlacement(placement),
|
|
|
|
onOpenChange: setShow,
|
|
|
|
middleware,
|
|
|
|
whileElementsMounted: autoUpdate,
|
2022-07-26 14:19:30 +02:00
|
|
|
});
|
|
|
|
|
2024-01-03 12:42:26 +00:00
|
|
|
const click = useClick(context);
|
|
|
|
const dismiss = useDismiss(context);
|
|
|
|
const { getReferenceProps, getFloatingProps } = useInteractions([dismiss, click]);
|
|
|
|
|
2022-07-26 14:19:30 +02:00
|
|
|
const animationDuration = 150;
|
|
|
|
const animationStyles = getStyles(animationDuration);
|
|
|
|
|
|
|
|
const onOverlayClicked = () => {
|
|
|
|
setShow(false);
|
|
|
|
};
|
|
|
|
|
2023-01-26 09:23:53 +00:00
|
|
|
const handleKeys = (event: React.KeyboardEvent) => {
|
2024-01-03 12:42:26 +00:00
|
|
|
if (event.key === 'Tab') {
|
2023-01-26 09:23:53 +00:00
|
|
|
setShow(false);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2022-07-26 14:19:30 +02:00
|
|
|
return (
|
|
|
|
<>
|
2023-04-04 14:40:44 +01:00
|
|
|
{React.cloneElement(children, {
|
2024-01-03 12:42:26 +00:00
|
|
|
ref: refs.setReference,
|
|
|
|
...getReferenceProps(),
|
2022-07-26 14:19:30 +02:00
|
|
|
})}
|
2024-01-03 12:42:26 +00:00
|
|
|
{show && (
|
2022-07-26 14:19:30 +02:00
|
|
|
<Portal>
|
2024-03-19 10:22:17 +00:00
|
|
|
<FloatingFocusManager context={context}>
|
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 */}
|
2024-01-03 12:42:26 +00:00
|
|
|
<div ref={refs.setFloating} style={floatingStyles} onClick={onOverlayClicked} onKeyDown={handleKeys}>
|
2022-07-26 14:19:30 +02:00
|
|
|
<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}
|
|
|
|
>
|
2024-01-03 12:42:26 +00:00
|
|
|
<div ref={transitionRef}>{ReactUtils.renderOrCallToRender(overlay, { ...getFloatingProps() })}</div>
|
2022-07-26 14:19:30 +02:00
|
|
|
</CSSTransition>
|
|
|
|
</div>
|
2024-03-19 10:22:17 +00:00
|
|
|
</FloatingFocusManager>
|
2022-07-26 14:19:30 +02:00
|
|
|
</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
|
|
|
};
|
|
|
|
};
|