Skip to content

Commit 41be1cb

Browse files
committed
[segment explorer] custom tooltip
1 parent 7f4e561 commit 41be1cb

File tree

3 files changed

+182
-3
lines changed

3 files changed

+182
-3
lines changed

packages/next/src/next-devtools/dev-overlay/components/errors/dev-tools-indicator/dev-tools-indicator.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -534,7 +534,7 @@ export const DEV_TOOLS_INDICATOR_STYLES = `
534534
border-radius: var(--rounded-xl);
535535
position: absolute;
536536
font-family: var(--font-stack-sans);
537-
z-index: 1000;
537+
z-index: 3;
538538
overflow: hidden;
539539
opacity: 0;
540540
outline: 0;

packages/next/src/next-devtools/dev-overlay/components/overlay/styles.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ const styles = `
55
right: 0;
66
bottom: 0;
77
left: 0;
8-
z-index: 9000;
8+
z-index: 3;
99
1010
display: flex;
1111
align-content: center;

packages/next/src/next-devtools/dev-overlay/components/overview/segment-explorer.tsx

Lines changed: 180 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { useState, useRef, useEffect } from 'react'
2+
import { createPortal } from 'react-dom'
13
import { useSegmentTree, type SegmentTrieNode } from '../../segment-explorer'
24
import { css } from '../../utils/css'
35
import { cx } from '../../utils/cx'
@@ -125,6 +127,7 @@ function PageSegmentTreeLayerPresentation({
125127
<span
126128
key={fileChildSegment}
127129
onClick={() => {
130+
if (isBuiltin) return
128131
openInEditor({ filePath })
129132
}}
130133
className={cx(
@@ -136,6 +139,7 @@ function PageSegmentTreeLayerPresentation({
136139
{fileName}
137140
{isBuiltin && (
138141
<TooltipSpan
142+
direction="right"
139143
title={`The default Next.js not found is being shown. You can customize this page by adding your own ${fileName} file to the app/ directory.`}
140144
>
141145
<InfoIcon />
@@ -175,6 +179,102 @@ function PageSegmentTreeLayerPresentation({
175179
)
176180
}
177181

182+
const tooltipStyles = `
183+
.tooltip-wrapper {
184+
position: relative;
185+
display: inline-block;
186+
}
187+
188+
.tooltip {
189+
position: absolute;
190+
background: var(--color-gray-1000);
191+
color: var(--color-gray-100);
192+
padding: 6px 12px;
193+
border-radius: 8px;
194+
font-size: 14px;
195+
line-height: 1.4;
196+
white-space: nowrap;
197+
min-width: 200px;
198+
white-space: normal;
199+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
200+
pointer-events: none;
201+
}
202+
203+
.tooltip-arrow {
204+
position: absolute;
205+
width: 0;
206+
height: 0;
207+
}
208+
209+
/* Top direction */
210+
.tooltip--top {
211+
bottom: 100%;
212+
left: 50%;
213+
transform: translateX(-50%);
214+
margin-bottom: 8px;
215+
}
216+
217+
.tooltip-arrow--top {
218+
top: 100%;
219+
left: 50%;
220+
transform: translateX(-50%);
221+
border-left: 6px solid transparent;
222+
border-right: 6px solid transparent;
223+
border-top: 6px solid var(--color-gray-1000);
224+
}
225+
226+
/* Bottom direction */
227+
.tooltip--bottom {
228+
top: 100%;
229+
left: 50%;
230+
transform: translateX(-50%);
231+
margin-top: 8px;
232+
}
233+
234+
.tooltip-arrow--bottom {
235+
bottom: 100%;
236+
left: 50%;
237+
transform: translateX(-50%);
238+
border-left: 6px solid transparent;
239+
border-right: 6px solid transparent;
240+
border-bottom: 6px solid var(--color-gray-1000);
241+
}
242+
243+
/* Left direction */
244+
.tooltip--left {
245+
right: 100%;
246+
top: 50%;
247+
transform: translateY(-50%);
248+
margin-right: 8px;
249+
}
250+
251+
.tooltip-arrow--left {
252+
left: 100%;
253+
top: 50%;
254+
transform: translateY(-50%);
255+
border-top: 6px solid transparent;
256+
border-bottom: 6px solid transparent;
257+
border-left: 6px solid var(--color-gray-1000);
258+
}
259+
260+
/* Right direction */
261+
.tooltip--right {
262+
left: 100%;
263+
top: 50%;
264+
transform: translateY(-50%);
265+
margin-left: 8px;
266+
}
267+
268+
.tooltip-arrow--right {
269+
right: 100%;
270+
top: 50%;
271+
transform: translateY(-50%);
272+
border-top: 6px solid transparent;
273+
border-bottom: 6px solid transparent;
274+
border-right: 6px solid var(--color-gray-1000);
275+
}
276+
`
277+
178278
export const DEV_TOOLS_INFO_RENDER_FILES_STYLES = css`
179279
.segment-explorer-content {
180280
font-size: var(--size-14);
@@ -268,12 +368,15 @@ export const DEV_TOOLS_INFO_RENDER_FILES_STYLES = css`
268368
background-color: transparent;
269369
color: var(--color-gray-900);
270370
border: 1px dashed var(--color-gray-500);
371+
cursor: default;
271372
}
272373
273374
.segment-explorer-file-label--builtin svg {
274375
margin-left: 4px;
275376
margin-right: -4px;
276377
}
378+
379+
${tooltipStyles}
277380
`
278381

279382
function openInEditor({ filePath }: { filePath: string }) {
@@ -312,12 +415,88 @@ function InfoIcon(props: React.SVGProps<SVGSVGElement>) {
312415
)
313416
}
314417

418+
type TooltipDirection = 'top' | 'bottom' | 'left' | 'right'
419+
315420
function TooltipSpan({
316421
children,
317422
title,
423+
direction = 'top',
318424
}: {
319425
children: React.ReactNode
320426
title: string
427+
direction: TooltipDirection
321428
}) {
322-
return <span title={title}>{children}</span>
429+
const [isVisible, setIsVisible] = useState(false)
430+
const [position, setPosition] = useState({ top: 0, left: 0 })
431+
const wrapperRef = useRef<HTMLSpanElement>(null)
432+
433+
useEffect(() => {
434+
if (isVisible && wrapperRef.current) {
435+
const rect = wrapperRef.current.getBoundingClientRect()
436+
const scrollTop = window.scrollY || document.documentElement.scrollTop
437+
const scrollLeft = window.scrollX || document.documentElement.scrollLeft
438+
439+
setPosition({
440+
top: rect.top + scrollTop,
441+
left: rect.left + scrollLeft,
442+
})
443+
}
444+
}, [isVisible])
445+
446+
const handleMouseEnter = () => {
447+
setIsVisible(true)
448+
}
449+
450+
const handleMouseLeave = () => {
451+
setIsVisible(false)
452+
}
453+
454+
const tooltip = isVisible ? (
455+
<div
456+
className="custom-tooltip-portal"
457+
style={{
458+
position: 'absolute',
459+
top: position.top,
460+
left: position.left,
461+
width: wrapperRef.current?.offsetWidth || 0,
462+
height: wrapperRef.current?.offsetHeight || 0,
463+
pointerEvents: 'none',
464+
zIndex: 99999,
465+
}}
466+
>
467+
<div className={cx('custom-tooltip', `custom-tooltip--${direction}`)}>
468+
{title}
469+
<div
470+
className={cx(
471+
'custom-tooltip-arrow',
472+
`custom-tooltip-arrow--${direction}`
473+
)}
474+
/>
475+
</div>
476+
</div>
477+
) : null
478+
479+
const [shadowRootRef] = useState<ShadowRoot | null>(() => {
480+
const portal = document.querySelector('nextjs-portal')
481+
if (!portal) return null
482+
return portal.shadowRoot as ShadowRoot
483+
})
484+
485+
if (!shadowRootRef) return null
486+
487+
return (
488+
<>
489+
<span
490+
ref={wrapperRef}
491+
className="tooltip-wrapper"
492+
onMouseEnter={handleMouseEnter}
493+
onMouseLeave={handleMouseLeave}
494+
>
495+
{children}
496+
</span>
497+
{typeof document !== 'undefined' &&
498+
tooltip &&
499+
createPortal(tooltip, shadowRootRef)}
500+
</>
501+
)
323502
}

0 commit comments

Comments
 (0)