UNPKG

grommet

Version:

The most advanced UX framework for enterprise applications.

488 lines (404 loc) 17.7 kB
'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); exports.dropAlignPropType = 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; }; }(); var _react = require('react'); var _react2 = _interopRequireDefault(_react); var _propTypes = require('prop-types'); var _propTypes2 = _interopRequireDefault(_propTypes); var _reactDom = require('react-dom'); var _classnames2 = require('classnames'); var _classnames3 = _interopRequireDefault(_classnames2); var _DOM = require('./DOM'); var _CSSClassnames = require('./CSSClassnames'); var _CSSClassnames2 = _interopRequireDefault(_CSSClassnames); var _KeyboardAccelerators = require('./KeyboardAccelerators'); var _KeyboardAccelerators2 = _interopRequireDefault(_KeyboardAccelerators); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 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; } 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; } // (C) Copyright 2014 Hewlett Packard Enterprise Development LP var CLASS_ROOT = _CSSClassnames2.default.DROP; var BACKGROUND_COLOR_INDEX = _CSSClassnames2.default.BACKGROUND_COLOR_INDEX; /* * Drop is a utility for rendering components like drop down menus layered above * their initiating controls. */ var VERTICAL_ALIGN_OPTIONS = ['top', 'bottom']; var HORIZONTAL_ALIGN_OPTIONS = ['right', 'left']; var DropContents = function (_Component) { _inherits(DropContents, _Component); function DropContents(props, context) { _classCallCheck(this, DropContents); var _this = _possibleConstructorReturn(this, (DropContents.__proto__ || Object.getPrototypeOf(DropContents)).call(this, props, context)); _this._processTab = _this._processTab.bind(_this); return _this; } _createClass(DropContents, [{ key: 'getChildContext', value: function getChildContext() { var context = this.props.context; return _extends({}, context); } }, { key: 'componentDidMount', value: function componentDidMount() { var focusControl = this.props.focusControl; if (focusControl) { this._keyboardHandlers = { tab: this._processTab }; _KeyboardAccelerators2.default.startListeningToKeyboard(this, this._keyboardHandlers); } } }, { key: 'componentWillUnmount', value: function componentWillUnmount() { var focusControl = this.props.focusControl; if (focusControl) { _KeyboardAccelerators2.default.stopListeningToKeyboard(this, this._keyboardHandlers); } } }, { key: '_processTab', value: function _processTab(event) { var items = this._containerRef.getElementsByTagName('*'); items = (0, _DOM.filterByFocusable)(items); if (!items || items.length === 0) { event.preventDefault(); } else { if (event.shiftKey) { if (event.target === items[0]) { items[items.length - 1].focus(); event.preventDefault(); } } else if (event.target === items[items.length - 1]) { items[0].focus(); event.preventDefault(); } } } }, { key: 'render', value: function render() { var _this2 = this; var _props = this.props, content = _props.content, focusControl = _props.focusControl; var anchorStep = void 0; if (focusControl) { anchorStep = _react2.default.createElement('a', { tabIndex: '-1', 'aria-hidden': 'true', className: CLASS_ROOT + '__anchor' }); } return _react2.default.createElement( 'div', { ref: function ref(_ref) { return _this2._containerRef = _ref; } }, anchorStep, content ); } }]); return DropContents; }(_react.Component); DropContents.displayName = 'DropContents'; DropContents.propTypes = { content: _propTypes2.default.node.isRequired, context: _propTypes2.default.any, focusControl: _propTypes2.default.bool }; DropContents.childContextTypes = { history: _propTypes2.default.object, intl: _propTypes2.default.object, onDropChange: _propTypes2.default.func, router: _propTypes2.default.any, store: _propTypes2.default.object }; var _normalizeOptions = function _normalizeOptions(options) { var opts = _extends({}, options); // normalize for older interface that just had align content if (options.top || options.bottom || options.left || options.right) { opts = { align: _extends({}, options) }; } // validate align if (options && options.align && options.align.top && VERTICAL_ALIGN_OPTIONS.indexOf(options.align.top) === -1) { console.warn("Warning: Invalid align.top value '" + options.align.top + "' supplied to Drop," + "expected one of [" + VERTICAL_ALIGN_OPTIONS.join(',') + "]"); } if (options.align && options.align.bottom && VERTICAL_ALIGN_OPTIONS.indexOf(options.align.bottom) === -1) { console.warn("Warning: Invalid align.bottom value '" + options.align.bottom + "' supplied to Drop," + "expected one of [" + VERTICAL_ALIGN_OPTIONS.join(',') + "]"); } if (options.align && options.align.left && HORIZONTAL_ALIGN_OPTIONS.indexOf(options.align.left) === -1) { console.warn("Warning: Invalid align.left value '" + options.align.left + "' supplied to Drop," + "expected one of [" + HORIZONTAL_ALIGN_OPTIONS.join(',') + "]"); } if (options.align && options.align.right && HORIZONTAL_ALIGN_OPTIONS.indexOf(options.align.right) === -1) { console.warn("Warning: Invalid align.right value '" + options.align.right + "' supplied to Drop," + "expected one of [" + HORIZONTAL_ALIGN_OPTIONS.join(',') + "]"); } opts.align = _extends({}, opts.align) || {}; if (!options.align.top && !options.align.bottom) { opts.align.top = "top"; } if (!options.align.left && !options.align.right) { opts.align.left = "left"; } opts.responsive = options.responsive !== false ? true : options.responsive; return opts; }; // Drop options: // // align: See dropAlignPropType // className: PropTypes.string // colorIndex: PropTypes.string // Background color // context: PropTypes.object // React context to pass through // focusControl: PropTypes.bool // Whether to focus inside the dropped content when added // responsive: PropTypes.bool // Whether to dynamically re-place when resized // var Drop = function () { function Drop(control, content, opts) { var _classnames, _this3 = this; _classCallCheck(this, Drop); var options = _normalizeOptions(opts); var context = options.context, focusControl = options.focusControl; // bind functions to instance this.render = this.render.bind(this); this.remove = this.remove.bind(this); this.place = this.place.bind(this); this._onResize = this._onResize.bind(this); this._control = control; // setup DOM var container = document.createElement('div'); container.className = (0, _classnames3.default)('grommet', CLASS_ROOT, (_classnames = {}, _defineProperty(_classnames, options.className, options.className), _defineProperty(_classnames, BACKGROUND_COLOR_INDEX + '-' + options.colorIndex, options.colorIndex), _classnames)); // prepend in body to avoid browser scroll issues document.body.insertBefore(container, document.body.firstChild); var scrollParents = (0, _DOM.findScrollParents)(control); // initialize state this.state = { container: container, control: control, initialFocusNeeded: focusControl, options: options, scrollParents: scrollParents }; (0, _reactDom.render)(_react2.default.createElement(DropContents, { content: content, context: context, focusControl: focusControl }), container, function () { return _this3.place(); }); this._listen(); } _createClass(Drop, [{ key: '_listen', value: function _listen() { var _this4 = this; var scrollParents = this.state.scrollParents; scrollParents.forEach(function (scrollParent) { scrollParent.addEventListener('scroll', _this4.place); }); // we intentionally skipped debounce as we believe resizing // will not be a common action. Also the UI looks better if the Drop // doesn’t lag to align with the control component. window.addEventListener('resize', this._onResize); } }, { key: '_onResize', value: function _onResize() { var _this5 = this; var scrollParents = this.state.scrollParents; // we need to update scroll parents as Responsive options may change // the parent for the target element scrollParents.forEach(function (scrollParent) { scrollParent.removeEventListener('scroll', _this5.place); }); var nextScrollParents = (0, _DOM.findScrollParents)(this._control); nextScrollParents.forEach(function (scrollParent) { scrollParent.addEventListener('scroll', _this5.place); }); this.state.scrollParents = nextScrollParents; this.place(); } }, { key: 'place', value: function place() { var _state = this.state, control = _state.control, container = _state.container, initialFocusNeeded = _state.initialFocusNeeded, _state$options = _state.options, align = _state$options.align, responsive = _state$options.responsive; var windowWidth = window.innerWidth; var windowHeight = window.innerHeight; // clear prior styling container.style.left = ''; container.style.width = ''; container.style.top = ''; container.style.maxHeight = ''; // get bounds var controlRect = control.getBoundingClientRect(); var containerRect = container.getBoundingClientRect(); // determine width var width = Math.min(Math.max(controlRect.width, containerRect.width), windowWidth); // set left position var left = void 0; if (align.left) { if ('left' === align.left) { left = controlRect.left; } else if ('right' === align.left) { left = controlRect.left - width; } } else if (align.right) { if ('left' === align.right) { left = controlRect.left - width; } else if ('right' === align.right) { left = controlRect.left + controlRect.width - width; } } if (left + width > windowWidth) { left -= left + width - windowWidth; } else if (left < 0) { left = 0; } // set top position var top = void 0, maxHeight = void 0; if (align.top) { if ('top' === align.top) { top = controlRect.top; maxHeight = Math.min(windowHeight - controlRect.top, windowHeight); } else { top = controlRect.bottom; maxHeight = Math.min(windowHeight - controlRect.bottom, windowHeight - controlRect.height); } } else if (align.bottom) { if ('bottom' === align.bottom) { top = controlRect.bottom - containerRect.height; maxHeight = Math.max(controlRect.bottom, 0); } else { top = controlRect.top - containerRect.height; maxHeight = Math.max(controlRect.top, 0); } } // if we can't fit it all, see if there's more room the other direction if (containerRect.height > maxHeight) { // We need more room than we have. if (align.top && top > windowHeight / 2) { // We put it below, but there's more room above, put it above if (align.top === 'bottom') { if (responsive) { top = Math.max(controlRect.top - containerRect.height, 0); } maxHeight = controlRect.top; } else { if (responsive) { top = Math.max(controlRect.bottom - containerRect.height, 0); } maxHeight = controlRect.bottom; } } else if (align.bottom && maxHeight < windowHeight / 2) { // We put it above but there's more room below, put it below if (align.bottom === 'bottom') { if (responsive) { top = controlRect.top; } maxHeight = Math.min(windowHeight - top, windowHeight); } else { if (responsive) { top = controlRect.bottom; } maxHeight = Math.min(windowHeight - top, windowHeight - controlRect.height); } } } container.style.left = left + 'px'; // offset width by 0.1 to avoid a bug in ie11 that // unnecessarily wraps the text if width is the same container.style.width = width + 0.1 + 'px'; // the (position:absolute + scrollTop) // is presenting issues with desktop scroll flickering container.style.top = top + 'px'; container.style.maxHeight = windowHeight - top + 'px'; if (initialFocusNeeded) { // Now that we've placed it, focus on it this._focus(); } } }, { key: '_focus', value: function _focus() { var container = this.state.container; this.state.originalFocusedElement = document.activeElement; if (!container.contains(document.activeElement)) { var anchor = container.querySelector(CLASS_ROOT + '__anchor'); if (anchor) { anchor.focus(); anchor.scrollIntoView(); } } delete this.state.initialFocusNeeded; } }, { key: 'render', value: function render(content) { var _this6 = this; var _state2 = this.state, container = _state2.container, _state2$options = _state2.options, context = _state2$options.context, focusControl = _state2$options.focusControl; var originalScrollPosition = container.scrollTop; (0, _reactDom.render)(_react2.default.createElement(DropContents, { content: content, context: context, focusControl: focusControl }), container, function () { _this6.place(); // reset container to its original scroll position container.scrollTop = originalScrollPosition; }); } }, { key: 'remove', value: function remove() { var _this7 = this; var _state3 = this.state, container = _state3.container, originalFocusedElement = _state3.originalFocusedElement, scrollParents = _state3.scrollParents; scrollParents.forEach(function (scrollParent) { scrollParent.removeEventListener('scroll', _this7.place); }); window.removeEventListener('resize', this._onResize); (0, _reactDom.unmountComponentAtNode)(container); document.body.removeChild(container); // weird bug in Chrome does not remove child if // document.body.insertBefore is called in another new drop. // the code below will go over remaining drop that was not removed [].forEach.call(document.getElementsByClassName(CLASS_ROOT), function (element) { if (element.getAttribute('style') === container.getAttribute('style')) { document.body.removeChild(element); } }); if (originalFocusedElement) { originalFocusedElement.focus(); } this.state = undefined; } }]); return Drop; }(); // How callers can validate a property for drop alignment which will be // passed to add(). exports.default = Drop; var dropAlignPropType = exports.dropAlignPropType = _propTypes2.default.shape({ top: _propTypes2.default.oneOf(VERTICAL_ALIGN_OPTIONS), bottom: _propTypes2.default.oneOf(VERTICAL_ALIGN_OPTIONS), left: _propTypes2.default.oneOf(HORIZONTAL_ALIGN_OPTIONS), right: _propTypes2.default.oneOf(HORIZONTAL_ALIGN_OPTIONS) }); Drop.add = function (control, content, options) { console.warn("Warning: Drop.add() is deprecated, use new Drop()."); return new Drop(control, content, options); };