UNPKG

wix-style-react

Version:
418 lines (321 loc) 14 kB
import defaultTo from 'lodash/defaultTo'; import isEqual from 'lodash/isEqual'; import sortBy from 'lodash/sortBy'; import { allValidators, extendPropTypes } from '../utils/propTypes'; import InputWithOptions from '../InputWithOptions'; import { st, classes } from './Dropdown.st.css'; import PropTypes from 'prop-types'; import { optionValidator } from '../DropdownLayout/DropdownLayout'; const NO_SELECTED_ID = null; class Dropdown extends InputWithOptions { constructor(props) { super(props); this.state = { value: '', selectedId: NO_SELECTED_ID, ...Dropdown.getNextState( props, defaultTo(props.selectedId, props.initialSelectedId), ), }; } isSelectedIdControlled() { return typeof this.props.selectedId !== 'undefined'; } static isOptionsEqual(optionsA, optionsB) { return isEqual(sortBy(optionsA, 'id'), sortBy(optionsB, 'id')); } getSelectedId() { return this.isSelectedIdControlled() ? this.props.selectedId : this.state.selectedId; } _onInputClicked(event) { if (this.props.onInputClicked) { this.props.onInputClicked(event); } if (this.props.readOnly) { return; } if ( this.state.showOptions && Date.now() - this.state.lastOptionsShow > 200 ) { this.hideOptions(); } else { this.showOptions(); } } /** * Updates the value by the selectedId. * If selectedId is not found in options, then value is NOT changed. */ static getNextState(props, selectedId) { if (typeof selectedId !== 'undefined') { const option = props.options.find(_option => { return _option.id === selectedId; }); if (option) { const value = props.valueParser(option) || ''; return { value, selectedId }; } } return { value: '', selectedId: NO_SELECTED_ID }; } UNSAFE_componentWillReceiveProps(nextProps) { if ( nextProps.selectedId !== this.props.selectedId || !Dropdown.isOptionsEqual(this.props.options, nextProps.options) ) { this.setState( Dropdown.getNextState( nextProps, nextProps.selectedId, this.state.selectedId, ), ); } } inputClasses() { const { noBorder } = this.props; return st(classes.showPointer, { noBorder }); } dropdownAdditionalProps() { return { selectedId: this.getSelectedId(), value: this.state.value, tabIndex: -1, withArrow: false, }; } inputAdditionalProps() { return { disableEditing: true, value: this.state.value, }; } _onSelect(option) { if (!this.isSelectedIdControlled()) { this.setState({ value: this.props.valueParser(option), selectedId: option.id, }); } super._onSelect(option); } _onChange(event) { this.setState({ value: event.target.value }); super._onChange(event); } } Dropdown.propTypes = { /** An initial selected option id. (Implies Uncontrolled mode) */ initialSelectedId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), /** Associate a control with the regions that it controls. */ ariaControls: PropTypes.string, /** Associate a region with its descriptions. Similar to aria-controls but instead associating descriptions to the region and description identifiers are separated with a space. */ ariaDescribedby: PropTypes.string, /** Define a string that labels the current element in case where a text label is not visible on the screen. */ ariaLabel: PropTypes.string, /** Focus the element on mount (standard React input autoFocus). */ autoFocus: PropTypes.bool, /** Select the entire text of the element on focus (standard React input autoSelect). */ autoSelect: PropTypes.bool, /** Control the border style of input. */ border: PropTypes.oneOf(['standard', 'round', 'bottomLine']), /** Specifies a CSS class name to be appended to the component’s root element. */ className: PropTypes.string, /** Displays clear button (X) on a non-empty input. */ clearButton: PropTypes.bool, /** Closes DropdownLayout when option is selected. */ closeOnSelect: PropTypes.bool, /** Render a custom input component instead of the default html input tag. */ customInput: PropTypes.node, /** Applies a data-hook HTML attribute that can be used in the tests. */ dataHook: PropTypes.string, /** Specifies whether input should be disabled or not. */ disabled: PropTypes.bool, /** Restricts input editing. */ disableEditing: PropTypes.bool, /** Sets the offset of the dropdown from the left in pixels. */ dropdownOffsetLeft: PropTypes.string, /** Sets the width of the dropdown in pixels. */ dropdownWidth: PropTypes.string, /** Adds a fixed footer container at the bottom of options list in `<DropdownLayout/>`. */ fixedFooter: PropTypes.node, /** Adds a fixed header container at the top of options list in `<DropdownLayout/>`. */ fixedHeader: PropTypes.node, /** Highlights and scrolls view to the specified option when dropdown layout is opened. It does not select the specified option. */ focusOnOption: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), /** Scrolls view to the selected option when dropdown layout is opened. */ focusOnSelectedOption: PropTypes.bool, /** USED FOR TESTING. Forces focus state on the input. */ forceFocus: PropTypes.bool, /** USED FOR TESTING. Forces hover state on the input. */ forceHover: PropTypes.bool, /** Specifies whether there are more items to be loaded. */ hasMore: PropTypes.bool, /** Specifies whether status suffix should be hidden. */ hideStatusSuffix: PropTypes.bool, /** Assigns an unique identifier for the root element. */ id: PropTypes.string, /** Specifies whether `<DropdownLayout/>` is in a container component. If true, some styles such as shadows, positioning and padding will be added to the component contentContainer. */ inContainer: PropTypes.bool, /** Specifies whether lazy loading of the dropdown layout items is enabled. */ infiniteScroll: PropTypes.bool, /** Allows to render a custom input component instead of the default `<Input/>`. */ inputElement: PropTypes.element, /** Defines a callback function which is called on a request to render more list items. */ loadMore: PropTypes.func, /** Sets the default hover behavior: * - `false` - no initially hovered list item * - `true` - hover first selectable option * - any `number/string` specify the id of an option to be hovered */ markedOption: PropTypes.oneOfType([ PropTypes.bool, PropTypes.string, PropTypes.number, ]), /** Sets the maximum height of the dropdownLayout in pixels. */ maxHeightPixels: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), /** Specifies whether input should have a dropdown menu arrow on the right side. */ menuArrow: PropTypes.bool, /** Sets a minimum value of an input. Similar to HTML5 min attribute. */ min: PropTypes.number, /** Sets the minimum width of dropdownLayout in pixels. */ minWidthPixels: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), /** Reference element data when a form is submitted. */ name: PropTypes.string, /** Render options list via native select element. */ native: PropTypes.bool, /** Specifies whether input shouldn’t have rounded corners on its left. */ noLeftBorderRadius: PropTypes.bool, /** Specifies whether input shouldn’t have rounded corners on its right. */ noRightBorderRadius: PropTypes.bool, /** Defines a standard input onBlur callback */ onBlur: PropTypes.func, /** Displays clear button (X) on a non-empty input and calls a callback function with no arguments. */ onClear: PropTypes.func, /** Defines a callback function which is called whenever the user presses the escape key. */ onClose: PropTypes.func, /** Defines a standard input onChange callback. */ onChange: PropTypes.func, /** Defines a callback function called on compositionstart/compositionend events. */ onCompositionChange: PropTypes.func, /** Defines a callback handler that is called when user presses -enter-. */ onEnterPressed: PropTypes.func, /** Defines a callback handler that is called when user presses -escape- */ onEscapePressed: PropTypes.func, /** Defines a standard input onFocus callback. */ onFocus: PropTypes.func, /** Defines a standard input onClick callback. */ onInputClicked: PropTypes.func, /** Defines a standard input onKeyDown callback. */ onKeyDown: PropTypes.func, /** Defines a standard input onKeyUp callback. */ onKeyUp: PropTypes.func, /** Defines a callback function which is called when user performs a submit action. Submit action triggers are: * "Enter", "Tab", [typing any defined delimiters], paste action. * `onManuallyInput(values: Array<string>): void - The array of strings is the result of splitting the input value by the given delimiters */ onManuallyInput: PropTypes.func, /** Defines a callback function which is called whenever the user enters dropdown layout with the mouse cursor. */ onMouseEnter: PropTypes.func, /** Defines a callback function which is called whenever the user exits from dropdown layout with a mouse cursor. */ onMouseLeave: PropTypes.func, /** Defines a callback function which is called whenever an option becomes focused (hovered/active). Receives the relevant option object from the original props.options array. */ onOptionMarked: PropTypes.func, /** Defines a callback function which is called when options dropdown is hidden. */ onOptionsHide: PropTypes.func, /** Defines a callback function which is called when options dropdown is shown. */ onOptionsShow: PropTypes.func, /** Defines a callback function which is called whenever user selects a different option in the list. */ onSelect: PropTypes.func, /** Array of objects: * - `id <string / number>` *required*: the id of the option, should be unique; * - value `<function / string / node>` *required*: can be a string, react element or a builder function; * - disabled `<bool>` *default value- false*: whether this option is disabled or not; * - linkTo `<string>`: when provided the option will be an anchor to the given value; * - title `<bool>` *default value- false* **deprecated**: please use `listItemSectionBuilder` for rendering a title; * - overrideStyle `<bool>` *default value- false* **deprecated**: please use `overrideOptionStyle` for override option styles; * - overrideOptionStyle `<bool>` *default value- false* - when set to `true`, the option will be responsible to its own styles. No styles will be applied from the DropdownLayout itself; * - label `<string>`: the string displayed within an input when the option is selected. This is used when using `<DropdownLayout/>` with an `<Input/>`. */ options: PropTypes.arrayOf(optionValidator), /** Handles container overflow. */ overflow: PropTypes.string, /** Sets a placeholder message to display. */ placeholder: PropTypes.string, /** Allows to pass all common popover props. */ popoverProps: PropTypes.shape({ appendTo: PropTypes.oneOf(['window', 'scrollParent', 'parent', 'viewport']), maxWidth: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), minWidth: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), flip: PropTypes.bool, fixed: PropTypes.bool, placement: PropTypes.oneOf([ 'auto-start', 'auto', 'auto-end', 'top-start', 'top', 'top-end', 'right-start', 'right', 'right-end', 'bottom-end', 'bottom', 'bottom-start', 'left-end', 'left', 'left-start', ]), dynamicWidth: PropTypes.bool, }), /** Pass a component you want to show as the prefix of the input, e.g., text string, icon. */ prefix: PropTypes.node, /** Specifies whether input is read only. */ readOnly: PropTypes.bool, /** Flip component horizontally so it would be more suitable to RTL. */ rtl: PropTypes.bool, /** Specifies whether selected option will be highlighted when dropdown is reopened. */ selectedHighlight: PropTypes.bool, /** Specifies selected option by its id. */ selectedId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), /** Controls whether to show options if input is empty. */ showOptionsIfEmptyInput: PropTypes.bool, /** Controls the size of the input */ size: PropTypes.oneOf(['small', 'medium', 'large']), /** Specify the status of a field. */ status: PropTypes.oneOf(['error', 'warning', 'loading']), /** Defines the message to display on status icon hover. If not given or empty there will be no tooltip. */ statusMessage: PropTypes.node, /** Pass a component you want to show as the suffix of the input, e.g., text string, icon. */ suffix: PropTypes.node, /** Indicates that element can be focused and where it participates in sequential keyboard navigation. */ tabIndex: PropTypes.number, /** Handles text overflow behavior. It can either clip (default) or display ellipsis. */ textOverflow: PropTypes.string, /** Controls placement of a status tooltip. */ tooltipPlacement: PropTypes.string, /** Specifies whether component should be shown or hidden. */ visible: PropTypes.bool, }; extendPropTypes(Dropdown, { selectedId: allValidators( PropTypes.oneOfType([PropTypes.string, PropTypes.number]), (props, propName) => { if ( props[propName] !== undefined && props['initialSelectedId'] !== undefined ) { return new Error( `'selectedId' and 'initialSelectedId' cannot both be used at the same time.`, ); } }, ), }); Dropdown.defaultProps = InputWithOptions.defaultProps; Dropdown.displayName = 'Dropdown'; export default Dropdown;