react-select
Version:
A Select control built with and for ReactJS
484 lines (382 loc) • 17.4 kB
JavaScript
;
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
};