-
Notifications
You must be signed in to change notification settings - Fork 1
feat: create branch picker element #608
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 22 commits
8549c01
1aad16e
e2d9758
35caae5
6d3fa15
b7c5061
71cb03e
b340983
3cce000
bbb0e77
f39853f
1e70866
c5da29c
c13dbe2
1cd1f3b
c5e3f69
a9e3b0e
b0c4556
dd38cd1
6fd76b5
1423ffc
f88907a
d7efdc8
9834228
3b06ead
602d4f6
1507e6a
184ec67
b117bcc
7be6942
0b91e85
9528ee0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| export type Route = "Red" | "Orange" | "Blue"; | ||
|
djpowers marked this conversation as resolved.
Outdated
|
||
|
|
||
| const branches = ["Alewife", "Ashmont", "Braintree"] as const; | ||
|
djpowers marked this conversation as resolved.
Outdated
|
||
| export type BranchPickerSelection = (typeof branches)[number]; | ||
|
|
||
| const defaultBg = | ||
| "bg-ladder-branch-picker-inactive-bg-dark light:bg-ladder-branch-picker-inactive-bg-light"; | ||
|
|
||
| const activeText = | ||
| "text-ladder-branch-picker-active-dot-dark light:text-ladder-branch-picker-active-dot-light"; | ||
|
|
||
| const branchColors: Pick< | ||
|
djpowers marked this conversation as resolved.
Outdated
|
||
| Record<Route, Record<BranchPickerSelection, { bg: string; dotText: string }>>, | ||
| "Red" | ||
| > = { | ||
| Red: { | ||
| Alewife: { | ||
| bg: "bg-ladder-branch-picker-alewife-dot-dark light:bg-ladder-branch-picker-alewife-dot-light", | ||
| dotText: | ||
| "text-ladder-branch-picker-alewife-dot-dark light:text-ladder-branch-picker-alewife-dot-light", | ||
| }, | ||
| Ashmont: { | ||
| bg: "bg-heavy-rail-ashmont", | ||
| dotText: "text-heavy-rail-ashmont", | ||
| }, | ||
| Braintree: { | ||
| bg: "bg-heavy-rail-braintree", | ||
| dotText: "text-heavy-rail-braintree", | ||
| }, | ||
| }, | ||
| }; | ||
|
|
||
| const BranchButton = ({ | ||
| branch, | ||
| isActive, | ||
| onClick, | ||
| }: { | ||
| branch: BranchPickerSelection; | ||
| isActive: boolean; | ||
| onClick: () => void; | ||
| }) => { | ||
| const { bg, dotText } = branchColors.Red[branch]; | ||
|
djpowers marked this conversation as resolved.
Outdated
|
||
| const buttonBg = isActive ? bg : defaultBg; | ||
| const labelText = isActive ? activeText : "text-white"; | ||
| const dotColor = isActive ? activeText : dotText; | ||
|
|
||
| return ( | ||
| <button | ||
| className={`flex flex-col justify-center items-center grow ${buttonBg} rounded-t text-center font-semibold h-16 p-4 gap-1`} | ||
| onClick={onClick} | ||
| > | ||
| <div className={labelText} data-testid="branch-label"> | ||
| {branch} | ||
| </div> | ||
| <div | ||
| className={`w-2 min-h-2 rounded-full bg-current ${dotColor}`} | ||
| data-testid="branch-dot" | ||
| /> | ||
| </button> | ||
| ); | ||
| }; | ||
|
|
||
| export const BranchPicker = ({ | ||
| selection, | ||
| setSelection, | ||
| }: { | ||
| selection: BranchPickerSelection; | ||
| setSelection: (selection: BranchPickerSelection) => void; | ||
| }) => { | ||
| return ( | ||
| <div | ||
| className="flex justify-between h-14 self-center gap-1 w-full max-w-[371px]" | ||
| data-testid="branch-picker" | ||
| > | ||
| {branches.map((branch) => ( | ||
| <BranchButton | ||
| key={branch} | ||
| branch={branch} | ||
| isActive={selection === branch} | ||
| onClick={() => { | ||
| setSelection(branch); | ||
| }} | ||
| /> | ||
| ))} | ||
| </div> | ||
| ); | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,12 +13,13 @@ import { CarId, RouteId } from "../../models/common"; | |
| import { Station } from "../../models/station"; | ||
| import { Vehicle } from "../../models/vehicle"; | ||
| import { consistsEqual, remapLabel } from "../../util/consist"; | ||
| import { BranchPickerSelection } from "./branchPicker"; | ||
| import { SideBarSelection } from "./sidebar"; | ||
| import { Ladder } from "rail-tech-ui"; | ||
| import type { VehicleSelection } from "rail-tech-ui/dist/src/components/ladderPage/types"; | ||
| import { RoutePatternId } from "rail-tech-ui/dist/src/models/route"; | ||
| import type { TrainLoc } from "rail-tech-ui/dist/src/models/trainLocation"; | ||
| import { ReactElement } from "react"; | ||
| import { ReactElement, Ref } from "react"; | ||
|
|
||
| const ROUTE_PATTERN_CONFIG: Readonly< | ||
| Record<RouteId, Record<RoutePatternId, { color: string; letter: string }>> | ||
|
|
@@ -64,6 +65,12 @@ export type VehicleWithHeight = { | |
| heights: TrainHeight; | ||
| }; | ||
|
|
||
| const branchForLadder = (ladderConfig: LadderConfig): BranchPickerSelection => { | ||
| if (ladderConfig.some((s) => s.id === "place-asmnl")) return "Ashmont"; | ||
| if (ladderConfig.some((s) => s.id === "place-brntn")) return "Braintree"; | ||
| return "Alewife"; | ||
| }; | ||
|
|
||
|
djpowers marked this conversation as resolved.
Outdated
|
||
| // Adapt Orbit's Station (uses `location`, no `shortName`) to rail-tech-ui's | ||
| // LadderStation shape (`latLng`, requires `shortName`). | ||
| // TODO: After we remove the old ladder, this can be simplified | ||
|
|
@@ -98,12 +105,16 @@ export const Ladders = ({ | |
| routeId, | ||
| sideBarSelection, | ||
| setSideBarSelection, | ||
| setBranchPickerSelection, | ||
| vehicles, | ||
| ref, | ||
| }: { | ||
| routeId: RouteId; | ||
| sideBarSelection: SideBarSelection | null; | ||
| setSideBarSelection: (selection: SideBarSelection | null) => void; | ||
| setBranchPickerSelection: (selection: BranchPickerSelection) => void; | ||
| vehicles: Vehicle[]; | ||
| ref?: Ref<HTMLDivElement>; | ||
| }): ReactElement => { | ||
| const stationLists = Stations[routeId]; | ||
| const vehiclesByBranch = vehicles.reduce( | ||
|
|
@@ -137,6 +148,18 @@ export const Ladders = ({ | |
| ), | ||
| ); | ||
| if (match) { | ||
| // update branch picker to reflect the branch the clicked train is on | ||
| const matchingStationList = stationLists.find((stations) => | ||
| stations.some((station) => | ||
| station.stop_ids.some( | ||
| (stopId) => stopId === match.vehiclePosition.stopId, | ||
| ), | ||
| ), | ||
| ); | ||
| if (matchingStationList) { | ||
| setBranchPickerSelection(branchForLadder(matchingStationList)); | ||
| } | ||
|
|
||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. question: I'm torn on this. spiritually it feels like we shouldn't have to search back through the stations to figure out what branch a train is on. is there a better way?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Tried a different approach in 3b06ead. This change sets the branch for each ladder on render, so we can avoid the lookup on each click. |
||
| const sameVehicle = | ||
| sideBarSelection !== null && | ||
| consistsEqual( | ||
|
|
@@ -158,7 +181,11 @@ export const Ladders = ({ | |
| : null; | ||
|
|
||
| return ( | ||
| <div className="relative flex w-full h-full justify-start min-[1485px]:justify-center overflow-x-auto snap-x snap-mandatory"> | ||
| <div | ||
| ref={ref} | ||
| data-testid="ladders-scroll-container" | ||
| className="relative flex w-full h-full justify-start min-[1485px]:justify-center overflow-x-auto snap-x snap-mandatory" | ||
| > | ||
| {Array.from(vehiclesByBranch.entries()).map( | ||
| ([stationList, branchVehicles], index) => ( | ||
| <div | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,10 +5,11 @@ import { RouteId } from "../../models/common"; | |
| import { Vehicle } from "../../models/vehicle"; | ||
| import { trackSideBarOpened } from "../../telemetry/trackingEvents"; | ||
| import { className } from "../../util/dom"; | ||
| import { BranchPicker, BranchPickerSelection } from "./branchPicker"; | ||
| import { Ladders } from "./ladder"; | ||
| import { SearchBar, VehicleSearchMatch } from "./search"; | ||
| import { SideBar, SideBarSelection } from "./sidebar"; | ||
| import { ReactElement, useCallback, useEffect, useState } from "react"; | ||
| import { ReactElement, useCallback, useEffect, useRef, useState } from "react"; | ||
|
|
||
| // Without this: each render on L17 will create a new array, causing the useEffect on L49 to run every time | ||
| const NO_VEHICLES: Vehicle[] = []; | ||
|
|
@@ -17,7 +18,11 @@ export const LadderPage = ({ routeId }: { routeId: RouteId }): ReactElement => { | |
| const vehicles = useVehicles() ?? NO_VEHICLES; | ||
| const [sideBarSelection, setSideBarSelection] = | ||
| useState<SideBarSelection | null>(null); | ||
| const [branchPickerSelection, setBranchPickerSelection] = | ||
| useState<BranchPickerSelection>("Ashmont"); | ||
| const [searchQuery, setSearchQuery] = useState(""); | ||
| const [isOverflowing, setIsOverflowing] = useState(false); | ||
| const laddersRef = useRef<HTMLDivElement>(null); | ||
|
|
||
| const openSideBar = useCallback( | ||
| (selection: SideBarSelection | null) => { | ||
|
|
@@ -52,6 +57,19 @@ export const LadderPage = ({ routeId }: { routeId: RouteId }): ReactElement => { | |
| }; | ||
| }, [onEscape]); | ||
|
|
||
| useEffect(() => { | ||
| const el = laddersRef.current; | ||
| if (!el) return; | ||
| const check = () => { | ||
| setIsOverflowing(el.scrollWidth > el.clientWidth); | ||
| }; | ||
| check(); | ||
| window.addEventListener("resize", check); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. question: does this need to be debounced?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Debounce added in 7be6942. |
||
| return () => { | ||
| window.removeEventListener("resize", check); | ||
| }; | ||
| }, []); | ||
|
|
||
| const onSearchMatch = useCallback( | ||
| (match: VehicleSearchMatch): boolean => { | ||
| openSideBar({ vehicle: match.vehicle, searchedCar: match.matchedCar }); | ||
|
|
@@ -89,31 +107,44 @@ export const LadderPage = ({ routeId }: { routeId: RouteId }): ReactElement => { | |
| ); | ||
|
|
||
| return ( | ||
| <main className="bg-glides-blue-700 flex overflow-y-auto overflow-x-hidden justify-center"> | ||
| {sideBarSelection !== null ? | ||
| <SideBar selection={sideBarSelection} close={close} /> | ||
| : null} | ||
| <div | ||
| className={className([ | ||
| "relative flex transition-all duration-300 ease-in-out overflow-x-auto w-full", | ||
| ])} | ||
| // Close sidebar when clicking anywhere in the background | ||
| onClick={close} | ||
| > | ||
| <SearchBar | ||
| vehicles={vehicles} | ||
| query={searchQuery} | ||
| onSearchMatch={onSearchMatch} | ||
| onSearchCleared={onSearchCleared} | ||
| onQueryChange={onQueryChange} | ||
| /> | ||
| <Ladders | ||
| routeId={routeId} | ||
| vehicles={vehicles} | ||
| setSideBarSelection={openSideBarFromLadder} | ||
| sideBarSelection={sideBarSelection} | ||
| /> | ||
| </div> | ||
| </main> | ||
| <div className="flex flex-col flex-1 min-h-0 overflow-hidden"> | ||
| <main className="bg-glides-blue-700 flex flex-1 min-h-0 overflow-y-auto overflow-x-hidden justify-center"> | ||
| {sideBarSelection !== null ? | ||
| <SideBar selection={sideBarSelection} close={close} /> | ||
| : null} | ||
| <div | ||
| data-testid="scroll-container" | ||
| className={className([ | ||
| "relative flex transition-all duration-300 ease-in-out overflow-x-auto snap-x snap-mandatory w-full", | ||
| ])} | ||
| // Close sidebar when clicking anywhere in the background | ||
| onClick={close} | ||
| > | ||
| <SearchBar | ||
| vehicles={vehicles} | ||
| query={searchQuery} | ||
| onSearchMatch={onSearchMatch} | ||
| onSearchCleared={onSearchCleared} | ||
| onQueryChange={onQueryChange} | ||
| /> | ||
| <Ladders | ||
| ref={laddersRef} | ||
| routeId={routeId} | ||
| vehicles={vehicles} | ||
| setSideBarSelection={openSideBarFromLadder} | ||
| setBranchPickerSelection={setBranchPickerSelection} | ||
| sideBarSelection={sideBarSelection} | ||
| /> | ||
| </div> | ||
| </main> | ||
| {isOverflowing && ( | ||
| <div className="flex justify-center w-full bg-glides-blue-700"> | ||
| <BranchPicker | ||
| selection={branchPickerSelection} | ||
| setSelection={setBranchPickerSelection} | ||
| /> | ||
| </div> | ||
| )} | ||
| </div> | ||
| ); | ||
| }; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Highlighting my commit description: a9e3b0e
If there's a better approach here I'm open to it, but this seemed like the most direct way to ensure the branch picker was not blocked by mobile address bars.