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 && ( + + )} + + {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 ( + +
+
+ + + + {mobileLabel} + + clearInput(inputRef)} + ariaLabel={ariaLabel} + inputRef={inputRef} + styles={styles} + renderLabel={false} + clearButtonColor={clearButtonColor} + autoFocus + inputClassName={inputClassName} + required={required} + isMobile + /> + +
+ 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.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 ( - - ); - } - 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 ( - - ); - }; - - const renderContent = () => { - return ( -
    - - - - {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 => ( - <> - - - {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"