UNPKG

@retailmenot/anchor

Version:

A React UI Library by RetailMeNot

189 lines (187 loc) 8.61 kB
var __rest = (this && this.__rest) || function (s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; }; // REACT import * as React from 'react'; // VENDOR import classNames from 'classnames'; import styled, { css } from '@xstyled/styled-components'; import { space as spaceStyles } from '@xstyled/system'; import { fromEvent, merge } from 'rxjs'; import { filter, map, mapTo, distinctUntilChanged, throttleTime, } from 'rxjs/operators'; // ANCHOR import { get } from '../utils/get/get'; import { PositionContainer } from '../utils/PositionContainer'; const StyledDropDown = styled('div') ` box-sizing: border-box; font-family: base; position: relative; cursor: pointer; display: inline-flex; justify-content: center; align-items: center; min-height: 1rem; min-width: 1rem; ${({ position, spacing }) => { if (!spacing) { return null; } switch (position) { case 'bottom': case 'bottomStart': case 'bottomEnd': return css({ paddingBottom: spacing, // this won't work for a negative spacing values. if we fix // these we could allow negative values marginBottom: `-${spacing}`, }); case 'right': case 'rightStart': case 'rightEnd': return css({ paddingRight: spacing, marginRight: `-${spacing}`, }); case 'left': case 'leftStart': case 'leftEnd': return css({ paddingLeft: spacing, marginLeft: `-${spacing}`, }); case 'top': case 'topStart': case 'topEnd': return css({ paddingTop: spacing, marginTop: `-${spacing}`, }); } return null; }}; ${spaceStyles} `; export class DropDown extends React.Component { constructor() { super(...arguments); // State this.state = { hovered: false, clicked: false, }; // Instance Reference this.dropDownReference = React.createRef(); this.containerReference = React.createRef(); } isOpen() { const { open, trigger = 'both' } = this.props; const { clicked, hovered } = this.state; return open !== undefined ? open : (trigger === 'click' && clicked) || (trigger === 'hover' && hovered) || (trigger === 'both' && (hovered || clicked)); } componentDidMount() { const { current: dropDown } = this.dropDownReference; const { current: container, } = this.containerReference; this.setState({ height: get(dropDown, 'clientHeight', 0), width: get(dropDown, 'clientWidth', 0), containerHeight: get(container, 'clientHeight', 0), containerWidth: get(container, 'clientWidth', 0), }); // Dropdown Instance this.rootElement = dropDown && dropDown.getRootNode ? dropDown.getRootNode() : document; const outsideClick$ = merge(fromEvent(this.rootElement, 'click'), fromEvent(this.rootElement, 'touchstart')).pipe(filter(() => this.isOpen()), filter(({ target }) => !dropDown.contains(target)), throttleTime(500)); const dropDownClick$ = merge(fromEvent(dropDown, 'click'), fromEvent(dropDown, 'touchstart')).pipe(throttleTime(500)); const escapeKey$ = fromEvent(this.rootElement, 'keyup').pipe(filter(() => this.isOpen()), filter((keyEvent) => keyEvent.key === 'Escape')); const mouseEnter$ = fromEvent(dropDown, 'mouseenter'); const mouseLeave$ = fromEvent(dropDown, 'mouseleave'); this.clickedSub = merge(outsideClick$.pipe(mapTo(false)), escapeKey$.pipe(mapTo(false)), dropDownClick$.pipe(map(() => !this.state.clicked))).subscribe(clicked => { this.setState(({ height, width }) => ({ clicked, height: clicked ? get(dropDown, 'clientHeight', 0) : height, width: clicked ? get(dropDown, 'clientWidth', 0) : width, })); if (this.props.onTriggered) { this.props.onTriggered(clicked, 'click'); } }); this.escapeSub = escapeKey$.subscribe(event => { if (this.props.onEscapeKey) { this.props.onEscapeKey(event); } }); this.outsideClickSub = outsideClick$.subscribe(event => { if (this.props.onOutsideClick) { this.props.onOutsideClick(event); } }); this.hoveredSub = merge(mouseEnter$.pipe(mapTo(true)), mouseLeave$.pipe(mapTo(false)), escapeKey$.pipe(mapTo(false)), dropDownClick$.pipe(mapTo(false))) .pipe(filter(() => this.props.trigger === 'hover' || this.props.trigger === 'both'), distinctUntilChanged()) .subscribe(hovered => { this.setState(({ height, width }) => ({ hovered, height: hovered ? get(dropDown, 'clientHeight', 0) : height, width: hovered ? get(dropDown, 'clientWidth', 0) : width, })); if (this.props.onTriggered) { this.props.onTriggered(hovered, 'hover'); } }); } componentWillUnmount() { this.hoveredSub.unsubscribe(); this.clickedSub.unsubscribe(); this.escapeSub.unsubscribe(); this.outsideClickSub.unsubscribe(); } componentDidUpdate(prevProps) { const { showArrow: prevShowArrow, position: prevPosition, spacing: prevSpacing, } = prevProps; const { showArrow, position, spacing } = this.props; if (showArrow !== prevShowArrow || position !== prevPosition || spacing !== prevSpacing) { const { current: dropDown, } = this.dropDownReference; const { current: container, } = this.containerReference; if (container) { this.setState({ height: get(dropDown, 'clientHeight', 0), width: get(dropDown, 'clientWidth', 0), containerHeight: get(container, 'clientHeight', 0), containerWidth: get(container, 'clientWidth', 0), }); } } } render() { const _a = this.props, { children, className, overlay, spacing, arrowSize, debug, trigger = 'both', arrowIndent, showArrow = true, position = 'bottom', shadow = '0 0 0.5rem 0 rgba(0,0,0,0.2)', border = 'light', borderRadius = 'base', background = 'white', onTriggered = () => null, onEscapeKey = () => null, onOutsideClick = () => null } = _a, props = __rest(_a, ["children", "className", "overlay", "spacing", "arrowSize", "debug", "trigger", "arrowIndent", "showArrow", "position", "shadow", "border", "borderRadius", "background", "onTriggered", "onEscapeKey", "onOutsideClick"]); const { height = 0, width = 0, containerHeight = 0, containerWidth = 0, } = this.state; const showDropdown = this.isOpen(); // if they don't specify a spacing then we'll default // based on whether the arrow is there or not const spacingWithDefault = typeof spacing === 'undefined' ? showArrow ? '0.75rem' : '0' : spacing; return (React.createElement(StyledDropDown, Object.assign({ ref: this.dropDownReference, className: classNames('anchor-drop-down', className), showArrow: showArrow, shadow: shadow, position: position, spacing: spacingWithDefault }, props), children, React.createElement(PositionContainer, { ref: this.containerReference, active: showDropdown, arrowIndent: arrowIndent, arrowSize: arrowSize, background: background, border: border, borderRadius: borderRadius, children: overlay, className: "anchor-drop-down-container", containerHeight: containerHeight, containerWidth: containerWidth, height: height, width: width, position: position, shadow: shadow, showArrow: showArrow, debug: debug }))); } } //# sourceMappingURL=DropDown.component.js.map