From 282e3c6b13203fde75b4e425904ac73432e60ba9 Mon Sep 17 00:00:00 2001 From: Maurice Date: Fri, 4 Sep 2026 22:37:00 +0200 Subject: [PATCH] refactor(roadtrip): TREK's own tooltips everywhere, and a visible edge on the warning badge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Eight native title attributes across the rail and the corridor panel, which render as the browser's own grey box after its own delay and ignore every appearance setting. All of them are the shared Tooltip now, the same one the rest of the app uses. The warning badge had its border in the soft tint of the warning colour, which at sixteen pixels disappears into the card and left it as the only badge in the row without a shell. It takes the warning colour itself. Two tests moved with it: a tooltip only enters the DOM on hover, so what they pin now is the visible difference between a pinned and a computed arrival — weight and ink — rather than an attribute that no longer exists. --- .../Roadtrip/RoadtripCorridorPanel.tsx | 20 +-- .../Roadtrip/RoadtripSidebar.test.tsx | 11 +- .../components/Roadtrip/RoadtripSidebar.tsx | 121 +++++++++--------- 3 files changed, 80 insertions(+), 72 deletions(-) diff --git a/client/src/components/Roadtrip/RoadtripCorridorPanel.tsx b/client/src/components/Roadtrip/RoadtripCorridorPanel.tsx index 5e2b14c26..01785ae95 100644 --- a/client/src/components/Roadtrip/RoadtripCorridorPanel.tsx +++ b/client/src/components/Roadtrip/RoadtripCorridorPanel.tsx @@ -4,6 +4,7 @@ import { AlertTriangle, MapPin, X, type LucideIcon, } from 'lucide-react' import { useTranslation } from '../../i18n/TranslationContext' +import { Tooltip } from '../shared/Tooltip' import { useSettingsStore } from '../../store/settingsStore' import { formatDistance } from '../../utils/units' import CustomSelect from '../shared/CustomSelect' @@ -89,15 +90,16 @@ function ResultRow({ poi, onAdd }: { poi: CorridorPoi; onAdd?: () => void }): Re {/* Always there, quiet until the row is under the pointer: a button that only exists on hover is one a keyboard user has to find by faith. */} {onAdd ? ( - + + + ) : null} ) diff --git a/client/src/components/Roadtrip/RoadtripSidebar.test.tsx b/client/src/components/Roadtrip/RoadtripSidebar.test.tsx index f8b6ed0d6..060b735cc 100644 --- a/client/src/components/Roadtrip/RoadtripSidebar.test.tsx +++ b/client/src/components/Roadtrip/RoadtripSidebar.test.tsx @@ -125,14 +125,13 @@ describe('RoadtripSidebar', () => { wrap() // Both are plain text at the row's edge — no pill, no icon. What separates the time - // somebody chose from the one the drive worked out is weight and ink, and the reason - // is named for a screen reader either way. + // somebody chose from the one the drive worked out is weight and ink. The reason is + // named in a tooltip, which only enters the DOM on hover, so the visible difference + // is what this pins. const pinned = screen.getByText('09:00') const computed = screen.getByText('10:30') expect(pinned.className).toContain('font-semibold') - expect(pinned).toHaveAttribute('title', 'Time you set') expect(computed.className).not.toContain('font-semibold') - expect(computed).toHaveAttribute('title', 'Calculated from the drive') }) it('FE-ROADTRIP-SIDEBAR-005: flags a stop the drive cannot reach in time', () => { @@ -202,7 +201,9 @@ describe('RoadtripSidebar', () => { // Said twice on purpose: once as the break in the chain, once on the arrival after // it, where "01:30" would otherwise read as tonight. expect(screen.getAllByText('Next day')).toHaveLength(2) - expect(screen.getAllByTitle('Calculated from the drive')[1].textContent).toContain('+1') + // The day marker rides on the arrival that crossed midnight, where "01:30" would + // otherwise read as tonight. + expect(screen.getByText('01:30').textContent).toContain('+1') }) it('FE-ROADTRIP-SIDEBAR-012: a late arrival past midnight keeps both findings', () => { diff --git a/client/src/components/Roadtrip/RoadtripSidebar.tsx b/client/src/components/Roadtrip/RoadtripSidebar.tsx index a8e3865bd..4bc6c5a37 100644 --- a/client/src/components/Roadtrip/RoadtripSidebar.tsx +++ b/client/src/components/Roadtrip/RoadtripSidebar.tsx @@ -5,6 +5,7 @@ import { type LucideIcon, } from 'lucide-react' import { useTranslation } from '../../i18n/TranslationContext' +import { Tooltip } from '../shared/Tooltip' import { useSettingsStore } from '../../store/settingsStore' import { formatDistance } from '../../utils/units' import { formatDate, formatClockTime } from '../../utils/formatters' @@ -107,23 +108,22 @@ function OffRoadBadge({ meters }: { meters: number }): React.ReactElement { const { t } = useTranslation() const distanceUnit = useSettingsStore(s => s.settings.distance_unit) return ( - - - + + + + + + + {formatDistance(meters / 1000, distanceUnit)} + - - {formatDistance(meters / 1000, distanceUnit)} - - + ) } @@ -157,24 +157,25 @@ function StayBadge({ minutes, onEdit }: { minutes: number | null; onEdit?: () => // and a button inside a button is invalid HTML that React warns about and that browsers // resolve by dropping the inner element. return ( - { e.stopPropagation(); onEdit() }} - onKeyDown={e => { - if (e.key !== 'Enter' && e.key !== ' ') return - e.preventDefault() - e.stopPropagation() - onEdit() - }} - title={t('roadtrip.stop.stay')} - aria-label={text ? `${t('roadtrip.stop.stay')}: ${text}` : t('roadtrip.stay.add')} - className={`${shell} cursor-pointer transition-colors hover:border-content-faint`} - > - {label}{value} - + + { e.stopPropagation(); onEdit() }} + onKeyDown={e => { + if (e.key !== 'Enter' && e.key !== ' ') return + e.preventDefault() + e.stopPropagation() + onEdit() + }} + aria-label={text ? `${t('roadtrip.stop.stay')}: ${text}` : t('roadtrip.stay.add')} + className={`${shell} cursor-pointer transition-colors hover:border-content-faint`} + > + {label}{value} + + ) } @@ -302,11 +303,11 @@ function DriveBand({ leg, onAskAlternatives, alternativesOpen }: { the gesture people try first — the shuffle mark stays as the sign that it can be clicked, and as where the open state shows. */} {onAskAlternatives && leg ? ( + + ) : (
{band} @@ -477,15 +479,17 @@ function DriveFindingBadge({ warning }: { warning: ScheduleWarning }): React.Rea if (!text) return null const Icon = warning.code === 'range' ? Fuel : Clock return ( + {warning.code === 'range' @@ -493,6 +497,7 @@ function DriveFindingBadge({ warning }: { warning: ScheduleWarning }): React.Rea : `+${formatDurationShort((warning.overMinutes ?? 0) * 60)}`} + ) } @@ -547,9 +552,9 @@ function Arrival({ entry }: { entry: ScheduleEntry }): React.ReactElement { // look like it had a button in it. What is left of the distinction is weight and ink — // enough to see which time somebody chose, without a second shape in the rail. return ( + + ) } @@ -641,15 +647,16 @@ function Stop({ stop, number, entry, late, driveFindings, selected, continues, o {(driveFindings ?? []).map(w => )} {lateText ? ( - - - {`+${formatDurationShort((late?.minutes ?? 0) * 60)}`} - + + + + {`+${formatDurationShort((late?.minutes ?? 0) * 60)}`} + + ) : null} {entry?.arrival ? : null} @@ -720,16 +727,14 @@ function DaySection({ day, selectedAssignmentId, onSelectStop, onReorderStop, on })} {day.dayWarning ? ( - - - {t('roadtrip.limit.dayOver', { - time: formatDurationShort((day.dayWarning.minutes - day.dayWarning.limitMinutes) * 60), - })} - + + + + {t('roadtrip.limit.dayOver', { + time: formatDurationShort((day.dayWarning.minutes - day.dayWarning.limitMinutes) * 60), + })} + + ) : null} {t('roadtrip.day.stopCount', { count: day.stops.filter(s => !isServiceStopType(s.stopType)).length })}