UNPKG

react-16-dropdown

Version:

A zero-dependency, lightweight and fully customizable dropdown (not select) for React.

877 lines (736 loc) 27.3 kB
(function webpackUniversalModuleDefinition(root, factory) { if(typeof exports === 'object' && typeof module === 'object') module.exports = factory(require("react"), require("react-dom")); else if(typeof define === 'function' && define.amd) define(["react", "react-dom"], factory); else if(typeof exports === 'object') exports["react16Dropdown"] = factory(require("react"), require("react-dom")); else root["react16Dropdown"] = factory(root["react"], root["react-dom"]); })(typeof self !== 'undefined' ? self : this, function(__WEBPACK_EXTERNAL_MODULE_0__, __WEBPACK_EXTERNAL_MODULE_3__) { return /******/ (function(modules) { // webpackBootstrap /******/ // The module cache /******/ var installedModules = {}; /******/ /******/ // The require function /******/ function __webpack_require__(moduleId) { /******/ /******/ // Check if module is in cache /******/ if(installedModules[moduleId]) { /******/ return installedModules[moduleId].exports; /******/ } /******/ // Create a new module (and put it into the cache) /******/ var module = installedModules[moduleId] = { /******/ i: moduleId, /******/ l: false, /******/ exports: {} /******/ }; /******/ /******/ // Execute the module function /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); /******/ /******/ // Flag the module as loaded /******/ module.l = true; /******/ /******/ // Return the exports of the module /******/ return module.exports; /******/ } /******/ /******/ /******/ // expose the modules object (__webpack_modules__) /******/ __webpack_require__.m = modules; /******/ /******/ // expose the module cache /******/ __webpack_require__.c = installedModules; /******/ /******/ // define getter function for harmony exports /******/ __webpack_require__.d = function(exports, name, getter) { /******/ if(!__webpack_require__.o(exports, name)) { /******/ Object.defineProperty(exports, name, { /******/ configurable: false, /******/ enumerable: true, /******/ get: getter /******/ }); /******/ } /******/ }; /******/ /******/ // getDefaultExport function for compatibility with non-harmony modules /******/ __webpack_require__.n = function(module) { /******/ var getter = module && module.__esModule ? /******/ function getDefault() { return module['default']; } : /******/ function getModuleExports() { return module; }; /******/ __webpack_require__.d(getter, 'a', getter); /******/ return getter; /******/ }; /******/ /******/ // Object.prototype.hasOwnProperty.call /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; /******/ /******/ // __webpack_public_path__ /******/ __webpack_require__.p = ""; /******/ /******/ // Load entry module and return exports /******/ return __webpack_require__(__webpack_require__.s = 1); /******/ }) /************************************************************************/ /******/ ([ /* 0 */ /***/ (function(module, exports) { module.exports = __WEBPACK_EXTERNAL_MODULE_0__; /***/ }), /* 1 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); 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; }; }(); var _react = __webpack_require__(0); var _react2 = _interopRequireDefault(_react); var _Menu = __webpack_require__(2); var _Menu2 = _interopRequireDefault(_Menu); var _Trigger = __webpack_require__(5); var _Trigger2 = _interopRequireDefault(_Trigger); var _utils = __webpack_require__(6); 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; } var Dropdown = function (_Component) { _inherits(Dropdown, _Component); function Dropdown(props) { _classCallCheck(this, Dropdown); var _this = _possibleConstructorReturn(this, (Dropdown.__proto__ || Object.getPrototypeOf(Dropdown)).call(this, props)); _this.state = { open: Boolean(props.open) }; _this.menuRef = _react2.default.createRef(); _this.triggerRef = _react2.default.createRef(); _this.controlled = Object.prototype.hasOwnProperty.call(_this.props, 'open'); _this.handleTriggerClick = _this.handleTriggerClick.bind(_this); _this.handleOptionClick = _this.handleOptionClick.bind(_this); _this.handleTriggerKeyDown = _this.handleTriggerKeyDown.bind(_this); _this.handleEscape = _this.handleEscape.bind(_this); _this.closeMenu = _this.closeMenu.bind(_this); _this.openMenu = _this.openMenu.bind(_this); _this.handleClickOutside = _this.handleClickOutside.bind(_this); _this.setTriggerRect = _this.setTriggerRect.bind(_this); _this.focusTrigger = _this.focusTrigger.bind(_this); return _this; } _createClass(Dropdown, [{ key: 'componentDidMount', value: function componentDidMount() { this.setTriggerRect(); this.props.autoFocus && this.focusTrigger(); _utils.optimizedResize.add(this.setTriggerRect); } }, { key: 'componentDidUpdate', value: function componentDidUpdate(prevProps, prevState) { if (this.controlled) { return; } if (this.state.open && !prevState.open) { typeof this.props.onOpen === 'function' && this.props.onOpen(); } if (this.state.open) { this.props.closeOnEscape && document.addEventListener('keyup', this.handleEscape); this.props.closeOnClickOutside && document.addEventListener('click', this.handleClickOutside); } else { this.props.closeOnEscape && document.removeEventListener('keyup', this.handleEscape); this.props.closeOnClickOutside && document.removeEventListener('click', this.handleClickOutside); } } }, { key: 'componentWillUnmount', value: function componentWillUnmount() { document.removeEventListener('keyup', this.handleEscape); document.removeEventListener('click', this.handleClickOutside); } }, { key: 'setTriggerRect', value: function setTriggerRect() { if (!this.triggerRef.current) { return; } this.setState({ triggerBoundingRect: (0, _utils.getAbsoluteBoundingRect)(this.triggerRef.current) }); } // focus the custom component passed or renderer }, { key: 'focusTrigger', value: function focusTrigger() { if (this.props.triggerComponent) { this.triggerRef.current.focus(); } else { this.triggerRef.current.firstChild.focus(); } } }, { key: 'closeMenu', value: function closeMenu(focus) { var _this2 = this; this.setState({ open: false }, function () { focus && _this2.focusTrigger(); }); } }, { key: 'openMenu', value: function openMenu() { this.setState({ open: true }); } }, { key: 'handleClickOutside', value: function handleClickOutside(e) { if (!this.menuRef.current) { return; } if (!this.menuRef.current.contains(e.target)) { this.closeMenu(); } } }, { key: 'handleEscape', value: function handleEscape(e) { if (e.key === 'Escape') { this.closeMenu(true); } } }, { key: 'handleTriggerClick', value: function handleTriggerClick() { // re-calculating the position of dropdown to remove scrolling side effects this.setTriggerRect(); typeof this.props.onTriggerClick === 'function' && this.props.onTriggerClick(); if (this.controlled) { return; } this.setState(function (prevState) { return { open: !prevState.open }; }); } }, { key: 'handleTriggerKeyDown', value: function handleTriggerKeyDown(e) { typeof this.props.onTriggerKeyDown === 'function' && this.props.onTriggerKeyDown(); if (this.controlled) { return; } if (e.key === 'ArrowDown') { this.openMenu(); e.preventDefault(); } } }, { key: 'handleOptionClick', value: function handleOptionClick(val) { typeof this.props.onClick === 'function' && this.props.onClick(val); !this.controlled && this.props.closeOnOptionClick && this.closeMenu(true); } }, { key: 'render', value: function render() { var TriggerElement = this.props.triggerComponent || _Trigger2.default; var open = this.controlled ? this.props.open : this.state.open; var classes = 'react-16-dropdown' + (this.props.className ? ' ' + this.props.className : ''); return _react2.default.createElement( 'div', { className: classes, id: this.props.id }, _react2.default.createElement(TriggerElement, { disabled: this.props.disabled, label: this.props.triggerLabel, renderer: this.props.triggerRenderer, triggerRef: this.triggerRef, onClick: this.handleTriggerClick, onKeyDown: this.handleTriggerKeyDown }), open && this.state.triggerBoundingRect && _react2.default.createElement(_Menu2.default, _extends({}, this.props, { controlled: this.controlled, menuRef: this.menuRef, triggerBoundingRect: this.state.triggerBoundingRect, onClick: this.handleOptionClick })) ); } }]); return Dropdown; }(_react.Component); exports.default = Dropdown; Dropdown.defaultProps = { autoFocus: false, triggerLabel: 'Open menu', closeOnEscape: true, closeOnClickOutside: true, closeOnOptionClick: false, disabled: false, align: 'left', options: [], sections: [] }; /***/ }), /* 2 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); 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; }; }(); var _react = __webpack_require__(0); var _react2 = _interopRequireDefault(_react); var _reactDom = __webpack_require__(3); var _reactDom2 = _interopRequireDefault(_reactDom); var _Option = __webpack_require__(4); var _Option2 = _interopRequireDefault(_Option); 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; } /** * Default menu renderer * * @param {Object} props - React props * @param {ReactElement} props.children - Options to render * @returns {ReactElement} menu */ function MenuRenderer(props) { return props.children; } /** * Default component for menu section * * @param {Object} props - React props * @param {String} props.title - Section title * @param {ReactElement} props.children - Options in the section * @returns {ReactElement} menu section */ function MenuSectionRenderer(props) { var className = 'menu-section' + (props.className ? ' ' + props.className : ''); return _react2.default.createElement( 'div', { className: className }, _react2.default.createElement( 'div', { className: 'menu-section__title' }, props.title ), _react2.default.createElement( 'div', { className: 'menu-section__body' }, props.children ) ); } /** * Default menu component * * @param {Object} props - React props * @param {ReactElement} props.renderer - Menu renderer * @param {ReactRef} props.menuRef - Ref for the menu component * @param {Object} props.style - Inline styles for menu * @param {Function} props.onKeyDown - Handler for keyboard events * @param {ReactElement} props.children - Option elements */ function Menu(props) { var Renderer = props.renderer; return _react2.default.createElement( 'div', { className: 'menu', role: 'listbox', ref: props.menuRef, tabIndex: -1, style: props.style, onKeyDown: props.onKeyDown }, _react2.default.createElement( Renderer, null, props.children ) ); } /** * Portal for the menu * * @help https://developer.mozilla.org/en-US/docs/Web/Accessibility/Keyboard-navigable_JavaScript_widgets#Using_tabindex */ var MenuPortal = function (_Component) { _inherits(MenuPortal, _Component); function MenuPortal(props) { _classCallCheck(this, MenuPortal); var _this = _possibleConstructorReturn(this, (MenuPortal.__proto__ || Object.getPrototypeOf(MenuPortal)).call(this, props)); _this.state = { focused: -1 }; _this.optionRefs = {}; _this.el = document.createElement('div'); _this.el.classList.add('react-16-dropdown-portal'); props.portalClassName && _this.el.classList.add(props.portalClassName); _this.handleKeyDown = _this.handleKeyDown.bind(_this); _this.getAlignment = _this.getAlignment.bind(_this); _this.setOptionRefs = _this.setOptionRefs.bind(_this); _this.getOptions = _this.getOptions.bind(_this); _this.getOptionElements = _this.getOptionElements.bind(_this); return _this; } _createClass(MenuPortal, [{ key: 'componentDidMount', value: function componentDidMount() { document.querySelector(this.props.menuPortalTarget).appendChild(this.el); this.props.controlled && this.props.focused && this.optionRefs[this.props.focused].focus(); if (!this.props.controlled || this.props.autoFocusMenu) { this.props.menuRef.current.focus(); } } }, { key: 'componentDidUpdate', value: function componentDidUpdate() { var options = this.getOptions(); var key = void 0; if (!this.props.controlled) { var selected = options[this.state.focused]; key = selected && selected.value; } else { key = this.props.focused; } key && this.optionRefs[key].focus(); } }, { key: 'componentWillUnmount', value: function componentWillUnmount() { document.querySelector(this.props.menuPortalTarget).removeChild(this.el); } }, { key: 'getAlignment', value: function getAlignment() { // @todo allow other alignments var boundingRect = this.props.triggerBoundingRect; var top = boundingRect.top + boundingRect.height; if (this.props.align === 'left') { return { top: top, left: boundingRect.left }; } if (this.props.align === 'right') { return { top: top, right: window.innerWidth - boundingRect.right - window.scrollX }; } return {}; } }, { key: 'getOptions', value: function getOptions() { var _props = this.props, options = _props.options, sections = _props.sections; if (sections.length) { return sections.reduce(function (res, sec) { return res.concat(sec.options); }, []); } return options; } }, { key: 'getOptionElements', value: function getOptionElements() { var _this2 = this; var sections = this.props.sections; var options = this.getOptions(); var OptionElement = this.props.optionComponent; var SectionRenderer = this.props.menuSectionRenderer; var focused = this.props.controlled ? options.map(function (o) { return o.value; }).indexOf(this.props.focused) : this.state.focused; if (sections.length) { return sections.map(function (sec, i) { return _react2.default.createElement( SectionRenderer, _extends({}, sec, { key: sec.id }), sec.options.map(function (option, j) { return _react2.default.createElement(OptionElement, { className: option.className, data: option, focused: focused === i * (i + 1) + j, key: option.value, optionRef: function optionRef(node) { return _this2.setOptionRefs(node, option.value); }, renderer: _this2.props.optionRenderer, onClick: function onClick() { _this2.props.onClick(option); } }); }) ); }); } return options.map(function (option, i) { return _react2.default.createElement(OptionElement, { className: option.className, data: option, focused: focused === i, key: option.value, optionRef: function optionRef(node) { return _this2.setOptionRefs(node, option.value); }, renderer: _this2.props.optionRenderer, onClick: function onClick() { _this2.props.onClick(option); } }); }); } }, { key: 'setOptionRefs', value: function setOptionRefs(node, key) { node && (this.optionRefs[key] = node); } }, { key: 'handleKeyDown', value: function handleKeyDown(e) { typeof this.props.onMenuKeyDown === 'function' && this.props.onMenuKeyDown(e); if (this.props.controlled) { return; } var options = this.getOptions(); var maxFocus = options.length - 1; var focusedOption = options[this.state.focused]; // NOTE: This method is called when the menu is // opened with the keyboard. This case handles it if (e.key === 'Enter' && focusedOption && !focusedOption.disabled) { this.props.onClick(focusedOption.value); } else if (e.key === 'ArrowDown') { this.setState(function (prevState) { return { focused: prevState.focused < maxFocus ? prevState.focused + 1 : maxFocus }; }); } else if (e.key === 'ArrowUp') { this.setState(function (prevState) { return { focused: prevState.focused > 0 ? prevState.focused - 1 : 0 }; }); } e.preventDefault(); } }, { key: 'render', value: function render() { var MenuElement = this.props.menuComponent; var menu = _react2.default.createElement( MenuElement, { menuRef: this.props.menuRef, renderer: this.props.menuRenderer, style: this.getAlignment(), onKeyDown: this.handleKeyDown }, this.getOptionElements() ); return _reactDom2.default.createPortal(menu, this.el); } }]); return MenuPortal; }(_react.Component); exports.default = MenuPortal; MenuPortal.defaultProps = { menuComponent: Menu, optionComponent: _Option2.default, menuRenderer: MenuRenderer, menuSectionRenderer: MenuSectionRenderer, menuPortalTarget: 'body' }; /***/ }), /* 3 */ /***/ (function(module, exports) { module.exports = __WEBPACK_EXTERNAL_MODULE_3__; /***/ }), /* 4 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); 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; }; exports.OptionRenderer = OptionRenderer; exports.default = Option; var _react = __webpack_require__(0); var _react2 = _interopRequireDefault(_react); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } /** * Default renderer for option. Renders a div with * child as label. * * @param {Object} props - React props * @param {String} props.className - Custom class * @param {Boolean} props.focused - Is focused? * @param {String|ReactElement} props.label - Option label * @returns {ReactElement} */ function OptionRenderer(props) { var classes = 'option' + (props.focused ? ' focused' : '') + (props.disabled ? ' disabled' : '') + (props.className ? ' ' + props.className : ''); return _react2.default.createElement( 'div', { className: classes }, props.label ); } /** * Default option component. It renders a div with * renderer as a child. * * @param {Object} props - React props * @param {Boolean} props.focused - Is option focused? * @param {ReactRef} props.optionRef - React ref for option * @param {Function} props.onClick - Click handler * @param {Object} props.data - Option data * @param {String} props.className - Custom class * @returns {ReactElement} */ function Option(props) { var Renderer = props.renderer; return _react2.default.createElement( 'div', { 'aria-selected': props.focused, role: 'option', tabIndex: -1, ref: props.optionRef, onClick: props.data.disabled ? undefined : props.onClick }, _react2.default.createElement(Renderer, _extends({}, props.data, { className: props.className, focused: props.focused })) ); } Option.defaultProps = { renderer: OptionRenderer, data: {} }; /***/ }), /* 5 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.TriggerRenderer = TriggerRenderer; exports.default = Trigger; var _react = __webpack_require__(0); var _react2 = _interopRequireDefault(_react); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } /** * Default trigger renderer - Displays a plain button * with label * * @param {Object} props - React props * @param {Boolean} props.disabled - Trigger disabled * @param {String|ReactElement} props.label - Trigger display label */ function TriggerRenderer(props) { return _react2.default.createElement( 'button', { className: 'trigger-renderer', disabled: props.disabled }, props.label ); } /** * Default trigger component - Renders a div with * all the handlers * * @param {Object} props - React props * @param {ReactElement} [props.renderer] - Custom trigger renderer * @param {ReactRef} props.triggerRef - React ref for trigger * @param {Function} props.onClick - Click handler * @param {Function} props.onKeyDown - Key down handler * @param {Boolean} props.disabled - Trigger disabled * @param {String|ReactElement} props.label - Trigger display label */ function Trigger(props) { var Renderer = props.renderer || TriggerRenderer; return _react2.default.createElement( 'div', { className: 'trigger', ref: props.triggerRef, role: 'button', onClick: props.onClick, onKeyDown: props.onKeyDown }, _react2.default.createElement(Renderer, { disabled: props.disabled, label: props.label }) ); } /***/ }), /* 6 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.getAbsoluteBoundingRect = getAbsoluteBoundingRect; var optimizedResize = exports.optimizedResize = function () { var callbacks = []; var running = false; // run the actual callbacks function runCallbacks() { callbacks.forEach(function (callback) { callback(); }); running = false; } // fired on resize event function resize() { if (!running) { running = true; if (window.requestAnimationFrame) { window.requestAnimationFrame(runCallbacks); } else { setTimeout(runCallbacks, 66); } } } // adds callback to loop function addCallback(callback) { if (callback) { callbacks.push(callback); } } return { // public method to add additional callback add: function add(callback) { if (!callbacks.length) { window.addEventListener('resize', resize); } addCallback(callback); } }; }(); function getAbsoluteBoundingRect(el) { var clientRect = el.getBoundingClientRect(); var rect = {}; rect.left = window.scrollX + clientRect.left; rect.top = window.scrollY + clientRect.top; rect.right = clientRect.right; rect.bottom = clientRect.bottom; rect.height = clientRect.height; return rect; } /***/ }) /******/ ]); });