diff --git a/__snapshots__/storybook.test.ts.snap b/__snapshots__/storybook.test.ts.snap
index 53243d008..e36567b5e 100644
--- a/__snapshots__/storybook.test.ts.snap
+++ b/__snapshots__/storybook.test.ts.snap
@@ -1323,6 +1323,446 @@ Array [
]
`;
+exports[`Storyshots Building-Blocks/Dropdown Dropdown As Otprr Nav Item 1`] = `
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`Storyshots Building-Blocks/Dropdown Dropdown As Otprr Nav Item 2`] = `
+.c2 {
+ background: #fff;
+ border: 1px solid black;
+ border-radius: 5px;
+ color: inherit;
+ padding: 5px 7px;
+ -webkit-transition: all 0.1s ease-in-out;
+ transition: all 0.1s ease-in-out;
+}
+
+.c2 span.caret {
+ border-left: 4px solid transparent;
+ border-right: 4px solid transparent;
+ border-top: 4px solid;
+ color: inherit;
+ display: inline-block;
+ height: 0;
+ margin-left: 5px;
+ vertical-align: middle;
+ width: 0;
+}
+
+.c2:hover,
+.c2[aria-expanded="true"] {
+ background: #ECECEC;
+ color: black;
+ cursor: pointer;
+}
+
+.c1 {
+ float: left;
+ position: relative;
+}
+
+.c0 {
+ -webkit-align-items: center;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+ background: #004686;
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ min-height: 50px;
+ padding: 0 10px;
+ position: relative;
+ width: 100%;
+}
+
+.c0 .navBarItem {
+ position: static;
+}
+
+.c0 .navBarItem > button {
+ background: transparent;
+ border: none;
+ color: white;
+ padding: 15px;
+}
+
+.c0 .navBarItem > button:hover {
+ background: rgba(0,0,0,0.05);
+ color: #ececec;
+}
+
+@media (max-width:768px) {
+ .c0 .navBarItem > button {
+ padding: 10px;
+ }
+}
+
+
+
+
+
+
+`;
+
+exports[`Storyshots Building-Blocks/Dropdown Dropdown With Label 1`] = `
+
+
+ More content here
+
+
+`;
+
+exports[`Storyshots Building-Blocks/Dropdown Dropdown With Label 2`] = `
+.c1 {
+ background: #fff;
+ border: 1px solid black;
+ border-radius: 5px;
+ color: inherit;
+ padding: 5px 7px;
+ -webkit-transition: all 0.1s ease-in-out;
+ transition: all 0.1s ease-in-out;
+}
+
+.c1 span.caret {
+ border-left: 4px solid transparent;
+ border-right: 4px solid transparent;
+ border-top: 4px solid;
+ color: inherit;
+ display: inline-block;
+ height: 0;
+ margin-left: 5px;
+ vertical-align: middle;
+ width: 0;
+}
+
+.c1:hover,
+.c1[aria-expanded="true"] {
+ background: #ECECEC;
+ color: black;
+ cursor: pointer;
+}
+
+.c0 {
+ float: left;
+ position: relative;
+}
+
+
+
+
+`;
+
+exports[`Storyshots Building-Blocks/Dropdown Dropdown With List 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`Storyshots Building-Blocks/Dropdown Dropdown With List 2`] = `
+.c1 {
+ background: #fff;
+ border: 1px solid black;
+ border-radius: 5px;
+ color: inherit;
+ padding: 5px 7px;
+ -webkit-transition: all 0.1s ease-in-out;
+ transition: all 0.1s ease-in-out;
+}
+
+.c1 span.caret {
+ border-left: 4px solid transparent;
+ border-right: 4px solid transparent;
+ border-top: 4px solid;
+ color: inherit;
+ display: inline-block;
+ height: 0;
+ margin-left: 5px;
+ vertical-align: middle;
+ width: 0;
+}
+
+.c1:hover,
+.c1[aria-expanded="true"] {
+ background: #ECECEC;
+ color: black;
+ cursor: pointer;
+}
+
+.c0 {
+ float: left;
+ position: relative;
+}
+
+
+
+
+`;
+
+exports[`Storyshots Building-Blocks/Dropdown Dropdown With List Align Menu Left 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`Storyshots Building-Blocks/Dropdown Dropdown With List Align Menu Left 2`] = `
+.c1 {
+ background: #fff;
+ border: 1px solid black;
+ border-radius: 5px;
+ color: inherit;
+ padding: 5px 7px;
+ -webkit-transition: all 0.1s ease-in-out;
+ transition: all 0.1s ease-in-out;
+}
+
+.c1 span.caret {
+ border-left: 4px solid transparent;
+ border-right: 4px solid transparent;
+ border-top: 4px solid;
+ color: inherit;
+ display: inline-block;
+ height: 0;
+ margin-left: 5px;
+ vertical-align: middle;
+ width: 0;
+}
+
+.c1:hover,
+.c1[aria-expanded="true"] {
+ background: #ECECEC;
+ color: black;
+ cursor: pointer;
+}
+
+.c0 {
+ float: left;
+ position: relative;
+}
+
+
+
+
+`;
+
exports[`Storyshots EndpointsOverlay Endpoints Overlay With Custom Map Markers 1`] = `
+
+
+`;
+
+exports[`Storyshots Trip Form Components/Advanced Mode Settings Buttons Advanced Mode Settings Buttons 2`] = `
+.c4 {
+ display: inline-block;
+ vertical-align: middle;
+ overflow: hidden;
+}
+
+.c9 {
+ font-weight: normal;
+ padding-left: 6px;
+}
+
+.c11 > div {
+ width: 50%;
+ display: inline-block;
+ box-sizing: border-box;
+ position: relative;
+}
+
+.c11 select {
+ width: 100%;
+ box-sizing: border-box;
+ cursor: pointer;
+}
+
+.c3 {
+ display: inline-block;
+ vertical-align: middle;
+ overflow: hidden;
+}
+
+.c7 {
+ display: grid;
+ grid-column: span 2;
+ grid-template-columns: 1fr 1fr;
+ width: 100%;
+}
+
+.c6 {
+ border: none;
+ pointer-events: auto;
+}
+
+.c6 div {
+ padding: 5px 0;
+}
+
+.c6 .wide {
+ grid-column: span 2;
+}
+
+.c6 .slim {
+ font-size: 125%;
+ font-weight: 125%;
+}
+
+.c6 legend {
+ font-size: 1.5em;
+ margin-bottom: 0.5rem;
+ padding-top: 15px;
+}
+
+.c8 {
+ -webkit-align-items: baseline;
+ -webkit-box-align: baseline;
+ -ms-flex-align: baseline;
+ align-items: baseline;
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ margin-left: 4px;
+}
+
+.c8 input {
+ -webkit-flex-shrink: 0;
+ -ms-flex-negative: 0;
+ flex-shrink: 0;
+}
+
+.c10 {
+ -webkit-align-items: baseline;
+ -webkit-box-align: baseline;
+ -ms-flex-align: baseline;
+ align-items: baseline;
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ gap: 4px;
+}
+
+.c10 svg {
+ width: 16px;
+ height: 16px;
+ display: inline-block;
+ margin-bottom: 4px;
+ vertical-align: middle;
+ overflow: hidden;
+}
+
+.c1 {
+ width: 100%;
+ max-width: 500px;
+}
+
+.c2 > input {
+ -webkit-clip: rect(0,0,0,0);
+ clip: rect(0,0,0,0);
+ height: 0;
+ overflow: hidden;
+ position: absolute;
+ width: 0;
+}
+
+.c2 > label {
+ -webkit-align-items: center;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+ background-color: #fff;
+ border: 1px solid #336B9E;
+ border-left-width: 2px;
+ border-right-width: 2px;
+ color: #336B9E;
+ display: grid;
+ gap: 20px;
+ grid-template-columns: 40px auto 40px;
+ height: 51px;
+ justify-items: center;
+ padding: 0 10px;
+}
+
+.c2 > input:checked + label {
+ background-color: #336B9E;
+ color: #fff;
+ border-bottom-left-radius: 0 !important;
+ border-bottom-right-radius: 0 !important;
+}
+
+.c2 span {
+ justify-self: flex-start;
+}
+
+.c2 svg {
+ height: 26px;
+ width: 26px;
+ fill: currentcolor;
+}
+
+.c2:hover {
+ cursor: pointer;
+}
+
+.c12 > input {
+ -webkit-clip: rect(0,0,0,0);
+ clip: rect(0,0,0,0);
+ height: 0;
+ overflow: hidden;
+ position: absolute;
+ width: 0;
+}
+
+.c12 > label {
+ -webkit-align-items: center;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+ background-color: #fff;
+ border: 1px solid #336B9E;
+ border-left-width: 2px;
+ border-right-width: 2px;
+ color: #336B9E;
+ display: grid;
+ gap: 20px;
+ grid-template-columns: 40px auto 40px;
+ height: 51px;
+ justify-items: center;
+ padding: 0 10px;
+}
+
+.c12 > input:checked + label {
+ background-color: #336B9E;
+ color: #fff;
+ border-bottom-left-radius: !important;
+ border-bottom-right-radius: !important;
+}
+
+.c12 span {
+ justify-self: flex-start;
+}
+
+.c12 svg {
+ height: 26px;
+ width: 26px;
+ fill: currentcolor;
+}
+
+.c12:hover {
+ cursor: pointer;
+}
+
+.c5 {
+ border: 1px solid #B3B3B3;
+ border-top: 0;
+ padding: 1em;
+}
+
+.c0 {
+ border: none;
+ margin: 0;
+ width: 90%;
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ -webkit-flex-direction: column;
+ -ms-flex-direction: column;
+ flex-direction: column;
+}
+
+.c0 legend {
+ -webkit-clip: rect(0,0,0,0);
+ clip: rect(0,0,0,0);
+ height: 0;
+ overflow: hidden;
+ position: absolute;
+ width: 0;
+}
+
+.c0 div:first-of-type div label {
+ border-top-width: 2px;
+ border-radius: 8px 8px 0 0;
+}
+
+.c0 div:last-of-type div label {
+ border-bottom-width: 2px;
+ border-radius: 0 0 8px 8px;
+}
+
+.c0 div.advanced-submode-container:last-of-type div:last-child {
+ border-radius: 0 0 8px 8px;
+}
+
+
+`;
+
exports[`Storyshots Trip Form Components/Metro Mode Selector Metro Mode Selector 1`] = `
{
+ alignMenuLeft?: boolean;
+ buttonStyle?: React.CSSProperties;
+ label?: string;
+ listLabel?: string;
+ text?: JSX.Element | string;
+ nav?: boolean;
+}
+
+/**
+ * Renders a dropdown menu. By default, only a passed "text" is rendered. If clicked,
+ * a floating div is rendered below the "text" with list contents inside. Clicking anywhere
+ * outside the floating div will close the dropdown.
+ */
+const Dropdown = ({
+ alignMenuLeft,
+ children,
+ className,
+ id,
+ label,
+ listLabel,
+ text,
+ buttonStyle
+}: Props): JSX.Element => {
+ const [open, setOpen] = useState(false);
+
+ const containerRef = useRef(null);
+
+ const toggleOpen = useCallback(() => setOpen(!open), [open, setOpen]);
+
+ // Argument for document.querySelectorAll to target focusable elements.
+ const queryId = `#${id} button, #${id}-label`;
+
+ const isList = Array.isArray(children)
+ ? children.every(
+ child => React.isValidElement(child) && child.type === "li"
+ )
+ : React.isValidElement(children) && children.type === "li";
+
+ // Adding document event listeners allows us to close the dropdown
+ // when the user interacts with any part of the page that isn't the dropdown
+ useEffect(() => {
+ const handleExternalAction = (e: Event): void => {
+ if (!containerRef?.current?.contains(e.target as HTMLElement)) {
+ setOpen(false);
+ }
+ };
+ document.addEventListener("mousedown", handleExternalAction);
+ document.addEventListener("focusin", handleExternalAction);
+ document.addEventListener("keydown", handleExternalAction);
+ return () => {
+ document.removeEventListener("mousedown", handleExternalAction);
+ document.removeEventListener("focusin", handleExternalAction);
+ document.removeEventListener("keydown", handleExternalAction);
+ };
+ }, [containerRef]);
+
+ const handleKeyDown = useCallback(
+ (e: KeyboardEvent): void => {
+ const element = e.target as HTMLElement;
+ switch (e.key) {
+ case "ArrowUp":
+ e.preventDefault();
+ getPreviousSibling(queryId, element)?.focus();
+ break;
+ case "ArrowDown":
+ e.preventDefault();
+ getNextSibling(queryId, element)?.focus();
+ break;
+ case "Escape":
+ setOpen(false);
+ break;
+ case " ":
+ case "Enter":
+ e.preventDefault();
+ element.click();
+ if (element.id === `${id}-label` || element.id === `${id}-wrapper`) {
+ toggleOpen();
+ }
+ break;
+ default:
+ }
+ },
+ [id, toggleOpen]
+ );
+
+ return (
+
+
+ {text}
+
+
+ {open && (
+
+ {children}
+
+ )}
+
+ );
+};
+
+export default Dropdown;
diff --git a/packages/building-blocks/src/dropdown/styled.tsx b/packages/building-blocks/src/dropdown/styled.tsx
new file mode 100644
index 000000000..a0364829f
--- /dev/null
+++ b/packages/building-blocks/src/dropdown/styled.tsx
@@ -0,0 +1,71 @@
+import styled from "styled-components";
+import grey from "../colors/grey";
+
+export const DropdownButton = styled.button`
+ background: #fff;
+ border: 1px solid black;
+ border-radius: 5px;
+ color: inherit;
+ padding: 5px 7px;
+ transition: all 0.1s ease-in-out;
+
+ span.caret {
+ border-left: 4px solid transparent;
+ border-right: 4px solid transparent;
+ border-top: 4px solid;
+ color: inherit;
+ display: inline-block;
+ height: 0;
+ margin-left: 5px;
+ vertical-align: middle;
+ width: 0;
+ }
+
+ &:hover,
+ &[aria-expanded="true"] {
+ background: ${grey[50]};
+ color: black;
+ cursor: pointer;
+ }
+`;
+
+export const DropdownMenu = styled.div<{ alignLeft?: boolean }>`
+ background-clip: padding-box;
+ background-color: #fff;
+ border-radius: 4px;
+ border: 1px solid rgba(0, 0, 0, 0.15);
+ box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
+ color: #333;
+ list-style: none;
+ margin: 2px 0 0;
+ min-width: 160px;
+ padding: 5px 0;
+ position: absolute;
+ ${props => (props.alignLeft ? "left: 0;" : "right: 0;")}
+ top: 100%;
+ width: 100%;
+ z-index: 1000;
+
+ hr {
+ margin: 0;
+ padding: 0;
+ }
+ a,
+ button {
+ background: transparent;
+ border: none;
+ cursor: pointer;
+ padding: 5px 15px;
+ text-align: start;
+ width: 100%;
+
+ &:hover {
+ background: ${grey[50]};
+ }
+ }
+`;
+
+export const DropdownWrapper = styled.span<{ pullRight?: boolean }>`
+ float: ${props => (props.pullRight ? "right" : "left")};
+ position: relative;
+`;
diff --git a/packages/building-blocks/src/index.ts b/packages/building-blocks/src/index.ts
index 287eb38e6..2ea4b722a 100644
--- a/packages/building-blocks/src/index.ts
+++ b/packages/building-blocks/src/index.ts
@@ -1,5 +1,7 @@
import blue from "./colors/blue";
import red from "./colors/red";
import grey from "./colors/grey";
+import Dropdown from "./dropdown";
+export { Dropdown };
export default { blue, red, grey };
diff --git a/packages/building-blocks/src/stories/dropdown.story.tsx b/packages/building-blocks/src/stories/dropdown.story.tsx
new file mode 100644
index 000000000..c9b8e7804
--- /dev/null
+++ b/packages/building-blocks/src/stories/dropdown.story.tsx
@@ -0,0 +1,105 @@
+import React from "react";
+import { ComponentMeta } from "@storybook/react";
+import styled from "styled-components";
+import Dropdown from "../dropdown";
+import blue from "../colors/blue";
+
+const options = [
+ { value: "1", label: "One" },
+ { value: "2", label: "Two" },
+ { value: "3", label: "Three" }
+];
+
+const NavItemWrapper = styled.div`
+ align-items: center;
+ background: ${blue[900]};
+ display: flex;
+ min-height: 50px;
+ padding: 0 10px;
+ position: relative;
+ width: 100%;
+
+ .navBarItem {
+ position: static;
+ & > button {
+ background: transparent;
+ border: none;
+ color: white;
+ padding: 15px;
+
+ @media (max-width: 768px) {
+ padding: 10px;
+ }
+
+ &:hover {
+ background: rgba(0, 0, 0, 0.05);
+ color: #ececec;
+ }
+ }
+ }
+`;
+
+export default {
+ title: "Building-Blocks/Dropdown",
+ component: Dropdown
+} as ComponentMeta;
+
+export const DropdownWithLabel = (): React.ReactElement => (
+
+ More content here
+
+);
+
+export const DropdownWithList = (): React.ReactElement => (
+
+ {options.map(option => (
+
+
+
+ ))}
+
+);
+
+export const DropdownWithListAlignMenuLeft = (): React.ReactElement => (
+
+ {options.map(option => (
+
+
+
+ ))}
+
+);
+
+export const DropdownAsOtprrNavItem = (): React.ReactElement => (
+
+
+ {options.map(option => (
+
+ ))}
+
+
+);
diff --git a/packages/building-blocks/src/utils/dom-query.ts b/packages/building-blocks/src/utils/dom-query.ts
new file mode 100644
index 000000000..83e134342
--- /dev/null
+++ b/packages/building-blocks/src/utils/dom-query.ts
@@ -0,0 +1,49 @@
+function getEntries(query: string) {
+ const entries = Array.from(document.querySelectorAll(query));
+ const firstElement = entries[0];
+ const lastElement = entries[entries.length - 1];
+
+ return { entries, firstElement, lastElement };
+}
+
+/**
+ * Helper method to find the next focusable sibling element relative to the
+ * specified element.
+ *
+ * @param {string} query - Argument that gets passed to document.querySelectorAll
+ * @param {HTMLElement} element - Specified element (e.target)
+ * @returns {HTMLElement} - element to be focused
+ */
+export function getNextSibling(
+ query: string,
+ element: EventTarget
+): HTMLElement {
+ const { entries, firstElement, lastElement } = getEntries(query);
+
+ if (element === lastElement) {
+ return firstElement as HTMLElement;
+ }
+ const elementIndex = entries.indexOf(element as HTMLElement);
+ return entries[elementIndex + 1] as HTMLElement;
+}
+
+/**
+ * Helper method to find the previous focusable sibling element relative to the
+ * specified element.
+ *
+ * @param {string} query - Argument that gets passed to document.querySelectorAll
+ * @param {HTMLElement} element - Specified element (e.target)
+ * @returns {HTMLElement} - element to be focused
+ */
+export function getPreviousSibling(
+ query: string,
+ element: EventTarget
+): HTMLElement {
+ const { entries, firstElement, lastElement } = getEntries(query);
+
+ if (element === firstElement) {
+ return lastElement as HTMLElement;
+ }
+ const elementIndex = entries.indexOf(element as HTMLButtonElement);
+ return entries[elementIndex - 1] as HTMLElement;
+}
diff --git a/packages/endpoints-overlay/i18n/zh_Hant.yml b/packages/endpoints-overlay/i18n/zh_Hant.yml
index 3678fac3f..f2ea30fb8 100644
--- a/packages/endpoints-overlay/i18n/zh_Hant.yml
+++ b/packages/endpoints-overlay/i18n/zh_Hant.yml
@@ -1,7 +1,7 @@
otpUi:
EndpointsOverlay:
- coordinates: '{lat, number, ::.00000}; {lon, number, ::.00000}'
clearLocation: 移除{locationType}位置
+ coordinates: "{lat, number, ::.00000}; {lon, number, ::.00000}"
forgetHome: 忘記住家
forgetWork: 忘記工作
saveAsHome: 儲存為住家
diff --git a/packages/from-to-location-picker/i18n/zh_Hant.yml b/packages/from-to-location-picker/i18n/zh_Hant.yml
index 7b7dba240..0d94bd232 100644
--- a/packages/from-to-location-picker/i18n/zh_Hant.yml
+++ b/packages/from-to-location-picker/i18n/zh_Hant.yml
@@ -1,5 +1,5 @@
otpUi:
FromToLocationPicker:
from: 從這裡
- to: 前往這裡
planATrip: 規劃行程:
+ to: 前往這裡
diff --git a/packages/from-to-location-picker/src/types.ts b/packages/from-to-location-picker/src/types.ts
index ebd111bf6..97d447ac2 100644
--- a/packages/from-to-location-picker/src/types.ts
+++ b/packages/from-to-location-picker/src/types.ts
@@ -26,7 +26,7 @@ export type FromToPickerProps = {
* Passes an argument as follows:
* { locationType: "from/to", location, reverseGeocode: false }
*/
- setLocation?: ({
+ setLocation?: (_: {
locationType: string,
// eslint-disable-next-line @typescript-eslint/no-shadow
location: Location,
diff --git a/packages/itinerary-body/i18n/es.yml b/packages/itinerary-body/i18n/es.yml
index a485a72ab..f62a0263e 100644
--- a/packages/itinerary-body/i18n/es.yml
+++ b/packages/itinerary-body/i18n/es.yml
@@ -92,14 +92,14 @@ otpUi:
viewOnMap: Ver en el mapa
TransitLegBody:
AlertsBody:
+ alertLinkText: Ver la alerta en la web {agency}
effectiveDate: A partir de {dateTime, date, long}
effectiveTimeAndDate: A partir de {dateTime, time, short}, {day}
+ linkOpensNewWindow: (Abrir una nueva ventana)
+ noAgencyAlertLinkText: Ver alerta en la web de la agencia
today: Hoy
tomorrow: Mañana
yesterday: Ayer
- linkOpensNewWindow: (Abrir una nueva ventana)
- noAgencyAlertLinkText: Ver alerta en la web de la agencia
- alertLinkText: Ver la alerta en la web {agency}
agencyExternalLink: "{agencyName} (Enlace external)"
alertsHeader: "{alertCount, plural, =1 {# alerta} other {# alertas}}"
arriveAt: Llegada a {place}
diff --git a/packages/itinerary-body/i18n/tr.yml b/packages/itinerary-body/i18n/tr.yml
index a2939b458..6cd69eed5 100644
--- a/packages/itinerary-body/i18n/tr.yml
+++ b/packages/itinerary-body/i18n/tr.yml
@@ -9,12 +9,12 @@ otpUi:
vehicleType:
bike: bisiklet
bikeshare: paylaşımlı bisiklet
- vehicle: araç
car: araba
escooter: E-Skutır
+ vehicle: araç
TncLeg:
- estimatedCost: 'Tahmini maliyet: {minFare} - {maxFare}'
- estimatedTravelTime: 'Tahmini seyahat süresi: {duration} (trafiği hesaba katmaz)'
+ estimatedCost: "Tahmini maliyet: {minFare} - {maxFare}"
+ estimatedTravelTime: "Tahmini seyahat süresi: {duration} (trafiği hesaba katmaz)"
mapillaryTooltip: Bu konumdaki sokak görüntülerini göster
step:
continue: Devam et
diff --git a/packages/itinerary-body/i18n/zh_Hant.yml b/packages/itinerary-body/i18n/zh_Hant.yml
index fba456b2f..bff734785 100644
--- a/packages/itinerary-body/i18n/zh_Hant.yml
+++ b/packages/itinerary-body/i18n/zh_Hant.yml
@@ -1,114 +1,114 @@
otpUi:
AccessLegBody:
+ LegDiagramPreview:
+ elevationChart: 海拔圖表
+ noElevationData: 沒有可用的海拔資料。
+ toggleElevationChart: 切換海拔圖表
RentedVehicleSubheader:
+ pickupRental: 領取{company} {vehicleType} {vehicleName}
+ resumeRentalRide: 繼續使用租賃
vehicleType:
bike: 騎行
bikeshare: 自行車共享
car: 汽車
- vehicle: 車輛
escooter: 電動滑板車
+ vehicle: 車輛
walkVehicle: 沿著{place}牽車步行
- resumeRentalRide: 繼續使用租賃
- pickupRental: 領取{company} {vehicleType} {vehicleName}
TncLeg:
bookRide: 預約乘車
- waitForPickup: '等待{minutes, plural, =0 {} other { #分鐘}}由{company}接車'
bookRideLater: 等到{timeMillis, time, short}再預約
estimatedCost: 預估費用:{minFare} - {maxFare}
estimatedTravelTime: 預估行程時間:{duration} (不考慮車流)
+ waitForPickup: "等待{minutes, plural, =0 {} other { #分鐘}}由{company}接車"
+ mapillaryTooltip: 顯示此位置的街道影像
step:
- left: 左
- right: 右
- slightlyLeft: 稍微向左
- slightlyRight: 稍微向右
circleClockwise: 順時鐘沿著圓環
circleCounterClockwise: 逆時鐘沿著圓環
continue: 繼續
+ enterStation: 輸入車站
+ exitStation: 退出車站
hardLeft: 向左急轉
hardRight: 向右急轉
+ left: 左
+ right: 右
+ slightlyLeft: 稍微向左
+ slightlyRight: 稍微向右
uTurnLeft: 左邊迴轉
uTurnRight: 右邊迴轉
- enterStation: 輸入車站
- exitStation: 退出車站
stepDepart: 往 {heading} 在 {street}
+ stepElevator: 搭乘電梯到{street}
+ stepFollowSigns: 跟著指示牌前往 {street}
+ stepGeneric: "{step} 在 {street}"
stepHeading:
- west: 西
- north: 北
east: 東
+ north: 北
northeast: 東北
- southeast: 東南
- southwest: 西南
northwest: 西北
south: 南
- summary: '{mode} 到{place}'
- summaryAndDistance: '{mode} {distance} 到{place}'
+ southeast: 東南
+ southwest: 西南
+ west: 西
+ stepStation: "{step} 在 {street}"
+ summary: "{mode} 到{place}"
+ summaryAndDistance: "{mode} {distance} 到{place}"
summaryMode:
- carHail: 乘車
- escooter: 騎車
+ bike: 自行車
bikeshare: 自行車共享
carDrive: 駕車
+ carHail: 乘車
+ escooter: 騎車
walk: 步行
- bike: 自行車
+ unnamedPath: 未命名路線
unnamedRoad: 未命名道路
+ vehicleTitle: "{company} {vehicleType}"
vehicleType:
bike: 騎行
bikeshare: 共享自行車
+ car: 汽車
escooter: 騎電動滑板車
vehicle: 車輛
- car: 汽車
- vehicleTitle: '{company} {vehicleType}'
- LegDiagramPreview:
- elevationChart: 海拔圖表
- noElevationData: 沒有可用的海拔資料。
- toggleElevationChart: 切換海拔圖表
- mapillaryTooltip: 顯示此位置的街道影像
- stepGeneric: '{step} 在 {street}'
- stepElevator: 搭乘電梯到{street}
- unnamedPath: 未命名路線
- stepFollowSigns: 跟著指示牌前往 {street}
- stepStation: '{step} 在 {street}'
ItineraryBody:
- flexAdvanceNotice: ' 至少提前{leadDays}天'
+ common:
+ durationShort: "{hours, plural, =0 {} other {#小時}}{minutes}分鐘"
+ flexAdvanceNotice: " 至少提前{leadDays}天"
+ flexCallAhead: 撥打事先
flexCallNumber: 撥打{phoneNumber}
+ flexPickupMessage: 若要使用此路線,您必須{action}{advanceNotice}.
+ stayOnBoard: 留在{place}車上
travelBy: 透過{mode}旅行
travelByMode:
bike: 騎自行車
car: 開車
- walk: 步行
escooter: 騎電動滑板車
- viewOnMap: 在地圖上檢視
- common:
- durationShort: '{hours, plural, =0 {} other {#小時}}{minutes}分鐘'
- flexPickupMessage: 若要使用此路線,您必須{action}{advanceNotice}.
- flexCallAhead: 撥打事先
- stayOnBoard: 留在{place}車上
+ walk: 步行
tripAccessibility:
inaccessible: 無法到達
+ itineraryAccessibility: "此行程的輪椅無障礙設施: "
+ legAccessibility: "此行程路段的輪椅無障礙設施: "
likelyAccessible: 可能可以到達
unclear: 未知
- itineraryAccessibility: '此行程的輪椅無障礙設施: '
- legAccessibility: '此行程路段的輪椅無障礙設施: '
+ viewOnMap: 在地圖上檢視
TransitLegBody:
AlertsBody:
- today: 今天
- tomorrow: 明天
- yesterday: 昨天
effectiveDate: 自{dateTime, date, long}起生效
effectiveTimeAndDate: 自{dateTime, time, short}, {day}起生效
linkOpensNewWindow: (開啟新視窗)
+ today: 今天
+ tomorrow: 明天
+ yesterday: 昨天
+ agencyExternalLink: "{agencyName} (外部連結)"
+ alertsHeader: "{alertCount}則警示"
+ expandDetails: (展開詳細資訊)
fare: 票價:{fare}
- alertsHeader: '{alertCount}則警示'
fromLocation: 從{location}
- ride: 搭車
- routeDescription: '{routeName}到{headsign}'
- tripViewer: 行程檢視器
- typicalWait: 一般等待時間:{waitTime}
- stopViewer: 車站檢視器
+ legDetails: 路段詳細資訊
operatedBy: 由{agencyLink}提供服務
- rideDurationAndStops: '搭乘{duration}{numStops, plural, =1 {} other { / # 站}}'
+ ride: 搭車
+ rideDurationAndStops: "搭乘{duration}{numStops, plural, =1 {} other { / # 站}}"
+ routeDescription: "{routeName}到{headsign}"
stopId: 車站ID {stopId}
stopIdBasic: ID {stopId}
- expandDetails: (展開詳細資訊)
- agencyExternalLink: '{agencyName} (外部連結)'
- legDetails: 路段詳細資訊
+ stopViewer: 車站檢視器
+ tripViewer: 行程檢視器
+ typicalWait: 一般等待時間:{waitTime}
zoomToLeg: 縮放至地圖上的路段
diff --git a/packages/itinerary-body/package.json b/packages/itinerary-body/package.json
index af30b5d26..20bef12c6 100644
--- a/packages/itinerary-body/package.json
+++ b/packages/itinerary-body/package.json
@@ -1,6 +1,6 @@
{
"name": "@opentripplanner/itinerary-body",
- "version": "5.3.2",
+ "version": "5.3.3",
"description": "A component for displaying an itinerary body of a trip planning result",
"main": "lib/index.js",
"module": "esm/index.js",
diff --git a/packages/location-field/i18n/zh_Hant.yml b/packages/location-field/i18n/zh_Hant.yml
index 3f3dfd0d6..1ef7587a3 100644
--- a/packages/location-field/i18n/zh_Hant.yml
+++ b/packages/location-field/i18n/zh_Hant.yml
@@ -1,25 +1,25 @@
otpUi:
LocationField:
+ beginTypingPrompt: 開始輸入以搜尋位置
clearLocation: 清除位置
currentLocationUnavailable: 目前的位置無法使用
fetchingLocation: 正在擷取位置……
- noResults: 找不到符合
- stationCount: '{count}車站'
- stopCount: '{count}車站'
- useCurrentLocation: 使用目前位置
- workLocation: 工作
- beginTypingPrompt: 開始輸入以搜尋位置
+ fetchingSuggestions: 正在擷取建議…
homeLocation: 住家
+ howToAccessResults: 使用向下箭頭鍵瀏覽結果清單。若要選取結果,請使用Enter鍵。
myPlaces: 我的地點
nearby: 附近的車站
+ noResults: 找不到符合
other: 其他
+ otherCount: "{count}興趣點"
+ parenthesisFormat: "{main} ({detail})"
recentlySearched: 最近搜尋過
+ resultsFound: 為 "{input}" 找到 {results}。
+ stationCount: "{count}車站"
stations: 車站
+ stopCount: "{count}車站"
stops: 車站
- fetchingSuggestions: 正在擷取建議…
- howToAccessResults: 使用向下箭頭鍵瀏覽結果清單。若要選取結果,請使用Enter鍵。
- otherCount: '{count}興趣點'
- parenthesisFormat: '{main} ({detail})'
- resultsFound: 為 "{input}" 找到 {results}。
suggestedLocations: 建議地點
suggestedLocationsLong: 顯示/不顯示建議位置清單
+ useCurrentLocation: 使用目前位置
+ workLocation: 工作
diff --git a/packages/printable-itinerary/i18n/zh_Hant.yml b/packages/printable-itinerary/i18n/zh_Hant.yml
index 89fd59614..813af1223 100644
--- a/packages/printable-itinerary/i18n/zh_Hant.yml
+++ b/packages/printable-itinerary/i18n/zh_Hant.yml
@@ -5,7 +5,11 @@ otpUi:
estimatedWaitTime: 預估等待接車時間:{duration}
header: 搭乘{company}前往{place}
TransitLeg:
- alight: {time, time, short}於{place} ({stopId})下車
+ alight: >-
+ {time, time, short}於{place}
+ ({stopId})下車
+ board: >-
+ {time, time, short}於{place}
+ ({stopId})上車
continuesAs: 繼續{routeDescription}
- board: {time, time, short}於{place} ({stopId})上車
depart: 從{place}出發
diff --git a/packages/transit-vehicle-overlay/i18n/zh_Hant.yml b/packages/transit-vehicle-overlay/i18n/zh_Hant.yml
index 4649baf05..9fed5537a 100644
--- a/packages/transit-vehicle-overlay/i18n/zh_Hant.yml
+++ b/packages/transit-vehicle-overlay/i18n/zh_Hant.yml
@@ -1,7 +1,8 @@
otpUi:
TransitVehicleOverlay:
+ defaultTooltip: "{route}: {duration}前"
+ durationWithSeconds: >-
+ {hours, plural, =0 {} other {#小時}}{minutes, plural, =0 {{seconds, plural,
+ =0 {#分鐘} other {}}} other {#分鐘}}{seconds, plural, =0 {} other {#秒}}
+ routeTitle: "{type} {name}"
transitLine: 路線
- durationWithSeconds: '{hours, plural, =0 {} other {#小時}}{minutes, plural, =0 {{seconds,
- plural, =0 {#分鐘} other {}}} other {#分鐘}}{seconds, plural, =0 {} other {#秒}}'
- defaultTooltip: '{route}: {duration}前'
- routeTitle: '{type} {name}'
diff --git a/packages/trip-details/i18n/zh_Hant.yml b/packages/trip-details/i18n/zh_Hant.yml
index 39afe5feb..29a3a40a4 100644
--- a/packages/trip-details/i18n/zh_Hant.yml
+++ b/packages/trip-details/i18n/zh_Hant.yml
@@ -1,11 +1,5 @@
otpUi:
TripDetails:
- co2: 排放的二氧化碳:{co2}
- hideDetail: 隱藏詳細資訊
- showDetail: 顯示詳細資訊
- transferDiscountExplanation: 已套用{transferAmount}的轉乘折扣
- departure: {departureDate, date, long}的{departureDate,
- time, short}出發
FareTable:
cash: 現金
electronic: 電子
@@ -13,12 +7,20 @@ otpUi:
senior: 年長者
special: 特殊
youth: 青少年
+ co2: 排放的二氧化碳:{co2}
+ co2description: 二氧化碳密度是以一個行程每條腿的距離乘以各個模式的二氧化碳密度來計算。各個模式的二氧化碳密度是從此試算表中取得。
+ departure: >-
+ {departureDate, date, long}的{departureDate, time,
+ short}出發
+ hideDetail: 隱藏詳細資訊
+ minutesActive: 活躍時間:{minutes}分鐘前
+ showDetail: 顯示詳細資訊
+ timeActiveDescription: >
+ 採用此行程即表示您將花費 {walkMinutes} 分鐘 的步行時間以及
+ {bikeMinutes} 分鐘 的自行車騎乘時間。
title: 行程詳細資訊
- tncFare: '{companies} 票價:{minTNCFare} - {maxTNCFare}'
+ tncFare: "{companies} 票價:{minTNCFare} - {maxTNCFare}"
+ transferDiscountExplanation: 已套用{transferAmount}的轉乘折扣
transitFare: 公共交通票價
- transitFareEntry: '{name}:{value}'
- co2description: 二氧化碳密度是以一個行程每條腿的距離乘以各個模式的二氧化碳密度來計算。各個模式的二氧化碳密度是從此試算表中取得。
+ transitFareEntry: "{name}:{value}"
tripIncludesFlex: 此行程包括彈性的路線。{extraMessage}
- minutesActive: 活躍時間:{minutes}分鐘前
- timeActiveDescription: "採用此行程即表示您將花費 {walkMinutes} 分鐘 的步行時間以及
- {bikeMinutes} 分鐘 的自行車騎乘時間。\n"
diff --git a/packages/trip-details/package.json b/packages/trip-details/package.json
index 6256bdaa6..87a6d64d4 100644
--- a/packages/trip-details/package.json
+++ b/packages/trip-details/package.json
@@ -1,6 +1,6 @@
{
"name": "@opentripplanner/trip-details",
- "version": "5.0.12",
+ "version": "5.0.13",
"description": "A component for displaying details about a trip planning itinerary",
"main": "lib/index.js",
"module": "esm/index.js",
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/zh_Hant.yml b/packages/trip-form/i18n/zh_Hant.yml
index 96ad680c0..06b663576 100644
--- a/packages/trip-form/i18n/zh_Hant.yml
+++ b/packages/trip-form/i18n/zh_Hant.yml
@@ -1,50 +1,51 @@
otpUi:
DateTimeSelector:
+ arrive: 最晚抵達時間
date: 日期
dateTimeSelector: 日期/時間選擇器
- time: 時間
- arrive: 最晚抵達時間
depart: 出發時間
now: 現在出發
+ time: 時間
ModeSelector:
labels:
- rent: 租用
bicycle: 騎行
car: 駕車
+ rent: 租用
transit: 公共交通
walk: 步行
settings:
bikeTolerance-label: 自行車接受度
+ bikeTolerance-labelHigh: 自行車騎乘較多
+ bikeTolerance-labelLow: 自行車騎乘較少
bus-label: 搭乘公車
+ carTolerance-label: 駕車接受度
+ carTolerance-labelHigh: 駕車較多
+ carTolerance-labelLow: 駕車較少
+ ferry-label: 渡船
rail-label: 火車
subway-label: 捷運
tram-label: 電車
- walkTolerance-label: 步行接受度
- wheelchair-label: 可到達路線
- ferry-label: 渡船
walkReluctance-label: 避開步行路線
+ walkTolerance-label: 步行接受度
walkTolerance-labelHigh: 步行較多
walkTolerance-labelLow: 步行較少
- bikeTolerance-labelHigh: 自行車騎乘較多
- bikeTolerance-labelLow: 自行車騎乘較少
- carTolerance-label: 駕車接受度
- carTolerance-labelHigh: 駕車較多
- carTolerance-labelLow: 駕車較少
- settingsLabel: '{mode} 設定'
- TripOptions:
- transitOnly: 公共交通
+ wheelchair-label: 可到達路線
+ settingsLabel: "{mode} 設定"
SettingsSelectorPanel:
bikeOnly: 僅限自行車
- use: 使用
escooterOnly: 僅限電動滑板車
takeTransit: 使用公共交通
travelPreferences: 旅行偏好
+ use: 使用
useCompanies: 使用公司
walkOnly: 僅限步行
+ TripOptions:
+ transitOnly: 公共交通
queryParameters:
- maxBikeDistance: 最大自行車
- distanceInMiles: "{miles, number, :: unit/mile unit-width-full-name}\n"
bikeSpeed: 自行車速度
+ distanceInMiles: |
+ {miles, number, :: unit/mile unit-width-full-name}
+ maxBikeDistance: 最大自行車
maxBikeTime: 最大自行車時間
maxEScooterDistance: 最大電動滑板車距離
maxWalkDistance: 最大步行
@@ -53,7 +54,7 @@ otpUi:
optimizeBikeFriendly: 適合自行車的行程
optimizeBikeSpeed: 速度
optimizeFor: 最佳化
- speedInMilesPerHour: '{mph} MPH'
+ speedInMilesPerHour: "{mph} MPH"
walkReluctance: 避免步行
walkReluctanceHigh: 更多公共交通
walkReluctanceLow: 步行較多
diff --git a/packages/trip-form/package.json b/packages/trip-form/package.json
index 2e584a85d..e1d36b6ac 100644
--- a/packages/trip-form/package.json
+++ b/packages/trip-form/package.json
@@ -11,18 +11,21 @@
"private": false,
"dependencies": {
"@opentripplanner/core-utils": "^11.4.2",
+ "@opentripplanner/building-blocks": "^1.0.3",
"@styled-icons/bootstrap": "^10.34.0",
"@styled-icons/boxicons-regular": "^10.38.0",
"@styled-icons/fa-regular": "^10.37.0",
"@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"
},
"devDependencies": {
"@types/flat": "^5.0.2",
"@opentripplanner/types": "6.5.1"
+
},
"peerDependencies": {
"react": "^16.14.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..bd3a8b4c0
--- /dev/null
+++ b/packages/trip-form/src/MetroModeSelector/AdvancedModeSettingsButton.story.tsx
@@ -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(...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;
+ 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 (
+
+
+
+ );
+};
+
+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" }
+ },
+ 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..335034f9e
--- /dev/null
+++ b/packages/trip-form/src/MetroModeSelector/AdvancedModeSettingsButton/index.tsx
@@ -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 "..";
+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;
+ 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 (
+
+ 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..e8c092b42
--- /dev/null
+++ b/packages/trip-form/src/MetroModeSelector/AdvancedModeSubsettingsContainer.tsx
@@ -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 {
+ 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 (
+
+
+ {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..d4003bd10 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 "./mockButtons.story";
const initialState = {
enabledModeButtons: ["transit"],
diff --git a/packages/trip-form/src/MetroModeSelector/index.tsx b/packages/trip-form/src/MetroModeSelector/index.tsx
index 0eda28358..58fefaab9 100644
--- a/packages/trip-form/src/MetroModeSelector/index.tsx
+++ b/packages/trip-form/src/MetroModeSelector/index.tsx
@@ -5,7 +5,7 @@ import styled, { css } from "styled-components";
import generateModeButtonLabel from "./i18n";
-const invisibleCss = css`
+export const invisibleCss = css`
clip: rect(0, 0, 0, 0);
height: 0;
overflow: hidden;
diff --git a/packages/trip-form/src/MetroModeSelector/mockButtons.story.tsx b/packages/trip-form/src/MetroModeSelector/mockButtons.story.tsx
new file mode 100644
index 000000000..2e146b6b3
--- /dev/null
+++ b/packages/trip-form/src/MetroModeSelector/mockButtons.story.tsx
@@ -0,0 +1,113 @@
+import {
+ Bus,
+ Car,
+ PersonWalking,
+ Train,
+ TrainSubway,
+ TrainTram
+} from "@styled-icons/fa-solid";
+
+import { ClassicBike } from "@opentripplanner/icons/src/classic";
+import React from "react";
+
+export const defaultModeButtonDefinitions = [
+ {
+ 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 = [
+ {
+ 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 7ce5058fd..d88d923bc 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -7101,11 +7101,16 @@ class-utils@^0.3.5:
isobject "^3.0.0"
static-extend "^0.1.1"
-classnames@^2.2.5, classnames@^2.2.6, classnames@^2.3.1:
+classnames@^2.2.5, classnames@^2.2.6:
version "2.3.1"
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e"
integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==
+classnames@^2.3.1:
+ version "2.5.1"
+ resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b"
+ integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==
+
clean-css@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.3.tgz#507b5de7d97b48ee53d84adb0160ff6216380f78"
@@ -15707,7 +15712,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==