123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173 |
- import {Fragment, useCallback, useEffect, useRef, useState} from 'react';
- import styled from '@emotion/styled';
- import {AnimatePresence, motion} from 'framer-motion';
- import {Overlay} from 'sentry/components/overlay';
- import {Sticky} from 'sentry/components/sticky';
- import {space} from 'sentry/styles/space';
- import testableTransition from 'sentry/utils/testableTransition';
- const TOOLTIP_OFFSET = 10;
- /**
- * Ensure the tooltip does not overlap with the navigation button at the right
- * of the timeline.
- */
- const TOOLTIP_RIGHT_CLAMP_OFFSET = 40;
- interface Options {
- /**
- * Function used to compute the text of the cursor tooltip. Receives the
- * offset value within the container.
- */
- labelText: (positionX: number) => string;
- /**
- * May be set to false to disable rendering the timeline cursor
- */
- enabled?: boolean;
- /**
- * Should the label stick to teh top of the screen?
- */
- sticky?: boolean;
- }
- function useTimelineCursor<E extends HTMLElement>({
- enabled = true,
- sticky,
- labelText,
- }: Options) {
- const rafIdRef = useRef<number | null>(null);
- const containerRef = useRef<E>(null);
- const labelRef = useRef<HTMLDivElement>(null);
- const [isVisible, setIsVisible] = useState(false);
- const handleMouseMove = useCallback(
- (e: MouseEvent) => {
- if (rafIdRef.current !== null) {
- window.cancelAnimationFrame(rafIdRef.current);
- }
- if (containerRef.current === null) {
- return;
- }
- const containerRect = containerRef.current.getBoundingClientRect();
- // Instead of using onMouseEnter / onMouseLeave we check if the mouse is
- // within the containerRect. This proves to be less glitchy as some
- // elements within the container may trigger an onMouseLeave even when
- // the mouse is still "inside" of the container
- const isInsideContainer =
- e.clientX > containerRect.left &&
- e.clientX < containerRect.right &&
- e.clientY > containerRect.top &&
- e.clientY < containerRect.bottom;
- if (isInsideContainer !== isVisible) {
- setIsVisible(isInsideContainer);
- }
- rafIdRef.current = window.requestAnimationFrame(() => {
- if (containerRef.current === null || labelRef.current === null) {
- return;
- }
- if (!isInsideContainer) {
- return;
- }
- const offset = e.clientX - containerRect.left;
- const tooltipWidth = labelRef.current.offsetWidth;
- labelRef.current.innerText = labelText(offset);
- containerRef.current.style.setProperty('--cursorOffset', `${offset}px`);
- containerRef.current.style.setProperty('--cursorMax', `${containerRect.width}px`);
- containerRef.current.style.setProperty('--cursorLabelWidth', `${tooltipWidth}px`);
- });
- },
- [isVisible, labelText]
- );
- useEffect(() => {
- if (enabled) {
- window.addEventListener('mousemove', handleMouseMove);
- } else {
- setIsVisible(false);
- }
- return () => window.removeEventListener('mousemove', handleMouseMove);
- }, [enabled, handleMouseMove]);
- const cursorLabel = sticky ? (
- <StickyLabel>
- <CursorLabel ref={labelRef} animated placement="right" />
- </StickyLabel>
- ) : (
- <CursorLabel ref={labelRef} animated placement="right" />
- );
- const timelineCursor = (
- <AnimatePresence>
- {isVisible && (
- <Fragment>
- <Cursor role="presentation" />
- {cursorLabel}
- </Fragment>
- )}
- </AnimatePresence>
- );
- return {cursorContainerRef: containerRef, timelineCursor};
- }
- const Cursor = styled(motion.div)`
- pointer-events: none;
- background: ${p => p.theme.translucentBorder};
- width: 2px;
- height: 100%;
- position: absolute;
- top: 0;
- left: clamp(0px, var(--cursorOffset), var(--cursorMax));
- transform: translateX(-2px);
- z-index: 3;
- `;
- Cursor.defaultProps = {
- initial: 'initial',
- animate: 'animate',
- exit: 'exit',
- transition: testableTransition({duration: 0.1}),
- variants: {
- initial: {opacity: 0},
- animate: {opacity: 1},
- exit: {opacity: 0},
- },
- };
- const CursorLabel = styled(Overlay)`
- font-variant-numeric: tabular-nums;
- width: max-content;
- padding: ${space(0.75)} ${space(1)};
- color: ${p => p.theme.textColor};
- font-size: ${p => p.theme.fontSizeSmall};
- line-height: 1.2;
- position: absolute;
- top: 12px;
- left: clamp(
- 0px,
- calc(var(--cursorOffset) + ${TOOLTIP_OFFSET}px),
- calc(
- var(--cursorMax) - var(--cursorLabelWidth) - ${TOOLTIP_RIGHT_CLAMP_OFFSET}px -
- ${TOOLTIP_OFFSET}px
- )
- );
- `;
- const StickyLabel = styled(Sticky)`
- z-index: 2;
- `;
- export {useTimelineCursor};
|