lucid-ui
Version:
A UI component library from AppNexus.
187 lines (186 loc) • 7.49 kB
JavaScript
import _ from 'lodash';
import React from 'react';
import PropTypes from 'react-peek/prop-types';
import { lucidClassNames } from '../../util/style-helpers';
import Overlay from '../Overlay/Overlay';
import GripperVerticalIcon from '../Icon/GripperVerticalIcon/GripperVerticalIcon';
import CloseIcon from '../Icon/CloseIcon/CloseIcon';
import DragCaptureZone from '../DragCaptureZone/DragCaptureZone';
import Button from '../Button/Button';
import { getFirst, omitProps } from '../../util/component-types';
const cx = lucidClassNames.bind('&-SidePanel');
const { any, bool, func, oneOf, node, number, string, oneOfType } = PropTypes;
const SidePanelHeader = (_props) => null;
SidePanelHeader.displayName = 'SidePanel.Header';
SidePanelHeader.propName = 'Header';
SidePanelHeader.peek = {
description: `
Defines the Header content of SidePanel. If no content is defined, it will
still show the close button.
`,
};
SidePanelHeader.propTypes = {
children: node `
Children that will be rendered.
`,
};
class SidePanel extends React.Component {
constructor() {
super(...arguments);
this.state = {
isResizing: false,
width: this.props.width,
startWidth: this.props.width,
isExpanded: this.props.isExpanded,
};
this.timerId = setTimeout(() => {
return;
}, 1);
this.handleResizeStart = () => {
this.setState({
isResizing: true,
});
};
this.handleResize = ({ dX }) => {
const { startWidth } = this.state;
const position = startWidth + dX * (this.props.position === 'right' ? -1 : 1);
this.setState({
width: _.clamp(position, this.props.minWidth, this.props.maxWidth === Infinity ? window.innerWidth : this.props.maxWidth),
});
};
this.handleResizeEnd = ({ dX }, { event }) => {
const { startWidth, width } = this.state;
this.setState({
width: startWidth + dX * (this.props.position === 'right' ? -1 : 1),
isResizing: false,
startWidth: width,
});
this.props.onResize(startWidth - dX, { props: this.props, event });
};
this.handleCollapse = ({ event, }) => {
this.props.onCollapse({ event, props: this.props });
};
}
componentDidUpdate(prevProps) {
if (prevProps.isExpanded !== this.props.isExpanded) {
this.timerId = setTimeout(() => {
this.setState({
isExpanded: this.props.isExpanded,
});
}, 1);
}
}
componentWillUnmount() {
const { preventBodyScroll } = this.props;
if (this.timerId) {
clearTimeout(this.timerId);
}
if (preventBodyScroll) {
window.document.body.style.overflow = '';
}
}
render() {
const { children, className, isAnimated, isExpanded, isResizeDisabled, position, preventBodyScroll, topOffset, ...passThroughs } = this.props;
const headerEl = getFirst(this.props, SidePanel.Header);
const headerChildren = _.get(headerEl, 'props.children');
if (preventBodyScroll) {
window.document.body.style.overflow = isExpanded ? 'hidden' : '';
}
return (React.createElement(Overlay, Object.assign({ className: cx('&', {
'&-is-expanded': isExpanded && this.state.isExpanded,
'&-position-left': position === 'left',
'&-position-right': position === 'right',
'&-is-animated': isAnimated,
}, className), isShown: isExpanded || this.state.isExpanded, onBackgroundClick: this.handleCollapse, onEscape: this.handleCollapse, isAnimated: isAnimated, style: {
marginTop: topOffset,
} }, omitProps(passThroughs, undefined, _.keys(SidePanel.propTypes), false)),
React.createElement("div", { className: cx('&-pane'), style: {
width: this.state.width,
marginTop: topOffset,
} },
headerEl && (React.createElement("div", { className: cx('&-header') },
React.createElement("div", { className: cx('&-header-inner-wrapper') },
React.createElement("div", { className: cx('&-header-content') }, headerChildren),
React.createElement(Button, { className: cx('&-header-closer-button'), kind: 'invisible', onClick: this.handleCollapse, hasOnlyIcon: true },
React.createElement(CloseIcon, { className: cx('&-header-closer'), isClickable: true, size: 14 }))))),
React.createElement("div", { className: cx('&-body') },
!isResizeDisabled && (React.createElement(DragCaptureZone, { className: cx('&-grabber'), onDragStart: this.handleResizeStart, onDrag: this.handleResize, onDragEnd: this.handleResizeEnd },
React.createElement(GripperVerticalIcon, { width: '20' }))),
React.createElement("div", { className: cx('&-content') }, children)))));
}
}
SidePanel.displayName = 'SidePanel';
SidePanel.peek = {
description: `
A fixed-positioned overlay positioned on the side of the screen at full
screen height. Supports variable widths resized by the user or defined as a
prop. Animated collapse and expand with optional Header and closer.
`,
categories: ['layout'],
};
SidePanel.propTypes = {
children: node `
Content of the SidePanel, but also accepts \`<SidePanel.Header>\` to define
header content.
`,
className: string `
Appended to the component-specific class names set on the root element.
`,
Header: any `
Alternative to using \`<SidePanel.Header>\`.
`,
isAnimated: bool `
Enables animated transitions during expansion and collapse.
`,
isExpanded: bool `
Controls the expanded/collapsed state as a boolean prop.
`,
isResizeDisabled: bool `
When true, hides the resizer at the edge of the SidePanel.
`,
onCollapse: func `
Callback triggered when user clicks the background, hits the Esc key, or
clicks the close button in the Header.
Signature: \`({ event, props }) => {}\`
`,
onResize: func `
Callback triggered after a user resizes to a new width.
Signature: \`(width, { event, props }) => {}\`
`,
position: oneOf(['left', 'right']) `
Controls the position on the screen.
`,
preventBodyScroll: bool `
When true, it will prevent scrolling in the background when \`isExpanded\`
is true. This is accomplished by setting \`document.body.style.overflow =
'hidden'\`.
`,
width: number `
Sets the initial width in pixels. The actual width may change if the user
resizes it.
`,
minWidth: number `
Sets the minimum width of the Side Panel.
`,
maxWidth: number `
Sets the maximum width of the Side Panel.
`,
topOffset: oneOfType([number, string]) `
Sets the top margin for the panel. Defaults to \`0\`.
`,
};
SidePanel.defaultProps = {
isAnimated: true,
isExpanded: true,
isResizeDisabled: false,
onCollapse: _.noop,
onResize: _.noop,
position: 'right',
preventBodyScroll: false,
topOffset: 0,
width: 240,
minWidth: -Infinity,
maxWidth: Infinity,
};
SidePanel.Header = SidePanelHeader;
export default SidePanel;