fix(planner): keep the PDF tooltip inside the window

The export button is the one toolbar button with a hand-rolled tooltip
instead of the shared one. It was anchored with `right: 0` and never
wrapped, so its label grew leftwards out of the day plan — the leftmost
pane — and off the window.

The shared tooltip is portalled and clamped to the viewport, which is what
every neighbouring button already uses. The hover state it needed was
threaded down from DayPlanSidebar and has no other reader, so it goes too.
This commit is contained in:
Maurice
2026-07-29 20:04:56 +02:00
committed by Maurice
parent be5b48532d
commit 5dbb2db3ee
3 changed files with 21 additions and 36 deletions
@@ -195,7 +195,6 @@ function useDayPlanSidebar(props: DayPlanSidebarProps) {
const [lockedIds, setLockedIds] = useState(new Set())
const [lockHoverId, setLockHoverId] = useState(null)
const [undoHover, setUndoHover] = useState(false)
const [pdfHover, setPdfHover] = useState(false)
const [icsHover, setIcsHover] = useState(false)
const [hoveredAssignmentId, setHoveredAssignmentId] = useState<number | null>(null)
// Transit rows fold their itinerary out inline (#1065).
@@ -984,8 +983,6 @@ function useDayPlanSidebar(props: DayPlanSidebarProps) {
setLockHoverId,
undoHover,
setUndoHover,
pdfHover,
setPdfHover,
icsHover,
setIcsHover,
hoveredAssignmentId,
@@ -1155,8 +1152,6 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
setLockHoverId,
undoHover,
setUndoHover,
pdfHover,
setPdfHover,
icsHover,
setIcsHover,
hoveredAssignmentId,
@@ -1267,8 +1262,6 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
t={t}
locale={locale}
toast={toast}
pdfHover={pdfHover}
setPdfHover={setPdfHover}
icsHover={icsHover}
setIcsHover={setIcsHover}
expandedDays={expandedDays}
@@ -46,8 +46,6 @@ function makeProps(overrides: Partial<React.ComponentProps<typeof DayPlanSidebar
t,
locale: 'en-US',
toast: makeToast(),
pdfHover: false,
setPdfHover: vi.fn((_v: boolean) => {}),
icsHover: false,
setIcsHover: vi.fn((_v: boolean) => {}),
expandedDays: new Set<number>(),
@@ -97,21 +95,29 @@ describe('DayPlanSidebarToolbar', () => {
await waitFor(() => expect(toast.error).toHaveBeenCalledWith('dayplan.pdfError: font missing'))
})
it('FE-PLANNER-DPTOOLBAR-003: hovering the PDF button reports the hover state upward', async () => {
it('FE-PLANNER-DPTOOLBAR-003: hovering the PDF button shows the export tooltip', async () => {
const user = userEvent.setup()
const setPdfHover = vi.fn((_v: boolean) => {})
render(<DayPlanSidebarToolbar {...makeProps({ setPdfHover })} />)
render(<DayPlanSidebarToolbar {...makeProps()} />)
expect(screen.queryByText('dayplan.pdfTooltip')).not.toBeInTheDocument()
await user.hover(screen.getByText('dayplan.pdf'))
expect(setPdfHover).toHaveBeenCalledWith(true)
await waitFor(() => expect(screen.getByText('dayplan.pdfTooltip')).toBeInTheDocument())
await user.unhover(screen.getByText('dayplan.pdf'))
expect(setPdfHover).toHaveBeenCalledWith(false)
await waitFor(() => expect(screen.queryByText('dayplan.pdfTooltip')).not.toBeInTheDocument())
})
it('FE-PLANNER-DPTOOLBAR-004: pdfHover renders the export tooltip', () => {
const { rerender } = render(<DayPlanSidebarToolbar {...makeProps()} />)
expect(screen.queryByText('dayplan.pdfTooltip')).not.toBeInTheDocument()
rerender(<DayPlanSidebarToolbar {...makeProps({ pdfHover: true })} />)
expect(screen.getByText('dayplan.pdfTooltip')).toBeInTheDocument()
// The button sits at the right edge of the leftmost pane. Its own tooltip was
// anchored with `right: 0` and never wrapped, so a long label ran off the left
// of the window. The shared tooltip is portalled and clamped to the viewport.
it('FE-PLANNER-DPTOOLBAR-004: the export tooltip stays inside the window', async () => {
const user = userEvent.setup()
render(<DayPlanSidebarToolbar {...makeProps()} />)
await user.hover(screen.getByText('dayplan.pdf'))
const tip = await screen.findByText('dayplan.pdfTooltip')
expect(tip.closest('[role="tooltip"]')).not.toBeNull()
expect(tip.parentElement).toBe(document.body)
expect(tip.style.position).toBe('fixed')
// Placed once measured, and never to the left of the window edge.
await waitFor(() => expect(parseFloat(tip.style.left)).toBeGreaterThanOrEqual(0))
})
// ── ICS menu ──────────────────────────────────────────────────────────────
@@ -22,8 +22,6 @@ interface DayPlanSidebarToolbarProps {
t: (key: string, params?: Record<string, any>) => string
locale: string
toast: ReturnType<typeof useToast>
pdfHover: boolean
setPdfHover: (v: boolean) => void
icsHover: boolean
setIcsHover: (v: boolean) => void
expandedDays: Set<number>
@@ -41,7 +39,7 @@ interface DayPlanSidebarToolbarProps {
export function DayPlanSidebarToolbar({
tripId, trip, days, places, categories, assignments, reservations, dayNotes,
allConnectionsShown = false, onToggleAllConnections,
t, locale, toast, pdfHover, setPdfHover, setIcsHover,
t, locale, toast, setIcsHover,
expandedDays, setExpandedDays, onUndo, canUndo, undoHover, setUndoHover, lastActionLabel,
canEditDays, onReorderDays, onAddDay,
}: DayPlanSidebarToolbarProps) {
@@ -74,6 +72,7 @@ export function DayPlanSidebarToolbar({
<div className="border-b border-edge-faint" style={{ padding: '12px 16px', flexShrink: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: 8 }}>
<div style={{ position: 'relative', flexShrink: 0 }}>
<Tooltip label={t('dayplan.pdfTooltip')} placement="bottom">
<button
onClick={async () => {
const flatNotes = Object.entries(dayNotes).flatMap(([dayId, notes]) =>
@@ -86,8 +85,6 @@ export function DayPlanSidebarToolbar({
toast.error(t('dayplan.pdfError') + ': ' + (e?.message || String(e)))
}
}}
onMouseEnter={() => setPdfHover(true)}
onMouseLeave={() => setPdfHover(false)}
className="bg-accent text-accent-text"
style={{
display: 'flex', alignItems: 'center', gap: 5,
@@ -99,18 +96,7 @@ export function DayPlanSidebarToolbar({
<FileDown size={13} strokeWidth={2} />
{t('dayplan.pdf')}
</button>
{pdfHover && (
<div style={{
position: 'absolute', top: 'calc(100% + 6px)', right: 0,
whiteSpace: 'nowrap', pointerEvents: 'none', zIndex: 200,
background: 'var(--bg-card, white)', color: 'var(--text-primary, #111827)',
fontSize: 'calc(11px * var(--fs-scale-caption, 1))', fontWeight: 500, padding: '5px 10px',
borderRadius: 8, boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
border: '1px solid var(--border-faint, #e5e7eb)',
}}>
{t('dayplan.pdfTooltip')}
</div>
)}
</Tooltip>
</div>
<div
style={{ position: 'relative', flexShrink: 0 }}