@retailmenot/anchor
Version:
A React UI Library by RetailMeNot
189 lines (187 loc) • 8.61 kB
JavaScript
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