UNPKG

optimizely-oui

Version:

Optimizely's Component Library.

326 lines (306 loc) 12.8 kB
import React from "react"; import classNames from "classnames"; import PropTypes from "prop-types"; import { Manager, Popper, Reference } from "react-popper"; import { withState } from "recompose"; import { withToggle } from "../../utils/recompose-utils"; import DropdownContents from "./DropdownContents"; import DropdownListItem from "./DropdownListItem"; import DropdownBlockLink from "./DropdownBlockLink"; import DropdownBlockLinkText from "./DropdownBlockLinkText"; import DropdownBlockLinkSecondaryText from "./DropdownBlockLinkSecondaryText"; @withToggle @withState("overChildren", "setOverChildren", false) class Dropdown extends React.Component { static displayName = "Dropdown"; static shouldDisplayChildren = ({ isOpen, isDisabled }) => isOpen && !isDisabled; componentDidUpdate = prevProps => { // For performance reasons, only listen to global clicks // while the children are displayed. if (Dropdown.shouldDisplayChildren(prevProps) === Dropdown.shouldDisplayChildren(this.props)) { return; } if (Dropdown.shouldDisplayChildren(this.props)) { document.addEventListener("click", this.handleOnBlur); } else { document.removeEventListener("click", this.handleOnBlur); } }; handleHideChildren = () => { const { hide, setOverChildren, shouldHideChildrenOnClick } = this.props; // We need to return early because handleToggle() and hide() will // cancel each other out if shouldHideChildrenOnClick is true if (shouldHideChildrenOnClick) { return; } hide(); setOverChildren(false); }; handleOnBlur = () => { if (!this.props.overChildren) { this.props.hide(); } }; handleToggle = event => { const { isDisabled, overChildren, shouldHideChildrenOnClick } = this.props; if (isDisabled || event.ignoreToggle || (!shouldHideChildrenOnClick && overChildren)) { return; } this.props.setOverChildren(false); this.props.toggle(); }; onMouseOver = () => this.props.setOverChildren(true); onMouseLeave = () => this.props.setOverChildren(false); render() { const { arrowIcon, buttonContent, children, className, displayError, fullWidth, hide, icon, isDisabled, isLabelTight, isOpen, overChildren, placement, renderActivator, setOverChildren, shouldHideChildrenOnClick, show, style, testSection, toggle, width, zIndex, ...props } = this.props; const groupClass = classNames( "oui-dropdown-group", { ["width--1-1"]: fullWidth }, { ["oui-form-bad-news"]: displayError }, { ["is-active"]: isOpen }, className, ); const buttonClass = classNames("oui-button", { [`oui-button--${style}`]: style, ["oui-button--full soft--left text--left"]: fullWidth, ["soft--sides"]: isLabelTight, }); const iconClass = classNames("push-half--left", { ["oui-arrow-inline--down"]: arrowIcon === "down", ["oui-arrow-inline--left"]: arrowIcon === "left", ["oui-arrow-inline--right"]: arrowIcon === "right", ["oui-arrow-inline--up"]: arrowIcon === "up", }); return ( <Manager> <div className={groupClass} data-oui-component={true} data-test-section={testSection} {...props}> <Reference> {({ ref }) => { if (buttonContent) { return ( <button type="button" aria-haspopup="listbox" className={buttonClass} disabled={isDisabled} onClick={this.handleToggle} onBlur={this.handleOnBlur} ref={ref} > <div className="flex flex-align--center"> <div className="flex--1 truncate"> {buttonContent.label ? ( <div className="line--tight text--left push--right"> <div className="micro muted"> {buttonContent.label} <span className="oui-assistive-text">:</span> </div> <div>{buttonContent.content}</div> </div> ) : ( buttonContent )} </div> {!!arrowIcon && arrowIcon !== "none" && ( <div className="text--right"> <span className={iconClass} /> </div> )} </div> </button> ); } if (renderActivator) { return renderActivator({ ref: ref, buttonRef: ref, disabled: isDisabled, onBlur: this.handleOnBlur, onClick: this.handleToggle, testSection: testSection, }); } else if (this.props.activator) { // To be deprecated in favor of renderActivator return React.cloneElement(this.props.activator, { buttonRef: ref, ref, onBlur: this.handleOnBlur, onClick: this.handleToggle, }); } }} </Reference> {Dropdown.shouldDisplayChildren(this.props) && ( <Popper placement={placement}> {({ ref, style: popperStyle, placement: popperPlacement }) => ( <div ref={ref} data-placement={popperPlacement} className="oui-dropdown-children" onMouseOver={this.onMouseOver} onMouseLeave={this.onMouseLeave} onClick={this.handleToggle} style={{ zIndex: zIndex, position: "absolute", width: fullWidth ? "100%" : width, marginTop: 2, marginBottom: 2, borderRadius: "var(--border-radius)", boxShadow: "0 2px 3px rgba(0,0,0,.1)", ...popperStyle, }} > {typeof children === "function" ? children({ handleHideChildren: this.handleHideChildren }) : children} </div> )} </Popper> )} </div> </Manager> ); } } Dropdown.propTypes = { /** * React element that when clicked activates the dropdown * Either this prop OR buttonContent should be used, not both * Not included in defaultProps because undefined is an expected value */ activator: PropTypes.elementType, /** Arrow icon direction: * - Defaults to 'none', which hides the arrow * - passing a prop value of false also hides the arrow * - passing a prop value of true uses down arrow */ arrowIcon: PropTypes.oneOf(["down", "left", "none", "right", "up"]), /** * Button text, can be a string or element. * Either this prop OR activator should be used, not both * Not included in defaultProps because undefined is an expected value */ buttonContent: PropTypes.oneOfType([ PropTypes.string, PropTypes.element, PropTypes.shape({ label: PropTypes.string, content: PropTypes.string }), ]), /** * Dropdown contents, typically using the <Blocklist> component * Use render prop function for custom hide configuration */ children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]).isRequired, /** CSS class names. */ className: PropTypes.string, /** Show error state. */ displayError: PropTypes.bool, /** Button width is either full or inline-block. */ fullWidth: PropTypes.bool, /** Provided by @withToggle HOC */ hide: PropTypes.func, /** Disable button. */ isDisabled: PropTypes.bool, /** Whether button width is narrow or full. Defaults to false. */ isLabelTight: PropTypes.bool, /** * Provided by @withToggle HOC * Defaults to false but Dropdown can be open by default if true */ isOpen: PropTypes.bool, /** * Provided by @withState HOC * Whether or not the children are currently moused over */ overChildren: PropTypes.bool, /** Popper placement property */ placement: PropTypes.oneOf([ "top", "top-start", "top-end", "bottom", "bottom-start", "bottom-end", "right", "right-start", "right-end", "left", "left-start", "left-end", ]), /** * A render prop to render the activator button that when clicked activates * the dropdown * Either this prop OR buttonContent should be used, not both */ renderActivator: PropTypes.func, /** Provided by @withToggle HOC. Control for overChildren value */ setOverChildren: PropTypes.func, /** * Whether Dropdown children should be hidden on children click * - Defaults to true * - Pass render prop function as child prop and use provided * handleHideChildren function to programmatically hide */ shouldHideChildrenOnClick: PropTypes.bool, /** * Button style, e.g. highlight, danger, outline * Not included in defaultProps because undefined is an expected value */ style: PropTypes.string, /** For automated testing only. */ testSection: PropTypes.string, /** Provided by @withToggle HOC. Toggle control for isOpen. */ toggle: PropTypes.func, /** Dropdown menu width, in pixels. */ width: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), /** Override default dropdown menu z-index. */ zIndex: PropTypes.number, }; Dropdown.defaultProps = { arrowIcon: "none", displayError: false, fullWidth: false, isDisabled: false, isLabelTight: false, isOpen: false, overChildren: false, placement: "bottom-start", renderActivator: null, setOverChildren: () => null, shouldHideChildrenOnClick: true, testSection: "", toggle: () => null, width: 200, zIndex: 999, }; Dropdown.Contents = DropdownContents; Dropdown.ListItem = DropdownListItem; Dropdown.BlockLink = DropdownBlockLink; Dropdown.BlockLinkText = DropdownBlockLinkText; Dropdown.BlockLinkSecondaryText = DropdownBlockLinkSecondaryText; export default Dropdown;