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 })}