// Copyright (c) 2022 Uber Technologies, Inc. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import React, {Component, ElementType, createRef} from 'react'; import classNames from 'classnames'; import styled from 'styled-components'; import ItemSelectorSubmenu from './item-selector-submenu'; import {ChevronRight} from 'components/common/icons'; import {FIELD_AGGREGATORS} from 'constants/default-settings'; import Portaled from '../portaled'; export const classList = { list: 'list-selector', listHeader: 'list__header', listSection: 'list__section', listItem: 'list__item', listItemAnchor: 'list__item__anchor' }; const defaultDisplay = d => d.value || d; const Divider = styled.div` border-top: 1px solid rgba(44, 48, 50, 0.12); margin: 8px 0; `; export const ListItem = ({value, displayOption = defaultDisplay, light}) => ( {displayOption(value)} ); interface DropdownListWrapperProps { light?: boolean; } const DropdownListWrapper = styled.div` background-color: ${props => props.light ? props.theme.dropdownListBgdLT : props.theme.dropdownListBgd}; border-top: 1px solid ${props => props.light ? props.theme.dropdownListBorderTopLT : props.theme.dropdownListBorderTop}; ${props => (props.light ? props.theme.dropdownListLT : props.theme.dropdownList)}; `; const DropdownListContainer = styled.div` position: relative; `; const Item = styled.div` position: relative; padding-right: 25px; display: flex; align-items: center; justify-content: space-between; svg { position: absolute; right: 0; top: 50%; transform: translateY(-50%); opacity: 0.6; } &:hover, &.hover { svg { opacity: 1; } } `; interface DropdownListProps { options?: any[]; subOptions?: any[]; allowCustomValues?: number; customClasses?: object; customValues?: any[]; customListItemComponent?: ElementType; customListHeaderComponent?: ElementType; selectionIndex?: number; onOptionSelected?: Function; displayOption?: Function; defaultClassNames?: boolean; areResultsTruncated?: boolean; resultsTruncatedMessage?: string; listItemComponent?: Function; light?: boolean; fixedOptions?: any[]; searchEntryValue?: string; withSubOptions?: boolean; }; export default class DropdownList extends Component { static defaultProps = { customClasses: {}, customListItemComponent: ListItem, customListHeaderComponent: null, allowCustomValues: 0, customValues: [], displayOption: defaultDisplay, onOptionSelected: () => {}, defaultClassNames: true, selectionIndex: null, withSubOptions: false }; state = { currentOption: {} as any, submenuVisible: false, submenuPosition: {} as {left: number, top: number} }; hasSubOptions = !!this.props.subOptions; ref = createRef(); _resetState(callback?) { this.setState({currentOption: {}, submenuVisible: false, submenuPosition: {}}, callback); } _onClick(option, subOption, event) { const optionSelected = typeof option !== 'object' ? option : { ...option, selectedAggregator: subOption?.id }; if ( (this.props.withSubOptions && (subOption?.id || !option.aggregators)) || !this.props.withSubOptions ) { event.preventDefault(); this.props.onOptionSelected?.(optionSelected, event); } } _onMouseOver(option, event) { const el = event.target this.setState((previousState) => ({ currentOption: option, submenuVisible: true, submenuPosition: { top: el.getBoundingClientRect().top, left: (previousState as any).submenuPosition.left ?(previousState as any).submenuPosition.left : this.ref.current?.getBoundingClientRect().right } })); } componentDidUpdate(prevProps) { if (prevProps.searchEntryValue !== this.props.searchEntryValue) { this._resetState(); } } componentDidMount() { this.setState({ subMenuPosition: { left: this.ref.current?.getBoundingClientRect().right } }) } render() { const { fixedOptions, light, allowCustomValues = 0, customListItemComponent: CustomListItemComponent = ListItem } = this.props; const {displayOption: display = defaultDisplay} = this.props; // Don't render if there are no options to display if (!this.props.options?.length && allowCustomValues <= 0) { return false; } const valueOffset = Array.isArray(fixedOptions) ? fixedOptions.length : 0; // For some reason onClick is not fired when clicked on an option // onMouseDown is used here as a workaround of #205 and other return ( this._resetState()}> {this.props.customListHeaderComponent ? (
) : null}
{valueOffset > 0 ? (
{fixedOptions?.map((value, i) => (
!this.hasSubOptions && this._onClick(value, null, e)} onClick={e => !this.hasSubOptions && this._onClick(value, null, e)} onMouseEnter={e => this._onMouseOver(value, e)} >
))}
) : null} {this.props.options?.map((value, i) => (
{value.divider && } !this.hasSubOptions && this._onClick(value, null, e)} onClick={e => !this.hasSubOptions && this._onClick(value, null, e)} onMouseEnter={e => this._onMouseOver(value, e)} > {this.props.withSubOptions && value.aggregators && }
))}
{this.props.withSubOptions && ( // The display: 'none' is a trick to be able to render the ItemSelectorSubmenu in the correct position // Even with display: 'none' the Portaled component render its child on the top of the DOM
FIELD_AGGREGATORS.find(am => am.id === a) )} onClick={(aggregator, e) => this._onClick(this.state.currentOption, aggregator, e)} />
)}
); } }