diff --git a/package.json b/package.json index b998e64ff..c1c46ac0b 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,10 @@ "workspaces": [ "packages/*" ], + "resolutions": { + "react": "18.2.0", + "react-animate-height": "3.0.4" + }, "devDependencies": { "@babel/cli": "^7.10", "@babel/core": "^7.10", diff --git a/packages/trip-form/README.md b/packages/trip-form/README.md index 1ef175f30..454da5818 100644 --- a/packages/trip-form/README.md +++ b/packages/trip-form/README.md @@ -2,4 +2,5 @@ ``` TBD + ``` diff --git a/packages/trip-form/i18n/en-US.yml b/packages/trip-form/i18n/en-US.yml index 524577bff..fc542ad3d 100644 --- a/packages/trip-form/i18n/en-US.yml +++ b/packages/trip-form/i18n/en-US.yml @@ -30,7 +30,6 @@ otpUi: walkTolerance-labelHigh: More Walking walkTolerance-labelLow: Less Walking wheelchair-label: Accessible Routing - settingsLabel: "{mode} settings" SettingsSelectorPanel: bikeOnly: Bike Only escooterOnly: E-scooter Only diff --git a/packages/trip-form/i18n/fr.yml b/packages/trip-form/i18n/fr.yml index e0055646c..0482f8e26 100644 --- a/packages/trip-form/i18n/fr.yml +++ b/packages/trip-form/i18n/fr.yml @@ -30,7 +30,6 @@ otpUi: walkTolerance-labelHigh: Plus de marche walkTolerance-labelLow: Moins de marche wheelchair-label: Accès avec mobilité réduite - settingsLabel: "Paramètres pour : {mode}" SettingsSelectorPanel: bikeOnly: Vélo uniquement escooterOnly: Trottinette uniquement diff --git a/packages/trip-form/package.json b/packages/trip-form/package.json index 6c308f3ef..a66c7a8f4 100644 --- a/packages/trip-form/package.json +++ b/packages/trip-form/package.json @@ -11,6 +11,7 @@ "private": false, "dependencies": { "@opentripplanner/core-utils": "^11.4.4", + "@opentripplanner/building-blocks": "^1.0.3", "@floating-ui/react": "^0.19.2", "@styled-icons/bootstrap": "^10.34.0", "@styled-icons/boxicons-regular": "^10.38.0", @@ -18,6 +19,7 @@ "@styled-icons/fa-solid": "^10.37.0", "date-fns": "^2.28.0", "flat": "^5.0.2", + "react-animate-height": "^3.0.4", "react-indiana-drag-scroll": "^2.0.1", "react-inlinesvg": "^2.3.0" }, diff --git a/packages/trip-form/src/MetroModeSelector/AdvancedModeSettingsButton.story.tsx b/packages/trip-form/src/MetroModeSelector/AdvancedModeSettingsButton.story.tsx new file mode 100644 index 000000000..90c240d2d --- /dev/null +++ b/packages/trip-form/src/MetroModeSelector/AdvancedModeSettingsButton.story.tsx @@ -0,0 +1,128 @@ +import React, { ReactElement, useState } from "react"; +import { ModeButtonDefinition } from "@opentripplanner/types"; +import * as Core from ".."; +import { QueryParamChangeEvent } from "../types"; +import { + addSettingsToButton, + extractModeSettingDefaultsToObject, + populateSettingWithValue, + setModeButtonEnabled +} from "./utils"; +import { + defaultModeButtonDefinitions, + getIcon, + modeSettingDefinitionsWithDropdown +} from "../__mocks__/mode-selector-buttons"; + +const initialState = { + enabledModeButtons: ["transit"], + modeSettingValues: {} +}; + +function pipe(...fns: Array<(arg: T) => T>) { + return (value: T) => fns.reduce((acc, fn) => fn(acc), value); +} + +const MetroModeSubsettingsComponent = ({ + fillModeIcons, + modeButtonDefinitions, + onAllSubmodesDisabled, + onSetModeSettingValue, + onToggleModeButton +}: { + fillModeIcons?: boolean; + modeButtonDefinitions: Array; + onAllSubmodesDisabled?: (modeButton: ModeButtonDefinition) => void; + onSetModeSettingValue: (event: QueryParamChangeEvent) => void; + onToggleModeButton: (key: string, newState: boolean) => void; +}): ReactElement => { + const [modeSettingValues, setModeSettingValues] = useState({}); + const modeSettingValuesWithDefaults = { + ...extractModeSettingDefaultsToObject(modeSettingDefinitionsWithDropdown), + ...initialState.modeSettingValues, + ...modeSettingValues + }; + + const [activeModeButtonKeys, setModeButtonKeys] = useState( + initialState.enabledModeButtons + ); + + const addIconToModeSetting = msd => ({ + ...msd, + icon: getIcon(msd.iconName) + }); + + const processedModeSettings = modeSettingDefinitionsWithDropdown.map( + pipe( + addIconToModeSetting, + populateSettingWithValue(modeSettingValuesWithDefaults) + ) + ); + + const processedModeButtons = modeButtonDefinitions.map( + pipe( + addSettingsToButton(processedModeSettings), + setModeButtonEnabled(activeModeButtonKeys) + ) + ); + + const toggleModeButtonAction = (key: string, newState: boolean) => { + if (newState) { + setModeButtonKeys([...activeModeButtonKeys, key]); + } else { + setModeButtonKeys(activeModeButtonKeys.filter(button => button !== key)); + } + // Storybook Action: + onToggleModeButton(key, newState); + }; + + const onAllSubmodesDisabledAction = (modeButton: ModeButtonDefinition) => { + toggleModeButtonAction(modeButton.key, false); + // Storybook Action: + onAllSubmodesDisabled?.(modeButton); + }; + + const setModeSettingValueAction = (event: QueryParamChangeEvent) => { + setModeSettingValues({ ...modeSettingValues, ...event }); + // Storybook Action: + onSetModeSettingValue(event); + }; + + return ( +
+ +
+ ); +}; + +const Template = (args: { + fillModeIcons?: boolean; + onSetModeSettingValue: (event: QueryParamChangeEvent) => void; + onToggleModeButton: (key: string, newState: boolean) => void; +}): ReactElement => ( + +); + +export const AdvancedModeSettingsButtons = Template.bind({}); + +export default { + argTypes: { + fillModeIcons: { control: "boolean" }, + onSetModeSettingValue: { action: "set mode setting value" }, + onToggleModeButton: { action: "toggle button" }, + onAllSubmodesDisabled: { action: "all submodes disabled" } + }, + component: MetroModeSubsettingsComponent, + title: "Trip Form Components/Advanced Mode Settings Buttons" +}; diff --git a/packages/trip-form/src/MetroModeSelector/AdvancedModeSettingsButton/index.tsx b/packages/trip-form/src/MetroModeSelector/AdvancedModeSettingsButton/index.tsx new file mode 100644 index 000000000..e8731aec3 --- /dev/null +++ b/packages/trip-form/src/MetroModeSelector/AdvancedModeSettingsButton/index.tsx @@ -0,0 +1,148 @@ +import React from "react"; +import AnimateHeight from "react-animate-height"; +import styled from "styled-components"; +import colors from "@opentripplanner/building-blocks"; +import { Check2 } from "@styled-icons/bootstrap"; +import { ModeButtonDefinition } from "@opentripplanner/types"; +import { useIntl } from "react-intl"; +import SubSettingsPane from "../SubSettingsPane"; +import generateModeButtonLabel from "../i18n"; +import { invisibleCss } from ".."; +import { QueryParamChangeEvent } from "../../types"; + +const { blue, grey } = colors; + +const SettingsContainer = styled.div` + width: 100%; +`; + +const StyledModeSettingsButton = styled.div<{ + accentColor: string; + fillModeIcons: boolean; + subsettings: boolean; +}>` + & > label { + align-items: center; + background-color: #fff; + border: 2px solid ${props => props.accentColor}; + border-left-width: 2px; + border-right-width: 2px; + color: ${props => props.accentColor}; + cursor: pointer; + display: grid; + font-size: 18px; + font-weight: 400; + gap: 20px; + grid-template-columns: 40px auto 40px; + height: 51px; + justify-items: center; + margin-bottom: 0; + margin-top: -2px; + padding: 0 10px; + } + & > input { + ${invisibleCss} + + &:checked + label { + background-color: ${props => props.accentColor}; + color: #fff; + border-bottom-left-radius: ${props => props.subsettings && 0} !important; + border-bottom-right-radius: ${props => props.subsettings && 0} !important; + } + + &:focus-visible + label, + &:focus + label { + outline: ${props => props.accentColor} 1px solid; + outline-offset: -4px; + } + } + + & > input:checked { + &:focus-visible + label, + &:focus + label { + outline: white 1px solid; + } + } + + span { + justify-self: flex-start; + } + + svg { + height: 26px; + width: 26px; + fill: ${props => + props.fillModeIcons === false ? "inherit" : "currentcolor"}; + } + + &:hover { + cursor: pointer; + } +`; + +const StyledSettingsContainer = styled.div` + border: 1px solid ${grey[300]}; + border-top: 0; + padding: 1em; +`; + +interface Props { + accentColor?: string; + fillModeIcons: boolean; + id: string; + modeButton: ModeButtonDefinition; + onAllSubmodesDisabled?: (modeButton: ModeButtonDefinition) => void; + onSettingsUpdate: (event: QueryParamChangeEvent) => void; + onToggle: () => void; +} + +const AdvancedModeSettingsButton = ({ + accentColor = blue[700], + fillModeIcons, + id, + modeButton, + onSettingsUpdate, + onAllSubmodesDisabled, + onToggle +}: Props): JSX.Element => { + const intl = useIntl(); + const label = generateModeButtonLabel(modeButton.key, intl, modeButton.label); + const checkboxId = `metro-submode-selector-mode-${id}`; + return ( + + 0} + > + + + + {modeButton.modeSettings.length > 0 && ( + + + + + + )} + + ); +}; + +export default AdvancedModeSettingsButton; diff --git a/packages/trip-form/src/MetroModeSelector/AdvancedModeSubsettingsContainer.tsx b/packages/trip-form/src/MetroModeSelector/AdvancedModeSubsettingsContainer.tsx new file mode 100644 index 000000000..515bf2813 --- /dev/null +++ b/packages/trip-form/src/MetroModeSelector/AdvancedModeSubsettingsContainer.tsx @@ -0,0 +1,80 @@ +import styled from "styled-components"; +import React, { useCallback } from "react"; +import { ModeButtonDefinition } from "@opentripplanner/types"; +import colors from "@opentripplanner/building-blocks"; +import AdvancedModeSettingsButton from "./AdvancedModeSettingsButton"; +import { invisibleCss } from "."; +import { QueryParamChangeEvent } from "../types"; + +const { grey } = colors; + +const SubsettingsContainer = styled.fieldset` + border: none; + margin: 0; + + legend { + ${invisibleCss} + } + + display: flex; + flex-direction: column; + + div:first-of-type div label { + border-top-width: 2px; + border-radius: 8px 8px 0 0; + } + + div:last-of-type div label { + border-bottom-width: 2px; + border-radius: 0 0 8px 8px; + } + + div.advanced-submode-container:last-of-type div.subsettings-container { + border-radius: 0 0 8px 8px; + border-bottom: 1px solid ${grey[300]}; + } +`; + +interface Props { + accentColor?: string; + fillModeIcons: boolean | undefined; + label: string; + modeButtons: ModeButtonDefinition[]; + onAllSubmodesDisabled?: (modeButton: ModeButtonDefinition) => void; + onSettingsUpdate: (event: QueryParamChangeEvent) => void; + onToggleModeButton: (key: string, newState: boolean) => void; +} + +const AdvancedModeSubsettingsContainer = ({ + accentColor, + fillModeIcons, + modeButtons, + label, + onAllSubmodesDisabled, + onSettingsUpdate, + onToggleModeButton +}: Props): JSX.Element => { + return ( + + {label} + {modeButtons.map(button => { + return ( + { + onToggleModeButton(button.key, !button.enabled); + }, [button, onToggleModeButton])} + id={button.key} + /> + ); + })} + + ); +}; + +export default AdvancedModeSubsettingsContainer; diff --git a/packages/trip-form/src/MetroModeSelector/MetroModeSelector.story.tsx b/packages/trip-form/src/MetroModeSelector/MetroModeSelector.story.tsx index de4e78150..ea29c898e 100644 --- a/packages/trip-form/src/MetroModeSelector/MetroModeSelector.story.tsx +++ b/packages/trip-form/src/MetroModeSelector/MetroModeSelector.story.tsx @@ -1,13 +1,4 @@ import { ModeButtonDefinition } from "@opentripplanner/types"; -import { - Bus, - Car, - PersonWalking, - Train, - TrainSubway, - TrainTram -} from "@styled-icons/fa-solid"; -import { ClassicBike } from "@opentripplanner/icons/src/classic"; import React, { ReactElement, useState } from "react"; import * as Core from ".."; import { QueryParamChangeEvent } from "../types"; @@ -17,108 +8,11 @@ import { populateSettingWithValue, setModeButtonEnabled } from "./utils"; - -const getIcon = (iconName: string): JSX.Element | null => { - switch (iconName) { - case "bus": - return ; - case "tram": - return ; - case "subway": - return ; - case "train": - return ; - default: - return null; - } -}; - -const defaultModeButtonDefinitions = [ - { - Icon: Bus, - iconName: "bus", - key: "transit", - label: "Bus", - modes: [{ mode: "TRANSIT" }] - }, - { - Icon: PersonWalking, - iconName: "person-walking", - key: "walk", - label: "Walk", - modes: [{ mode: "WALK" }] - }, - { - // Using TriMet icon here to illustrate the use of fillModeIcons prop. - Icon: ClassicBike, - iconName: "bicycle", - key: "bicycle", - label: "Bike", - modes: [{ mode: "BICYCLE" }] - }, - { - Icon: Car, - iconName: "car", - key: "car", - label: "Car", - modes: [{ mode: "CAR" }] - } -]; - -// TODO: add more test settings? -const modeSettingDefinitionsWithDropdown = [ - { - applicableMode: "TRANSIT", - default: "blue", - key: "busColor", - label: "Bus Color", - options: [{ value: "blue", text: "Blue" }], - type: "DROPDOWN" - }, - { - applicableMode: "TRANSIT", - default: true, - key: "tram", - iconName: "tram", - label: "Tram", - addTransportMode: { - mode: "TRAM" - }, - type: "SUBMODE" - }, - { - applicableMode: "TRANSIT", - default: true, - key: "bus", - label: "MARTA Rail", - iconName: "bus", - addTransportMode: { - mode: "BUS" - }, - type: "SUBMODE" - }, - { - applicableMode: "TRANSIT", - default: true, - key: "subway", - label: "Subway", - iconName: "subway", - addTransportMode: { - mode: "SUBWAY" - }, - type: "SUBMODE" - }, - { - applicableMode: "TRANSIT", - default: true, - key: "ferry", - label: "Ferry", - addTransportMode: { - mode: "FERRY" - }, - type: "SUBMODE" - } -]; +import { + modeSettingDefinitionsWithDropdown, + getIcon, + defaultModeButtonDefinitions +} from "../__mocks__/mode-selector-buttons"; const initialState = { enabledModeButtons: ["transit"], diff --git a/packages/trip-form/src/MetroModeSelector/SubSettingsPane.tsx b/packages/trip-form/src/MetroModeSelector/SubSettingsPane.tsx index 759ae63c8..43c79e904 100644 --- a/packages/trip-form/src/MetroModeSelector/SubSettingsPane.tsx +++ b/packages/trip-form/src/MetroModeSelector/SubSettingsPane.tsx @@ -1,6 +1,11 @@ import { flatten } from "flat"; -import { ModeButtonDefinition, ModeSetting } from "@opentripplanner/types"; -import React, { ReactElement } from "react"; +import { + ModeButtonDefinition, + ModeSetting, + ModeSettingBase, + TransitSubmodeCheckboxOption +} from "@opentripplanner/types"; +import React, { ReactElement, useCallback } from "react"; import { useIntl } from "react-intl"; import styled from "styled-components"; @@ -10,6 +15,7 @@ import SliderSelector from "../SliderSelector"; import generateModeButtonLabel, { generateModeSettingLabels } from "./i18n"; import defaultEnglishMessages from "../../i18n/en-US.yml"; +import { QueryParamChangeEvent } from "../types"; // HACK: We should flatten the messages loaded above because // the YAML loaders behave differently between webpack and our version of jest: // - the yaml loader for webpack returns a nested object, @@ -152,10 +158,12 @@ export const ModeSettingRenderer = ({ interface Props { modeButton: ModeButtonDefinition; + onAllSubmodesDisabled?: (modeButton: ModeButtonDefinition) => void; onSettingUpdate: (QueryParamChangeEvent) => void; } export default function SubSettingsPane({ modeButton, + onAllSubmodesDisabled, onSettingUpdate }: Props): ReactElement { const intl = useIntl(); @@ -166,7 +174,10 @@ export default function SubSettingsPane({ const { settingsNoSubmodes, settingsOnlySubmodes - } = modeButton.modeSettings.reduce( + } = modeButton.modeSettings.reduce<{ + settingsNoSubmodes: ModeSetting[]; + settingsOnlySubmodes: (TransitSubmodeCheckboxOption & ModeSettingBase)[]; + }>( (accumulator, cur) => { if (cur.type === "SUBMODE") { accumulator.settingsOnlySubmodes.push(cur); @@ -178,6 +189,35 @@ export default function SubSettingsPane({ { settingsNoSubmodes: [], settingsOnlySubmodes: [] } ); + // rental mode settings do not have type "SUBMODE" + const settingsWithTransportMode = modeButton.modeSettings.filter( + (s: ModeSetting) => + (s.type === "CHECKBOX" || s.type === "SUBMODE") && s.addTransportMode + ); + + const handleSettingChange = useCallback( + (setting: ModeSetting) => (evt: QueryParamChangeEvent) => { + // check if setting is a transport mode setting + if (settingsWithTransportMode.find(s => s.key === setting.key)) { + // check if all submodes are disabled + if ( + settingsWithTransportMode.every( + s => Object.keys(evt).includes(s.key) || s.value === false + ) && + onAllSubmodesDisabled + ) { + settingsWithTransportMode.forEach(s => { + evt[s.key] = Object.keys(evt).includes(s.key) || !s.value; + }); + onAllSubmodesDisabled(modeButton); + } + } + + onSettingUpdate(evt); + }, + [onSettingUpdate] + ); + return ( @@ -189,7 +229,7 @@ export default function SubSettingsPane({ {settingsOnlySubmodes.map(setting => ( ))} @@ -197,7 +237,7 @@ export default function SubSettingsPane({ {settingsNoSubmodes.map(setting => ( ))} diff --git a/packages/trip-form/src/MetroModeSelector/__snapshots__/AdvancedModeSettingsButton.story.tsx.snap b/packages/trip-form/src/MetroModeSelector/__snapshots__/AdvancedModeSettingsButton.story.tsx.snap new file mode 100644 index 000000000..186d7cb14 --- /dev/null +++ b/packages/trip-form/src/MetroModeSelector/__snapshots__/AdvancedModeSettingsButton.story.tsx.snap @@ -0,0 +1,267 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Trip Form Components/Advanced Mode Settings Buttons AdvancedModeSettingsButtons smoke-test 1`] = ` +
+
+ + Select a transit mode + +
+
+ + +
+
+
+
+
+ + + Transit + + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ +
+
+ +
+
+
+
+
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+`; diff --git a/packages/trip-form/src/MetroModeSelector/__snapshots__/MetroModeSelector.story.tsx.snap b/packages/trip-form/src/MetroModeSelector/__snapshots__/MetroModeSelector.story.tsx.snap index bbc305de0..2038f2a0a 100644 --- a/packages/trip-form/src/MetroModeSelector/__snapshots__/MetroModeSelector.story.tsx.snap +++ b/packages/trip-form/src/MetroModeSelector/__snapshots__/MetroModeSelector.story.tsx.snap @@ -32,30 +32,6 @@ exports[`Trip Form Components/Metro Mode Selector MetroModeSelector smoke-test 1 Transit - - - - `; diff --git a/packages/trip-form/src/MetroModeSelector/index.tsx b/packages/trip-form/src/MetroModeSelector/index.tsx index 26fa1b919..58fefaab9 100644 --- a/packages/trip-form/src/MetroModeSelector/index.tsx +++ b/packages/trip-form/src/MetroModeSelector/index.tsx @@ -1,27 +1,11 @@ -import { - arrow, - FloatingFocusManager, - offset, - safePolygon, - shift, - useClick, - useDismiss, - useFloating, - useHover, - useInteractions, - useRole -} from "@floating-ui/react"; import { ModeButtonDefinition } from "@opentripplanner/types"; -import { CaretDown } from "@styled-icons/fa-solid/CaretDown"; -import { CaretUp } from "@styled-icons/fa-solid/CaretUp"; -import React, { ReactElement, useCallback, useRef, useState } from "react"; -import { FormattedMessage, useIntl } from "react-intl"; +import React, { ReactElement, useCallback } from "react"; +import { useIntl } from "react-intl"; import styled, { css } from "styled-components"; -import SubSettingsPane, { defaultMessages } from "./SubSettingsPane"; import generateModeButtonLabel from "./i18n"; -const invisibleCss = css` +export const invisibleCss = css` clip: rect(0, 0, 0, 0); height: 0; overflow: hidden; @@ -151,34 +135,6 @@ const ModeButtonWrapper = styled.span<{ } `; -const HoverPanel = styled.div` - min-width: 300px; - padding: 0 10px; - width: 75%; - z-index: 100; -`; - -const HoverInnerContainer = styled.div` - background: #fff; - border-radius: 4px; - color: #2e2e2e; - font-size: 90%; - font-weight: bold; - padding: 0px 20px 10px; - pointer-events: none; - ${boxShadowCss} -`; - -const Arrow = styled.div` - background: #fff; - height: 10px; - margin-top: -5px; - position: absolute; - transform: rotate(-45deg); - width: 10px; - ${boxShadowCss} -`; - interface ModeButtonProps { // Optional properties for styling accentColor?: string; @@ -186,10 +142,7 @@ interface ModeButtonProps { fillModeIcons?: boolean; id: string; - itemWithKeyboard?: string; modeButton: ModeButtonDefinition; - onPopupClose: () => void; - onPopupKeyboardExpand: (id: string) => void; onSettingsUpdate: (QueryParamChangeEvent) => void; onToggle: () => void; } @@ -199,85 +152,13 @@ function ModeButton({ activeHoverColor, fillModeIcons, id, - itemWithKeyboard, modeButton, - onPopupClose, - onPopupKeyboardExpand, - onSettingsUpdate, onToggle }: ModeButtonProps) { const intl = useIntl(); - const [open, setOpen] = useState(false); - const [hoverEnabled, setHoverEnabled] = useState(true); - const arrowRef = useRef(null); - const onOpenChange = useCallback( - value => { - setOpen(value); - if (!value && typeof onPopupClose === "function") { - onPopupClose(); - } - }, - [onPopupClose, setOpen] - ); - const { - context, - floating, - middlewareData: { arrow: { x: arrowX, y: arrowY } = {} }, - reference, - strategy, - x, - y - } = useFloating({ - middleware: [offset(8), shift(), arrow({ element: arrowRef })], - onOpenChange, - open - }); - - const { getFloatingProps, getReferenceProps } = useInteractions([ - useHover(context, { - // Enable hover only if no popup has been triggered via keyboard. - // (This is to avoid focus being stolen by hovering out of another button.) - enabled: itemWithKeyboard === null && hoverEnabled, - handleClose: safePolygon({ - blockPointerEvents: false, - restMs: 500, - buffer: 0 - }) - }), - useClick(context), - useRole(context), - useDismiss(context) - ]); - - const renderDropdown = - open && modeButton.enabled && modeButton.modeSettings?.length > 0; - const interactionProps = getReferenceProps(); - - // ARIA roles are added by the `useRole` hook. - // Remove the aria-controls, aria-expanded, and aria-haspopup props from the label, they will - // instead be passed to the button for keyboard/screen reader users to trigger the popup. - const { - "aria-controls": ariaControls, - "aria-expanded": ariaExpanded, - "aria-haspopup": ariaHasPopup, - ...labelInteractionProps - } = interactionProps; - const checkboxId = `metro-mode-selector-mode-${id}`; - const handleButtonClick = useCallback( - e => { - if (typeof onPopupKeyboardExpand === "function") { - onPopupKeyboardExpand(id); - } - if (typeof interactionProps.onClick === "function") { - interactionProps.onClick(e); - } - }, - [id, interactionProps, onPopupKeyboardExpand] - ); - const label = generateModeButtonLabel(modeButton.key, intl, modeButton.label); return ( @@ -299,79 +180,13 @@ function ModeButton({ - - {renderDropdown && ( - - { - setHoverEnabled(false); - setTimeout(() => setHoverEnabled(true), 100); - }} - ref={floating} - style={{ - left: x ?? 0, - position: strategy, - top: y ?? 0 - }} - > - - - - - - - )} ); } @@ -417,10 +232,6 @@ export default function ModeSelector({ onSettingsUpdate, onToggleModeButton }: Props): ReactElement { - // State that holds the id of the active mode combination popup that was triggered via keyboard. - // It is used to enable/disable hover effects to avoid keyboard focus being stolen - // and overlapping popups on mouse hover. - const [itemWithKeyboard, setItemWithKeyboard] = useState(null); return ( {label} @@ -430,13 +241,8 @@ export default function ModeSelector({ activeHoverColor={activeHoverColor} fillModeIcons={fillModeIcons} id={button.key} - itemWithKeyboard={itemWithKeyboard} key={button.label} modeButton={button} - onPopupClose={useCallback(() => { - setItemWithKeyboard(null); - }, [setItemWithKeyboard])} - onPopupKeyboardExpand={setItemWithKeyboard} onSettingsUpdate={onSettingsUpdate} onToggle={useCallback(() => { onToggleModeButton(button.key, !button.enabled); diff --git a/packages/trip-form/src/__mocks__/mode-selector-buttons.tsx b/packages/trip-form/src/__mocks__/mode-selector-buttons.tsx new file mode 100644 index 000000000..67b7d6a83 --- /dev/null +++ b/packages/trip-form/src/__mocks__/mode-selector-buttons.tsx @@ -0,0 +1,114 @@ +import { + Bus, + Car, + PersonWalking, + Train, + TrainSubway, + TrainTram +} from "@styled-icons/fa-solid"; + +import { ModeButtonDefinition, ModeSetting } from "@opentripplanner/types"; + +import { ClassicBike } from "@opentripplanner/icons/src/classic"; +import React from "react"; + +export const defaultModeButtonDefinitions: ModeButtonDefinition[] = [ + { + Icon: Bus, + iconName: "bus", + key: "transit", + label: "Transit", + modes: [{ mode: "TRANSIT" }] + }, + { + Icon: PersonWalking, + iconName: "person-walking", + key: "walk", + label: "Walk", + modes: [{ mode: "WALK" }] + }, + { + // Using TriMet icon here to illustrate the use of fillModeIcons prop. + Icon: ClassicBike, + iconName: "bicycle", + key: "bicycle", + label: "Bike", + modes: [{ mode: "BICYCLE" }] + }, + { + Icon: Car, + iconName: "car", + key: "car", + label: "Car", + modes: [{ mode: "CAR" }] + } +]; +// TODO: add more test settings? +export const modeSettingDefinitionsWithDropdown: ModeSetting[] = [ + { + applicableMode: "TRANSIT", + default: "blue", + key: "busColor", + label: "Bus Color", + options: [{ value: "blue", text: "Blue" }], + type: "DROPDOWN" + }, + { + applicableMode: "TRANSIT", + default: true, + key: "tram", + iconName: "tram", + label: "Tram", + addTransportMode: { + mode: "TRAM" + }, + type: "SUBMODE" + }, + { + applicableMode: "TRANSIT", + default: true, + key: "bus", + label: "MARTA Rail", + iconName: "bus", + addTransportMode: { + mode: "BUS" + }, + type: "SUBMODE" + }, + { + applicableMode: "TRANSIT", + default: true, + key: "subway", + label: "Subway", + iconName: "subway", + addTransportMode: { + mode: "SUBWAY" + }, + type: "SUBMODE" + }, + { + applicableMode: "TRANSIT", + default: true, + key: "ferry", + label: "Ferry", + addTransportMode: { + mode: "FERRY" + }, + type: "SUBMODE" + } +]; + +export const getIcon = (iconName: string): JSX.Element | null => { + switch (iconName) { + case "bus": + return ; + case "tram": + return ; + case "subway": + return ; + case "train": + return ; + default: + return null; + } +}; diff --git a/packages/trip-form/src/index.ts b/packages/trip-form/src/index.ts index 507b0b6a1..3ca9361fd 100644 --- a/packages/trip-form/src/index.ts +++ b/packages/trip-form/src/index.ts @@ -9,6 +9,8 @@ import SliderSelector from "./SliderSelector"; import * as Styled from "./styled"; import SubmodeSelector from "./SubmodeSelector"; import MetroModeSelector from "./MetroModeSelector"; +import AdvancedModeSettingsButton from "./MetroModeSelector/AdvancedModeSettingsButton"; +import AdvancedModeSubsettingsContainer from "./MetroModeSelector/AdvancedModeSubsettingsContainer"; import { addSettingsToButton, aggregateModes, @@ -31,6 +33,8 @@ export { GeneralSettingsPanel, getBannedRoutesFromSubmodes, MetroModeSelector, + AdvancedModeSettingsButton, + AdvancedModeSubsettingsContainer, ModeButton, ModeSelector, ModeSettingRenderer, diff --git a/yarn.lock b/yarn.lock index 6f0dcc55c..e38831978 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18920,7 +18920,7 @@ rc@^1.2.8: minimist "^1.2.0" strip-json-comments "~2.0.1" -react-animate-height@^3.0.4: +react-animate-height@3.0.4, react-animate-height@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/react-animate-height/-/react-animate-height-3.0.4.tgz#80c9cc25e8569709ad1c626b968dbe5108d0ce46" integrity sha512-k+mBS8yCzpFp+7BdrHsL5bXd6CO/2bYO2SvRGKfxK+Ss3nzplAJLlgnd6Zhcxe/avdpy/CgcziicFj7pIHgG5g== @@ -19106,7 +19106,7 @@ react-test-renderer@^16.14.0: react-shallow-renderer "^16.15.0" scheduler "^0.23.0" -react@^18.2.0: +react@18.2.0, react@^18.2.0: version "18.2.0" resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ== @@ -20717,8 +20717,7 @@ string-similarity@^4.0.4: resolved "https://registry.yarnpkg.com/string-similarity/-/string-similarity-4.0.4.tgz#42d01ab0b34660ea8a018da8f56a3309bb8b2a5b" integrity sha512-/q/8Q4Bl4ZKAPjj8WerIBJWALKkaPRfrvhfF8k/B23i4nzrlRj2/go1m90In7nG/3XDSbOo0+pu6RvCTM9RGMQ== -"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: - name string-width-cjs +"string-width-cjs@npm:string-width@^4.2.0": version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -20736,6 +20735,15 @@ string-width@^1.0.1: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + string-width@^2.0.0, string-width@^2.1.0, string-width@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" @@ -20816,8 +20824,7 @@ stringify-package@^1.0.1: resolved "https://registry.yarnpkg.com/stringify-package/-/stringify-package-1.0.1.tgz#e5aa3643e7f74d0f28628b72f3dad5cecfc3ba85" integrity sha512-sa4DUQsYciMP1xhKWGuFM04fB0LG/9DlluZoSVywUMRNvzid6XucHK0/90xGxRoHrAaROrcHK1aPKaijCtSrhg== -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: - name strip-ansi-cjs +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -20845,6 +20852,13 @@ strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: dependencies: ansi-regex "^4.1.0" +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + strip-ansi@^7.0.1: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -21173,9 +21187,9 @@ synchronous-promise@^2.0.15, synchronous-promise@^2.0.6: integrity sha512-k8uzYIkIVwmT+TcglpdN50pS2y1BDcUnBPK9iJeGu0Pl1lOI8pD6wtzgw91Pjpe+RxtTncw32tLxs/R0yNL2Mg== tabbable@^6.0.1: - version "6.1.1" - resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-6.1.1.tgz#40cfead5ed11be49043f04436ef924c8890186a0" - integrity sha512-4kl5w+nCB44EVRdO0g/UGoOp3vlwgycUVtkk/7DPyeLZUCuNFFKCFG6/t/DgHLrUPHjrZg6s5tNm+56Q2B0xyg== + version "6.2.0" + resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-6.2.0.tgz#732fb62bc0175cfcec257330be187dcfba1f3b97" + integrity sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew== table@^5.2.3: version "5.4.6" @@ -22620,8 +22634,7 @@ worker-farm@^1.7.0: dependencies: errno "~0.1.7" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: - name wrap-ansi-cjs +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -22656,6 +22669,15 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"