-
Notifications
You must be signed in to change notification settings - Fork 34
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’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(trip-form): Advanced Mode Settings Panel #749
base: master
Are you sure you want to change the base?
Changes from 15 commits
98ee788
f870be4
8c15d5f
b1d9825
d99e2cd
2c8e3ee
8145226
1564638
154a6bc
6954860
a2f34cc
f97065e
7c4e5dd
3c4ff04
bea6c49
796bf0c
7426879
a8d4ca8
6da3d7f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,4 +2,5 @@ | |
|
||
``` | ||
TBD | ||
|
||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
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 "./mockButtons.story"; | ||
|
||
const initialState = { | ||
enabledModeButtons: ["transit"], | ||
modeSettingValues: {} | ||
}; | ||
|
||
function pipe<T>(...fns: Array<(arg: T) => T>) { | ||
return (value: T) => fns.reduce((acc, fn) => fn(acc), value); | ||
} | ||
|
||
const MetroModeSubsettingsComponent = ({ | ||
fillModeIcons, | ||
modeButtonDefinitions, | ||
onSetModeSettingValue, | ||
onToggleModeButton | ||
}: { | ||
fillModeIcons?: boolean; | ||
modeButtonDefinitions: Array<ModeButtonDefinition>; | ||
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 setModeSettingValueAction = (event: QueryParamChangeEvent) => { | ||
setModeSettingValues({ ...modeSettingValues, ...event }); | ||
// Storybook Action: | ||
onSetModeSettingValue(event); | ||
}; | ||
|
||
return ( | ||
<div style={{ maxWidth: "500px" }}> | ||
<Core.AdvancedModeSubsettingsContainer | ||
fillModeIcons={fillModeIcons} | ||
label="Select a transit mode" | ||
modeButtons={processedModeButtons} | ||
onSettingsUpdate={setModeSettingValueAction} | ||
onToggleModeButton={toggleModeButtonAction} | ||
/> | ||
</div> | ||
); | ||
}; | ||
|
||
const Template = (args: { | ||
fillModeIcons?: boolean; | ||
onSetModeSettingValue: (event: QueryParamChangeEvent) => void; | ||
onToggleModeButton: (key: string, newState: boolean) => void; | ||
}): ReactElement => ( | ||
<MetroModeSubsettingsComponent | ||
modeButtonDefinitions={defaultModeButtonDefinitions} | ||
// eslint-disable-next-line react/jsx-props-no-spreading | ||
{...args} | ||
/> | ||
); | ||
|
||
export const AdvancedModeSettingsButtons = Template.bind({}); | ||
|
||
export default { | ||
argTypes: { | ||
fillModeIcons: { control: "boolean" }, | ||
onSetModeSettingValue: { action: "set mode setting value" }, | ||
onToggleModeButton: { action: "toggle button" } | ||
}, | ||
component: MetroModeSubsettingsComponent, | ||
title: "Trip Form Components/Advanced Mode Settings Buttons" | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
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 ".."; | ||
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. I think there should be a an empty line between the import groups here? 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. I'm not getting any errors or warnings here, but it looks like there should be right? |
||
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 => | ||
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. sort css |
||
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; | ||
onSettingsUpdate: (event: QueryParamChangeEvent) => void; | ||
onToggle: () => void; | ||
} | ||
|
||
const AdvancedModeSettingsButton = ({ | ||
accentColor = blue[700], | ||
fillModeIcons, | ||
id, | ||
modeButton, | ||
onSettingsUpdate, | ||
onToggle | ||
}: Props): JSX.Element => { | ||
const intl = useIntl(); | ||
const label = generateModeButtonLabel(modeButton.key, intl, modeButton.label); | ||
const checkboxId = `metro-submode-selector-mode-${id}`; | ||
return ( | ||
<SettingsContainer className="advanced-submode-container"> | ||
<StyledModeSettingsButton | ||
accentColor={accentColor} | ||
className="advanced-submode-mode-button" | ||
fillModeIcons={fillModeIcons} | ||
id={modeButton.key} | ||
subsettings={modeButton.modeSettings.length > 0} | ||
> | ||
<input | ||
aria-label={label} | ||
checked={modeButton.enabled ?? undefined} | ||
id={checkboxId} | ||
onChange={onToggle} | ||
type="checkbox" | ||
/> | ||
<label htmlFor={checkboxId}> | ||
<modeButton.Icon /> | ||
<span>{modeButton?.label}</span> | ||
{modeButton.enabled && <Check2 />} | ||
</label> | ||
</StyledModeSettingsButton> | ||
{modeButton.modeSettings.length > 0 && ( | ||
<AnimateHeight duration={300} height={modeButton.enabled ? "auto" : 0}> | ||
<StyledSettingsContainer className="subsettings-container"> | ||
<SubSettingsPane | ||
onSettingUpdate={onSettingsUpdate} | ||
modeButton={modeButton} | ||
/> | ||
</StyledSettingsContainer> | ||
</AnimateHeight> | ||
)} | ||
</SettingsContainer> | ||
); | ||
}; | ||
|
||
export default AdvancedModeSettingsButton; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
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 { | ||
amy-corson-ibigroup marked this conversation as resolved.
Show resolved
Hide resolved
|
||
border-radius: 0 0 8px 8px; | ||
border-bottom: 1px solid ${grey[300]}; | ||
} | ||
`; | ||
|
||
interface Props { | ||
accentColor?: string; | ||
fillModeIcons: boolean | undefined; | ||
label: string; | ||
modeButtons: ModeButtonDefinition[]; | ||
onSettingsUpdate: (event: QueryParamChangeEvent) => void; | ||
onToggleModeButton: (key: string, newState: boolean) => void; | ||
} | ||
|
||
const AdvancedModeSubsettingsContainer = ({ | ||
accentColor, | ||
fillModeIcons, | ||
modeButtons, | ||
label, | ||
onSettingsUpdate, | ||
onToggleModeButton | ||
}: Props): JSX.Element => { | ||
return ( | ||
<SubsettingsContainer> | ||
<legend>{label}</legend> | ||
{modeButtons.map(button => { | ||
return ( | ||
<AdvancedModeSettingsButton | ||
accentColor={accentColor} | ||
fillModeIcons={fillModeIcons} | ||
key={button.label} | ||
modeButton={button} | ||
onSettingsUpdate={onSettingsUpdate} | ||
onToggle={useCallback(() => { | ||
onToggleModeButton(button.key, !button.enabled); | ||
}, [button, onToggleModeButton])} | ||
id={button.key} | ||
/> | ||
); | ||
})} | ||
</SubsettingsContainer> | ||
); | ||
}; | ||
|
||
export default AdvancedModeSubsettingsContainer; |
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.
Would you mind moving this function into core utils? We are using it in a lot of places
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.
if this feels irrelevant to this PR we can do it later