grommet
Version:
The most advanced UX framework for enterprise applications.
468 lines (383 loc) • 15.7 kB
JavaScript
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.dropAlignPropType = undefined;
var _extends2 = require('babel-runtime/helpers/extends');
var _extends3 = _interopRequireDefault(_extends2);
var _getPrototypeOf = require('babel-runtime/core-js/object/get-prototype-of');
var _getPrototypeOf2 = _interopRequireDefault(_getPrototypeOf);
var _classCallCheck2 = require('babel-runtime/helpers/classCallCheck');
var _classCallCheck3 = _interopRequireDefault(_classCallCheck2);
var _createClass2 = require('babel-runtime/helpers/createClass');
var _createClass3 = _interopRequireDefault(_createClass2);
var _possibleConstructorReturn2 = require('babel-runtime/helpers/possibleConstructorReturn');
var _possibleConstructorReturn3 = _interopRequireDefault(_possibleConstructorReturn2);
var _inherits2 = require('babel-runtime/helpers/inherits');
var _inherits3 = _interopRequireDefault(_inherits2);
var _react = require('react');
var _react2 = _interopRequireDefault(_react);
var _reactDom = require('react-dom');
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 }; }
var CLASS_ROOT = _CSSClassnames2.default.DROP; // (C) Copyright 2014 Hewlett Packard Enterprise Development LP
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) {
(0, _inherits3.default)(DropContents, _Component);
function DropContents(props, context) {
(0, _classCallCheck3.default)(this, DropContents);
var _this = (0, _possibleConstructorReturn3.default)(this, (DropContents.__proto__ || (0, _getPrototypeOf2.default)(DropContents)).call(this, props, context));
_this._processTab = _this._processTab.bind(_this);
return _this;
}
(0, _createClass3.default)(DropContents, [{
key: 'getChildContext',
value: function getChildContext() {
var context = this.props.context;
return (0, _extends3.default)({}, context);
}
}, {
key: 'componentDidMount',
value: function componentDidMount() {
var focusControl = this.props.focusControl;
if (focusControl) {
this.originalFocusedElement = document.activeElement;
if (!this.containerRef.contains(document.activeElement)) {
this.anchorStepRef.focus();
this.anchorStepRef.scrollIntoView();
}
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);
this.originalFocusedElement.focus();
}
}
}, {
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',
ref: function ref(_ref) {
return _this2.anchorStepRef = _ref;
} });
}
return _react2.default.createElement(
'div',
{ ref: function ref(_ref2) {
return _this2.containerRef = _ref2;
} },
anchorStep,
content
);
}
}]);
return DropContents;
}(_react.Component);
DropContents.displayName = 'DropContents';
DropContents.propTypes = {
content: _react.PropTypes.node.isRequired,
context: _react.PropTypes.any,
focusControl: _react.PropTypes.bool
};
DropContents.childContextTypes = {
history: _react.PropTypes.object,
intl: _react.PropTypes.object,
onDropChange: _react.PropTypes.func,
router: _react.PropTypes.any,
store: _react.PropTypes.object
};
var _normalizeOptions = function _normalizeOptions(options) {
options = (0, _extends3.default)({}, options);
// normalize for older interface that just had align content
if (options.top || options.bottom || options.left || options.right) {
options = { align: 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(',') + "]");
}
options.align = options.align || {};
if (!options.align.top && !options.align.bottom) {
options.align.top = "top";
}
if (!options.align.left && !options.align.right) {
options.align.left = "left";
}
options.responsive = options.responsive !== false ? true : options.responsive;
return options;
};
// 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, options) {
(0, _classCallCheck3.default)(this, Drop);
options = _normalizeOptions(options);
var _options = options,
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);
// setup DOM
var container = document.createElement('div');
container.className = 'grommet ' + CLASS_ROOT + ' ' + (options.className || '');
if (options.colorIndex) {
container.className += ' ' + BACKGROUND_COLOR_INDEX + '-' + options.colorIndex;
}
// prepend in body to avoid browser scroll issues
document.body.insertBefore(container, document.body.firstChild);
(0, _reactDom.render)(_react2.default.createElement(DropContents, { content: content, context: context,
focusControl: focusControl }), container);
var scrollParents = (0, _DOM.findScrollParents)(control);
// initialize state
this.state = { container: container, control: control, options: options, scrollParents: scrollParents };
this._listen();
// position content
this.place();
}
(0, _createClass3.default)(Drop, [{
key: '_listen',
value: function _listen() {
var _this3 = this;
var scrollParents = this.state.scrollParents;
scrollParents.forEach(function (scrollParent) {
scrollParent.addEventListener('scroll', _this3.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 _this4 = 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', _this4.place);
});
var nextScrollParents = (0, _DOM.findScrollParents)(this._control);
nextScrollParents.forEach(function (scrollParent) {
scrollParent.addEventListener('scroll', _this4.place);
});
this.state.scrollParents = nextScrollParents;
this.place();
}
}, {
key: 'place',
value: function place() {
var _state = this.state,
control = _state.control,
container = _state.container,
_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);
}
}
}
//for Chrome, Safari, and Opera, use document.body
//for Firefox and IE, use document.documentElement
var scrollTop = document.documentElement && document.documentElement.scrollTop || document.body.scrollTop;
container.style.left = left + 'px';
container.style.width = width + 'px';
// We use position:absolute and the body element's position
// to handle mobile browsers better. We used to use position:fixed
// but that didn't work on mobile browsers as well.
container.style.top = top + scrollTop + 'px';
container.style.maxHeight = maxHeight + 'px';
}
}, {
key: 'render',
value: function render(content) {
var _this5 = 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 () {
_this5.place();
// reset container to its original scroll position
container.scrollTop = originalScrollPosition;
});
}
}, {
key: 'remove',
value: function remove() {
var _this6 = this;
var _state3 = this.state,
container = _state3.container,
scrollParents = _state3.scrollParents;
scrollParents.forEach(function (scrollParent) {
scrollParent.removeEventListener('scroll', _this6.place);
});
window.removeEventListener('resize', this._onResize);
(0, _reactDom.unmountComponentAtNode)(container);
document.body.removeChild(container);
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 = _react.PropTypes.shape({
top: _react.PropTypes.oneOf(VERTICAL_ALIGN_OPTIONS),
bottom: _react.PropTypes.oneOf(VERTICAL_ALIGN_OPTIONS),
left: _react.PropTypes.oneOf(HORIZONTAL_ALIGN_OPTIONS),
right: _react.PropTypes.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);
};