mirror of
https://github.com/HSLdevcom/digitransit-ui
synced 2025-12-23 00:00:48 +01:00
273 lines
8.1 KiB
JavaScript
273 lines
8.1 KiB
JavaScript
import React from 'react';
|
|
import PropTypes from 'prop-types';
|
|
import { useSelect } from 'downshift';
|
|
import { Link, routerShape } from 'found';
|
|
import { FormattedMessage, intlShape } from 'react-intl';
|
|
import cx from 'classnames';
|
|
import Icon from '../Icon';
|
|
import { routePagePath } from '../../util/path';
|
|
import { addAnalyticsEvent } from '../../util/analyticsUtils';
|
|
import { configShape, patternShape } from '../../util/shapes';
|
|
|
|
function patternOptionText(pattern) {
|
|
return pattern
|
|
? `${pattern.stops[0].name} ➔ ${
|
|
pattern.headsign || pattern.stops[pattern.stops.length - 1].name
|
|
}`
|
|
: '';
|
|
}
|
|
|
|
export function patternTextWithIcon(pattern) {
|
|
if (pattern) {
|
|
const text = patternOptionText(pattern);
|
|
const i = text.search(/➔/);
|
|
if (i === -1) {
|
|
return text;
|
|
}
|
|
return (
|
|
<>
|
|
{text.slice(0, i)}
|
|
<Icon className="in-text-arrow" img="icon_arrow-right-long" />
|
|
<span className="sr-only">➔</span>
|
|
{text.slice(i + 1)}
|
|
</>
|
|
);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Renders a single option as a list item
|
|
* @param props
|
|
* @param props.option option to be rendered, can be a pattern or a similar route
|
|
* @param props.optionIndexTable lookup table from option id to index
|
|
* @param props.highlightedIndex index of the currently highlighted option
|
|
* @param props.getItemProps returns downshift item props
|
|
* @param currentPattern currently selected pattern
|
|
* @returns {JSX.Element}
|
|
*/
|
|
function PatternOption(
|
|
{ option, optionIndexTable, highlightedIndex, getItemProps, currentPattern },
|
|
{ intl },
|
|
) {
|
|
const isSelected = option.code === currentPattern.code;
|
|
const selectedText = isSelected
|
|
? intl.formatMessage({ id: 'route-page.pattern-chosen' })
|
|
: '';
|
|
return (
|
|
// option is a pattern
|
|
(option.stops && (
|
|
<li
|
|
aria-label={`${patternOptionText(option)}, ${selectedText}`}
|
|
className={cx(
|
|
'suggestion',
|
|
optionIndexTable[option.code] === highlightedIndex &&
|
|
'suggestion--highlighted',
|
|
)}
|
|
{...getItemProps({
|
|
item: option,
|
|
index: optionIndexTable[option.code],
|
|
})}
|
|
>
|
|
{patternTextWithIcon(option)}
|
|
{isSelected && (
|
|
<Icon aria-hidden="true" className="check" img="icon_check" />
|
|
)}
|
|
</li>
|
|
)) ||
|
|
// option is a similar route
|
|
(option.shortName && option.longName && option.mode && (
|
|
<li
|
|
className={cx(
|
|
'suggestion',
|
|
optionIndexTable[option.gtfsId] === highlightedIndex &&
|
|
'suggestion--highlighted',
|
|
)}
|
|
{...getItemProps({
|
|
item: option,
|
|
index: optionIndexTable[option.code],
|
|
})}
|
|
>
|
|
<Link
|
|
to={routePagePath(option.gtfsId)}
|
|
onClick={e => {
|
|
e.stopPropagation();
|
|
}}
|
|
>
|
|
<div className="similar-route">
|
|
<Icon
|
|
className={option.mode.toLowerCase()}
|
|
img={`icon_${option.mode.toLowerCase()}`}
|
|
color={option.color ? `#${option.color}` : null}
|
|
/>
|
|
<div className="similar-route-text">
|
|
<span className="similar-route-name">{option.shortName}</span>
|
|
<span className="similar-route-longname">{option.longName}</span>
|
|
</div>
|
|
<div className="similar-route-arrow-container">
|
|
<Icon
|
|
className="similar-route-arrow"
|
|
img="icon_arrow-collapse--right"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</Link>
|
|
</li>
|
|
)) ||
|
|
null
|
|
);
|
|
}
|
|
|
|
PatternOption.propTypes = {
|
|
option: patternShape.isRequired,
|
|
optionIndexTable: PropTypes.objectOf(PropTypes.number.isRequired).isRequired,
|
|
highlightedIndex: PropTypes.number.isRequired,
|
|
getItemProps: PropTypes.func.isRequired,
|
|
currentPattern: patternShape.isRequired,
|
|
};
|
|
|
|
PatternOption.contextTypes = {
|
|
intl: intlShape.isRequired,
|
|
};
|
|
|
|
export default function RoutePatternSelect(
|
|
{ currentPattern, optionArray, onSelectChange, className },
|
|
{ config, router },
|
|
) {
|
|
// flatten optionArray to an ungrouped 1-D array
|
|
const flattenedOptions = optionArray.reduce(
|
|
(options, group) => options.concat(group.options),
|
|
[],
|
|
);
|
|
// lookup table for getting the index of an option, used for highlighting
|
|
const optionIndexTable = flattenedOptions.reduce(
|
|
(map, { code, gtfsId }, index) => {
|
|
// eslint-disable-next-line no-param-reassign
|
|
map[code || gtfsId] = index;
|
|
return map;
|
|
},
|
|
{},
|
|
);
|
|
|
|
// refer to useSelect hook in downshift.js documentation
|
|
const {
|
|
isOpen,
|
|
highlightedIndex,
|
|
getToggleButtonProps,
|
|
getMenuProps,
|
|
getItemProps,
|
|
getLabelProps,
|
|
} = useSelect({
|
|
selectedItem: currentPattern,
|
|
items: flattenedOptions,
|
|
onSelectedItemChange: ({ selectedItem }) =>
|
|
// if selected item is a similar route, redirect to route page
|
|
(selectedItem.gtfsId &&
|
|
router.push(routePagePath(selectedItem.gtfsId))) ||
|
|
onSelectChange(selectedItem.code),
|
|
onIsOpenChange: changes =>
|
|
changes.isOpen &&
|
|
addAnalyticsEvent({
|
|
category: 'Route',
|
|
action: 'OpenDirectionMenu',
|
|
name: null,
|
|
}),
|
|
labelId: 'route-pattern-select-label',
|
|
menuId: 'route-pattern-select-menu',
|
|
toggleButtonId: 'route-pattern-select-toggle',
|
|
});
|
|
|
|
const sectionTitleFontWeight = config.appBarStyle === 'hsl' ? 500 : 600;
|
|
return (
|
|
<div
|
|
className={cx('route-pattern-select', className)}
|
|
aria-atomic="true"
|
|
style={{
|
|
'--sectionTitleFontWeight': `${sectionTitleFontWeight}`,
|
|
}}
|
|
>
|
|
<div
|
|
className={cx(
|
|
'pattern-select-container',
|
|
isOpen && 'pattern-select-container--open',
|
|
)}
|
|
>
|
|
<div>
|
|
<label {...getLabelProps()}>
|
|
<span tabIndex={-1} className="sr-only">
|
|
{patternOptionText(currentPattern)}
|
|
</span>
|
|
<span className="sr-only">
|
|
<FormattedMessage id="route-page.pattern-select-title" />
|
|
</span>
|
|
</label>
|
|
<div {...getToggleButtonProps()}>
|
|
<span>
|
|
<div className="input-display" aria-hidden="true">
|
|
{patternTextWithIcon(currentPattern)}
|
|
<Icon className="dropdown-arrow" img="icon_arrow-collapse" />
|
|
</div>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
className={cx(
|
|
'suggestions-container',
|
|
isOpen && 'suggestions-container--open',
|
|
)}
|
|
hidden={!isOpen}
|
|
{...getMenuProps({})}
|
|
>
|
|
{optionArray.map((section, sectionIndex) => {
|
|
return (
|
|
<div
|
|
key={`section-${section.name}`}
|
|
className="section-container"
|
|
>
|
|
<ul aria-labelledby={`section-${sectionIndex}`} role="group">
|
|
<label
|
|
id={`section-${sectionIndex}`}
|
|
className={cx([
|
|
'section-title',
|
|
section.name ? '' : 'sr-only',
|
|
])}
|
|
>
|
|
{section.name}
|
|
</label>
|
|
{section.options.map(option => (
|
|
<PatternOption
|
|
key={option.code || option.gtfsId}
|
|
option={option}
|
|
optionIndexTable={optionIndexTable}
|
|
highlightedIndex={highlightedIndex}
|
|
getItemProps={getItemProps}
|
|
currentPattern={currentPattern}
|
|
/>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
RoutePatternSelect.propTypes = {
|
|
currentPattern: patternShape.isRequired,
|
|
optionArray: PropTypes.arrayOf(
|
|
PropTypes.shape({
|
|
name: PropTypes.string,
|
|
options: PropTypes.arrayOf(patternShape).isRequired,
|
|
}),
|
|
).isRequired,
|
|
onSelectChange: PropTypes.func.isRequired,
|
|
className: PropTypes.string.isRequired,
|
|
};
|
|
|
|
RoutePatternSelect.contextTypes = {
|
|
config: configShape.isRequired,
|
|
router: routerShape.isRequired,
|
|
};
|