optimizely-oui
Version:
Optimizely's Component Library.
326 lines (306 loc) • 12.8 kB
JavaScript
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";
("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;