diff --git a/.github/workflows/node-ci.yml b/.github/workflows/node-ci.yml index 15dffceb6..79874aecd 100644 --- a/.github/workflows/node-ci.yml +++ b/.github/workflows/node-ci.yml @@ -32,6 +32,8 @@ jobs: run: yarn lint-all || true - name: Run type check run: yarn typecheck + - name: Check i18n messages (en-US and fr) + run: yarn check:i18n-en-fr - name: Run tests run: yarn unit - name: Build example project diff --git a/.github/workflows/percy.yml b/.github/workflows/percy.yml index 5dc6abc37..ffde64290 100644 --- a/.github/workflows/percy.yml +++ b/.github/workflows/percy.yml @@ -8,31 +8,7 @@ on: pull_request: jobs: - run-pixel-tests-with-otp1-real-server: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - with: - # This allows us to work with the repository during the lint step - fetch-depth: 2 - - name: Use Node.js 16.x - uses: actions/setup-node@v1 - with: - node-version: 16.x - - name: Install npm packages using cache - uses: bahmutov/npm-install@v1 - - name: Download OTP1 config file - run: curl $PERCY_OTP1_CONFIG_URL --output /tmp/otp1config.yml - env: - PERCY_OTP1_CONFIG_URL: ${{ secrets.PERCY_OTP1_CONFIG_URL_METRO }} - - name: Take Percy Snapshots - # Move everything from latest commit back to staged - run: npx percy exec -- npx jest percy/percy.test.js --force-exit - env: - PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }} - PERCY_OTP_CONFIG_OVERRIDE: /tmp/otp1config.yml - run-pixel-tests-with-otp2-real-server: + run-pixel-tests-with-otp2-real-server-mobile: runs-on: ubuntu-latest steps: @@ -49,7 +25,7 @@ jobs: - name: Download OTP2 config file run: curl $PERCY_OTP2_CONFIG_URL --output /tmp/otp2config.yml env: - PERCY_OTP2_CONFIG_URL: ${{ secrets.PERCY_OTP2_CONFIG_URL_METRO }} + PERCY_OTP2_CONFIG_URL: ${{ secrets.PERCY_OTP2_CONFIG_URL_METRO_MODE_SELECTOR }} - name: Take Percy Snapshots # Move everything from latest commit back to staged run: npx percy exec -- npx jest percy/percy.test.js --force-exit @@ -74,7 +50,7 @@ jobs: - name: Download OTP2 config file run: curl $PERCY_OTP2_CONFIG_URL --output /tmp/otp2config.yml env: - PERCY_OTP2_CONFIG_URL: ${{ secrets.PERCY_OTP2_CONFIG_URL_METRO }} + PERCY_OTP2_CONFIG_URL: ${{ secrets.PERCY_OTP2_CONFIG_URL_METRO_MODE_SELECTOR }} - name: Take Percy Snapshots # Move everything from latest commit back to staged run: npx percy exec -- npx jest percy/percy.test.js --force-exit diff --git a/README.md b/README.md index 1ab2e1878..ff8e2054d 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,9 @@ +[![Join the chat at https://gitter.im/opentripplanner/otp-react-redux](https://badges.gitter.im/repo.png)](https://gitter.im/opentripplanner/otp-react-redux) +[![Build process badge](https://img.shields.io/github/actions/workflow/status/opentripplanner/otp-react-redux/node-ci.yml)](https://github.com/opentripplanner/otp-react-redux/actions/workflows/node-ci.yml) + A library for writing modern [OpenTripPlanner](http://www.opentripplanner.org/)-compatible multimodal journey planning applications using [React]() and [Redux](). ## Running the Example @@ -119,6 +122,6 @@ If changes to a specific language file is needed but not enabled in Weblate, ple ## Library Documentation -More coming soon... +You can chat with the main OTP-RR developers in our [Gitter chat](https://gitter.im/opentripplanner/otp-react-redux). Support is not guaranteed, but we may be able to answer questions and assist people wishing to make contributions. As of version 2.0, otp-react-redux utilizes React's context API in a number of components. This changed the way that some components receive props such that they will not work properly unless wrapped with the context provider used in the `ResponsiveWebapp` component. diff --git a/__tests__/actions/__snapshots__/api.js.snap b/__tests__/actions/__snapshots__/api.js.snap index c8b742c84..1082730fd 100644 --- a/__tests__/actions/__snapshots__/api.js.snap +++ b/__tests__/actions/__snapshots__/api.js.snap @@ -5,62 +5,9 @@ Array [ Array [ [Function], ], - Array [ - Object { - "payload": Object { - "activeItinerary": 0, - "pending": 1, - "routingType": "ITINERARY", - "searchId": "abcd1234", - "updateSearchInReducer": false, - }, - "type": "ROUTING_REQUEST", - }, - ], - Array [ - [Function], - ], - Array [ - Object { - "payload": Object { - "requestId": "abcd1237", - "response": Object { - "fake": "response", - }, - "searchId": "abcd1234", - }, - "type": "ROUTING_RESPONSE", - }, - ], - Array [ - [Function], - ], - Array [ - Object { - "payload": Object { - "activeItinerary": 0, - "pending": 1, - "routingType": "ITINERARY", - "searchId": "abcd1236", - "updateSearchInReducer": false, - }, - "type": "ROUTING_REQUEST", - }, - ], Array [ [Function], ], - Array [ - Object { - "payload": Object { - "error": [Error: Received error from server], - "requestId": "abcd1238", - "searchId": "abcd1236", - "url": "http://mock-host.com:80/api/plan?fromPlace=%2812%2C34%29%3A%3A12%2C34&toPlace=%2834%2C12%29%3A%3A34%2C12&mode=WALK%2CTRANSIT&ignoreRealtimeUpdates=false&batchId=abcd1236", - }, - "type": "ROUTING_ERROR", - }, - ], ] `; @@ -69,34 +16,5 @@ Array [ Array [ [Function], ], - Array [ - Object { - "payload": Object { - "activeItinerary": 0, - "pending": 1, - "routingType": "ITINERARY", - "searchId": "abcd1234", - "updateSearchInReducer": false, - }, - "type": "ROUTING_REQUEST", - }, - ], - Array [ - [Function], - ], - Array [ - Object { - "payload": Object { - "requestId": "abcd1235", - "response": Object { - "fake": "response", - }, - "searchId": "abcd1234", - }, - "type": "ROUTING_RESPONSE", - }, - ], ] `; - -exports[`actions > api routingQuery should make a query to OTP: OTP Query Path 1`] = `"/api/plan?fromPlace=%2812%2C34%29%3A%3A12%2C34&toPlace=%2834%2C12%29%3A%3A34%2C12&mode=WALK%2CTRANSIT&ignoreRealtimeUpdates=false&batchId=abcd1234"`; diff --git a/__tests__/actions/form.ts b/__tests__/actions/form.ts new file mode 100644 index 000000000..8140c437e --- /dev/null +++ b/__tests__/actions/form.ts @@ -0,0 +1,25 @@ +import '../test-utils/mock-window-url' +import { checkShouldReplanTrip } from '../../lib/actions/form' + +describe('actions > form', () => { + describe('checkShouldReplanTrip', () => { + it('should not replan trip on mobile (with default autoPlan settings) if both locations change from null', () => { + const autoPlan = { + default: 'ONE_LOCATION_CHANGED', + mobile: 'BOTH_LOCATIONS_CHANGED' + } + const oldQuery = { + from: null, + to: null + } + const newQuery = { + from: { name: 'From place' }, + to: { name: 'To place' } + } + expect( + checkShouldReplanTrip(autoPlan, true, oldQuery, newQuery) + .shouldReplanTrip + ).toBe(null) + }) + }) +}) diff --git a/__tests__/components/__snapshots__/date-time-options.js.snap b/__tests__/components/__snapshots__/date-time-options.js.snap index 620284217..b6c3fb84f 100644 --- a/__tests__/components/__snapshots__/date-time-options.js.snap +++ b/__tests__/components/__snapshots__/date-time-options.js.snap @@ -90,10 +90,10 @@ exports[`components > form > call-taker > date time options should correctly han "lineHeight": ".8em", "marginLeft": "3px", "padding": "0px", - "width": "50px", + "width": "65px", } } - value="12a" + value="12:00 AM" /> form > call-taker > date time options should correctly han "lineHeight": ".8em", "marginLeft": "3px", "padding": "0px", - "width": "50px", + "width": "65px", } } - value="12p" + value="12:00 PM" /> form > call-taker > date time options should correctly han "lineHeight": ".8em", "marginLeft": "3px", "padding": "0px", - "width": "50px", + "width": "65px", } } - value="133" + value="1:03 PM" /> form > call-taker > date time options should correctly han "lineHeight": ".8em", "marginLeft": "3px", "padding": "0px", - "width": "50px", + "width": "65px", } } - value="133p" + value="1:33 PM" /> form > call-taker > date time options should correctly han "lineHeight": ".8em", "marginLeft": "3px", "padding": "0px", - "width": "50px", + "width": "65px", } } - value="135p" + value="1:35 PM" /> form > call-taker > date time options should correctly han "lineHeight": ".8em", "marginLeft": "3px", "padding": "0px", - "width": "50px", + "width": "65px", } } - value="1335" + value="1:35 PM" /> form > call-taker > date time options should render 1`] = "lineHeight": ".8em", "marginLeft": "3px", "padding": "0px", - "width": "50px", + "width": "65px", } } - value="12:34" + value="12:34 PM" /> viewers > stop viewer should render countdown times after > viewers > stop viewer should render countdown times after iconViewBox="0 0 448 512" > viewers > stop viewer should render countdown times after > viewers > stop viewer should render countdown times after iconViewBox="0 0 512 512" > viewers > stop viewer should render countdown times after > viewers > stop viewer should render countdown times after iconViewBox="0 0 448 512" > viewers > stop viewer should render countdown times after >
viewers > stop viewer should render countdown times after
- - - - - - 20 - - - - - + 20 + + + + @@ -1674,7 +1681,7 @@ exports[`components > viewers > stop viewer should render countdown times after title="components.StopTimeCell.scheduled" > viewers > stop viewer should render countdown times after iconViewBox="0 0 512 512" > viewers > stop viewer should render countdown times after > viewers > stop viewer should render countdown times after iconViewBox="0 0 512 512" > viewers > stop viewer should render countdown times after > viewers > stop viewer should render countdown times after title="components.StopTimeCell.scheduled" > viewers > stop viewer should render countdown times after iconViewBox="0 0 512 512" > viewers > stop viewer should render countdown times after > viewers > stop viewer should render countdown times after margin={0.25} > viewers > stop viewer should render countdown times after spin={true} > viewers > stop viewer should render countdown times after iconViewBox="0 0 512 512" > viewers > stop viewer should render countdown times after >
P
@@ -3262,7 +3269,7 @@ exports[`components > viewers > stop viewer should render countdown times for st > viewers > stop viewer should render countdown times for st iconViewBox="0 0 448 512" > viewers > stop viewer should render countdown times for st > viewers > stop viewer should render countdown times for st iconViewBox="0 0 512 512" > viewers > stop viewer should render countdown times for st > viewers > stop viewer should render countdown times for st iconViewBox="0 0 448 512" > viewers > stop viewer should render countdown times for st >
viewers > stop viewer should render countdown times for st
- - - - - - 20 - - - - - + 20 + + + + @@ -4181,7 +4195,7 @@ exports[`components > viewers > stop viewer should render countdown times for st title="components.StopTimeCell.scheduled" > viewers > stop viewer should render countdown times for st iconViewBox="0 0 512 512" > viewers > stop viewer should render countdown times for st > viewers > stop viewer should render countdown times for st spin={true} > viewers > stop viewer should render countdown times for st iconViewBox="0 0 512 512" > viewers > stop viewer should render countdown times for st >
P
@@ -5186,7 +5200,7 @@ exports[`components > viewers > stop viewer should render times after midnight w > viewers > stop viewer should render times after midnight w iconViewBox="0 0 448 512" > viewers > stop viewer should render times after midnight w > viewers > stop viewer should render times after midnight w iconViewBox="0 0 512 512" > viewers > stop viewer should render times after midnight w > viewers > stop viewer should render times after midnight w iconViewBox="0 0 448 512" > viewers > stop viewer should render times after midnight w >
viewers > stop viewer should render times after midnight w
- - - - - - 20 - - - - - + 20 + + + + @@ -6483,7 +6504,7 @@ exports[`components > viewers > stop viewer should render times after midnight w title="components.StopTimeCell.scheduled" > viewers > stop viewer should render times after midnight w iconViewBox="0 0 512 512" > viewers > stop viewer should render times after midnight w > viewers > stop viewer should render times after midnight w title="components.StopTimeCell.scheduled" > viewers > stop viewer should render times after midnight w iconViewBox="0 0 512 512" > viewers > stop viewer should render times after midnight w > viewers > stop viewer should render times after midnight w title="components.StopTimeCell.scheduled" > viewers > stop viewer should render times after midnight w iconViewBox="0 0 512 512" > viewers > stop viewer should render times after midnight w > viewers > stop viewer should render times after midnight w margin={0.25} > viewers > stop viewer should render times after midnight w spin={true} > viewers > stop viewer should render times after midnight w iconViewBox="0 0 512 512" > viewers > stop viewer should render times after midnight w >
P
@@ -8457,7 +8478,7 @@ exports[`components > viewers > stop viewer should render with OTP transit index > viewers > stop viewer should render with OTP transit index iconViewBox="0 0 448 512" > viewers > stop viewer should render with OTP transit index > viewers > stop viewer should render with OTP transit index iconViewBox="0 0 512 512" > viewers > stop viewer should render with OTP transit index > viewers > stop viewer should render with OTP transit index iconViewBox="0 0 448 512" > viewers > stop viewer should render with OTP transit index >
viewers > stop viewer should render with OTP transit index
- - - - - - 20 - - - - - + 20 + + + + @@ -10266,7 +10294,7 @@ exports[`components > viewers > stop viewer should render with OTP transit index title="components.StopTimeCell.scheduled" > viewers > stop viewer should render with OTP transit index iconViewBox="0 0 512 512" > viewers > stop viewer should render with OTP transit index > viewers > stop viewer should render with OTP transit index title="components.StopTimeCell.scheduled" > viewers > stop viewer should render with OTP transit index iconViewBox="0 0 512 512" > viewers > stop viewer should render with OTP transit index > viewers > stop viewer should render with OTP transit index title="components.StopTimeCell.scheduled" > viewers > stop viewer should render with OTP transit index iconViewBox="0 0 512 512" > viewers > stop viewer should render with OTP transit index > viewers > stop viewer should render with OTP transit index
- - - - - - 94 - - - - - + 94 + + + + @@ -11098,7 +11133,7 @@ exports[`components > viewers > stop viewer should render with OTP transit index title="components.StopTimeCell.scheduled" > viewers > stop viewer should render with OTP transit index iconViewBox="0 0 512 512" > viewers > stop viewer should render with OTP transit index > viewers > stop viewer should render with OTP transit index title="components.StopTimeCell.scheduled" > viewers > stop viewer should render with OTP transit index iconViewBox="0 0 512 512" > viewers > stop viewer should render with OTP transit index > viewers > stop viewer should render with OTP transit index title="components.StopTimeCell.scheduled" > viewers > stop viewer should render with OTP transit index iconViewBox="0 0 512 512" > viewers > stop viewer should render with OTP transit index > viewers > stop viewer should render with OTP transit index
- - - - - - 94 - - - - - + 94 + + + + @@ -11808,7 +11850,7 @@ exports[`components > viewers > stop viewer should render with OTP transit index title="components.StopTimeCell.scheduled" > viewers > stop viewer should render with OTP transit index iconViewBox="0 0 512 512" > viewers > stop viewer should render with OTP transit index > viewers > stop viewer should render with OTP transit index title="components.StopTimeCell.scheduled" > viewers > stop viewer should render with OTP transit index iconViewBox="0 0 512 512" > viewers > stop viewer should render with OTP transit index > viewers > stop viewer should render with OTP transit index title="components.StopTimeCell.scheduled" > viewers > stop viewer should render with OTP transit index iconViewBox="0 0 512 512" > viewers > stop viewer should render with OTP transit index > viewers > stop viewer should render with OTP transit index
- - - - - - 36 - - - - - + 36 + + + + @@ -12518,7 +12567,7 @@ exports[`components > viewers > stop viewer should render with OTP transit index title="components.StopTimeCell.scheduled" > viewers > stop viewer should render with OTP transit index iconViewBox="0 0 512 512" > viewers > stop viewer should render with OTP transit index > viewers > stop viewer should render with OTP transit index title="components.StopTimeCell.scheduled" > viewers > stop viewer should render with OTP transit index iconViewBox="0 0 512 512" > viewers > stop viewer should render with OTP transit index > viewers > stop viewer should render with OTP transit index title="components.StopTimeCell.scheduled" > viewers > stop viewer should render with OTP transit index iconViewBox="0 0 512 512" > viewers > stop viewer should render with OTP transit index > viewers > stop viewer should render with OTP transit index margin={0.25} > viewers > stop viewer should render with OTP transit index spin={true} > viewers > stop viewer should render with OTP transit index iconViewBox="0 0 512 512" > viewers > stop viewer should render with OTP transit index >
P
@@ -15519,7 +15568,7 @@ exports[`components > viewers > stop viewer should render with TriMet transit in > viewers > stop viewer should render with TriMet transit in iconViewBox="0 0 448 512" > viewers > stop viewer should render with TriMet transit in > viewers > stop viewer should render with TriMet transit in iconViewBox="0 0 512 512" > viewers > stop viewer should render with TriMet transit in > viewers > stop viewer should render with TriMet transit in iconViewBox="0 0 448 512" > viewers > stop viewer should render with TriMet transit in >
viewers > stop viewer should render with TriMet transit in
- - - - - - 20 - - - - - + 20 + + + + @@ -17322,7 +17378,7 @@ exports[`components > viewers > stop viewer should render with TriMet transit in title="components.StopTimeCell.scheduled" > viewers > stop viewer should render with TriMet transit in iconViewBox="0 0 512 512" > viewers > stop viewer should render with TriMet transit in > viewers > stop viewer should render with TriMet transit in title="components.StopTimeCell.scheduled" > viewers > stop viewer should render with TriMet transit in iconViewBox="0 0 512 512" > viewers > stop viewer should render with TriMet transit in > viewers > stop viewer should render with TriMet transit in title="components.StopTimeCell.scheduled" > viewers > stop viewer should render with TriMet transit in iconViewBox="0 0 512 512" > viewers > stop viewer should render with TriMet transit in > viewers > stop viewer should render with TriMet transit in margin={0.25} > viewers > stop viewer should render with TriMet transit in spin={true} > viewers > stop viewer should render with TriMet transit in iconViewBox="0 0 512 512" > viewers > stop viewer should render with TriMet transit in >
P
@@ -19885,7 +19941,7 @@ exports[`components > viewers > stop viewer should render with initial stop id a > viewers > stop viewer should render with initial stop id a iconViewBox="0 0 448 512" > viewers > stop viewer should render with initial stop id a >
+ + +
{mailable.largePrint && (
-
@@ -107,7 +124,7 @@ class MailablesWindow extends Component { _addMailable = (mailable) => { if (!this.state.mailables.find((m) => m.name === mailable.name)) { const mailables = [...this.state.mailables] - mailables.push({ ...mailable, quantity: 1 }) + mailables.push({ ...mailable, quantity: 1, smallFormat: true }) this.setState({ mailables }) } } diff --git a/lib/components/app/app-menu-item.tsx b/lib/components/app/app-menu-item.tsx index bab4f3632..f3a8e533b 100644 --- a/lib/components/app/app-menu-item.tsx +++ b/lib/components/app/app-menu-item.tsx @@ -75,6 +75,7 @@ export default class AppMenuItem extends Component { void + skipLocales?: boolean + subMenuDivider: boolean +} type AppMenuProps = { activeLocale: string callTakerEnabled?: boolean - extraMenuItems?: menuItem[] + extraMenuItems?: MenuItem[] fieldTripEnabled?: boolean - // Typescript TODO language options based on configLanguage. + // Typescript TODO language and language options based on configLanguage. + language: Record | null languageOptions: Record | null location: { search: string } mailablesEnabled?: boolean @@ -46,18 +62,6 @@ type AppMenuProps = { type AppMenuState = { isPaneOpen: boolean } -type menuItem = { - children?: menuItem[] - href?: string - iconType: string | JSX.Element - iconUrl?: string - id: string - isSelected?: boolean - label: string | JSX.Element - lang?: string - onClick?: () => void - subMenuDivider: boolean -} /** * Sidebar which appears to show user list of options and links @@ -86,7 +90,6 @@ class AppMenu extends Component< _triggerPopup = () => { const { popupTarget, setPopupContent } = this.props setPopupContent(popupTarget) - this._togglePane() } _togglePane = () => { @@ -103,7 +106,7 @@ class AppMenu extends Component< document.querySelector('main')?.focus() } - _addExtraMenuItems = (menuItems?: menuItem[] | null) => { + _addExtraMenuItems = (menuItems?: MenuItem[] | null) => { return ( menuItems && menuItems.map((menuItem) => { @@ -117,18 +120,14 @@ class AppMenu extends Component< label: configLabel, lang, onClick, + skipLocales, subMenuDivider } = menuItem - const { intl } = this.props - const localizationId = `config.menuItems.${id}` - const localizedLabel = intl.formatMessage({ - // Add the string id as the default message to limit error messages. - defaultMessage: localizationId, - id: localizationId - }) + const { activeLocale, language } = this.props + const localizedLabel = language?.[activeLocale]?.config?.menuItems?.[id] + const useLocalizedLabel = !skipLocales && localizedLabel // Override the config label if a localized label exists - const label = - localizedLabel === localizationId ? configLabel : localizedLabel + const label = useLocalizedLabel ? localizedLabel : configLabel return ( ({ iconType: , @@ -179,11 +178,13 @@ class AppMenu extends Component< label: languageOptions[locale].name, lang: locale, onClick: () => setLocale(locale), + skipLocales: true, subMenuDivider: false })), iconType: , id: 'app-menu-locale-selector', label: , + skipLocales: true, subMenuDivider: false } ] @@ -252,7 +253,7 @@ class AppMenu extends Component< } onClick={this._triggerPopup} - text={} + text={} /> )} {callTakerEnabled && ( @@ -296,6 +297,7 @@ const mapStateToProps = (state: Record) => { callTakerEnabled: isModuleEnabled(state, Modules.CALL_TAKER), extraMenuItems, fieldTripEnabled: isModuleEnabled(state, Modules.FIELD_TRIP), + language, languageOptions: getLanguageOptions(language), mailablesEnabled: isModuleEnabled(state, Modules.MAILABLES), popupTarget: state.otp.config?.popups?.launchers?.sidebarLink @@ -318,7 +320,7 @@ export default injectIntl( /** * Renders a label and icon either from url or font awesome type */ -const Icon = ({ +export const Icon = ({ iconType, iconUrl }: { diff --git a/lib/components/app/app.css b/lib/components/app/app.css index a052d4600..80eceea03 100644 --- a/lib/components/app/app.css +++ b/lib/components/app/app.css @@ -92,8 +92,8 @@ /* Full screen modal styling */ .fullscreen-modal { - width: 90vw; - height: 100vh; + width: 75vw; + height: 60vh; } .fullscreen-modal .modal-content { height: 90vh; diff --git a/lib/components/app/batch-routing-panel.js b/lib/components/app/batch-routing-panel.tsx similarity index 77% rename from lib/components/app/batch-routing-panel.js rename to lib/components/app/batch-routing-panel.tsx index a87d91e5b..9115ecbb3 100644 --- a/lib/components/app/batch-routing-panel.js +++ b/lib/components/app/batch-routing-panel.tsx @@ -1,7 +1,6 @@ -/* eslint-disable react/prop-types */ import { connect } from 'react-redux' -import { FormattedMessage, injectIntl } from 'react-intl' -import React, { Component } from 'react' +import { FormattedMessage, injectIntl, IntlShape } from 'react-intl' +import React, { Component, FormEvent } from 'react' import { getActiveSearch, getShowUserSettings } from '../../util/state' import BatchSettings from '../form/batch-settings' @@ -12,14 +11,30 @@ import SwitchButton from '../form/switch-button' import UserSettings from '../form/user-settings' import ViewerContainer from '../viewers/viewer-container' +interface Props { + activeSearch: any + intl: IntlShape + mobile?: boolean + showUserSettings: boolean +} + /** * Main panel for the batch/trip comparison form. */ -class BatchRoutingPanel extends Component { - handleSubmit = (e) => e.preventDefault() +class BatchRoutingPanel extends Component { + state = { + planTripClicked: false + } + + handleSubmit = (e: FormEvent) => e.preventDefault() + + handlePlanTripClick = () => { + this.setState({ planTripClicked: true }) + } render() { const { activeSearch, intl, mobile, showUserSettings } = this.props + const { planTripClicked } = this.state const mapAction = mobile ? intl.formatMessage({ id: 'common.searchForms.tap' @@ -53,22 +68,26 @@ class BatchRoutingPanel extends Component { { id: 'common.searchForms.enterStartLocation' }, { mapAction } )} + isRequired locationType="from" - showClearButton + selfValidate={planTripClicked} + showClearButton={!mobile} />
- + {!activeSearch && showUserSettings && ( @@ -88,7 +107,7 @@ class BatchRoutingPanel extends Component { } // connect to the redux store -const mapStateToProps = (state) => { +const mapStateToProps = (state: any) => { const showUserSettings = getShowUserSettings(state) return { activeSearch: getActiveSearch(state), diff --git a/lib/components/app/call-taker-panel.js b/lib/components/app/call-taker-panel.js index a36279d1b..74f5b8c23 100644 --- a/lib/components/app/call-taker-panel.js +++ b/lib/components/app/call-taker-panel.js @@ -13,13 +13,15 @@ import * as apiActions from '../../actions/api' import * as callTakerActions from '../../actions/call-taker' import * as fieldTripActions from '../../actions/field-trip' import * as formActions from '../../actions/form' +import { defaultDropdownConfig, getGroupSize } from '../../util/call-taker' +import { generateModeSettingValues } from '../../util/api' import { getActiveSearch, getShowUserSettings, getSortedFilteredRoutes, hasValidLocation } from '../../util/state' -import { getGroupSize } from '../../util/call-taker' +import { getModuleConfig, Modules } from '../../util/config' import { StyledIconWrapper } from '../util/styledIcon' import AddPlaceButton from '../form/add-place-button' import AdvancedOptions from '../form/call-taker/advanced-options' @@ -47,8 +49,9 @@ class CallTakerPanel extends Component { constructor(props) { super(props) this.state = { + enabledModes: props.modeDropdownOptions?.[0].combination, expandAdvanced: props.expandAdvanced, - transitModes: props.modes.transitModes.map((m) => m.mode) + planTripClicked: false } } @@ -57,6 +60,7 @@ class CallTakerPanel extends Component { const issues = [] if (!hasValidLocation(currentQuery, 'from')) issues.push('from') if (!hasValidLocation(currentQuery, 'to')) issues.push('to') + this.setState({ planTripClicked: true }) if (issues.length > 0) { // TODO: replace with less obtrusive validation. window.alert( @@ -69,6 +73,7 @@ class CallTakerPanel extends Component { if (this.state.expandAdvanced) this.setState({ expandAdvanced: false }) // Ensure call is started, or start a new one. beginCallIfNeeded() + routingQuery() } @@ -118,8 +123,17 @@ class CallTakerPanel extends Component { } } + _handleUpdateModes = (modes) => { + this.setState({ enabledModes: modes }) + this.props.setQueryParam({ modes }) + } + _updateGroupSize = (evt) => this.props.setGroupSize(+evt.target.value) + componentDidMount() { + this.props.setQueryParam({ modes: this.state.enabledModes }) + } + render() { const { activeSearch, @@ -132,6 +146,7 @@ class CallTakerPanel extends Component { modes, routes, setQueryParam, + setUrlSearch, showUserSettings, timeFormat, usingOtp2 @@ -140,17 +155,11 @@ class CallTakerPanel extends Component { const showPlanTripButton = mainPanelContent === 'EDIT_DATETIME' || mainPanelContent === 'EDIT_SETTINGS' - const { date, departArrive, from, intermediatePlaces, mode, time, to } = + const { date, departArrive, from, intermediatePlaces, time, to } = currentQuery - const { expandAdvanced } = this.state + const { expandAdvanced, planTripClicked } = this.state const advancedSearchStyle = { - background: 'white', - display: expandAdvanced ? undefined : 'none', - left: '0px', - padding: '0px 8px 5px', - position: 'absolute', - right: '0px', - zIndex: 99999 + display: expandAdvanced ? undefined : 'none' } const mapAction = mobile ? intl.formatMessage({ @@ -175,7 +184,9 @@ class CallTakerPanel extends Component { { id: 'common.searchForms.enterStartLocation' }, { mapAction } )} + isRequired locationType="from" + selfValidate={planTripClicked} showClearButton /> {Array.isArray(intermediatePlaces) && @@ -192,7 +203,7 @@ class CallTakerPanel extends Component { locationType="to" onLocationCleared={this._removePlace} // FIXME: function def - onLocationSelected={(_, result) => this._addPlace(result, i)} + onLocationSelected={(result) => this._addPlace(result, i)} showClearButton={!mobile} /> ) @@ -202,7 +213,9 @@ class CallTakerPanel extends Component { { id: 'common.searchForms.enterDestination' }, { mapAction } )} + isRequired locationType="to" + selfValidate={planTripClicked} showClearButton={!mobile} />
@@ -224,14 +237,8 @@ class CallTakerPanel extends Component { timeFormat={timeFormat} /> { - this.setState({ transitModes }) - }} />
@@ -278,10 +285,11 @@ class CallTakerPanel extends Component { currentQuery={currentQuery} findRoutesIfNeeded={this.props.findRoutesIfNeeded} modes={modes} + modeSettingValues={this.props.modeSettingValues} onKeyDown={this._handleFormKeyDown} routes={routes} setQueryParam={setQueryParam} - usingOtp2={usingOtp2} + setUrlSearch={setUrlSearch} />
@@ -308,13 +316,23 @@ const mapStateToProps = (state) => { const { activeId, requests } = state.callTaker.fieldTrip const request = requests.data.find((req) => req.id === activeId) const showUserSettings = getShowUserSettings(state) + const moduleConfig = getModuleConfig(state, Modules.CALL_TAKER)?.options + const urlSearchParams = new URLSearchParams(state.router.location.search) + const modeSettingValues = generateModeSettingValues( + urlSearchParams, + state.otp?.modeSettingDefinitions || [], + state.otp.config.modes?.initialState?.modeSettingValues + ) return { activeSearch: getActiveSearch(state), currentQuery: state.otp.currentQuery, groupSize: state.callTaker.fieldTrip.groupSize, mainPanelContent: state.otp.ui.mainPanelContent, maxGroupSize: getGroupSize(request), + modeDropdownOptions: + moduleConfig?.modeDropdownOptions || defaultDropdownConfig, modes: state.otp.config.modes, + modeSettingValues, routes: getSortedFilteredRoutes(state), showUserSettings, timeFormat: getTimeFormat(state.otp.config), @@ -329,7 +347,8 @@ const mapDispatchToProps = { findRoutesIfNeeded: apiActions.findRoutesIfNeeded, routingQuery: apiActions.routingQuery, setGroupSize: fieldTripActions.setGroupSize, - setQueryParam: formActions.setQueryParam + setQueryParam: formActions.setQueryParam, + setUrlSearch: apiActions.setUrlSearch } export default connect( diff --git a/lib/components/app/desktop-nav.tsx b/lib/components/app/desktop-nav.tsx index b5c549144..bfe583c1f 100644 --- a/lib/components/app/desktop-nav.tsx +++ b/lib/components/app/desktop-nav.tsx @@ -1,26 +1,45 @@ import { connect } from 'react-redux' -import { FormattedMessage } from 'react-intl' -import { Nav, Navbar, NavItem } from 'react-bootstrap' +import { Nav, Navbar } from 'react-bootstrap' +import { useIntl } from 'react-intl' import React from 'react' import styled from 'styled-components' import * as uiActions from '../../actions/ui' import { accountLinks, getAuth0Config } from '../../util/auth' import { DEFAULT_APP_TITLE } from '../../util/constants' +import InvisibleA11yLabel from '../util/invisible-a11y-label' import NavLoginButtonAuth0 from '../user/nav-login-button-auth0' -import AppMenu from './app-menu' +import AppMenu, { Icon } from './app-menu' import LocaleSelector from './locale-selector' +import NavbarItem from './nav-item' import ViewSwitcher from './view-switcher' -const NavItemOnLargeScreens = styled(NavItem)` +const StyledNav = styled(Nav)` + /* Almost override bootstrap's margin-right: -15px */ + margin-right: -5px; + /* Target only the svgs in the Navbar */ + & > li > button > svg, + & > li > span > button > span > svg { + height: 18px; + } + + & .caret { + margin-left: 5px; + margin-right: -10px; + } +` + +const NavItemOnLargeScreens = styled(NavbarItem)` display: block; @media (max-width: 768px) { display: none !important; } ` + // Typscript TODO: otpConfig type export type Props = { + locale: string otpConfig: any popupTarget?: string setPopupContent: (url: string) => void @@ -39,10 +58,42 @@ export type Props = { * * TODO: merge with the mobile navigation bar. */ -const DesktopNav = ({ otpConfig, popupTarget, setPopupContent }: Props) => { - const { branding, persistence, title = DEFAULT_APP_TITLE } = otpConfig +const DesktopNav = ({ + locale, + otpConfig, + popupTarget, + setPopupContent +}: Props) => { + const { + brandClickable, + branding, + extraMenuItems, + persistence, + title = DEFAULT_APP_TITLE + } = otpConfig + const intl = useIntl() const showLogin = Boolean(getAuth0Config(persistence)) + const BrandingElement = brandClickable ? 'a' : 'div' + + const commonStyles = { marginLeft: 50 } + const brandingProps = brandClickable + ? { + href: '/#/', + style: { + ...commonStyles, + display: 'block', + position: 'relative', + zIndex: 10 + } + } + : { style: { ...commonStyles } } + const popupButtonText = + popupTarget && + intl.formatMessage({ + id: `config.popups.${popupTarget}` + }) + return (
@@ -51,35 +102,40 @@ const DesktopNav = ({ otpConfig, popupTarget, setPopupContent }: Props) => { > -
{/* A title is always rendered (e.g.for screen readers) but is visually-hidden if a branding icon is used. */}
{title}
-
+
- +
@@ -90,6 +146,7 @@ const DesktopNav = ({ otpConfig, popupTarget, setPopupContent }: Props) => { // Typescript TODO: state type const mapStateToProps = (state: any) => { return { + locale: state.otp.ui.locale, otpConfig: state.otp.config, popupTarget: state.otp.config?.popups?.launchers?.toolbar } diff --git a/lib/components/app/locale-selector.tsx b/lib/components/app/locale-selector.tsx index 39e71051d..a3681fe86 100644 --- a/lib/components/app/locale-selector.tsx +++ b/lib/components/app/locale-selector.tsx @@ -4,9 +4,9 @@ import { useIntl } from 'react-intl' import React from 'react' import * as uiActions from '../../actions/ui' +import { Dropdown } from '../util/dropdown' import { getLanguageOptions } from '../../util/i18n' import { UnstyledButton } from '../util/unstyled-button' -import Dropdown from '../util/dropdown' interface LocaleSelectorProps { // Typescript TODO languageOptions based on configLanguage type. @@ -21,34 +21,27 @@ const LocaleSelector = (props: LocaleSelectorProps): JSX.Element | null => { // Only render if two or more languages are configured. return languageOptions ? ( - - - - } - style={{ display: 'block ruby' }} - // TODO: How to make this work without block ruby? - > - {Object.keys(languageOptions).map((locale: string) => ( -
  • - setLocale(locale)} - role="option" - > - {languageOptions[locale].name} - -
  • - ))} -
    +
  • + } + style={{ display: 'block ruby' }} + > + {Object.keys(languageOptions).map((locale: string) => ( +
  • + setLocale(locale)} + role="option" + > + {languageOptions[locale].name} + +
  • + ))} + + ) : null } diff --git a/lib/components/app/nav-item.tsx b/lib/components/app/nav-item.tsx new file mode 100644 index 000000000..f70a4cc97 --- /dev/null +++ b/lib/components/app/nav-item.tsx @@ -0,0 +1,51 @@ +import React from 'react' +import styled from 'styled-components' + +type Props = { + children: React.ReactNode + className?: string + onClick: () => void + title: string | undefined +} + +export const NavbarButton = styled.button` + background: transparent; + border: none; + color: white; + display: block; + float: right; + line-height: 20px; + padding: 15px; + transition: all 0.1s ease-in-out; + + @media (max-width: 768px) { + padding: 10px; + } + + &:hover, + &[aria-expanded='true'] { + background: rgba(0, 0, 0, 0.05); + color: #ddd; + cursor: pointer; + } + &.active { + background: rgba(0, 0, 0, 0.05); + } +` + +const NavbarItem = ({ + children, + className, + onClick, + title +}: Props): JSX.Element => { + return ( +
  • + + {children} + +
  • + ) +} + +export default NavbarItem diff --git a/lib/components/app/popup-trigger-text.tsx b/lib/components/app/popup-trigger-text.tsx new file mode 100644 index 000000000..feb4b9edd --- /dev/null +++ b/lib/components/app/popup-trigger-text.tsx @@ -0,0 +1,43 @@ +import { FormattedMessage, useIntl } from 'react-intl' +import coreUtils from '@opentripplanner/core-utils' +import React from 'react' + +import InvisibleA11yLabel from '../util/invisible-a11y-label' + +interface Props { + compact?: boolean + popupTarget: string +} + +const isMobile = coreUtils.ui.isMobile() + +/** + * Renders the text for the button that triggers the survey/other popup. + * Includes "Opens in a new window" a11y indication in mobile view. + */ +const PopupTriggerText = ({ compact, popupTarget }: Props): JSX.Element => { + const intl = useIntl() + const normalText = intl.formatMessage({ + id: `config.popups.${popupTarget}` + }) + + return ( + <> + {compact ? ( + + ) : ( + normalText + )} + {isMobile && ( + + + + )} + + ) +} + +export default PopupTriggerText diff --git a/lib/components/app/popup.tsx b/lib/components/app/popup.tsx index f7f2b089c..d506ea5c1 100644 --- a/lib/components/app/popup.tsx +++ b/lib/components/app/popup.tsx @@ -1,8 +1,11 @@ import { Modal } from 'react-bootstrap' +import { Times } from '@styled-icons/fa-solid' import { useIntl } from 'react-intl' import coreUtils from '@opentripplanner/core-utils' -import React, { useEffect } from 'react' +import React, { useCallback, useEffect } from 'react' +import styled from 'styled-components' +import { StyledIconWrapper } from '../util/styledIcon' import PageTitle from '../util/page-title' type Props = { @@ -15,6 +18,14 @@ type Props = { hideModal: () => void } +const CloseModalButton = styled.button` + &.close-button { + padding: 0.25em; + right: 0.75em; + top: 0.5em; + } +` + const isMobile = coreUtils.ui.isMobile() /** @@ -29,6 +40,8 @@ const PopupWrapper = ({ content, hideModal }: Props): JSX.Element | null => { const { appendLocale, id, modal, url } = content || {} + const closeText = intl.formatMessage({ id: 'common.forms.close' }) + const useIframe = !isMobile && modal const shown = !!url @@ -44,12 +57,46 @@ const PopupWrapper = ({ content, hideModal }: Props): JSX.Element | null => { } }, [compiledUrl, hideModal, useIframe, shown]) - if (!compiledUrl || !useIframe) return null + const title = id ? intl.formatMessage({ id: `config.popups.${id}` }) : '' + + /* HACK: Since Bootstrap 3.x does not support adding id or name to navItem, + we have to grab a list of all navItems by className and find the correct button. + Since the sliding pane "Leave Feedback" button will always be in the DOM after + the navbar button, reverse + find should always find the correct button to return to. */ - const title = intl.formatMessage({ id: `config.popups.${id}` }) + // TODO: Replace this method with refs + + const navItemList = Array.from( + document.getElementsByClassName('navItem') + ).reverse() as HTMLElement[] + const focusElement = navItemList.find((el) => el.innerText === title) + + const closeModal = useCallback(() => { + hideModal() + focusElement?.focus() + }, [focusElement, hideModal]) + + if (!compiledUrl || !useIframe) return null return ( - + + + + + +