UNPKG

react-select

Version:

A Select control built with and for ReactJS

484 lines (382 loc) 17.4 kB
'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); exports.MenuPortal = exports.menuPortalCSS = exports.LoadingMessage = exports.NoOptionsMessage = exports.loadingMessageCSS = exports.noOptionsMessageCSS = exports.MenuList = exports.menuListCSS = exports.Menu = exports.menuCSS = undefined; var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); exports.getMenuPlacement = getMenuPlacement; var _react = require('react'); var _react2 = _interopRequireDefault(_react); var _emotion = require('emotion'); var _reactDom = require('react-dom'); var _propTypes = require('prop-types'); var _propTypes2 = _interopRequireDefault(_propTypes); var _utils = require('../utils'); var _theme = require('../theme'); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } // ============================== // Menu // ============================== // Get Menu Placement // ------------------------------ function getMenuPlacement(_ref) { var maxHeight = _ref.maxHeight, menuEl = _ref.menuEl, minHeight = _ref.minHeight, placement = _ref.placement, shouldScroll = _ref.shouldScroll, isFixedPosition = _ref.isFixedPosition; var scrollParent = (0, _utils.getScrollParent)(menuEl); var defaultState = { placement: 'bottom', maxHeight: maxHeight }; // something went wrong, return default state if (!menuEl || !menuEl.offsetParent) return defaultState; // we can't trust `scrollParent.scrollHeight` --> it may increase when // the menu is rendered var _scrollParent$getBoun = scrollParent.getBoundingClientRect(), scrollHeight = _scrollParent$getBoun.height; var _menuEl$getBoundingCl = menuEl.getBoundingClientRect(), menuBottom = _menuEl$getBoundingCl.bottom, menuHeight = _menuEl$getBoundingCl.height, menuTop = _menuEl$getBoundingCl.top; // $FlowFixMe function returns above if there's no offsetParent var _menuEl$offsetParent$ = menuEl.offsetParent.getBoundingClientRect(), containerTop = _menuEl$offsetParent$.top; var viewHeight = window.innerHeight; var scrollTop = (0, _utils.getScrollTop)(scrollParent); var gutter = _theme.spacing.menuGutter; var viewSpaceAbove = containerTop - gutter; var viewSpaceBelow = viewHeight - menuTop; var scrollSpaceAbove = viewSpaceAbove + scrollTop; var scrollSpaceBelow = scrollHeight - scrollTop - menuTop; var scrollDown = menuBottom - viewHeight + scrollTop + gutter; var scrollUp = scrollTop + menuTop - gutter; var scrollDuration = 160; switch (placement) { case 'auto': case 'bottom': // 1: the menu will fit, do nothing if (viewSpaceBelow >= menuHeight) { return { placement: 'bottom', maxHeight: maxHeight }; } // 2: the menu will fit, if scrolled if (scrollSpaceBelow >= menuHeight && !isFixedPosition) { if (shouldScroll) { (0, _utils.animatedScrollTo)(scrollParent, scrollDown, scrollDuration); } return { placement: 'bottom', maxHeight: maxHeight }; } // 3: the menu will fit, if constrained if (!isFixedPosition && scrollSpaceBelow >= minHeight || isFixedPosition && viewSpaceBelow >= minHeight) { if (shouldScroll) { (0, _utils.animatedScrollTo)(scrollParent, scrollDown, scrollDuration); } // we want to provide as much of the menu as possible to the user, // so give them whatever is available below rather than the minHeight. var constrainedHeight = isFixedPosition ? viewSpaceBelow - gutter : scrollSpaceBelow - gutter; return { placement: 'bottom', maxHeight: constrainedHeight }; } // 4. Forked beviour when there isn't enough space below // AUTO: flip the menu, render above if (placement === 'auto' || isFixedPosition) { // may need to be constrained after flipping var _constrainedHeight = maxHeight; if (!isFixedPosition && scrollSpaceAbove >= minHeight || isFixedPosition && viewSpaceAbove >= minHeight) { _constrainedHeight = isFixedPosition ? viewSpaceAbove - gutter - _theme.spacing.controlHeight : scrollSpaceAbove - gutter - _theme.spacing.controlHeight; } return { placement: 'top', maxHeight: _constrainedHeight }; } // BOTTOM: allow browser to increase scrollable area and immediately set scroll if (placement === 'bottom') { (0, _utils.scrollTo)(scrollParent, scrollDown); return { placement: 'bottom', maxHeight: maxHeight }; } break; case 'top': // 1: the menu will fit, do nothing if (viewSpaceAbove >= menuHeight) { return { placement: 'top', maxHeight: maxHeight }; } // 2: the menu will fit, if scrolled if (scrollSpaceAbove >= menuHeight && !isFixedPosition) { if (shouldScroll) { (0, _utils.animatedScrollTo)(scrollParent, scrollUp, scrollDuration); } return { placement: 'top', maxHeight: maxHeight }; } // 3: the menu will fit, if constrained if (!isFixedPosition && scrollSpaceAbove >= minHeight || isFixedPosition && viewSpaceAbove >= minHeight) { var _constrainedHeight2 = maxHeight; // we want to provide as much of the menu as possible to the user, // so give them whatever is available below rather than the minHeight. if (!isFixedPosition && scrollSpaceAbove >= minHeight || isFixedPosition && viewSpaceAbove >= minHeight) { _constrainedHeight2 = isFixedPosition ? viewSpaceAbove - gutter : scrollSpaceAbove - gutter; } if (shouldScroll) { (0, _utils.animatedScrollTo)(scrollParent, scrollUp, scrollDuration); } return { placement: 'top', maxHeight: _constrainedHeight2 }; } // 4. not enough space, the browser WILL NOT increase scrollable area when // absolutely positioned element rendered above the viewport (only below). // Flip the menu, render below return { placement: 'bottom', maxHeight: maxHeight }; default: throw new Error('Invalid placement provided "' + placement + '".'); } // fulfil contract with flow: implicit return value of undefined return defaultState; } // Menu Component // ------------------------------ function alignToControl(placement) { var placementToCSSProp = { bottom: 'top', top: 'bottom' }; return placement ? placementToCSSProp[placement] : 'bottom'; } var coercePlacement = function coercePlacement(p) { return p === 'auto' ? 'bottom' : p; }; var menuCSS = exports.menuCSS = function menuCSS(_ref2) { var _ref3; var placement = _ref2.placement; return _ref3 = {}, _defineProperty(_ref3, alignToControl(placement), '100%'), _defineProperty(_ref3, 'backgroundColor', _theme.colors.neutral0), _defineProperty(_ref3, 'borderRadius', _theme.borderRadius), _defineProperty(_ref3, 'boxShadow', '0 0 0 1px ' + _theme.colors.neutral10a + ', 0 4px 11px ' + _theme.colors.neutral10a), _defineProperty(_ref3, 'marginBottom', _theme.spacing.menuGutter), _defineProperty(_ref3, 'marginTop', _theme.spacing.menuGutter), _defineProperty(_ref3, 'position', 'absolute'), _defineProperty(_ref3, 'width', '100%'), _defineProperty(_ref3, 'zIndex', 1), _ref3; }; var Menu = exports.Menu = function (_Component) { _inherits(Menu, _Component); function Menu() { var _ref4; var _temp, _this, _ret; _classCallCheck(this, Menu); for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } return _ret = (_temp = (_this = _possibleConstructorReturn(this, (_ref4 = Menu.__proto__ || Object.getPrototypeOf(Menu)).call.apply(_ref4, [this].concat(args))), _this), _this.state = { maxHeight: _this.props.maxMenuHeight, placement: null }, _this.getPlacement = function (ref) { var _this$props = _this.props, minMenuHeight = _this$props.minMenuHeight, maxMenuHeight = _this$props.maxMenuHeight, menuPlacement = _this$props.menuPlacement, menuPosition = _this$props.menuPosition, menuShouldScrollIntoView = _this$props.menuShouldScrollIntoView; var getPortalPlacement = _this.context.getPortalPlacement; if (!ref) return; // DO NOT scroll if position is fixed var isFixedPosition = menuPosition === 'fixed'; var shouldScroll = menuShouldScrollIntoView && !isFixedPosition; var state = getMenuPlacement({ maxHeight: maxMenuHeight, menuEl: ref, minHeight: minMenuHeight, placement: menuPlacement, shouldScroll: shouldScroll, isFixedPosition: isFixedPosition }); if (getPortalPlacement) getPortalPlacement(state); _this.setState(state); }, _this.getState = function () { var menuPlacement = _this.props.menuPlacement; var placement = _this.state.placement || coercePlacement(menuPlacement); return _extends({}, _this.props, { placement: placement, maxHeight: _this.state.maxHeight }); }, _temp), _possibleConstructorReturn(_this, _ret); } _createClass(Menu, [{ key: 'render', value: function render() { var _props = this.props, children = _props.children, className = _props.className, cx = _props.cx, getStyles = _props.getStyles, innerProps = _props.innerProps; return _react2.default.createElement( 'div', _extends({ className: cx( /*#__PURE__*/(0, _emotion.css)(getStyles('menu', this.getState())), { 'menu': true }, className), ref: this.getPlacement }, innerProps), children ); } }]); return Menu; }(_react.Component); Menu.contextTypes = { getPortalPlacement: _propTypes2.default.func }; exports.default = Menu; // ============================== // Menu List // ============================== var menuListCSS = exports.menuListCSS = function menuListCSS(_ref5) { var maxHeight = _ref5.maxHeight; return { maxHeight: maxHeight, overflowY: 'auto', paddingBottom: _theme.spacing.baseUnit, paddingTop: _theme.spacing.baseUnit, position: 'relative', // required for offset[Height, Top] > keyboard scroll WebkitOverflowScrolling: 'touch' }; }; var MenuList = exports.MenuList = function MenuList(props) { var children = props.children, className = props.className, cx = props.cx, getStyles = props.getStyles, isMulti = props.isMulti, innerRef = props.innerRef; return _react2.default.createElement( 'div', { className: cx( /*#__PURE__*/(0, _emotion.css)(getStyles('menuList', props)), { 'menu-list': true, 'menu-list--is-multi': isMulti }, className), ref: innerRef }, children ); }; // ============================== // Menu Notices // ============================== var noticeCSS = function noticeCSS() { return { color: _theme.colors.neutral40, padding: _theme.spacing.baseUnit * 2 + 'px ' + _theme.spacing.baseUnit * 3 + 'px', textAlign: 'center' }; }; var noOptionsMessageCSS = exports.noOptionsMessageCSS = noticeCSS; var loadingMessageCSS = exports.loadingMessageCSS = noticeCSS; var NoOptionsMessage = exports.NoOptionsMessage = function NoOptionsMessage(props) { var children = props.children, className = props.className, cx = props.cx, getStyles = props.getStyles, innerProps = props.innerProps; return _react2.default.createElement( 'div', _extends({ className: cx( /*#__PURE__*/(0, _emotion.css)(getStyles('noOptionsMessage', props)), { 'menu-notice': true, 'menu-notice--no-options': true }, className) }, innerProps), children ); }; NoOptionsMessage.defaultProps = { children: 'No options' }; var LoadingMessage = exports.LoadingMessage = function LoadingMessage(props) { var children = props.children, className = props.className, cx = props.cx, getStyles = props.getStyles, innerProps = props.innerProps; return _react2.default.createElement( 'div', _extends({ className: cx( /*#__PURE__*/(0, _emotion.css)(getStyles('loadingMessage', props)), { 'menu-notice': true, 'menu-notice--loading': true }, className) }, innerProps), children ); }; LoadingMessage.defaultProps = { children: 'Loading...' }; // ============================== // Menu Portal // ============================== var menuPortalCSS = exports.menuPortalCSS = function menuPortalCSS(_ref6) { var rect = _ref6.rect, offset = _ref6.offset, position = _ref6.position; return { left: rect.left, position: position, top: offset, width: rect.width, zIndex: 1 }; }; var MenuPortal = exports.MenuPortal = function (_Component2) { _inherits(MenuPortal, _Component2); function MenuPortal() { var _ref7; var _temp2, _this2, _ret2; _classCallCheck(this, MenuPortal); for (var _len2 = arguments.length, args = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { args[_key2] = arguments[_key2]; } return _ret2 = (_temp2 = (_this2 = _possibleConstructorReturn(this, (_ref7 = MenuPortal.__proto__ || Object.getPrototypeOf(MenuPortal)).call.apply(_ref7, [this].concat(args))), _this2), _this2.state = { placement: null }, _this2.getPortalPlacement = function (_ref8) { var placement = _ref8.placement; var initialPlacement = coercePlacement(_this2.props.menuPlacement); // avoid re-renders if the placement has not changed if (placement !== initialPlacement) { _this2.setState({ placement: placement }); } }, _temp2), _possibleConstructorReturn(_this2, _ret2); } _createClass(MenuPortal, [{ key: 'getChildContext', value: function getChildContext() { return { getPortalPlacement: this.getPortalPlacement }; } // callback for occassions where the menu must "flip" }, { key: 'render', value: function render() { var _props2 = this.props, appendTo = _props2.appendTo, children = _props2.children, controlElement = _props2.controlElement, menuPlacement = _props2.menuPlacement, position = _props2.menuPosition, getStyles = _props2.getStyles; var isFixed = position === 'fixed'; // bail early if required elements aren't present if (!appendTo && !isFixed || !controlElement) { return null; } var placement = this.state.placement || coercePlacement(menuPlacement); var rect = (0, _utils.getBoundingClientObj)(controlElement); var scrollDistance = isFixed ? 0 : window.pageYOffset; var offset = rect[placement] + scrollDistance; var state = { offset: offset, position: position, rect: rect }; // same wrapper element whether fixed or portalled var menuWrapper = _react2.default.createElement( 'div', { className: /*#__PURE__*/ /*#__PURE__*/(0, _emotion.css)(getStyles('menuPortal', state)) }, children ); return appendTo ? (0, _reactDom.createPortal)(menuWrapper, appendTo) : menuWrapper; } }]); return MenuPortal; }(_react.Component); MenuPortal.childContextTypes = { getPortalPlacement: _propTypes2.default.func };