Skip to content

Commit

Permalink
Move tab states to the search params
Browse files Browse the repository at this point in the history
  • Loading branch information
colbychaskell committed Dec 30, 2023
1 parent 590940c commit 85ac663
Show file tree
Hide file tree
Showing 7 changed files with 135 additions and 76 deletions.
61 changes: 61 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-favicon": "1.0.1",
"react-router-dom": "^6.21.1",
"react-spring": "^8.0.27",
"reakit": "^1.3.11",
"seamless-scroll-polyfill": "^1.0.10",
Expand Down
40 changes: 14 additions & 26 deletions src/components/AgeTabPicker.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useRef, useLayoutEffect } from 'react';
import { TabList, Tab, TabStateReturn } from 'reakit';
import { TabList, Tab } from 'reakit';
import { VehiclesAge } from '../types';
import { useAgeSearchParam } from '../hooks/searchParams';

type TrainAge = { key: VehiclesAge; label: string };

Expand All @@ -11,60 +12,47 @@ const trainTypes: TrainAge[] = [
];

interface AgeTabPickerProps {
tabState: TabStateReturn;
tabColor: string;
}

export const AgeTabPicker: React.FC<AgeTabPickerProps> = ({ tabState, tabColor }) => {
export const AgeTabPicker: React.FC<AgeTabPickerProps> = ({ tabColor }) => {
// Get train age ID from serach params
const [ageSearchParam, setAgeSearchParam] = useAgeSearchParam();

const wrapperRef = useRef<HTMLDivElement>(null);
const selectedIndicatorRef = useRef<HTMLDivElement>(null);

useLayoutEffect(() => {
const { current: wrapper } = wrapperRef;
const { current: selectedIndicator } = selectedIndicatorRef;
if (wrapper && selectedIndicator) {
const selectedEl = wrapper.querySelector(
`#${tabState.selectedId}`
) as HTMLElement | null;
const selectedEl = wrapper.querySelector(`#${ageSearchParam}`) as HTMLElement | null;
if (selectedEl) {
selectedIndicator.style.width = selectedEl.clientWidth + 'px';
selectedIndicator.style.transform = `translateX(${selectedEl.offsetLeft}px)`;
selectedIndicator.style.transition = '500ms all cubic-bezier(0.86, 0, 0.07, 1)';
}
}
}, [tabState.selectedId]);

// Handle color change immediate transition
useLayoutEffect(() => {
const { current: wrapper } = wrapperRef;
const { current: selectedIndicator } = selectedIndicatorRef;
if (wrapper && selectedIndicator) {
const selectedEl = wrapper.querySelector(
`#${tabState.selectedId}`
) as HTMLElement | null;
if (selectedEl) {
selectedIndicator.style.backgroundColor = tabColor;
selectedIndicator.style.transition = '0ms background-color';
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [tabColor]);
}, [tabColor, ageSearchParam]);

return (
<TabList {...tabState} className="tab-picker" aria-label="Select a line" ref={wrapperRef}>
<TabList className="tab-picker" aria-label="Select train age" ref={wrapperRef}>
<div className="selected-indicator" ref={selectedIndicatorRef} />

{trainTypes.map((trainType) => {
return (
<Tab
{...tabState}
id={trainType.key}
className="tab"
key={trainType.key}
as="div"
data-color={tabColor}
onClick={() => {
setAgeSearchParam(trainType.key);
}}
>
<div
aria-label={trainType.label}
aria-label={trainType.key}
className="icon age"
style={{ backgroundColor: tabColor }}
>
Expand Down
69 changes: 34 additions & 35 deletions src/components/App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { useEffect, useLayoutEffect } from 'react';
import Favicon from 'react-favicon';
import { useTabState } from 'reakit';

import { greenLine, orangeLine, redLine, blueLine } from '../lines';
import { useMbtaApi } from '../hooks/useMbtaApi';
Expand All @@ -9,30 +8,32 @@ import { getInitialDataByKey } from '../initialData';
import { Line } from './Line';
import { Header } from './Header';
import { Footer } from './Footer';
import { LineTabPicker, getTabIdForLine } from './LineTabPicker';
import { LineTabPicker } from './LineTabPicker';
import { LineStats } from './LineStats/LineStats';
import { setCssVariable } from './util';

// @ts-expect-error Favicon png seems to throw typescript error
import favicon from '../../static/images/favicon.png';
import { AgeTabPicker } from './AgeTabPicker';
import { Line as TLine, VehiclesAge } from '../types';
import { Line as TLine } from '../types';

import { useSearchParams } from 'react-router-dom';
import { useLineSearchParam, useAgeSearchParam } from '../hooks/searchParams';

const lineByTabId: Record<string, TLine> = {
'tab-Green': greenLine,
'tab-Orange': orangeLine,
'tab-Red': redLine,
'tab-Blue': blueLine,
Green: greenLine,
Orange: orangeLine,
Red: redLine,
Blue: blueLine,
};

export const App: React.FC = () => {
const tabState = useTabState({ loop: false });
const ageTabState = useTabState({ currentId: 'new_vehicles', loop: false });
const [searchParams] = useSearchParams();
const [lineSearchParam, setLineSearchParam] = useLineSearchParam();
const [ageSearchParam] = useAgeSearchParam();

const api = useMbtaApi(Object.values(lineByTabId), ageTabState.currentId as VehiclesAge);
const selectedLine = tabState.currentId
? lineByTabId[tabState.currentId]
: lineByTabId['tab-Green'];
const api = useMbtaApi(Object.values(lineByTabId), ageSearchParam);
const selectedLine = lineByTabId[lineSearchParam];

useLayoutEffect(() => {
const backgroundColor = selectedLine.colorSecondary;
Expand All @@ -41,17 +42,25 @@ export const App: React.FC = () => {
}, [selectedLine]);

useEffect(() => {
if (api.isReady) {
const lineWithTrains = Object.values(lineByTabId).find((line) => {
const routeIds = Object.keys(line.routes);
if (api.trainsByRoute !== null) {
// @ts-expect-error Despite the above check, Typescript fails to understand that trainsByRoute won't be null
return routeIds.some((routeId) => api.trainsByRoute[routeId].length > 0);
}
});
if (lineWithTrains) {
tabState.setCurrentId(getTabIdForLine(lineWithTrains));
// Do not override the line if a query string param exists
if (searchParams.get('line') !== null) {
return;
}

// Do not run until api is ready
if (!api.isReady) {
return;
}

const lineWithTrains = Object.values(lineByTabId).find((line) => {
const routeIds = Object.keys(line.routes);
if (api.trainsByRoute) {
return routeIds.some((routeId) => api.trainsByRoute![routeId].length > 0);
}
});

if (lineWithTrains) {
setLineSearchParam(lineWithTrains);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [api.isReady]);
Expand All @@ -66,17 +75,12 @@ export const App: React.FC = () => {
return () => document.removeEventListener('keydown', listener);
}, []);

const selectedLineColor: string = tabState.currentId
? lineByTabId[tabState.currentId]?.color
: greenLine.color;

const renderControls = () => {
return (
<div className={'selectors'}>
<AgeTabPicker tabState={ageTabState} tabColor={selectedLineColor} />
<AgeTabPicker tabColor={selectedLine.color} />
{api.trainsByRoute && (
<LineTabPicker
tabState={tabState}
lines={Object.values(lineByTabId)}
trainsByRoute={api.trainsByRoute}
/>
Expand All @@ -90,12 +94,7 @@ export const App: React.FC = () => {
<>
<Favicon url={favicon} />
<Header controls={renderControls()} />
<Line
key={selectedLine?.name}
line={selectedLine}
api={api}
age={ageTabState.currentId ?? 'vehicles'}
/>
<Line key={selectedLine?.name} line={selectedLine} api={api} age={ageSearchParam} />
<LineStats line={selectedLine?.name} />
<Footer version={getInitialDataByKey('version')} />
</>
Expand Down
24 changes: 12 additions & 12 deletions src/components/LineTabPicker.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import { useRef, useLayoutEffect } from 'react';
import { TabList, Tab, TabStateReturn } from 'reakit';
import { TabList, Tab } from 'reakit';
import { Line, Train } from '../types';

import { getTrainRoutePairsForLine } from './util';

export const getTabIdForLine = (line: Line) => `tab-${line.name}`;
import { useLineSearchParam } from '../hooks/searchParams';

interface LineTabPickerProps {
lines: Line[];
tabState: TabStateReturn;
trainsByRoute: { [key: string]: Train[] };
}

export const LineTabPicker: React.FC<LineTabPickerProps> = ({ lines, tabState, trainsByRoute }) => {
export const LineTabPicker: React.FC<LineTabPickerProps> = ({ lines, trainsByRoute }) => {
const [lineSearchParam, setLineSearchParam] = useLineSearchParam();

const wrapperRef = useRef<HTMLDivElement>(null);
const selectedIndicatorRef = useRef<HTMLDivElement>(null);

Expand All @@ -23,9 +23,7 @@ export const LineTabPicker: React.FC<LineTabPickerProps> = ({ lines, tabState, t
const { current: wrapper } = wrapperRef;
const { current: selectedIndicator } = selectedIndicatorRef;
if (wrapper && selectedIndicator) {
const selectedEl = wrapper.querySelector(
`#${tabState.selectedId}`
) as HTMLElement | null;
const selectedEl = wrapper.querySelector(`#${lineSearchParam}`) as HTMLElement | null;
if (selectedEl) {
selectedIndicator.style.width = selectedEl.clientWidth + 'px';
selectedIndicator.style.transform = `translateX(${selectedEl.offsetLeft}px)`;
Expand All @@ -34,21 +32,23 @@ export const LineTabPicker: React.FC<LineTabPickerProps> = ({ lines, tabState, t
) as string;
}
}
}, [tabState.selectedId, totalTrainCount]);
}, [lineSearchParam, totalTrainCount]);

return (
<TabList {...tabState} className="tab-picker" aria-label="Select a line" ref={wrapperRef}>
<TabList className="tab-picker" aria-label="Select a line" ref={wrapperRef}>
<div className="selected-indicator" ref={selectedIndicatorRef} />
{lines.map((line) => {
const trains = getTrainRoutePairsForLine(trainsByRoute, line.routes);
return (
<Tab
{...tabState}
id={getTabIdForLine(line)}
id={line.name}
className="tab"
key={line.name}
as="div"
data-color={line.color}
onClick={() => {
setLineSearchParam(line);
}}
>
<div
aria-label={line.name + ' line'}
Expand Down
10 changes: 9 additions & 1 deletion src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { createBrowserRouter, RouterProvider } from 'react-router-dom';

import { App } from './components/App';
import './main.css';

const router = createBrowserRouter([
{
path: '/',
element: <App />,
},
]);

const container = document.getElementById('root');
ReactDOM.render(
<React.StrictMode>
<App />
<RouterProvider router={router} />
</React.StrictMode>,
container
);
Loading

0 comments on commit 85ac663

Please sign in to comment.