From 5dbb2db3eec39a056aaf8ef3ac91e43f251c51fa Mon Sep 17 00:00:00 2001 From: Maurice Date: Wed, 29 Jul 2026 20:04:56 +0200 Subject: [PATCH] fix(planner): keep the PDF tooltip inside the window MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../src/components/Planner/DayPlanSidebar.tsx | 7 ----- .../Planner/DayPlanSidebarToolbar.test.tsx | 30 +++++++++++-------- .../Planner/DayPlanSidebarToolbar.tsx | 20 ++----------- 3 files changed, 21 insertions(+), 36 deletions(-) diff --git a/client/src/components/Planner/DayPlanSidebar.tsx b/client/src/components/Planner/DayPlanSidebar.tsx index a52d3afbb..3566e83c5 100644 --- a/client/src/components/Planner/DayPlanSidebar.tsx +++ b/client/src/components/Planner/DayPlanSidebar.tsx @@ -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(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} diff --git a/client/src/components/Planner/DayPlanSidebarToolbar.test.tsx b/client/src/components/Planner/DayPlanSidebarToolbar.test.tsx index b0a3ec181..a354acb3e 100644 --- a/client/src/components/Planner/DayPlanSidebarToolbar.test.tsx +++ b/client/src/components/Planner/DayPlanSidebarToolbar.test.tsx @@ -46,8 +46,6 @@ function makeProps(overrides: Partial {}), icsHover: false, setIcsHover: vi.fn((_v: boolean) => {}), expandedDays: new Set(), @@ -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() + render() + 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() - expect(screen.queryByText('dayplan.pdfTooltip')).not.toBeInTheDocument() - rerender() - 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() + 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 ────────────────────────────────────────────────────────────── diff --git a/client/src/components/Planner/DayPlanSidebarToolbar.tsx b/client/src/components/Planner/DayPlanSidebarToolbar.tsx index e52ea50c8..eb1925583 100644 --- a/client/src/components/Planner/DayPlanSidebarToolbar.tsx +++ b/client/src/components/Planner/DayPlanSidebarToolbar.tsx @@ -22,8 +22,6 @@ interface DayPlanSidebarToolbarProps { t: (key: string, params?: Record) => string locale: string toast: ReturnType - pdfHover: boolean - setPdfHover: (v: boolean) => void icsHover: boolean setIcsHover: (v: boolean) => void expandedDays: Set @@ -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({
+ - {pdfHover && ( -
- {t('dayplan.pdfTooltip')} -
- )} +