-
-
Notifications
You must be signed in to change notification settings - Fork 4.7k
fix(stack-trace): show full file path in tooltip without truncation #117904
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 5 commits
73a135f
a7cb4b4
41805ce
a97567f
0e5f883
77b6b71
8acca54
620eb2a
92f91e9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,203 @@ | ||
| import {useLayoutEffect, useRef, useState} from 'react'; | ||
| import styled from '@emotion/styled'; | ||
|
|
||
| import {Container} from '@sentry/scraps/layout'; | ||
|
|
||
| import {OverflowText} from 'sentry/components/OverflowText'; | ||
|
|
||
| interface HoverScrollableProps { | ||
| value: string; | ||
| className?: string; | ||
| edgeWidth?: number; | ||
| leftTrim?: boolean; | ||
| speed?: number; | ||
| } | ||
|
|
||
| export function HoverScrollable({ | ||
| value, | ||
| className, | ||
| leftTrim = false, | ||
| speed = 0.1, | ||
| edgeWidth = 40, | ||
| }: HoverScrollableProps) { | ||
| const [isHovered, setIsHovered] = useState(false); | ||
|
|
||
| const wrapperRef = useRef<HTMLSpanElement>(null); | ||
| const containerRef = useRef<HTMLSpanElement>(null); | ||
| const contentRef = useRef<HTMLSpanElement>(null); | ||
| const animationRef = useRef<Animation | null>(null); | ||
| const direction = useRef<boolean | null>(null); | ||
| const [isTruncated, setIsTruncated] = useState(false); | ||
| const targetRef = useRef(0); | ||
| const scrollWidthRef = useRef(0); | ||
|
|
||
| const showSlidingText = isHovered && isTruncated; | ||
|
|
||
| useLayoutEffect(() => { | ||
| const container = containerRef.current; | ||
| const content = contentRef.current; | ||
|
|
||
| if (!showSlidingText || !container || !content) { | ||
| return; | ||
| } | ||
|
|
||
| const maxTranslate = content.scrollWidth - container.clientWidth; | ||
| const truncated = maxTranslate > 0; | ||
|
|
||
| setIsTruncated(truncated); | ||
|
|
||
| if (!truncated) { | ||
| animationRef.current?.cancel(); | ||
| animationRef.current = null; | ||
| direction.current = null; | ||
| content.style.transform = ''; | ||
| return; | ||
| } | ||
|
|
||
| // When the hover container appears or its text changes, snap left-trimmed | ||
| // content to the rightmost edge. | ||
| if (leftTrim) { | ||
| content.style.transform = `translateX(${-maxTranslate}px)`; | ||
| } | ||
| }, [leftTrim, showSlidingText, value]); | ||
|
|
||
| const handleMouseEnter = () => { | ||
| setIsHovered(true); | ||
| }; | ||
|
|
||
| const handleMouseHover = (event: React.MouseEvent<HTMLSpanElement>) => { | ||
| const container = containerRef.current; | ||
| const content = contentRef.current; | ||
|
|
||
| if (!container || !content) { | ||
| return; | ||
| } | ||
|
|
||
| const rect = container.getBoundingClientRect(); | ||
| const maxTranslate = content.scrollWidth - container.clientWidth; | ||
|
|
||
| if (maxTranslate <= 0) { | ||
| return; | ||
| } | ||
|
|
||
| if (scrollWidthRef.current !== content.scrollWidth) { | ||
| scrollWidthRef.current = content.scrollWidth; | ||
| direction.current = null; | ||
| } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. First mousemove pauses scrollHigh Severity On the first Reviewed by Cursor Bugbot for commit 92f91e9. Configure here.
Comment on lines
+91
to
+94
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: On the first hover, the scroll animation is immediately paused and does not resume because Suggested FixInitialize Prompt for AI Agent |
||
|
|
||
| const mouseX = event.clientX - rect.left; | ||
|
|
||
| let target: number | null = null; | ||
|
|
||
| // left | ||
| if (mouseX <= edgeWidth) { | ||
| target = 0; | ||
| if (direction.current === false) { | ||
| // already moving left, no need to restart animation | ||
| return; | ||
| } | ||
| direction.current = false; | ||
| } | ||
|
|
||
| // right | ||
| if (rect.width - mouseX <= edgeWidth) { | ||
| target = -maxTranslate; | ||
| if (direction.current === true) { | ||
| // already moving right, no need to restart animation | ||
| return; | ||
| } | ||
|
cursor[bot] marked this conversation as resolved.
Outdated
|
||
| direction.current = true; | ||
| } | ||
|
cursor[bot] marked this conversation as resolved.
Outdated
|
||
|
|
||
| // middle | ||
| if (target === null) { | ||
| if (direction.current !== null) { | ||
| animationRef.current?.commitStyles(); | ||
| animationRef.current?.cancel(); | ||
| animationRef.current = null; | ||
| direction.current = null; | ||
| } | ||
| return; | ||
| } | ||
|
|
||
| // a new target | ||
| // animationRef.current?.play(); | ||
|
|
||
| targetRef.current = target; | ||
|
|
||
| const currentX = getCurrentTranslateX(content); | ||
| const distance = Math.abs(target - currentX); | ||
| const duration = distance / speed; | ||
|
|
||
| animationRef.current?.cancel(); | ||
|
cursor[bot] marked this conversation as resolved.
Outdated
|
||
|
|
||
| animationRef.current = content.animate( | ||
| [ | ||
| { | ||
| transform: getComputedStyle(content).transform, | ||
|
sentry[bot] marked this conversation as resolved.
Outdated
|
||
| }, | ||
| { | ||
| transform: `translateX(${target}px)`, | ||
| }, | ||
| ], | ||
| { | ||
| duration, | ||
| easing: 'linear', | ||
| fill: 'forwards', | ||
| } | ||
| ); | ||
| }; | ||
|
|
||
| const handleMouseLeave = () => { | ||
| animationRef.current?.cancel(); | ||
| animationRef.current = null; | ||
| direction.current = null; | ||
|
|
||
| setIsHovered(false); | ||
| }; | ||
|
|
||
| return ( | ||
| <Container | ||
| ref={wrapperRef} | ||
| as="span" | ||
| className={className} | ||
| display="inline-block" | ||
| maxWidth="100%" | ||
| width="100%" | ||
| overflow="hidden" | ||
| onMouseEnter={handleMouseEnter} | ||
| onMouseLeave={handleMouseLeave} | ||
| > | ||
| {showSlidingText ? ( | ||
| <SlidingContainer ref={containerRef} onMouseMove={handleMouseHover}> | ||
| <SlidingText ref={contentRef}>{value}</SlidingText> | ||
| </SlidingContainer> | ||
| ) : ( | ||
| <OverflowText value={value} leftTrim={leftTrim} onTrimChange={setIsTruncated} /> | ||
| )} | ||
|
cursor[bot] marked this conversation as resolved.
|
||
| </Container> | ||
| ); | ||
| } | ||
|
|
||
| function getCurrentTranslateX(element: HTMLElement) { | ||
| const transform = window.getComputedStyle(element).transform; | ||
|
|
||
| if (transform === 'none') { | ||
| return 0; | ||
| } | ||
|
|
||
| return new DOMMatrixReadOnly(transform).m41; | ||
| } | ||
|
|
||
| const SlidingContainer = styled('span')` | ||
| display: inline-block; | ||
| width: 100%; | ||
| max-width: 100%; | ||
| overflow: hidden; | ||
| vertical-align: bottom; | ||
| `; | ||
|
|
||
| const SlidingText = styled('span')` | ||
| display: inline-block; | ||
| white-space: nowrap; | ||
| `; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| import {useEffect, useRef} from 'react'; | ||
| import {css} from '@emotion/react'; | ||
| import styled from '@emotion/styled'; | ||
|
|
||
| interface OverflowTextProps { | ||
| value: string; | ||
| className?: string; | ||
| leftTrim?: boolean; | ||
| onTrimChange?: (trimmed: boolean) => void; | ||
| } | ||
|
|
||
| export function OverflowText({ | ||
| value, | ||
| className, | ||
| leftTrim = false, | ||
| onTrimChange, | ||
| }: OverflowTextProps) { | ||
| const ref = useRef<HTMLSpanElement>(null); | ||
|
|
||
| useEffect(() => { | ||
| const el = ref.current; | ||
| if (!el) { | ||
| return; | ||
| } | ||
|
|
||
| const trimmed = el.scrollWidth > el.clientWidth; | ||
|
|
||
| onTrimChange?.(trimmed); | ||
| }, [value, onTrimChange]); | ||
|
cursor[bot] marked this conversation as resolved.
|
||
|
|
||
| return ( | ||
| <TruncatedSpan ref={ref} className={className} leftTrim={leftTrim}> | ||
| {leftTrim ? <span dir="ltr">{value}</span> : value} | ||
| </TruncatedSpan> | ||
| ); | ||
| } | ||
|
|
||
| const TruncatedSpan = styled('span')<{leftTrim: boolean}>` | ||
| display: inline-block; | ||
| max-width: 100%; | ||
| overflow: hidden; | ||
| white-space: nowrap; | ||
| text-overflow: ellipsis; | ||
| vertical-align: bottom; | ||
|
|
||
| ${p => | ||
| p.leftTrim && | ||
| css` | ||
| direction: rtl; | ||
| text-align: left; | ||
| `} | ||
| `; | ||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: A render loop occurs because
SlidingContainerusesbox-sizing: content-boxwith padding, causing it to miscalculate text truncation and oscillate withOverflowText.Severity: MEDIUM
Suggested Fix
Apply
box-sizing: border-box;to theSlidingContainercomponent. This will ensure that its padding is included within its defined width, leading to accurate truncation calculations and preventing the render loop.Prompt for AI Agent