// "Generic dropdown component" // Renders something and then when that "something" is focused renders a popup layer next to it // For example, a text input with a popup selection list // ...Or maybe a button with a popup menu // (c) Vitaliy Filippov 2019+ // Version 2019-08-27 import React from 'react'; import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; export class Picker extends React.Component { static propTypes = { direction: PropTypes.string, clearOnClick: PropTypes.bool, minWidth: PropTypes.number, style: PropTypes.object, renderInput: PropTypes.func.isRequired, renderPicker: PropTypes.func.isRequired, } state = { focused: false, height: 0, width: 0, top: 0, left: 0, } focus = () => { this.setState({ focused: true, height: 0 }); this.calculateDirection(); if (this.props.clearOnClick) { document.body.addEventListener('click', this.blurExt); } } blur = () => { this.setState({ focused: false }); if (this.props.clearOnClick) { document.body.removeEventListener('click', this.blurExt); } } blurExt = (ev) => { let n = this.input ? ReactDOM.findDOMNode(this.input) : null; let e = ev.target||ev.srcElement; while (e) { // calendar-box is calendar.js's class if (e == this.picker || e == n || /\bcalendar-box\b/.exec(e.className||'')) { return; } e = e.parentNode; } this.blur(); } setInput = (e) => { this.input = e; } setPicker = (e) => { this.picker = e; } render() { return (
{this.props.renderInput({ onFocus: this.focus, onBlur: this.blur, focused: this.state.focused, ref: this.setInput, })} {this.state.focused ?
{this.props.renderPicker()}
: null}
); } componentDidUpdate() { if (this.state.focused && !this.state.height) { this.calculateDirection(); } } calculateDirection() { if (!this.input || !this.picker) { return; } const picker_height = ReactDOM.findDOMNode(this.picker).getBoundingClientRect().height; const client = ReactDOM.findDOMNode(this.input).getBoundingClientRect(); const screen_height = window.innerHeight || document.documentElement.offsetHeight; let direction = this.props.direction; if (!direction || direction === 'auto') { const down = client.top + picker_height < screen_height; direction = down ? 'down' : 'up'; } let top = client.top + (window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop) - (document.documentElement.clientTop || document.body.clientTop || 0); top = direction == 'down' ? (top + client.height) + 'px' : (top - picker_height) + 'px'; const left = (client.left + (window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft) - (document.documentElement.clientLeft || document.body.clientLeft || 0)) + 'px'; const width = (this.props.minWidth && client.width < this.props.minWidth ? this.props.minWidth : client.width)+'px'; if (this.state.top !== top || this.state.left !== left || this.state.width !== width || this.state.height !== picker_height) { this.setState({ top, left, width, height: picker_height }); } } }