123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301 |
- import {
- cloneElement,
- isValidElement,
- useCallback,
- useEffect,
- useMemo,
- useRef,
- useState,
- } from 'react';
- import {PopperProps, usePopper} from 'react-popper';
- import {useTheme} from '@emotion/react';
- import styled from '@emotion/styled';
- import domId from 'sentry/utils/domId';
- import {ColorOrAlias} from 'sentry/utils/theme';
- const OPEN_DELAY = 50;
- const CLOSE_DELAY = 50;
- interface UseHoverOverlayProps {
-
- className?: string;
-
- containerDisplayMode?: React.CSSProperties['display'];
-
- delay?: number;
-
- displayTimeout?: number;
-
- forceVisible?: boolean;
-
- isHoverable?: boolean;
-
- offset?: number;
-
- position?: PopperProps<any>['placement'];
-
- showOnlyOnOverflow?: boolean;
-
- showUnderline?: boolean;
-
- skipWrapper?: boolean;
-
- underlineColor?: ColorOrAlias;
- }
- function isOverflown(el: Element): boolean {
- return el.scrollWidth > el.clientWidth || Array.from(el.children).some(isOverflown);
- }
- function useHoverOverlay(
- overlayType: string,
- {
- className,
- delay,
- displayTimeout,
- isHoverable,
- showUnderline,
- underlineColor,
- showOnlyOnOverflow,
- skipWrapper,
- forceVisible,
- offset = 8,
- position = 'top',
- containerDisplayMode = 'inline-block',
- }: UseHoverOverlayProps
- ) {
- const [isVisible, setVisible] = useState(false);
- const describeById = useMemo(() => domId(`${overlayType}-`), [overlayType]);
- const theme = useTheme();
- const isOpen = forceVisible ?? isVisible;
- const [triggerElement, setTriggerElement] = useState<HTMLElement | null>(null);
- const [overlayElement, setOverlayElement] = useState<HTMLElement | null>(null);
- const [arrowElement, setArrowElement] = useState<HTMLElement | null>(null);
- const modifiers = useMemo(
- () => [
- {
- name: 'hide',
- enabled: false,
- },
- {
- name: 'computeStyles',
- options: {
-
-
-
-
-
- gpuAcceleration: false,
- },
- },
- {
- name: 'arrow',
- options: {
- element: arrowElement,
-
-
- padding: 4,
- },
- },
- {
- name: 'offset',
- options: {
- offset: [0, offset],
- },
- },
- {
- name: 'preventOverflow',
- enabled: true,
- options: {
- padding: 12,
- altAxis: true,
- },
- },
- ],
- [arrowElement, offset]
- );
- const {styles, state, update} = usePopper(triggerElement, overlayElement, {
- modifiers,
- placement: position,
- });
-
- const delayOpenTimeoutRef = useRef<number | undefined>(undefined);
- const delayHideTimeoutRef = useRef<number | undefined>(undefined);
-
- useEffect(() => {
- return () => {
- window.clearTimeout(delayOpenTimeoutRef.current);
- window.clearTimeout(delayHideTimeoutRef.current);
- };
- }, []);
- const handleMouseEnter = useCallback(() => {
-
- if (showOnlyOnOverflow && triggerElement && !isOverflown(triggerElement)) {
- return;
- }
- window.clearTimeout(delayHideTimeoutRef.current);
- window.clearTimeout(delayOpenTimeoutRef.current);
- if (delay === 0) {
- setVisible(true);
- return;
- }
- delayOpenTimeoutRef.current = window.setTimeout(
- () => setVisible(true),
- delay ?? OPEN_DELAY
- );
- }, [delay, showOnlyOnOverflow, triggerElement]);
- const handleMouseLeave = useCallback(() => {
- window.clearTimeout(delayOpenTimeoutRef.current);
- window.clearTimeout(delayHideTimeoutRef.current);
- if (!isHoverable && !displayTimeout) {
- setVisible(false);
- return;
- }
- delayHideTimeoutRef.current = window.setTimeout(
- () => setVisible(false),
- displayTimeout ?? CLOSE_DELAY
- );
- }, [isHoverable, displayTimeout]);
-
- const wrapTrigger = useCallback(
- (triggerChildren: React.ReactNode) => {
- const props = {
- 'aria-describedby': describeById,
- ref: setTriggerElement,
- onFocus: handleMouseEnter,
- onBlur: handleMouseLeave,
- onPointerEnter: handleMouseEnter,
- onPointerLeave: handleMouseLeave,
- };
-
-
-
-
- if (
- isValidElement(triggerChildren) &&
- (skipWrapper || typeof triggerChildren.type === 'string')
- ) {
- const triggerStyle = {
- ...triggerChildren.props.style,
- ...(showUnderline && theme.tooltipUnderline(underlineColor)),
- };
-
- return cloneElement<any>(triggerChildren, {...props, style: triggerStyle});
- }
- const ourContainerProps = {
- ...props,
- containerDisplayMode,
- style: showUnderline ? theme.tooltipUnderline(underlineColor) : undefined,
- className,
- };
- return <Container {...ourContainerProps}>{triggerChildren}</Container>;
- },
- [
- className,
- containerDisplayMode,
- handleMouseEnter,
- handleMouseLeave,
- showUnderline,
- skipWrapper,
- describeById,
- theme,
- underlineColor,
- ]
- );
- const overlayProps = {
- id: describeById,
- ref: setOverlayElement,
- style: styles.popper,
- onMouseEnter: isHoverable ? handleMouseEnter : undefined,
- onMouseLeave: isHoverable ? handleMouseLeave : undefined,
- };
- const arrowProps = {
- ref: setArrowElement,
- style: styles.arrow,
- placement: state?.placement,
- };
- return {
- wrapTrigger,
- isOpen,
- overlayProps,
- arrowProps,
- placement: state?.placement,
- arrowData: state?.modifiersData?.arrow,
- update,
- };
- }
- const Container = styled('span')<{containerDisplayMode: React.CSSProperties['display']}>`
- ${p => p.containerDisplayMode && `display: ${p.containerDisplayMode}`};
- max-width: 100%;
- `;
- export {useHoverOverlay, UseHoverOverlayProps};
|