digitransit-ui/app/component/itinerary/customizesearch/SearchSettingsDropdown.js
2026-02-20 14:41:28 +02:00

275 lines
8.1 KiB
JavaScript

/* eslint-disable jsx-a11y/click-events-have-key-events, no-sequences */
/* eslint-disable jsx-a11y/no-noninteractive-element-interactions */
import PropTypes from 'prop-types';
import React, { useEffect, useState, useRef } from 'react';
import Icon from '../../Icon';
import { useTranslationsContext } from '../../../util/useTranslationsContext';
const roundToOneDecimal = number => {
const rounded = Math.round(number * 10) / 10;
return rounded.toFixed(1).replace('.', ',');
};
/**
* Builds an array of options: least, less, default, more, most with preset
* multipliers or given values for each option. Note: a higher value (relative to
* the given value) means less/worse.
*
* @param {*} options The options to select from.
*/
export const getFiveStepOptions = options => [
{
title: 'option-least',
value: options.least || options[0],
kmhValue: `${roundToOneDecimal(options[0] * 3.6)} km/h`,
},
{
title: 'option-less',
value: options.less || options[1],
kmhValue: `${roundToOneDecimal(options[1] * 3.6)} km/h`,
},
{
title: 'option-default',
value: options[2],
kmhValue: `${roundToOneDecimal(options[2] * 3.6)} km/h`,
},
{
title: 'option-more',
value: options.more || options[3],
kmhValue: `${roundToOneDecimal(options[3] * 3.6)} km/h`,
},
{
title: 'option-most',
value: options.most || options[4],
kmhValue: `${roundToOneDecimal(options[4] * 3.6)} km/h`,
},
];
export const getFiveStepOptionsNumerical = options => {
const numericalOptions = [];
options.forEach(item => {
numericalOptions.push({
title: `${Math.round(item * 3.6)} km/h`,
value: item,
});
});
return numericalOptions;
};
/**
* Represents the types of acceptable values.
*/
export const valueShape = PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
// eslint-disable-next-line
PropTypes.object,
]);
function SearchSettingsDropdown(props) {
const {
labelText,
currentSelection,
options,
displayValueFormatter,
highlightDefaultValue,
defaultValue,
formatOptions,
translateLabels,
overrideStyle,
} = props;
const intl = useTranslationsContext();
const [showDropdown, setShowDropdown] = useState(false);
const labelRef = useRef(null);
useEffect(() => {
if (showDropdown && labelRef.current) {
labelRef.current.scrollIntoView({ block: 'nearest' });
}
}, [showDropdown]);
const toggleDropdown = prevState => {
setShowDropdown(!prevState);
};
const handleDropdownClick = prevState => {
toggleDropdown(prevState);
};
const handleChangeOnly = value => {
props.onOptionSelected(value);
};
const getOptionTags = (dropdownOptions, prevState) => {
return dropdownOptions.map(option => (
<li key={option.displayName + option.value}>
<label
className={`settings-dropdown-choice ${
option.value === currentSelection.value ? 'selected' : ''
}`}
htmlFor={`dropdown-${props.name}-${option.value}`}
>
<span>
{option.displayNameObject
? option.displayNameObject
: option.displayName}
</span>
<span className="right-side">
<span className="kmh-value">{option.kmhValue}</span>
<span className="checkmark">
&nbsp;
{option.value === currentSelection.value && (
<Icon
className="selected-checkmark"
img="icon_check"
viewBox="0 0 15 11"
/>
)}
</span>
<input
id={`dropdown-${props.name}-${option.value}`}
type="radio"
name={props.name}
checked={option.value === currentSelection.value}
value={option.value}
onChange={e => {
handleChangeOnly(option.value);
// try to detect if event is from an actual click or keyboard navigation
if (e.nativeEvent.clientX || e.nativeEvent.clientY) {
handleDropdownClick(prevState);
}
}}
/>
</span>
</label>
</li>
));
};
const applyDefaultValueIdentifier = (value, str) =>
highlightDefaultValue && value === defaultValue
? `${intl.formatMessage({
id: 'option-default',
})} (${str})`
: str;
const getFormattedValue = value =>
displayValueFormatter ? displayValueFormatter(value) : value;
const selectOptions = formatOptions
? options.map(o =>
o.title && o.value
? {
displayName: `${o.title}_${o.value}`,
displayNameObject: applyDefaultValueIdentifier(
o.value,
translateLabels
? intl.formatMessage(
{ id: o.title },
{
title: o.title,
},
)
: o.title,
),
value: o.value,
kmhValue: o.kmhValue || undefined,
}
: {
displayName: `${props.displayPattern}_${o}`,
displayNameObject: applyDefaultValueIdentifier(
o,
// eslint-disable-next-line no-nested-ternary
props.displayPattern
? translateLabels
? intl.formatMessage(
{ id: props.displayPattern },
{
number: getFormattedValue(o),
},
)
: ({ id: props.displayPattern },
{ number: getFormattedValue(o) })
: getFormattedValue(o),
),
value: o,
kmhValue: o.kmhValue || undefined,
},
)
: options;
return (
<div className="settings-dropdown-wrapper" ref={labelRef}>
<button
type="button"
className="settings-dropdown-label"
style={overrideStyle}
onClick={() => toggleDropdown(showDropdown)}
>
<p className="settings-dropdown-label-text">{labelText}</p>
<span className="settings-dropdown-text-container">
<p className="settings-dropdown-label-value">
{/* eslint-disable-next-line no-nested-ternary */}
{displayValueFormatter
? displayValueFormatter(currentSelection.title)
: translateLabels
? `${intl.formatMessage({
id: currentSelection.title,
})}`
: currentSelection.title}
</p>
<span
aria-label={intl.formatMessage({
id: showDropdown
? 'settings-dropdown-close-label'
: 'settings-dropdown-open-label',
})}
/>
<Icon
className={
showDropdown ? 'fake-select-arrow inverted' : 'fake-select-arrow'
}
img="icon_arrow-dropdown"
/>
</span>
</button>
{showDropdown && (
<ul role="radiogroup" className="settings-dropdown">
{getOptionTags(selectOptions, showDropdown)}
</ul>
)}
</div>
);
}
SearchSettingsDropdown.propTypes = {
labelText: PropTypes.string.isRequired,
options: PropTypes.arrayOf(valueShape).isRequired,
displayValueFormatter: PropTypes.func,
currentSelection: PropTypes.shape({
title: PropTypes.string,
value: valueShape,
}).isRequired,
highlightDefaultValue: PropTypes.bool,
defaultValue: valueShape,
displayPattern: PropTypes.string,
onOptionSelected: PropTypes.func.isRequired,
formatOptions: PropTypes.bool,
name: PropTypes.string.isRequired,
translateLabels: PropTypes.bool,
// eslint-disable-next-line
overrideStyle: PropTypes.object,
};
SearchSettingsDropdown.defaultProps = {
displayValueFormatter: undefined,
highlightDefaultValue: false,
displayPattern: undefined,
defaultValue: undefined,
formatOptions: false,
translateLabels: true,
overrideStyle: {},
};
export default SearchSettingsDropdown;