diff --git a/config/rollup.config.js b/config/rollup.config.js
index f3c210bed1..723a7a77cb 100644
--- a/config/rollup.config.js
+++ b/config/rollup.config.js
@@ -1,7 +1,7 @@
import path from 'path';
import fs from 'fs';
import autoprefixer from 'autoprefixer';
-import commonjs from 'rollup-plugin-commonjs';
+import commonjs from '@rollup/plugin-commonjs';
import { nodeResolve } from '@rollup/plugin-node-resolve';
import postcss from 'rollup-plugin-postcss';
import babel from 'rollup-plugin-babel';
@@ -17,6 +17,7 @@ const globals = {
'react-dom': 'ReactDOM',
classnames: 'cx',
'prop-types': 'PropTypes',
+ 'react-is': 'react-is',
i18next: 'i18next',
'react-autosuggest': 'Autosuggest',
'react-sortablejs': 'reactSortablejs',
diff --git a/digitransit-component/packages/digitransit-component-autosuggest-panel/package.json b/digitransit-component/packages/digitransit-component-autosuggest-panel/package.json
index f9b5f19b84..92b6a33c79 100644
--- a/digitransit-component/packages/digitransit-component-autosuggest-panel/package.json
+++ b/digitransit-component/packages/digitransit-component-autosuggest-panel/package.json
@@ -1,6 +1,6 @@
{
"name": "@digitransit-component/digitransit-component-autosuggest-panel",
- "version": "7.0.4",
+ "version": "8.0.0",
"description": "digitransit-component autosuggest-panel module",
"main": "index.js",
"files": [
@@ -28,17 +28,16 @@
"author": "Digitransit Authors",
"license": "(AGPL-3.0 OR EUPL-1.2)",
"peerDependencies": {
- "@digitransit-component/digitransit-component-autosuggest": "^6.0.4",
+ "@digitransit-component/digitransit-component-autosuggest": "^7.0.0",
"@digitransit-component/digitransit-component-icon": "^1.2.0",
"@hsl-fi/sass": "^0.2.0",
"classnames": "2.5.1",
+ "downshift": "9.0.10",
"i18next": "^22.5.1",
"lodash": "4.17.21",
"lodash-es": "4.17.21",
"prop-types": "^15.8.1",
"react": "^16.13.0",
- "react-autosuggest": "^10.0.0",
- "react-autowhatever": "10.2.1",
"react-i18next": "^12.3.1",
"react-sortablejs": "2.0.11"
}
diff --git a/digitransit-component/packages/digitransit-component-autosuggest/package.json b/digitransit-component/packages/digitransit-component-autosuggest/package.json
index 84efda3b07..0436c09123 100644
--- a/digitransit-component/packages/digitransit-component-autosuggest/package.json
+++ b/digitransit-component/packages/digitransit-component-autosuggest/package.json
@@ -1,6 +1,6 @@
{
"name": "@digitransit-component/digitransit-component-autosuggest",
- "version": "6.0.4",
+ "version": "7.0.0",
"description": "digitransit-component autosuggest module",
"main": "index.js",
"files": [
@@ -40,13 +40,13 @@
"@digitransit-component/digitransit-component-suggestion-item": "^2.3.1",
"@hsl-fi/sass": "^0.2.0",
"classnames": "2.5.1",
+ "downshift": "9.0.10",
"i18next": "^22.5.1",
"lodash": "4.17.21",
"lodash-es": "4.17.21",
"luxon": "^3.6.1",
"prop-types": "^15.8.1",
"react": "^16.13.0",
- "react-autosuggest": "^10.0.0",
"react-i18next": "^12.3.1",
"react-modal": "~3.11.2"
}
diff --git a/digitransit-component/packages/digitransit-component-autosuggest/src/components/ClearButton.js b/digitransit-component/packages/digitransit-component-autosuggest/src/components/ClearButton.js
new file mode 100644
index 0000000000..3066abe66a
--- /dev/null
+++ b/digitransit-component/packages/digitransit-component-autosuggest/src/components/ClearButton.js
@@ -0,0 +1,37 @@
+import React from 'react';
+import Icon from '@digitransit-component/digitransit-component-icon';
+import { useTranslation } from 'react-i18next';
+import PropTypes from 'prop-types';
+
+/**
+ * Input clear button element
+ * @typedef {object} ClearButtonProps
+ * @property {string} color // hex color string
+ * @property {string} lng // language, eg. 'fi'
+ * @property {() => void} clearInput
+ * @property {() => void} [onKeyDown]
+ * @param {ClearButtonProps} props
+ * @returns {JSX.Element}
+ */
+export const ClearButton = ({ color, lng, clearInput, onKeyDown, styles }) => {
+ const [t] = useTranslation();
+ return (
+
+
+
+ );
+};
+
+ClearButton.propTypes = {
+ color: PropTypes.string.isRequired,
+ lng: PropTypes.string.isRequired,
+ clearInput: PropTypes.func.isRequired,
+ onKeyDown: PropTypes.func.isRequired,
+ styles: PropTypes.objectOf(PropTypes.string).isRequired,
+};
diff --git a/digitransit-component/packages/digitransit-component-autosuggest/src/components/Input.js b/digitransit-component/packages/digitransit-component-autosuggest/src/components/Input.js
new file mode 100644
index 0000000000..2959efcd4f
--- /dev/null
+++ b/digitransit-component/packages/digitransit-component-autosuggest/src/components/Input.js
@@ -0,0 +1,94 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import cx from 'classnames';
+import { ClearButton } from './ClearButton';
+
+export function Input({
+ id,
+ lng,
+ value,
+ placeholder,
+ required,
+ getInputProps,
+ getLabelProps,
+ clearInput,
+ ariaLabel,
+ inputRef,
+ styles,
+ renderLabel,
+ isMobile,
+ inputClassName,
+ transportMode,
+ clearButtonColor,
+ autoFocus,
+}) {
+ return (
+
+ {renderLabel && (
+
+ {ariaLabel}
+
+ )}
+
+ {value && (
+ clearInput(inputRef)}
+ styles={styles}
+ color={clearButtonColor}
+ onKeyDown={clearInput}
+ />
+ )}
+
+ );
+}
+
+Input.propTypes = {
+ id: PropTypes.string.isRequired,
+ lng: PropTypes.string.isRequired,
+ placeholder: PropTypes.string.isRequired,
+ required: PropTypes.bool.isRequired,
+ value: PropTypes.string.isRequired,
+ getInputProps: PropTypes.func.isRequired,
+ getLabelProps: PropTypes.func.isRequired,
+ clearInput: PropTypes.func.isRequired,
+ ariaLabel: PropTypes.string.isRequired,
+ inputRef: PropTypes.shape({
+ current: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
+ }).isRequired,
+ styles: PropTypes.objectOf(PropTypes.string).isRequired,
+ renderLabel: PropTypes.bool,
+ isMobile: PropTypes.bool.isRequired,
+ inputClassName: PropTypes.string.isRequired,
+ transportMode: PropTypes.string,
+ clearButtonColor: PropTypes.string.isRequired,
+ autoFocus: PropTypes.bool,
+};
+
+Input.defaultProps = {
+ autoFocus: false,
+ renderLabel: false,
+ transportMode: undefined,
+};
diff --git a/digitransit-component/packages/digitransit-component-autosuggest/src/helpers/MobileNoScroll.scss b/digitransit-component/packages/digitransit-component-autosuggest/src/components/MobileNoScroll.scss
similarity index 100%
rename from digitransit-component/packages/digitransit-component-autosuggest/src/helpers/MobileNoScroll.scss
rename to digitransit-component/packages/digitransit-component-autosuggest/src/components/MobileNoScroll.scss
diff --git a/digitransit-component/packages/digitransit-component-autosuggest/src/helpers/MobileSearch.scss b/digitransit-component/packages/digitransit-component-autosuggest/src/components/MobileSearch.scss
similarity index 100%
rename from digitransit-component/packages/digitransit-component-autosuggest/src/helpers/MobileSearch.scss
rename to digitransit-component/packages/digitransit-component-autosuggest/src/components/MobileSearch.scss
diff --git a/digitransit-component/packages/digitransit-component-autosuggest/src/components/MobileView.js b/digitransit-component/packages/digitransit-component-autosuggest/src/components/MobileView.js
new file mode 100644
index 0000000000..b09efa6399
--- /dev/null
+++ b/digitransit-component/packages/digitransit-component-autosuggest/src/components/MobileView.js
@@ -0,0 +1,262 @@
+import React, { useState, useEffect } from 'react';
+import PropTypes from 'prop-types';
+import ReactModal from 'react-modal';
+import DialogModal from '@digitransit-component/digitransit-component-dialog-modal';
+import Icon from '@digitransit-component/digitransit-component-icon';
+import hooks from '@hsl-fi/hooks';
+import { useTranslation } from 'react-i18next';
+import { useCombobox } from 'downshift';
+import mobileStyles from './MobileSearch.scss';
+import mobileNoScrollStyles from './MobileNoScroll.scss';
+import { Suggestions } from './Suggestions';
+import { Input } from './Input';
+
+const isKeyboardSelectionEvent = event => {
+ const space = [13, ' ', 'Spacebar'];
+ const enter = [32, 'Enter'];
+ const key = (event && (event.key || event.which || event.keyCode)) || '';
+
+ if (!key || !space.concat(enter).includes(key)) {
+ return false;
+ }
+ event.preventDefault();
+ return true;
+};
+
+/**
+ * @typedef MobileViewProps
+ * @property {string} appElement
+ * @property {string} id
+ * @property {string} placeholder
+ * @property {function} closeHandle
+ * @property {function} clearInput
+ * @property {array} suggestions
+ * @property {function} onSelectedItemChange
+ * @property {object} fontWeights
+ * @property {function} clearOldSearches
+ * @property {object} itemProps
+ * @property {string} color
+ * @property {string} accessiblePrimaryColor
+ * @property {string} hoverColor
+ * @property {string} lng
+ * @property {object} ariaProps
+ * @property {boolean} renderMobile
+ * @property {string} clearButtonColor
+ * @property {string} inputValue
+ * @property {function} setInputValue
+ * @property {string} inputClassName
+ * @property {boolean} required
+ * @property {string} [mobileLabel]
+ * @property {boolean} [showScroll]
+ *
+ * @param {MobileViewProps} props
+ * @returns {JSX.Element}
+ */
+const MobileView = ({
+ appElement,
+ id,
+ placeholder,
+ closeHandle,
+ clearInput,
+ suggestions,
+ onSelectedItemChange,
+ fontWeights,
+ clearOldSearches,
+ itemProps,
+ color,
+ accessiblePrimaryColor,
+ hoverColor,
+ showScroll,
+ lng,
+ ariaProps,
+ mobileLabel,
+ renderMobile,
+ clearButtonColor,
+ inputValue,
+ setInputValue,
+ inputClassName,
+ required,
+}) => {
+ const [t] = useTranslation();
+ const { lock, unlock } = hooks.useScrollLock();
+ const styles = showScroll ? mobileStyles : mobileNoScrollStyles;
+ const inputId = `${id}-input`;
+ const labelId = `${id}-label`;
+ const [isDialogOpen, setDialogOpen] = useState(false);
+ const inputRef = React.useRef();
+
+ useEffect(() => {
+ ReactModal.setAppElement(appElement);
+ }, []);
+
+ useEffect(() => {
+ if (renderMobile) {
+ lock();
+ } else {
+ unlock();
+ }
+ }, [renderMobile]);
+
+ /**
+ * independent hooks in mobile view.
+ * inputValue and suggestion states are kept in the parent
+ */
+ const {
+ highlightedIndex,
+ getLabelProps,
+ getMenuProps,
+ getInputProps,
+ getItemProps,
+ } = useCombobox({
+ items: suggestions,
+ inputValue,
+ onInputValueChange: ({ inputValue: newValue }) => setInputValue(newValue),
+ onSelectedItemChange,
+ defaultHighlightedIndex: -1,
+ });
+ // call to suppress ref errors from downshift, might need better solution
+ getLabelProps({}, { suppressRefError: true });
+ getMenuProps({}, { suppressRefError: true });
+ getInputProps({}, { suppressRefError: true });
+ getItemProps({ index: 0 }, { suppressRefError: true });
+
+ const isOriginDestinationOrViapoint =
+ id === 'origin' ||
+ id === 'destination' ||
+ id === 'via-point' ||
+ id === 'origin-stop-near-you';
+
+ const { ariaRequiredText, SearchBarId, ariaCurrentSuggestion } = ariaProps;
+ const ariaLabel = ariaRequiredText
+ .concat(' ')
+ .concat(SearchBarId)
+ .concat(' ')
+ .concat(ariaCurrentSuggestion);
+ return (
+
+
+
+ isKeyboardSelectionEvent(e) && closeHandle()}
+ aria-label={t('cancel', { lng })}
+ tabIndex={0}
+ >
+
+
+
+
+ {mobileLabel}
+
+ clearInput(inputRef)}
+ ariaLabel={ariaLabel}
+ inputRef={inputRef}
+ styles={styles}
+ renderLabel={false}
+ clearButtonColor={clearButtonColor}
+ autoFocus
+ inputClassName={inputClassName}
+ required={required}
+ isMobile
+ />
+ setDialogOpen(true)}
+ />
+
+
+
setDialogOpen(false)}
+ headerText={t('delete-old-searches-header', { lng })}
+ primaryButtonText={t('delete', { lng })}
+ secondaryButtonText={t('cancel', { lng })}
+ primaryButtonOnClick={() => {
+ clearOldSearches();
+ setDialogOpen(false);
+ }}
+ secondaryButtonOnClick={() => setDialogOpen(false)}
+ color={color}
+ hoverColor={hoverColor}
+ fontWeights={fontWeights}
+ lang={lng}
+ />
+
+
+ );
+};
+
+MobileView.propTypes = {
+ appElement: PropTypes.string.isRequired,
+ id: PropTypes.string.isRequired,
+ clearInput: PropTypes.func.isRequired,
+ clearOldSearches: PropTypes.func.isRequired,
+ closeHandle: PropTypes.func.isRequired,
+ onSelectedItemChange: PropTypes.func.isRequired,
+ inputValue: PropTypes.string.isRequired,
+ setInputValue: PropTypes.func.isRequired,
+ color: PropTypes.string.isRequired,
+ accessiblePrimaryColor: PropTypes.string.isRequired,
+ clearButtonColor: PropTypes.string.isRequired,
+ hoverColor: PropTypes.string.isRequired,
+ fontWeights: PropTypes.shape({
+ medium: PropTypes.number.isRequired,
+ }).isRequired,
+ itemProps: PropTypes.shape({}).isRequired,
+ suggestions: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
+ ariaProps: PropTypes.shape({
+ ariaRequiredText: PropTypes.string.isRequired,
+ SearchBarId: PropTypes.string.isRequired,
+ ariaCurrentSuggestion: PropTypes.string.isRequired,
+ }).isRequired,
+ inputClassName: PropTypes.string.isRequired,
+ placeholder: PropTypes.string.isRequired,
+ mobileLabel: PropTypes.string.isRequired,
+ required: PropTypes.bool.isRequired,
+ renderMobile: PropTypes.bool.isRequired,
+ showScroll: PropTypes.bool.isRequired,
+ lng: PropTypes.string.isRequired,
+};
+
+export default MobileView;
diff --git a/digitransit-component/packages/digitransit-component-autosuggest/src/components/Suggestions.js b/digitransit-component/packages/digitransit-component-autosuggest/src/components/Suggestions.js
new file mode 100644
index 0000000000..0d8f4c27ed
--- /dev/null
+++ b/digitransit-component/packages/digitransit-component-autosuggest/src/components/Suggestions.js
@@ -0,0 +1,155 @@
+import SuggestionItem from '@digitransit-component/digitransit-component-suggestion-item';
+import React from 'react';
+import PropTypes from 'prop-types';
+import { useTranslation } from 'react-i18next';
+import cx from 'classnames';
+import {
+ getSuggestionContent,
+ translateFutureRouteSuggestionTime,
+} from '../utils/utils';
+
+const itemShape = PropTypes.shape({
+ name: PropTypes.string,
+ type: PropTypes.string,
+ address: PropTypes.string,
+ selectedIconId: PropTypes.string,
+ iconColor: PropTypes.string,
+ translatedText: PropTypes.string,
+ properties: PropTypes.shape({
+ layer: PropTypes.string,
+ color: PropTypes.string,
+ localadmin: PropTypes.string,
+ mode: PropTypes.string,
+ id: PropTypes.string,
+ source: PropTypes.string,
+ arrowClicked: PropTypes.bool,
+ destination: PropTypes.shape({
+ name: PropTypes.string,
+ localadmin: PropTypes.string,
+ }),
+ origin: PropTypes.shape({
+ name: PropTypes.string,
+ localadmin: PropTypes.string,
+ }),
+ }),
+});
+
+function Suggestion({
+ item,
+ lng,
+ getItemProps,
+ suggestionItemProps,
+ highlightedIndex,
+ itemIndex,
+ styles,
+}) {
+ const [t] = useTranslation();
+ const newItem =
+ item.type === 'FutureRoute'
+ ? {
+ ...item,
+ translatedText: translateFutureRouteSuggestionTime(item, lng, t),
+ }
+ : item;
+ const content = getSuggestionContent(item, lng, t);
+ return (
+
+
+
+ );
+}
+
+Suggestion.propTypes = {
+ item: itemShape.isRequired,
+ lng: PropTypes.string.isRequired,
+ getItemProps: PropTypes.func.isRequired,
+ suggestionItemProps: PropTypes.shape({}).isRequired,
+ highlightedIndex: PropTypes.number.isRequired,
+ itemIndex: PropTypes.number.isRequired,
+ styles: PropTypes.objectOf(PropTypes.string).isRequired,
+};
+
+export function Suggestions({
+ suggestions,
+ getMenuProps,
+ getItemProps,
+ itemProps: suggestionItemProps,
+ highlightedIndex,
+ lng,
+ styles,
+ hidden,
+ renderClearHistoryButton,
+ handleClearHistory,
+}) {
+ const [t] = useTranslation();
+ return (
+
+
+ {suggestions.map((suggestion, i) => {
+ return (
+
+ );
+ })}
+ {renderClearHistoryButton && (
+
+
+ {t('clear-search-history', { lng })}
+
+
+ )}
+
+
+ );
+}
+
+Suggestions.propTypes = {
+ suggestions: PropTypes.arrayOf(itemShape).isRequired,
+ getMenuProps: PropTypes.func.isRequired,
+ getItemProps: PropTypes.func.isRequired,
+ itemProps: PropTypes.shape({}).isRequired,
+ highlightedIndex: PropTypes.number.isRequired,
+ lng: PropTypes.string.isRequired,
+ styles: PropTypes.objectOf(PropTypes.string).isRequired,
+ hidden: PropTypes.bool.isRequired,
+ renderClearHistoryButton: PropTypes.bool,
+ handleClearHistory: PropTypes.func,
+};
+
+Suggestions.defaultProps = {
+ renderClearHistoryButton: false,
+ handleClearHistory: () => undefined,
+};
diff --git a/digitransit-component/packages/digitransit-component-autosuggest/src/helpers/styles.scss b/digitransit-component/packages/digitransit-component-autosuggest/src/components/styles.scss
similarity index 100%
rename from digitransit-component/packages/digitransit-component-autosuggest/src/helpers/styles.scss
rename to digitransit-component/packages/digitransit-component-autosuggest/src/components/styles.scss
diff --git a/digitransit-component/packages/digitransit-component-autosuggest/src/helpers/MobileSearch.js b/digitransit-component/packages/digitransit-component-autosuggest/src/helpers/MobileSearch.js
deleted file mode 100644
index 03deecf80f..0000000000
--- a/digitransit-component/packages/digitransit-component-autosuggest/src/helpers/MobileSearch.js
+++ /dev/null
@@ -1,273 +0,0 @@
-import PropTypes from 'prop-types';
-import React, { useState, useEffect, useCallback } from 'react';
-import ReactModal from 'react-modal';
-import cx from 'classnames';
-import Icon from '@digitransit-component/digitransit-component-icon';
-import DialogModal from '@digitransit-component/digitransit-component-dialog-modal';
-import Autosuggest from 'react-autosuggest';
-import mobileStyles from './MobileSearch.scss';
-import mobileNoScrollStyles from './MobileNoScroll.scss';
-
-class AutosuggestPatch extends Autosuggest {
- constructor(props) {
- super(props);
- const self = this;
- self.onSuggestionTouchMove = () => {
- self.justSelectedSuggestion = false;
- self.pressedSuggestion = null;
- };
- }
-}
-
-const MobileSearch = ({
- appElement,
- id,
- closeHandle,
- clearInput,
- value,
- suggestions,
- inputProps,
- fetchFunction,
- renderSuggestion,
- getSuggestionValue,
- ariaLabel,
- label,
- onSuggestionSelected,
- clearOldSearches,
- dialogHeaderText,
- dialogPrimaryButtonText,
- dialogSecondaryButtonText,
- clearInputButtonText,
- focusInput,
- color,
- hoverColor,
- accessiblePrimaryColor,
- searchOpen,
- fontWeights,
- showScroll,
- lang,
-}) => {
- const styles = showScroll ? mobileStyles : mobileNoScrollStyles;
-
- const inputId = `${id}-input`;
- const labelId = `${id}-label`;
-
- const [isDialogOpen, setDialogOpen] = useState(false);
- const inputRef = React.useRef();
-
- useEffect(() => {
- ReactModal.setAppElement(appElement);
- }, []);
-
- const onClose = useCallback(() => {
- closeHandle();
- });
-
- const onSelect = (e, ref) => {
- if (ref.suggestion.type === 'clear-search-history') {
- setDialogOpen(true);
- } else if (!ref.suggestion.properties.arrowClicked) {
- // Select item if fill input button is not pressed (diagonal arrow in suggestion items)
- onSuggestionSelected(e, ref);
- }
- };
-
- const isKeyboardSelectionEvent = event => {
- const space = [13, ' ', 'Spacebar'];
- const enter = [32, 'Enter'];
- const key = (event && (event.key || event.which || event.keyCode)) || '';
-
- if (!key || !space.concat(enter).includes(key)) {
- return false;
- }
- event.preventDefault();
- return true;
- };
-
- const getValue = suggestion => {
- if (suggestion.type === 'clear-search-history') {
- return '';
- }
- return getSuggestionValue(suggestion);
- };
-
- const renderItem = item => {
- if (item.type === 'clear-search-history') {
- return (
-
- {item.labelId}
-
- );
- }
- return renderSuggestion(item);
- };
-
- const renderDialogModal = () => {
- return (
- setDialogOpen(false)}
- headerText={dialogHeaderText}
- primaryButtonText={dialogPrimaryButtonText}
- secondaryButtonText={dialogSecondaryButtonText}
- primaryButtonOnClick={() => {
- clearOldSearches();
- setDialogOpen(false);
- }}
- secondaryButtonOnClick={() => setDialogOpen(false)}
- color={color}
- hoverColor={hoverColor}
- fontWeights={fontWeights}
- lang={lang}
- />
- );
- };
-
- const clearButton = () => {
- return (
- isKeyboardSelectionEvent(e) && clearInput()}
- aria-label={clearInputButtonText}
- >
-
-
- );
- };
-
- const renderContent = () => {
- return (
-
-
onClose()}
- onKeyDown={e => isKeyboardSelectionEvent(e) && onClose()}
- aria-label={dialogSecondaryButtonText}
- tabIndex={0}
- >
-
-
-
-
- {label}
-
- true}
- inputProps={{
- ...inputProps,
- className: cx(
- `${styles.input} ${styles[id] || ''} ${
- inputProps.value ? styles.hasValue : ''
- }`,
- ),
- autoFocus: true,
- }}
- renderInputComponent={p => (
- <>
-
- {value && clearButton()}
- >
- )}
- theme={styles}
- onSuggestionSelected={onSelect}
- ref={inputRef}
- />
-
-
- );
- };
- // This does not seem to do anything.
- // When doing quick testing on this it looked like inputRef.current is always undefined.
- if (focusInput && inputRef.current?.input) {
- inputRef.current.input.focus();
- }
-
- return (
-
-
- {renderContent()}
- {renderDialogModal()}
-
-
- );
-};
-
-MobileSearch.propTypes = {
- appElement: PropTypes.string.isRequired,
- id: PropTypes.string.isRequired,
- closeHandle: PropTypes.func.isRequired,
- clearInput: PropTypes.func.isRequired,
- value: PropTypes.string,
- clearInputButtonText: PropTypes.string.isRequired,
- // eslint-disable-next-line
- suggestions: PropTypes.arrayOf(PropTypes.object).isRequired,
- inputProps: PropTypes.shape({
- onChange: PropTypes.func.isRequired,
- onBlur: PropTypes.func.isRequired,
- value: PropTypes.string.isRequired,
- placeholder: PropTypes.string.isRequired,
- className: PropTypes.string.isRequired,
- }).isRequired,
- fetchFunction: PropTypes.func.isRequired,
- getSuggestionValue: PropTypes.func.isRequired,
- renderSuggestion: PropTypes.func.isRequired,
- onSuggestionSelected: PropTypes.func.isRequired,
- label: PropTypes.string.isRequired,
- clearOldSearches: PropTypes.func.isRequired,
- ariaLabel: PropTypes.string,
- dialogHeaderText: PropTypes.string,
- dialogPrimaryButtonText: PropTypes.string,
- dialogSecondaryButtonText: PropTypes.string,
- focusInput: PropTypes.bool,
- color: PropTypes.string,
- hoverColor: PropTypes.string,
- accessiblePrimaryColor: PropTypes.string.isRequired,
- searchOpen: PropTypes.bool.isRequired,
- fontWeights: PropTypes.shape({
- medium: PropTypes.number.isRequired,
- }).isRequired,
- showScroll: PropTypes.bool,
- lang: PropTypes.string.isRequired,
-};
-
-MobileSearch.defaultProps = {
- value: undefined,
- ariaLabel: undefined,
- dialogHeaderText: undefined,
- dialogPrimaryButtonText: undefined,
- dialogSecondaryButtonText: undefined,
- focusInput: false,
- color: undefined,
- hoverColor: undefined,
- showScroll: undefined,
-};
-
-export default MobileSearch;
diff --git a/digitransit-component/packages/digitransit-component-autosuggest/src/helpers/withScrollLock.js b/digitransit-component/packages/digitransit-component-autosuggest/src/helpers/withScrollLock.js
deleted file mode 100644
index a891f56a11..0000000000
--- a/digitransit-component/packages/digitransit-component-autosuggest/src/helpers/withScrollLock.js
+++ /dev/null
@@ -1,12 +0,0 @@
-import React from 'react';
-import hooks from '@hsl-fi/hooks';
-
-const withScrollLock = Component => {
- return props => {
- const { lock, unlock } = hooks.useScrollLock();
-
- return ;
- };
-};
-
-export default withScrollLock;
diff --git a/digitransit-component/packages/digitransit-component-autosuggest/src/index.js b/digitransit-component/packages/digitransit-component-autosuggest/src/index.js
index 6416ffdec1..40786eb88a 100644
--- a/digitransit-component/packages/digitransit-component-autosuggest/src/index.js
+++ b/digitransit-component/packages/digitransit-component-autosuggest/src/index.js
@@ -1,163 +1,111 @@
/* eslint-disable import/no-extraneous-dependencies */
import PropTypes from 'prop-types';
-import React from 'react';
-import { withTranslation, I18nextProvider } from 'react-i18next';
+import React, { useState, useEffect, useCallback, useRef } from 'react';
+import { I18nextProvider, useTranslation } from 'react-i18next';
import cx from 'classnames';
-import Autosuggest from 'react-autosuggest';
import { executeSearch } from '@digitransit-search-util/digitransit-search-util-execute-search-immidiate';
-import SuggestionItem from '@digitransit-component/digitransit-component-suggestion-item';
-import {
- getNameLabel,
- getStopCode,
-} from '@digitransit-search-util/digitransit-search-util-uniq-by-label';
-import { getStopName } from '@digitransit-search-util/digitransit-search-util-helpers';
-import getLabel from '@digitransit-search-util/digitransit-search-util-get-label';
+import { useCombobox } from 'downshift';
import Icon from '@digitransit-component/digitransit-component-icon';
-import { DateTime, Settings } from 'luxon';
-import isEqual from 'lodash/isEqual';
-import isEmpty from 'lodash/isEmpty';
-import i18n from './helpers/i18n';
-import styles from './helpers/styles.scss';
-import MobileSearch from './helpers/MobileSearch';
-import withScrollLock from './helpers/withScrollLock';
-
-Settings.defaultLocale = 'en';
+import i18n from './utils/i18n';
+import styles from './components/styles.scss';
+import { getSuggestionValue, suggestionAsAriaContent } from './utils/utils';
+import MobileView from './components/MobileView';
+import { Input } from './components/Input';
+import { Suggestions } from './components/Suggestions';
+
+const getAriaProps = ({
+ id,
+ ariaLabel,
+ lng,
+ t,
+ isMobile,
+ suggestions,
+ value,
+ required,
+}) => {
+ const ariaBarId = id.replace('searchfield-', '');
+ const SearchBarId =
+ ariaLabel || t(ariaBarId, { lng }).replace('searchfield-', '').concat('.'); // Full stop makes screen reader speech clearer.
+ const ariaRequiredText = required ? `${t('required', { lng })}.` : '';
+ const ariaLabelInstructions = isMobile
+ ? t('search-autosuggest-label-instructions-mobile', { lng })
+ : t('search-autosuggest-label-instructions-desktop', { lng });
+ const movingToDestinationFieldText =
+ id === 'origin'
+ ? t('search-autosuggest-label-move-to-destination', { lng })
+ : '';
+ const ariaLabelText = ariaLabelInstructions
+ .concat(' ')
+ .concat(movingToDestinationFieldText);
+
+ const ariaSuggestionLen = t('search-autosuggest-len', {
+ count: suggestions.length,
+ lng,
+ });
+
+ const ariaCurrentSuggestion =
+ suggestionAsAriaContent({ suggestions, t, lng }) || value
+ ? t('search-current-suggestion', {
+ lng,
+ selection:
+ suggestionAsAriaContent({ suggestions, t, lng }).toLowerCase() ||
+ value,
+ })
+ : '';
-const getPlatform = (addendum, lng, t) => {
- // check if i81n is initialized
- if (!t) {
- return undefined;
- }
- if (!addendum || !addendum.GTFS.platform) {
- return undefined;
- }
- const { modes, platform } = addendum.GTFS;
- const type =
- modes && modes[0] === 'RAIL' ? t('track', { lng }) : t('platform', { lng });
- return [type, platform];
+ return {
+ SearchBarId,
+ ariaRequiredText,
+ ariaLabelText,
+ ariaSuggestionLen,
+ ariaCurrentSuggestion,
+ };
};
-function getSuggestionContent(item, lng, t) {
- // check if i81n is initialized
- if (!t) {
- return undefined;
- }
- if (item.type !== 'FutureRoute') {
- if (item.type === 'SelectFromMap') {
- return ['', t('select-from-map', { lng })];
- }
- if (item.type === 'CurrentLocation') {
- return ['', t('use-own-position', { lng })];
- }
- if (item.type === 'SelectFromOwnLocations') {
- return ['', t('select-from-own-locations', { lng })];
- }
- /* eslint-disable-next-line prefer-const */
- let [name, label] = getNameLabel(item.properties, true);
- let suggestionType;
- if (
- item.properties.layer.toLowerCase().includes('bikerental') ||
- item.properties.layer.toLowerCase().includes('bikestation')
- ) {
- suggestionType = t('vehiclerentalstation', { lng });
- const stopCode = item.properties.labelId;
- return [suggestionType, name, undefined, stopCode];
- }
-
- if (item.properties.layer === 'bikepark') {
- suggestionType = t('bikepark', { lng });
- return [suggestionType, name, undefined, undefined];
- }
-
- if (item.properties.layer === 'carpark') {
- suggestionType = t('carpark', { lng });
- return [suggestionType, name, undefined, undefined];
+/**
+ * Takes the targets and modifies them based on ownPlaces and isLocationSearch
+ * @param {object} props
+ * @param {string[]} props.targets
+ * @param {boolean} props.isLocationSearch
+ * @param {boolean} props.isMobile
+ * @param {boolean} props.ownPlaces
+ * @param {string[]} props.sources
+ * @returns {string[]} newTargets
+ */
+const getNewTargets = ({
+ targets,
+ isLocationSearch,
+ isMobile,
+ ownPlaces,
+ sources,
+}) => {
+ const useAll = !targets?.length;
+ let newTargets;
+ if (ownPlaces) {
+ newTargets = ['Locations'];
+ if (useAll || targets.includes('Stops')) {
+ newTargets.push('Stops');
}
-
- if (item.properties.mode) {
- suggestionType = t(
- item.properties.mode.toLowerCase().replace('favourite', ''),
- { lng },
- );
- } else {
- const layer = item.properties.layer
- .replace('route-', '')
- .toLowerCase()
- .replace('favourite', '');
- suggestionType = t(layer, { lng });
+ if (useAll || targets.includes('Stations')) {
+ newTargets.push('Stations');
}
-
- if (
- item.properties.id &&
- (item.properties.layer === 'stop' || item.properties.layer === 'station')
- ) {
- const stopCode = getStopCode(item.properties);
- const mode = item.properties.addendum?.GTFS.modes;
- const platform = getPlatform(item.properties.addendum, lng, t);
- return [
- suggestionType,
- getStopName(name, stopCode),
- label,
- stopCode,
- mode,
- platform,
- ];
+ if (useAll || targets.includes('VehicleRentalStations')) {
+ newTargets.push('VehicleRentalStations');
}
+ } else if (!useAll) {
+ newTargets = [...targets];
+ // in desktop, favorites are accessed via sub search
if (
- item.properties.layer === 'favouriteStop' ||
- item.properties.layer === 'favouriteStation'
+ isLocationSearch &&
+ !isMobile &&
+ (!sources.length || sources.includes('Favourite'))
) {
- const { address, code } = item.properties;
- const stoName = address ? getStopName(address.split(',')[0], code) : name;
- const platform = getPlatform(item.properties.addendum, lng);
- return [suggestionType, stoName, label, code, undefined, platform];
+ newTargets.push('SelectFromOwnLocations');
}
- return [suggestionType, name, label];
}
- const { origin, destination } = item.properties;
- const tail1 = origin.locality ? `, ${origin.locality} foobar` : '';
- const tail2 = destination.locality ? `, ${destination.locality}` : '';
- const name1 = origin.name;
- const name2 = destination.name;
- return [
- t('future-route', { lng }),
- `${t('origin', { lng })} ${name1}${tail1} ${t('destination', {
- lng,
- })} ${name2}${tail2}`,
- item.translatedText,
- ];
-}
-
-function translateFutureRouteSuggestionTime(item, lng, t) {
- // check if i81n is initialized
- if (!t) {
- return undefined;
- }
- const time = DateTime.fromSeconds(Number(item.properties.time));
- const now = DateTime.now();
- let str = item.properties.arriveBy
- ? t('arrival', { lng })
- : t('departure', { lng });
- if (time.hasSame(now, 'day')) {
- str = `${str} ${t('today-at')}`;
- } else if (time.hasSame(now.plus({ days: 1 }), 'day')) {
- str = `${str} ${t('tomorrow-at', { lng })}`;
- } else {
- str = `${str} ${time.toFormat('ccc d.L.')}`;
- }
- str = `${str} ${time.toFormat('HH:mm')}`;
- return str;
-}
-
-const getSuggestionValue = suggestion => {
- if (
- suggestion.type === 'SelectFromOwnLocations' ||
- suggestion.type === 'back'
- ) {
- return '';
- }
- return getLabel(suggestion.properties);
+ return newTargets;
};
+
/**
* @example
* const searchContext = {
@@ -218,7 +166,6 @@ const getSuggestionValue = suggestion => {
* value="" // e.g. user typed string that is shown in search field
* onSelect={onSelect}
* onClear={onClear}
- * autoFocus={false} // defines that should this field be automatically focused when page is loaded.
* lang={lang}
* getAutoSuggestIcons={getAutoSuggestIcons}
* transportMode={transportMode} // transportmode with which we filter the routes, e.g. route-BUS
@@ -233,891 +180,574 @@ const getSuggestionValue = suggestion => {
* mobileLabel="Custom label" // Optional. Custom label text for autosuggest field on mobile.
* inputClassName="" // Optional. Custom classname applied to the input element of the component for providing CSS styles.
* translatedPlaceholder= // Optional. Custon translated placeholder text for autosuggest field.
+ *
+ * @typedef DTAutosuggestProps
+ * @property {string} appElement
+ * @property {string} id
+ * @property {string} placeholder
+ * @property {function} onSelect
+ * @property {string} [icon]
+ * @property {string} [value]
+ * @property {function} [onClear]
+ * @property {string} [lang]
+ * @property {Object} [getAutoSuggestIcons]
+ * @property {function} [handleViaPoints]
+ * @property {function} [focusChange]
+ * @property {function} [storeRef]
+ * @property {boolean} [isMobile]
+ * @property {string} [mobileLabel]
+ * @property {string} [inputClassName]
+ * @property {string} [translatedPlaceholder]
+ * @property {boolean} [required]
+ * @property {string} [color]
+ * @property {string} [hoverColor]
+ * @property {string} [accessiblePrimaryColor]
+ * @property {Object} [fontWeights]
+ * @property {Object} [modeIconColors]
+ * @property {string} [modeSet]
+ * @property {boolean} [showScroll]
+ * @property {boolean} [isEmbedded]
+ * Geocoding related props
+ * @property {Object} searchContext
+ * @property {string} [transportMode]
+ * @property {string[]} [targets]
+ * @property {string[]} [sources]
+ * @property {number} [geocodingSize]
+ * @property {function} [filterResults]
+ * @property {Object} [pathOpts]
+ * @property {Object} [refPoint]
+ * @param {DTAutosuggestProps} props
+ * @returns {JSX.Element}
*/
-class DTAutosuggest extends React.Component {
- static propTypes = {
- appElement: PropTypes.string.isRequired,
- autoFocus: PropTypes.bool,
- className: PropTypes.string,
- icon: PropTypes.string,
- id: PropTypes.string.isRequired,
- placeholder: PropTypes.string.isRequired,
- translatedPlaceholder: PropTypes.string,
- value: PropTypes.string,
- searchContext: PropTypes.shape({
- URL_PELIAS: PropTypes.string,
- // eslint-disable-next-line
- context: PropTypes.object,
- clearOldSearches: PropTypes.func,
- clearFutureRoutes: PropTypes.func,
- }).isRequired,
- ariaLabel: PropTypes.string,
- onSelect: PropTypes.func.isRequired,
- transportMode: PropTypes.string,
- filterResults: PropTypes.func,
- geocodingSize: PropTypes.number,
- onClear: PropTypes.func,
- storeRef: PropTypes.func,
- handleViaPoints: PropTypes.func,
- focusChange: PropTypes.func,
- lang: PropTypes.string,
- sources: PropTypes.arrayOf(PropTypes.string),
- targets: PropTypes.arrayOf(PropTypes.string),
- isMobile: PropTypes.bool,
- color: PropTypes.string,
- hoverColor: PropTypes.string,
- accessiblePrimaryColor: PropTypes.string,
- timeZone: PropTypes.string,
- pathOpts: PropTypes.shape({
- routesPrefix: PropTypes.string,
- stopsPrefix: PropTypes.string,
- }),
- mobileLabel: PropTypes.string,
- lock: PropTypes.func.isRequired,
- unlock: PropTypes.func.isRequired,
- refPoint: PropTypes.shape({
- address: PropTypes.string,
- lat: PropTypes.number,
- lon: PropTypes.number,
- }),
- inputClassName: PropTypes.string,
- fontWeights: PropTypes.shape({
- medium: PropTypes.number,
- }),
- modeIconColors: PropTypes.objectOf(PropTypes.string),
- getAutoSuggestIcons: PropTypes.objectOf(PropTypes.func),
- required: PropTypes.bool,
- modeSet: PropTypes.string,
- showScroll: PropTypes.bool,
- isEmbedded: PropTypes.bool,
- t: PropTypes.func,
- };
-
- static defaultProps = {
- autoFocus: false,
- className: '',
- icon: undefined,
- value: '',
- transportMode: undefined,
- filterResults: undefined,
- onClear: undefined,
- lang: 'fi',
- storeRef: undefined,
- handleViaPoints: undefined,
- focusChange: undefined,
- getAutoSuggestIcons: undefined,
- sources: [],
- targets: undefined,
- isMobile: false,
- isEmbedded: false,
- geocodingSize: undefined,
- color: '#007ac9',
- hoverColor: '#0062a1',
- accessiblePrimaryColor: '#0074be',
- timeZone: 'Europe/Helsinki',
- pathOpts: {
- routesPrefix: 'linjat',
- stopsPrefix: 'pysakit',
- },
- ariaLabel: undefined,
- mobileLabel: undefined,
- inputClassName: '',
- translatedPlaceholder: undefined,
- fontWeights: {
- medium: 500,
- },
- modeIconColors: undefined,
- required: false,
- modeSet: undefined,
- showScroll: false,
- refPoint: {},
- t: undefined,
- };
-
- constructor(props) {
- super(props);
- Settings.defaultZone = props.timeZone;
- Settings.defaultLocale = props.lang;
- this.state = {
- value: props.value,
- suggestions: [],
- editing: false,
- valid: true,
- renderMobileSearch: false,
- sources: props.sources,
- ownPlaces: false,
- typingTimer: null,
- typing: false,
- pendingSelection: null,
- suggestionIndex: 0,
- cleanExecuted: false,
- scrollY: 0,
- };
- }
-
- // DT-4074: When a user's location is updated DTAutosuggest would re-render causing suggestion list to reset.
- // This will prevent it.
- shouldComponentUpdate(nextProps, nextState) {
- return !isEqual(nextState, this.state) || !isEqual(nextProps, this.props);
- }
-
- // eslint-disable-next-line camelcase
- UNSAFE_componentWillReceiveProps(nextProps) {
- // wait until address is set or geolocationing fails
- if (nextProps.value !== this.state.value && !this.state.editing) {
- this.setState({
- value: nextProps.value,
- });
+function DTAutosuggest({
+ appElement,
+ id,
+ placeholder,
+ onSelect,
+ icon,
+ value,
+ onClear,
+ lang: lng,
+ getAutoSuggestIcons,
+ handleViaPoints,
+ focusChange,
+ storeRef,
+ isMobile,
+ mobileLabel,
+ inputClassName,
+ translatedPlaceholder,
+ required,
+ color,
+ hoverColor,
+ ariaLabel,
+ accessiblePrimaryColor,
+ fontWeights,
+ modeIconColors,
+ modeSet,
+ showScroll,
+ isEmbedded,
+ transportMode,
+ targets,
+ sources,
+ geocodingSize,
+ filterResults,
+ searchContext,
+ pathOpts,
+ refPoint,
+}) {
+ const [t] = useTranslation();
+ const [shouldRenderMobile, setShouldRenderMobile] = useState(false);
+
+ const [suggestions, setSuggestions] = useState([]);
+ const [isLoading, setLoading] = useState(false);
+ const [currentSources, setCurrentSources] = useState(sources);
+ const [showOwnPlaces, setShowOwnPlaces] = useState(false);
+ const [pendingSelection, setPendingSelection] = useState(null);
+ const [cleared, setCleared] = useState(false);
+
+ const enterPressedRef = useRef(null);
+
+ // create and store input ref in the parent if storeRef is provided
+ const inputRef = React.useRef(id);
+ useEffect(() => {
+ if (storeRef) {
+ storeRef(inputRef.current);
}
- }
+ }, [inputRef.current]);
- onChange = (event, { newValue, method }) => {
- const newState = {
- value: this.fInput || newValue || '',
- renderMobileSearch: this.props.isMobile,
- };
- // Remove filled input value so it wont be reused unnecessary
- this.fInput = null;
- if (!this.state.editing) {
- newState.editing = true;
- this.setState(newState, () =>
- this.fetchFunction({ value: newValue || '' }),
- );
- } else if (method !== 'enter' || this.state.valid) {
- // test above drops unnecessary update
- // when user hits enter but search is unfinished
- if (this.state.typingTimer) {
- clearTimeout(this.state.typingTimer);
+ const selectSuggestion = useCallback(
+ (suggestion, index) => {
+ if (!suggestion) {
+ return;
}
- if (method === 'type') {
- // after timeout runs, aria alert will announce current selection
- const timer = setTimeout(() => {
- this.setState({ typing: false });
- }, 1000);
- newState.typingTimer = timer;
- newState.typing = true;
+ if (handleViaPoints) {
+ handleViaPoints(suggestion, index);
+ } else {
+ onSelect(suggestion, id);
}
- this.setState(newState);
- }
- };
-
- onBlur = () => {
- if (this.state.editing) {
- this.input.focus();
- }
- this.setState({
- editing: false,
- renderMobileSearch: false,
- value: this.props.value,
- });
- if (this.props.isMobile && this.state.renderMobileSearch) {
- this.closeMobileSearch();
- }
- };
-
- onSelected = (e, ref) => {
- if (this.state.valid) {
- if (ref.suggestion.type === 'SelectFromOwnLocations') {
- this.setState(
- {
- sources: ['Favourite', 'Back'],
- ownPlaces: true,
- pendingSelection: ref.suggestion.type,
- value: '',
- },
- () => {
- this.fetchFunction({ value: '' });
- },
- );
- return;
+ if (focusChange && (!isMobile || isEmbedded)) {
+ focusChange();
}
- if (
- ref.suggestion.type === 'back' ||
- ref.suggestion.type === 'FutureRoute'
- ) {
- this.setState(
- {
- sources: this.props.sources,
- ownPlaces: false,
- pendingSelection: ref.suggestion.type,
- suggestionIndex: ref.suggestionIndex,
- },
- () => {
- this.fetchFunction({ value: '' });
- },
- );
- return;
+ if (isMobile) {
+ setShouldRenderMobile(false);
}
- this.selectionDone = true; // selection done, do not let upcoming keyboard events confuse the flow
- this.setState(
- {
- editing: false,
- value: ref.suggestionValue,
- },
- () => {
- this.input.blur();
- if (this.props.handleViaPoints) {
- this.props.handleViaPoints(ref.suggestion, ref.suggestionIndex);
- } else {
- this.props.onSelect(ref.suggestion, this.props.id);
- }
- this.setState(
- {
- renderMobileSearch: false,
- sources: this.props.sources,
- ownPlaces: false,
- suggestions: [],
- },
- () => {
- this.selectionDone = false;
- },
- );
- if (
- this.props.focusChange &&
- (!this.props.isMobile || this.props.isEmbedded)
- ) {
- this.props.focusChange();
- }
- if (this.props.isMobile && this.state.renderMobileSearch) {
- this.closeMobileSearch();
- }
- },
- );
- } else {
- this.setState(
- prevState => ({
- pendingSelection: prevState.value,
- }),
- () => this.checkPendingSelection(), // search may finish during state change
- );
- }
- };
-
- onSuggestionsClearRequested = () => {
- this.setState({
- suggestions: [],
- sources: this.props.sources,
- ownPlaces: false,
- editing: false,
- });
- };
+ },
+ [isMobile],
+ );
- checkPendingSelection = () => {
- if (
- (this.state.pendingSelection === 'SelectFromOwnLocations' ||
- this.state.pendingSelection === 'back') &&
- this.state.valid
- ) {
- this.setState(
- {
- pendingSelection: null,
- editing: true,
- },
- () => {
- this.input.focus();
- },
- );
- // accept after all ongoing searches have finished
- } else if (this.state.pendingSelection && this.state.valid) {
- // finish the selection by picking first = best match or with 'FutureRoute' by suggestionIndex
- this.setState(
- {
- pendingSelection: null,
- editing: false,
- },
- () => {
- if (this.state.suggestions.length) {
- this.input.blur();
- const item = this.state.suggestions[this.state.suggestionIndex];
- if (item.type !== 'back') {
- this.props.onSelect(
- this.state.suggestions[this.state.suggestionIndex],
- this.props.id,
- );
+ const onSelectedItemChange = changes =>
+ selectSuggestion(changes.selectedItem, changes.highlightedIndex);
+
+ const {
+ inputValue,
+ isOpen,
+ highlightedIndex,
+ setInputValue,
+ getLabelProps,
+ getMenuProps,
+ getInputProps,
+ getItemProps,
+ selectItem,
+ openMenu,
+ } = useCombobox({
+ inputId: id,
+ defaultHighlightedIndex: 0,
+ stateReducer: useCallback(
+ (state, { type, changes }) => {
+ switch (type) {
+ case useCombobox.stateChangeTypes.ItemClick:
+ case useCombobox.stateChangeTypes.InputKeyDownEnter: {
+ // keep enterPressedRef to make selection when suggestions have loaded
+ if (isLoading) {
+ enterPressedRef.current = true;
+ const { selectedItem, ...changesWitoutSelection } = changes;
+ return {
+ ...changesWitoutSelection,
+ inputValue: state.inputValue,
+ };
}
- if (this.props.isMobile && this.state.renderMobileSearch) {
- this.closeMobileSearch();
+ // if selecting from own locations, keep menu open and keep old state
+ if (changes.selectedItem.type === 'SelectFromOwnLocations') {
+ setCurrentSources(['Favourite', 'Back']);
+ setShowOwnPlaces(true);
+ setPendingSelection(changes.selectedItem.type);
+ return state;
}
- if (
- this.props.focusChange &&
- (!this.props.isMobile || this.props.isEmbedded)
- ) {
- this.props.focusChange();
+ if (changes.selectedItem.type === 'back') {
+ setCurrentSources(sources);
+ setShowOwnPlaces(false);
+ setPendingSelection(null);
+ return state;
}
+ return changes;
}
- },
- );
- }
- };
-
- clearButton = () => {
- return (
-
-
-
- );
- };
-
- fetchFunction = ({ value, cleanExecuted }) => {
- return this.setState(
- { valid: false, cleanExecuted: !cleanExecuted ? false : cleanExecuted },
- () => {
- if (this.selectionDone) {
- // do not let component cast unnecessary requests
- return;
- }
- const { targets } = this.props;
- const useAll = isEmpty(targets);
- const isLocationSearch =
- isEmpty(targets) || targets.includes('Locations');
- let newTargets;
- if (this.state.ownPlaces) {
- newTargets = ['Locations'];
- if (useAll || targets.includes('Stops')) {
- newTargets.push('Stops');
- }
- if (useAll || targets.includes('Stations')) {
- newTargets.push('Stations');
+ case useCombobox.stateChangeTypes.InputClick: {
+ return {
+ ...changes,
+ isOpen: true,
+ };
}
- if (useAll || targets.includes('VehicleRentalStations')) {
- newTargets.push('VehicleRentalStations');
+ case useCombobox.stateChangeTypes.InputBlur: {
+ setPendingSelection(null);
+ setShowOwnPlaces(false);
+ if (changes.selectedItem !== undefined) {
+ const { selectedItem, ...changesWitoutSelection } = changes;
+ return changesWitoutSelection;
+ }
+ return changes;
}
- } else if (!useAll) {
- newTargets = [...targets];
- // in desktop, favorites are accessed via sub search
- if (
- isLocationSearch &&
- !this.props.isMobile &&
- (isEmpty(this.props.sources) ||
- this.props.sources.includes('Favourite'))
- ) {
- newTargets.push('SelectFromOwnLocations');
+ default: {
+ return changes;
}
}
- // remove location favourites in desktop search (collection item replaces it in target array)
- const sources =
- this.state.sources &&
- this.state.sources.filter(
- s =>
- !(
- isLocationSearch &&
- s === 'Favourite' &&
- !this.state.ownPlaces &&
- !this.props.isMobile
- ),
- );
-
- executeSearch(
- newTargets,
- sources,
- this.props.transportMode,
- this.props.searchContext,
- this.props.filterResults,
- this.props.geocodingSize,
- {
- input: value || '',
- },
- searchResult => {
- if (searchResult == null) {
- return;
- }
- // XXX translates current location
- const suggestions = (searchResult.results || [])
- .filter(
- suggestion =>
- suggestion.type !== 'FutureRoute' ||
- (suggestion.type === 'FutureRoute' &&
- suggestion.properties.time >
- DateTime.now().toUnixInteger()),
- )
- .map(suggestion => {
- if (
- suggestion.type === 'CurrentLocation' ||
- suggestion.type === 'SelectFromMap' ||
- suggestion.type === 'SelectFromOwnLocations' ||
- suggestion.type === 'back'
- ) {
- const translated = { ...suggestion };
- translated.properties.labelId = this.props.t(
- suggestion.properties.labelId,
- { lng: this.props.lang },
- );
- return translated;
- }
- return suggestion;
- });
- if (
- value === this.state.value ||
- value === this.state.pendingSelection ||
- this.state.pendingSelection === 'SelectFromOwnLocations' ||
- this.state.pendingSelection === 'back' ||
- this.state.pendingSelection === 'FutureRoute'
- ) {
- this.setState(
- {
- valid: true,
- suggestions,
- },
- () => this.checkPendingSelection(),
- );
- }
- },
- this.props.pathOpts,
- this.props.refPoint,
- );
},
- );
- };
-
- clearInput = () => {
- const newState = {
- editing: true,
- value: '',
- sources: this.props.sources,
- ownPlaces: false,
- renderMobileSearch: this.props.isMobile,
- };
- // must update suggestions
- this.setState(newState, () =>
- this.fetchFunction({ value: '', cleanExecuted: true }),
- );
- if (this.props.onClear) {
- this.props.onClear(this.props.id);
- }
- this.input.focus();
- };
-
- inputClicked = inputValue => {
- this.input.focus();
- this.clearLocationText();
- if (this.props.isMobile) {
- this.props.lock();
- }
- if (!this.state.editing) {
- const newState = {
- editing: true,
- // reset at start, just in case we missed something
- pendingSelection: null,
- renderMobileSearch: this.props.isMobile,
- };
-
- // DT-3263: added stateKeyDown
- const stateKeyDown = {
- editing: true,
- pendingSelection: null,
- value: inputValue,
- };
+ [isLoading],
+ ),
+ items: suggestions,
+ itemToString(suggestion) {
+ return suggestion ? getSuggestionValue(suggestion) : '';
+ },
+ onSelectedItemChange,
+ });
- if (!this.state.suggestions.length) {
- // DT-3263: added if-else statement
- if (typeof inputValue === 'object' || !inputValue) {
- this.setState(newState, () =>
- this.fetchFunction({ value: this.state.value }),
- );
- } else {
- this.setState(stateKeyDown, () =>
- this.fetchFunction({ value: inputValue }),
- );
- }
- } else {
- this.fetchFunction({ value: this.state.value });
- this.setState(newState);
- }
- } else if (this.props.isMobile && !this.state.renderMobileSearch) {
- this.setState({ renderMobileSearch: true });
+ const clearInput = ref => {
+ if (onClear) {
+ onClear(id);
}
- };
-
- storeInputReference = autosuggest => {
- if (autosuggest !== null) {
- this.input = autosuggest.input;
- if (this.props.storeRef) {
- this.props.storeRef(autosuggest.input);
- }
+ if (ref.current) {
+ ref.current.focus();
}
+ setCleared(true);
+ openMenu();
};
- // Fill input when user clicks fill input button in street suggestion item
- fillInput = newValue => {
- this.fInput = newValue.properties.name;
- const newState = {
- editing: true,
- value: newValue.properties.name,
- checkPendingSelection: newValue,
- valid: true,
- };
- // must update suggestions
- this.setState(newState);
- this.fetchFunction({ value: newValue.properties.name });
- this.input.focus();
- };
-
- renderItem = item => {
- const newItem =
- item.type === 'FutureRoute'
- ? {
- ...item,
- translatedText: translateFutureRouteSuggestionTime(
- item,
- this.props.lang,
- this.props.t,
- ),
+ const fetchSuggestions = useCallback(
+ input => {
+ const useAll = !targets?.length;
+ const isLocationSearch = useAll || targets.includes('Locations');
+
+ const newTargets = getNewTargets({
+ targets,
+ isLocationSearch,
+ isMobile,
+ ownPlaces: showOwnPlaces,
+ sources: currentSources,
+ });
+ // remove location favourites in desktop search (collection item replaces it in target array)
+ const newSources = currentSources
+ ? currentSources.filter(
+ s =>
+ !isLocationSearch ||
+ !s === 'Favourite' ||
+ showOwnPlaces ||
+ isMobile,
+ )
+ : currentSources;
+ executeSearch(
+ newTargets,
+ newSources,
+ transportMode,
+ searchContext,
+ filterResults,
+ geocodingSize,
+ {
+ input: input || '',
+ },
+ searchResult => {
+ if (searchResult == null) {
+ setLoading(true);
+ return;
}
- : item;
- const content = getSuggestionContent(item, this.props.lang, this.props.t);
- return (
-
- );
- };
-
- closeMobileSearch = () => {
- this.props.unlock();
- this.setState(
- {
- renderMobileSearch: false,
- value: this.props.value,
- },
- () => {
- window.scrollTo(0, this.state.scrollY);
- this.onSuggestionsClearRequested();
- },
- );
- this.input.focus();
- // This closes the mobile keyboard
- this.input.blur();
- };
-
- keyDown = event => {
- if (this.selectionDone) {
- return;
- }
- const keyCode = event.key;
- if (keyCode === 'Shift') {
- // This enables shift + tab to be used
- return;
- }
- if (keyCode === 'Escape') {
- // Using onBlur makes 'Escape' act similarly to using 'Tab'
- this.onBlur();
- }
- if (this.state.editing) {
- if (keyCode === 'Enter' && this.state.value !== '') {
- this.setState({ pendingSelection: true }, () => {
- this.fetchFunction({ value: this.state.value });
- });
- }
- this.inputClicked();
- return;
- }
-
- if (
- (keyCode === 'Enter' || keyCode === 'ArrowDown') &&
- this.state.value === ''
- ) {
- this.clearInput();
- return;
- }
- if (keyCode === 'ArrowDown' && this.state.value !== '') {
- const newState = {
- editing: true,
- value: this.state.value,
- };
- // must update suggestions
- this.setState(newState, () =>
- this.fetchFunction({ value: this.state.value }),
+ const newSuggestions = (searchResult.results || [])
+ .filter(
+ suggestion =>
+ suggestion.type !== 'FutureRoute' ||
+ (suggestion.type === 'FutureRoute' &&
+ suggestion.properties.time > Date.now() / 1000),
+ )
+ .map(suggestion => {
+ if (
+ suggestion.type === 'CurrentLocation' ||
+ suggestion.type === 'SelectFromMap' ||
+ suggestion.type === 'SelectFromOwnLocations' ||
+ suggestion.type === 'back'
+ ) {
+ const translatedSuggestion = { ...suggestion };
+ translatedSuggestion.properties.labelId = t(
+ suggestion.properties.labelId,
+ { lng },
+ );
+ return translatedSuggestion;
+ }
+ return suggestion;
+ });
+ setSuggestions(newSuggestions);
+ setLoading(false);
+ },
+ pathOpts,
+ refPoint,
);
+ },
+ [
+ targets,
+ currentSources,
+ transportMode,
+ searchContext,
+ filterResults,
+ geocodingSize,
+ showOwnPlaces,
+ isMobile,
+ lng,
+ pathOpts,
+ refPoint,
+ id,
+ t,
+ ],
+ );
+
+ // TODO: this logic needs to be revisited
+ useEffect(() => {
+ if (!shouldRenderMobile && !isOpen && !enterPressedRef.current) {
+ setInputValue(cleared ? '' : value || '');
+ setCleared(false);
+ } else {
+ setInputValue(cleared ? '' : value || '');
}
- if (!this.state.editing) {
- this.setState({ editing: true });
- this.clearLocationText();
- }
+ }, [cleared, value, shouldRenderMobile, isOpen, setInputValue]);
- if (keyCode === 'Tab') {
- this.onBlur();
+ // Fetch suggestions when isOpen, value, or fetchSuggestions dependies change
+ useEffect(() => {
+ if (isOpen || shouldRenderMobile) {
+ fetchSuggestions(inputValue);
}
- };
+ }, [isOpen, shouldRenderMobile, inputValue, fetchSuggestions]);
- suggestionAsAriaContent = () => {
- let label = [];
- const firstSuggestion = this.state.suggestions[0];
- if (firstSuggestion) {
- if (firstSuggestion.type && firstSuggestion.type.includes('Favourite')) {
- label.push(this.props.t('favourite', { lng: this.props.lang }));
- }
- label = label.concat(
- getSuggestionContent(this.state.suggestions[0], this.props.lang),
- );
+ useEffect(() => {
+ if (enterPressedRef.current && !isLoading) {
+ selectSuggestion(suggestions[0], 0);
+ enterPressedRef.current = false;
}
- return [...new Set(label)].join(' - ');
+ }, [isLoading]);
+
+ const baseItemProps = {
+ loading: isLoading,
+ isMobile,
+ ariaFavouriteString: t('favourite', { lng }),
+ color,
+ accessiblePrimaryColor,
+ fontWeights,
+ getAutoSuggestIcons,
+ modeIconColors,
+ modeSet,
};
- clearOldSearches = () => {
- const { context, clearOldSearches, clearFutureRoutes } =
- this.props.searchContext;
+ const {
+ ariaCurrentSuggestion,
+ ariaRequiredText,
+ SearchBarId,
+ ariaLabelText,
+ } = getAriaProps({
+ id,
+ lng,
+ t,
+ isMobile,
+ ariaLabel,
+ suggestions,
+ inputValue,
+ required,
+ });
+
+ const mobileClearOldSearches = () => {
+ const { context, clearOldSearches, clearFutureRoutes } = searchContext;
if (context && clearOldSearches) {
clearOldSearches(context);
if (clearFutureRoutes) {
clearFutureRoutes(context);
}
- this.fetchFunction({ value: this.state.value });
+ fetchSuggestions(inputValue);
}
};
- isOriginDestinationOrViapoint = () =>
- this.props.id === 'origin' ||
- this.props.id === 'destination' ||
- this.props.id === 'via-point' ||
- this.props.id === 'origin-stop-near-you';
+ useEffect(() => {
+ if (isMobile && isOpen) {
+ setShouldRenderMobile(true);
+ }
+ }, [isMobile, isOpen]);
- clearLocationText = () => {
- const positions = [
- 'Valittu sijainti',
- 'Nykyinen sijaintisi',
- 'Current position',
- 'Selected location',
- 'Vald position',
- 'Använd min position',
- 'Min position',
- 'Käytä nykyistä sijaintia',
- 'Use current location',
- 'Your current location',
- 'Wybrane miejsce',
- ];
- if (positions.includes(this.state.value)) {
- this.clearInput();
+ const closeHandle = () => {
+ setShouldRenderMobile(false);
+ if (inputRef.current) {
+ inputRef.current.blur();
}
};
- onFocus = () => {
- const scrollY = window.pageYOffset;
- return this.setState({ scrollY });
+ const checkPendingSelection = () => {
+ if (
+ pendingSelection === 'SelectFromOwnLocations' ||
+ pendingSelection === 'back'
+ ) {
+ setInputValue('');
+ openMenu();
+ }
};
- render() {
- const { t, lang: lng } = this.props;
- const { value, suggestions, renderMobileSearch, cleanExecuted } =
- this.state;
- const inputProps = {
- placeholder: this.props.translatedPlaceholder
- ? this.props.translatedPlaceholder
- : t(this.props.placeholder, { lng }),
- value,
- onChange: this.onChange,
- onBlur: this.onBlur,
- className: cx(
- `${styles.input} ${
- this.props.isMobile && this.props.transportMode ? styles.thin : ''
- } ${styles[this.props.id] || ''} ${
- this.state.value ? styles.hasValue : ''
- } ${this.props.inputClassName}`,
- ),
- onKeyDown: this.keyDown, // DT-3263
- required: this.props.required,
- };
- const ariaBarId = this.props.id.replace('searchfield-', '');
- let SearchBarId = this.props.ariaLabel || t(ariaBarId, { lng });
- SearchBarId = SearchBarId.replace('searchfield-', '').concat('.'); // Full stop makes screen reader speech clearer.
- const ariaRequiredText = this.props.required
- ? `${t('required', { lng })}.`
- : '';
- const ariaLabelInstructions = this.props.isMobile
- ? t('search-autosuggest-label-instructions-mobile', { lng })
- : t('search-autosuggest-label-instructions-desktop', { lng });
- const movingToDestinationFieldText =
- this.props.id === 'origin'
- ? t('search-autosuggest-label-move-to-destination', { lng })
- : '';
- const ariaLabelText = ariaLabelInstructions
- .concat(' ')
- .concat(movingToDestinationFieldText);
-
- const ariaSuggestionLen = t('search-autosuggest-len', {
- count: suggestions.length,
- lng,
- });
-
- const ariaCurrentSuggestion = () => {
- if (this.suggestionAsAriaContent() || this.props.value) {
- return t('search-current-suggestion', {
- lng,
- selection:
- this.suggestionAsAriaContent().toLowerCase() || this.props.value,
- });
- }
- return '';
- };
+ useEffect(() => {
+ checkPendingSelection();
+ }, [pendingSelection]);
- return (
-
-
- {!this.state.typing &&
- this.state.editing &&
- `${ariaSuggestionLen} ${ariaCurrentSuggestion()}`}
-
- {this.props.isMobile && (
-
- )}
- {!renderMobileSearch && (
+ return (
+ <>
+ {isMobile && (
+
+ )}
+
+
+ {icon && (
- {this.props.icon && (
-
-
-
- )}
-
this.state.editing}
- highlightFirstSuggestion={!this.state.ownPlaces}
- theme={styles}
- renderInputComponent={p => (
- <>
-
- {ariaCurrentSuggestion()
- .concat(' ')
- .concat(ariaRequiredText)
- .concat(' ')
- .concat(SearchBarId)
- .concat(' ')
- .concat(ariaLabelText)}
-
-
- {this.state.value && this.clearButton()}
- >
- )}
- onSuggestionSelected={this.onSelected}
- ref={this.storeInputReference}
- />
+
)}
-
- );
- }
+
+
+
+
+ >
+ );
}
-const DTAutosuggestWithScrollLock = withTranslation()(
- withScrollLock(DTAutosuggest),
-);
+DTAutosuggest.propTypes = {
+ appElement: PropTypes.string.isRequired,
+ icon: PropTypes.string,
+ id: PropTypes.string.isRequired,
+ placeholder: PropTypes.string.isRequired,
+ translatedPlaceholder: PropTypes.string,
+ value: PropTypes.string,
+ transportMode: PropTypes.string,
+ geocodingSize: PropTypes.number,
+ filterResults: PropTypes.func,
+ searchContext: PropTypes.shape({
+ URL_PELIAS: PropTypes.string,
+ // eslint-disable-next-line
+ context: PropTypes.object,
+ clearOldSearches: PropTypes.func,
+ clearFutureRoutes: PropTypes.func,
+ }).isRequired,
+ sources: PropTypes.arrayOf(PropTypes.string),
+ targets: PropTypes.arrayOf(PropTypes.string),
+ ariaLabel: PropTypes.string,
+ onSelect: PropTypes.func.isRequired,
+ onClear: PropTypes.func,
+ storeRef: PropTypes.func,
+ handleViaPoints: PropTypes.func,
+ focusChange: PropTypes.func,
+ lang: PropTypes.string,
+ isMobile: PropTypes.bool,
+ color: PropTypes.string,
+ hoverColor: PropTypes.string,
+ accessiblePrimaryColor: PropTypes.string,
+ pathOpts: PropTypes.shape({
+ routesPrefix: PropTypes.string,
+ stopsPrefix: PropTypes.string,
+ }),
+ mobileLabel: PropTypes.string,
+ refPoint: PropTypes.shape({
+ address: PropTypes.string,
+ lat: PropTypes.number,
+ lon: PropTypes.number,
+ }),
+ inputClassName: PropTypes.string,
+ fontWeights: PropTypes.shape({
+ medium: PropTypes.number,
+ }),
+ modeIconColors: PropTypes.objectOf(PropTypes.string),
+ getAutoSuggestIcons: PropTypes.objectOf(PropTypes.func),
+ required: PropTypes.bool,
+ modeSet: PropTypes.string,
+ showScroll: PropTypes.bool,
+ isEmbedded: PropTypes.bool,
+};
+
+DTAutosuggest.defaultProps = {
+ icon: undefined,
+ value: '',
+ transportMode: undefined,
+ filterResults: undefined,
+ onClear: undefined,
+ lang: 'fi',
+ storeRef: undefined,
+ handleViaPoints: undefined,
+ focusChange: undefined,
+ getAutoSuggestIcons: undefined,
+ sources: [],
+ targets: undefined,
+ isMobile: false,
+ isEmbedded: false,
+ geocodingSize: undefined,
+ color: '#007ac9',
+ hoverColor: '#0062a1',
+ accessiblePrimaryColor: '#0074be',
+ pathOpts: {
+ routesPrefix: 'linjat',
+ stopsPrefix: 'pysakit',
+ },
+ ariaLabel: undefined,
+ mobileLabel: undefined,
+ inputClassName: '',
+ translatedPlaceholder: undefined,
+ fontWeights: {
+ medium: 500,
+ },
+ modeIconColors: undefined,
+ required: false,
+ modeSet: undefined,
+ showScroll: false,
+ refPoint: {},
+};
export default props => {
return (
-
+
);
};
diff --git a/digitransit-component/packages/digitransit-component-autosuggest/src/helpers/i18n.js b/digitransit-component/packages/digitransit-component-autosuggest/src/utils/i18n.js
similarity index 100%
rename from digitransit-component/packages/digitransit-component-autosuggest/src/helpers/i18n.js
rename to digitransit-component/packages/digitransit-component-autosuggest/src/utils/i18n.js
diff --git a/digitransit-component/packages/digitransit-component-autosuggest/src/helpers/translations.js b/digitransit-component/packages/digitransit-component-autosuggest/src/utils/translations.js
similarity index 100%
rename from digitransit-component/packages/digitransit-component-autosuggest/src/helpers/translations.js
rename to digitransit-component/packages/digitransit-component-autosuggest/src/utils/translations.js
diff --git a/digitransit-component/packages/digitransit-component-autosuggest/src/utils/utils.js b/digitransit-component/packages/digitransit-component-autosuggest/src/utils/utils.js
new file mode 100644
index 0000000000..648ff3f3c0
--- /dev/null
+++ b/digitransit-component/packages/digitransit-component-autosuggest/src/utils/utils.js
@@ -0,0 +1,161 @@
+import {
+ getNameLabel,
+ getStopCode,
+} from '@digitransit-search-util/digitransit-search-util-uniq-by-label';
+import { getStopName } from '@digitransit-search-util/digitransit-search-util-helpers';
+import getLabel from '@digitransit-search-util/digitransit-search-util-get-label';
+import { DateTime } from 'luxon';
+
+export const isOriginDestinationOrViapoint = id =>
+ id === 'origin' ||
+ id === 'destination' ||
+ id === 'via-point' ||
+ id === 'origin-stop-near-you';
+
+export const getPlatform = (addendum, lng, t) => {
+ // check if i81n is initialized
+ if (!t) {
+ return undefined;
+ }
+ if (!addendum || !addendum.GTFS.platform) {
+ return undefined;
+ }
+ const { modes, platform } = addendum.GTFS;
+ const type =
+ modes && modes[0] === 'RAIL' ? t('track', { lng }) : t('platform', { lng });
+ return [type, platform];
+};
+
+export function getSuggestionContent(item, lng, t) {
+ // check if i81n is initialized
+ if (!t) {
+ return undefined;
+ }
+ if (item.type !== 'FutureRoute') {
+ if (item.type === 'SelectFromMap') {
+ return ['', t('select-from-map', { lng })];
+ }
+ if (item.type === 'CurrentLocation') {
+ return ['', t('use-own-position', { lng })];
+ }
+ if (item.type === 'SelectFromOwnLocations') {
+ return ['', t('select-from-own-locations', { lng })];
+ }
+ /* eslint-disable-next-line prefer-const */
+ let [name, label] = getNameLabel(item.properties, true);
+ let suggestionType;
+ if (
+ item.properties.layer.toLowerCase().includes('bikerental') ||
+ item.properties.layer.toLowerCase().includes('bikestation')
+ ) {
+ suggestionType = t('vehiclerentalstation', { lng });
+ const stopCode = item.properties.labelId;
+ return [suggestionType, name, undefined, stopCode];
+ }
+
+ if (item.properties.layer === 'bikepark') {
+ suggestionType = t('bikepark', { lng });
+ return [suggestionType, name, undefined, undefined];
+ }
+
+ if (item.properties.layer === 'carpark') {
+ suggestionType = t('carpark', { lng });
+ return [suggestionType, name, undefined, undefined];
+ }
+
+ if (item.properties.mode) {
+ suggestionType = t(
+ item.properties.mode.toLowerCase().replace('favourite', ''),
+ { lng },
+ );
+ } else {
+ const layer = item.properties.layer
+ .replace('route-', '')
+ .toLowerCase()
+ .replace('favourite', '');
+ suggestionType = t(layer, { lng });
+ }
+
+ if (
+ item.properties.id &&
+ (item.properties.layer === 'stop' || item.properties.layer === 'station')
+ ) {
+ const stopCode = getStopCode(item.properties);
+ const mode = item.properties.addendum?.GTFS.modes;
+ const platform = getPlatform(item.properties.addendum, lng, t);
+ return [
+ suggestionType,
+ getStopName(name, stopCode),
+ label,
+ stopCode,
+ mode,
+ platform,
+ ];
+ }
+ if (
+ item.properties.layer === 'favouriteStop' ||
+ item.properties.layer === 'favouriteStation'
+ ) {
+ const { address, code } = item.properties;
+ const stoName = address ? getStopName(address.split(',')[0], code) : name;
+ const platform = getPlatform(item.properties.addendum, lng);
+ return [suggestionType, stoName, label, code, undefined, platform];
+ }
+ return [suggestionType, name, label];
+ }
+ const { origin, destination } = item.properties;
+ const tail1 = origin.locality ? `, ${origin.locality} foobar` : '';
+ const tail2 = destination.locality ? `, ${destination.locality}` : '';
+ const name1 = origin.name;
+ const name2 = destination.name;
+ return [
+ t('future-route', { lng }),
+ `${t('origin', { lng })} ${name1}${tail1} ${t('destination', {
+ lng,
+ })} ${name2}${tail2}`,
+ item.translatedText,
+ ];
+}
+
+export function translateFutureRouteSuggestionTime(item, lng, t) {
+ // check if i81n is initialized
+ if (!t) {
+ return undefined;
+ }
+ const time = DateTime.fromSeconds(Number(item.properties.time));
+ const now = DateTime.now();
+ let str = item.properties.arriveBy
+ ? t('arrival', { lng })
+ : t('departure', { lng });
+ if (time.hasSame(now, 'day')) {
+ str = `${str} ${t('today-at')}`;
+ } else if (time.hasSame(now.plus({ days: 1 }), 'day')) {
+ str = `${str} ${t('tomorrow-at', { lng })}`;
+ } else {
+ str = `${str} ${time.toFormat('ccc d.L.')}`;
+ }
+ str = `${str} ${time.toFormat('HH:mm')}`;
+ return str;
+}
+
+export const getSuggestionValue = suggestion => {
+ if (
+ suggestion.type === 'SelectFromOwnLocations' ||
+ suggestion.type === 'back'
+ ) {
+ return '';
+ }
+ return getLabel(suggestion.properties);
+};
+
+export const suggestionAsAriaContent = ({ suggestions, t, lng }) => {
+ let label = [];
+ const firstSuggestion = suggestions[0];
+ if (firstSuggestion) {
+ if (firstSuggestion.type && firstSuggestion.type.includes('Favourite')) {
+ label.push(t('favourite', { lng }));
+ }
+ label = label.concat(getSuggestionContent(suggestions[0], lng));
+ }
+ return [...new Set(label)].join(' - ');
+};
diff --git a/digitransit-component/packages/digitransit-component/package.json b/digitransit-component/packages/digitransit-component/package.json
index 663e8bbba9..ed0cef7031 100644
--- a/digitransit-component/packages/digitransit-component/package.json
+++ b/digitransit-component/packages/digitransit-component/package.json
@@ -1,6 +1,6 @@
{
"name": "@digitransit-component/digitransit-component",
- "version": "4.0.2",
+ "version": "5.0.0",
"description": "a JavaScript library for Digitransit",
"main": "digitransit-component",
"module": "digitransit-component.mjs",
@@ -18,8 +18,8 @@
"url": "git://github.com/HSLdevcom/digitransit-ui.git"
},
"dependencies": {
- "@digitransit-component/digitransit-component-autosuggest": "^6.0.4",
- "@digitransit-component/digitransit-component-autosuggest-panel": "^7.0.4",
+ "@digitransit-component/digitransit-component-autosuggest": "^7.0.0",
+ "@digitransit-component/digitransit-component-autosuggest-panel": "^8.0.0",
"@digitransit-component/digitransit-component-control-panel": "^6.0.2",
"@digitransit-component/digitransit-component-favourite-bar": "^4.0.3",
"@digitransit-component/digitransit-component-favourite-editing-modal": "^4.0.1",
diff --git a/package.json b/package.json
index e3b9ce38b8..d4588b2547 100644
--- a/package.json
+++ b/package.json
@@ -227,8 +227,9 @@
"@lerna/filter-packages": "3.18.0",
"@lerna/project": "3.21.0",
"@rollup/plugin-babel": "5.2.2",
+ "@rollup/plugin-commonjs": "^28.0.8",
"@rollup/plugin-json": "4.1.0",
- "@rollup/plugin-node-resolve": "11.0.1",
+ "@rollup/plugin-node-resolve": "^16.0.3",
"@testing-library/react-hooks": "^8.0.1",
"async": "^3.2.6",
"autoprefixer": "9.8.6",
diff --git a/yarn.lock b/yarn.lock
index 2f74658238..e5b67d5211 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2003,27 +2003,26 @@ __metadata:
languageName: unknown
linkType: soft
-"@digitransit-component/digitransit-component-autosuggest-panel@^7.0.4, @digitransit-component/digitransit-component-autosuggest-panel@workspace:digitransit-component/packages/digitransit-component-autosuggest-panel":
+"@digitransit-component/digitransit-component-autosuggest-panel@^8.0.0, @digitransit-component/digitransit-component-autosuggest-panel@workspace:digitransit-component/packages/digitransit-component-autosuggest-panel":
version: 0.0.0-use.local
resolution: "@digitransit-component/digitransit-component-autosuggest-panel@workspace:digitransit-component/packages/digitransit-component-autosuggest-panel"
peerDependencies:
- "@digitransit-component/digitransit-component-autosuggest": ^6.0.4
+ "@digitransit-component/digitransit-component-autosuggest": ^7.0.0
"@digitransit-component/digitransit-component-icon": ^1.2.0
"@hsl-fi/sass": ^0.2.0
classnames: 2.5.1
+ downshift: 9.0.10
i18next: ^22.5.1
lodash: 4.17.21
lodash-es: 4.17.21
prop-types: ^15.8.1
react: ^16.13.0
- react-autosuggest: ^10.0.0
- react-autowhatever: 10.2.1
react-i18next: ^12.3.1
react-sortablejs: 2.0.11
languageName: unknown
linkType: soft
-"@digitransit-component/digitransit-component-autosuggest@^6.0.4, @digitransit-component/digitransit-component-autosuggest@workspace:digitransit-component/packages/digitransit-component-autosuggest":
+"@digitransit-component/digitransit-component-autosuggest@^7.0.0, @digitransit-component/digitransit-component-autosuggest@workspace:digitransit-component/packages/digitransit-component-autosuggest":
version: 0.0.0-use.local
resolution: "@digitransit-component/digitransit-component-autosuggest@workspace:digitransit-component/packages/digitransit-component-autosuggest"
dependencies:
@@ -2037,13 +2036,13 @@ __metadata:
"@digitransit-component/digitransit-component-suggestion-item": ^2.3.1
"@hsl-fi/sass": ^0.2.0
classnames: 2.5.1
+ downshift: 9.0.10
i18next: ^22.5.1
lodash: 4.17.21
lodash-es: 4.17.21
luxon: ^3.6.1
prop-types: ^15.8.1
react: ^16.13.0
- react-autosuggest: ^10.0.0
react-i18next: ^12.3.1
react-modal: ~3.11.2
languageName: unknown
@@ -2202,8 +2201,8 @@ __metadata:
version: 0.0.0-use.local
resolution: "@digitransit-component/digitransit-component@workspace:digitransit-component/packages/digitransit-component"
dependencies:
- "@digitransit-component/digitransit-component-autosuggest": ^6.0.4
- "@digitransit-component/digitransit-component-autosuggest-panel": ^7.0.4
+ "@digitransit-component/digitransit-component-autosuggest": ^7.0.0
+ "@digitransit-component/digitransit-component-autosuggest-panel": ^8.0.0
"@digitransit-component/digitransit-component-control-panel": ^6.0.2
"@digitransit-component/digitransit-component-favourite-bar": ^4.0.3
"@digitransit-component/digitransit-component-favourite-editing-modal": ^4.0.1
@@ -4429,6 +4428,13 @@ __metadata:
languageName: node
linkType: hard
+"@jridgewell/sourcemap-codec@npm:^1.5.5":
+ version: 1.5.5
+ resolution: "@jridgewell/sourcemap-codec@npm:1.5.5"
+ checksum: c2e36e67971f719a8a3a85ef5a5f580622437cc723c35d03ebd0c9c0b06418700ef006f58af742791f71f6a4fc68fcfaf1f6a74ec2f9a3332860e9373459dae7
+ languageName: node
+ linkType: hard
+
"@jridgewell/trace-mapping@npm:^0.3.12, @jridgewell/trace-mapping@npm:^0.3.18, @jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.25":
version: 0.3.25
resolution: "@jridgewell/trace-mapping@npm:0.3.25"
@@ -5385,6 +5391,26 @@ __metadata:
languageName: node
linkType: hard
+"@rollup/plugin-commonjs@npm:^28.0.8":
+ version: 28.0.8
+ resolution: "@rollup/plugin-commonjs@npm:28.0.8"
+ dependencies:
+ "@rollup/pluginutils": ^5.0.1
+ commondir: ^1.0.1
+ estree-walker: ^2.0.2
+ fdir: ^6.2.0
+ is-reference: 1.2.1
+ magic-string: ^0.30.3
+ picomatch: ^4.0.2
+ peerDependencies:
+ rollup: ^2.68.0||^3.0.0||^4.0.0
+ peerDependenciesMeta:
+ rollup:
+ optional: true
+ checksum: 048577825668c2aa30bbe268f63a811467892ce6123a920e1bcbcc1f0cbfc2239cc6d6a6271d6e14ce8b4d3f1a1fecf1ff9a297f57b4c36ef79c013d3a1ee233
+ languageName: node
+ linkType: hard
+
"@rollup/plugin-json@npm:4.1.0":
version: 4.1.0
resolution: "@rollup/plugin-json@npm:4.1.0"
@@ -5396,19 +5422,21 @@ __metadata:
languageName: node
linkType: hard
-"@rollup/plugin-node-resolve@npm:11.0.1":
- version: 11.0.1
- resolution: "@rollup/plugin-node-resolve@npm:11.0.1"
+"@rollup/plugin-node-resolve@npm:^16.0.3":
+ version: 16.0.3
+ resolution: "@rollup/plugin-node-resolve@npm:16.0.3"
dependencies:
- "@rollup/pluginutils": ^3.1.0
- "@types/resolve": 1.17.1
- builtin-modules: ^3.1.0
+ "@rollup/pluginutils": ^5.0.1
+ "@types/resolve": 1.20.2
deepmerge: ^4.2.2
is-module: ^1.0.0
- resolve: ^1.19.0
+ resolve: ^1.22.1
peerDependencies:
- rollup: ^1.20.0||^2.0.0
- checksum: c493e6a7228c2159f532c8bf551b6d94da6dc5474240614931216ce86c08427dd4fef7b7fec1e6014073a63f02f647981a5f19abe53f72a1486ca5bbda02b0cf
+ rollup: ^2.78.0||^3.0.0||^4.0.0
+ peerDependenciesMeta:
+ rollup:
+ optional: true
+ checksum: 2e453cf365f1fa5602c94d2344468cb6c4c0b777ade97694bbe14913aa29f65e11d0be6ef840cb3088c746b8f5055623714cd168a5f948dc9cfef896875e616e
languageName: node
linkType: hard
@@ -5425,6 +5453,22 @@ __metadata:
languageName: node
linkType: hard
+"@rollup/pluginutils@npm:^5.0.1":
+ version: 5.3.0
+ resolution: "@rollup/pluginutils@npm:5.3.0"
+ dependencies:
+ "@types/estree": ^1.0.0
+ estree-walker: ^2.0.2
+ picomatch: ^4.0.2
+ peerDependencies:
+ rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0
+ peerDependenciesMeta:
+ rollup:
+ optional: true
+ checksum: 2df47496f1f380ce67426b6d31cada1354b40844bb333b365653720b0847ce45446f347ae50313ed17a56c1b4cbba27431c42733ad75ad08764df5b4312946d9
+ languageName: node
+ linkType: hard
+
"@sideway/address@npm:^4.1.5":
version: 4.1.5
resolution: "@sideway/address@npm:4.1.5"
@@ -5809,6 +5853,13 @@ __metadata:
languageName: node
linkType: hard
+"@types/estree@npm:^1.0.0":
+ version: 1.0.8
+ resolution: "@types/estree@npm:1.0.8"
+ checksum: bd93e2e415b6f182ec4da1074e1f36c480f1d26add3e696d54fb30c09bc470897e41361c8fd957bf0985024f8fbf1e6e2aff977d79352ef7eb93a5c6dcff6c11
+ languageName: node
+ linkType: hard
+
"@types/express-serve-static-core@npm:*, @types/express-serve-static-core@npm:^5.0.0":
version: 5.0.6
resolution: "@types/express-serve-static-core@npm:5.0.6"
@@ -6117,12 +6168,10 @@ __metadata:
languageName: node
linkType: hard
-"@types/resolve@npm:1.17.1":
- version: 1.17.1
- resolution: "@types/resolve@npm:1.17.1"
- dependencies:
- "@types/node": "*"
- checksum: dc6a6df507656004e242dcb02c784479deca516d5f4b58a1707e708022b269ae147e1da0521f3e8ad0d63638869d87e0adc023f0bd5454aa6f72ac66c7525cf5
+"@types/resolve@npm:1.20.2":
+ version: 1.20.2
+ resolution: "@types/resolve@npm:1.20.2"
+ checksum: 61c2cad2499ffc8eab36e3b773945d337d848d3ac6b7b0a87c805ba814bc838ef2f262fc0f109bfd8d2e0898ff8bd80ad1025f9ff64f1f71d3d4294c9f14e5f6
languageName: node
linkType: hard
@@ -8338,13 +8387,6 @@ __metadata:
languageName: node
linkType: hard
-"builtin-modules@npm:^3.1.0":
- version: 3.3.0
- resolution: "builtin-modules@npm:3.3.0"
- checksum: db021755d7ed8be048f25668fe2117620861ef6703ea2c65ed2779c9e3636d5c3b82325bd912244293959ff3ae303afa3471f6a15bf5060c103e4cc3a839749d
- languageName: node
- linkType: hard
-
"builtin-status-codes@npm:^3.0.0":
version: 3.0.0
resolution: "builtin-status-codes@npm:3.0.0"
@@ -11135,8 +11177,9 @@ __metadata:
"@mapbox/sphericalmercator": 1.1.0
"@mapbox/vector-tile": 1.3.1
"@rollup/plugin-babel": 5.2.2
+ "@rollup/plugin-commonjs": ^28.0.8
"@rollup/plugin-json": 4.1.0
- "@rollup/plugin-node-resolve": 11.0.1
+ "@rollup/plugin-node-resolve": ^16.0.3
"@testing-library/react-hooks": ^8.0.1
async: ^3.2.6
autoprefixer: 9.8.6
@@ -13248,7 +13291,7 @@ __metadata:
languageName: node
linkType: hard
-"fdir@npm:^6.4.3":
+"fdir@npm:^6.2.0, fdir@npm:^6.4.3":
version: 6.5.0
resolution: "fdir@npm:6.5.0"
peerDependencies:
@@ -16033,7 +16076,7 @@ __metadata:
languageName: node
linkType: hard
-"is-core-module@npm:^2.13.0, is-core-module@npm:^2.13.1, is-core-module@npm:^2.15.1, is-core-module@npm:^2.16.0, is-core-module@npm:^2.5.0":
+"is-core-module@npm:^2.13.0, is-core-module@npm:^2.13.1, is-core-module@npm:^2.15.1, is-core-module@npm:^2.16.0, is-core-module@npm:^2.16.1, is-core-module@npm:^2.5.0":
version: 2.16.1
resolution: "is-core-module@npm:2.16.1"
dependencies:
@@ -16394,7 +16437,7 @@ __metadata:
languageName: node
linkType: hard
-"is-reference@npm:^1.1.2":
+"is-reference@npm:1.2.1, is-reference@npm:^1.1.2":
version: 1.2.1
resolution: "is-reference@npm:1.2.1"
dependencies:
@@ -18597,6 +18640,15 @@ __metadata:
languageName: node
linkType: hard
+"magic-string@npm:^0.30.3":
+ version: 0.30.21
+ resolution: "magic-string@npm:0.30.21"
+ dependencies:
+ "@jridgewell/sourcemap-codec": ^1.5.5
+ checksum: 4ff76a4e8d439431cf49f039658751ed351962d044e5955adc257489569bd676019c906b631f86319217689d04815d7d064ee3ff08ab82ae65b7655a7e82a414
+ languageName: node
+ linkType: hard
+
"make-dir@npm:4.0.0, make-dir@npm:^4.0.0":
version: 4.0.0
resolution: "make-dir@npm:4.0.0"
@@ -24763,6 +24815,19 @@ __metadata:
languageName: node
linkType: hard
+"resolve@npm:^1.22.1":
+ version: 1.22.11
+ resolution: "resolve@npm:1.22.11"
+ dependencies:
+ is-core-module: ^2.16.1
+ path-parse: ^1.0.7
+ supports-preserve-symlinks-flag: ^1.0.0
+ bin:
+ resolve: bin/resolve
+ checksum: 6d5baa2156b95a65ac431e7642e21106584e9f4194da50871cae8bc1bbd2b53bb7cee573c92543d83bb999620b224a087f62379d800ed1ccb189da6df5d78d50
+ languageName: node
+ linkType: hard
+
"resolve@npm:^2.0.0-next.4, resolve@npm:^2.0.0-next.5":
version: 2.0.0-next.5
resolution: "resolve@npm:2.0.0-next.5"
@@ -24796,6 +24861,19 @@ __metadata:
languageName: node
linkType: hard
+"resolve@patch:resolve@^1.22.1#~builtin":
+ version: 1.22.11
+ resolution: "resolve@patch:resolve@npm%3A1.22.11#~builtin::version=1.22.11&hash=c3c19d"
+ dependencies:
+ is-core-module: ^2.16.1
+ path-parse: ^1.0.7
+ supports-preserve-symlinks-flag: ^1.0.0
+ bin:
+ resolve: bin/resolve
+ checksum: 1462da84ac3410d7c2e12e4f5f25c1423d8a174c3b4245c43eafea85e7bbe6af3eb7ec10a4850b5e518e8531608604742b8cbd761e1acd7ad1035108b7c98013
+ languageName: node
+ linkType: hard
+
"resolve@patch:resolve@^2.0.0-next.4#~builtin, resolve@patch:resolve@^2.0.0-next.5#~builtin":
version: 2.0.0-next.5
resolution: "resolve@patch:resolve@npm%3A2.0.0-next.5#~builtin::version=2.0.0-next.5&hash=c3c19d"