react-tray
Version:
An accessible tray component useful for navigation menus
586 lines (479 loc) • 16.2 kB
JavaScript
(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["ReactTray"] = factory(require("react"), require("react-dom"));
else
root["ReactTray"] = factory(root["React"], root["ReactDOM"]);
})(this, function(__WEBPACK_EXTERNAL_MODULE_2__, __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] = {
/******/ exports: {},
/******/ id: moduleId,
/******/ loaded: false
/******/ };
/******/
/******/ // Execute the module function
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ // Flag the module as loaded
/******/ module.loaded = 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;
/******/
/******/ // __webpack_public_path__
/******/ __webpack_require__.p = "";
/******/
/******/ // Load entry module and return exports
/******/ return __webpack_require__(0);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ function(module, exports, __webpack_require__) {
'use strict';
module.exports = __webpack_require__(1);
/***/ },
/* 1 */
/***/ function(module, exports, __webpack_require__) {
'use strict';
Object.defineProperty(exports, '__esModule', {
value: true
});
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
var _react = __webpack_require__(2);
var _react2 = _interopRequireDefault(_react);
var _reactDom = __webpack_require__(3);
var _reactDom2 = _interopRequireDefault(_reactDom);
var _TrayPortal = __webpack_require__(4);
var _TrayPortal2 = _interopRequireDefault(_TrayPortal);
var _helpersCustomPropTypes = __webpack_require__(9);
var renderSubtreeIntoContainer = _reactDom2['default'].unstable_renderSubtreeIntoContainer;
exports['default'] = _react2['default'].createClass({
displayName: 'Tray',
propTypes: {
isOpen: _react2['default'].PropTypes.bool,
onBlur: _react2['default'].PropTypes.func,
onOpen: _react2['default'].PropTypes.func,
closeTimeoutMS: _react2['default'].PropTypes.number,
closeOnBlur: _react2['default'].PropTypes.bool,
maintainFocus: _react2['default'].PropTypes.bool,
getElementToFocus: _helpersCustomPropTypes.a11yFunction,
getAriaHideElement: _helpersCustomPropTypes.a11yFunction
},
getDefaultProps: function getDefaultProps() {
return {
isOpen: false,
closeTimeoutMS: 0,
closeOnBlur: true,
maintainFocus: true
};
},
componentDidMount: function componentDidMount() {
this.node = document.createElement('div');
this.node.className = 'ReactTrayPortal';
document.body.appendChild(this.node);
this.renderPortal(this.props);
},
componentWillReceiveProps: function componentWillReceiveProps(props) {
this.renderPortal(props);
},
componentWillUnmount: function componentWillUnmount() {
_reactDom2['default'].unmountComponentAtNode(this.node);
document.body.removeChild(this.node);
},
renderPortal: function renderPortal(props) {
delete props.ref;
renderSubtreeIntoContainer(this, _react2['default'].createElement(_TrayPortal2['default'], props), this.node);
},
render: function render() {
return null;
}
});
module.exports = exports['default'];
/***/ },
/* 2 */
/***/ function(module, exports) {
module.exports = __WEBPACK_EXTERNAL_MODULE_2__;
/***/ },
/* 3 */
/***/ function(module, exports) {
module.exports = __WEBPACK_EXTERNAL_MODULE_3__;
/***/ },
/* 4 */
/***/ function(module, exports, __webpack_require__) {
'use strict';
Object.defineProperty(exports, '__esModule', {
value: true
});
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
var _react = __webpack_require__(2);
var _react2 = _interopRequireDefault(_react);
var _classnames = __webpack_require__(5);
var _classnames2 = _interopRequireDefault(_classnames);
var _helpersFocusManager = __webpack_require__(6);
var _helpersFocusManager2 = _interopRequireDefault(_helpersFocusManager);
var _helpersIsLeavingNode = __webpack_require__(7);
var _helpersIsLeavingNode2 = _interopRequireDefault(_helpersIsLeavingNode);
var _helpersTabbable = __webpack_require__(8);
var _helpersTabbable2 = _interopRequireDefault(_helpersTabbable);
var styles = {
overlay: {
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0
},
content: {
position: 'absolute',
background: '#fff'
}
};
var CLASS_NAMES = {
overlay: {
base: 'ReactTray__Overlay',
afterOpen: 'ReactTray__Overlay--after-open',
beforeClose: 'ReactTray__Overlay--before-close'
},
content: {
base: 'ReactTray__Content',
afterOpen: 'ReactTray__Content--after-open',
beforeClose: 'ReactTray__Content--before-close'
}
};
function isChild(parent, child) {
if (parent === child) {
return true;
}
var node = child;
/* eslint no-cond-assign:0 */
while (node = node.parentNode) {
if (node === parent) {
return true;
}
}
return false;
}
exports['default'] = _react2['default'].createClass({
displayName: 'TrayPortal',
propTypes: {
className: _react.PropTypes.string,
overlayClassName: _react.PropTypes.string,
isOpen: _react.PropTypes.bool,
onBlur: _react.PropTypes.func,
onOpen: _react.PropTypes.func,
closeOnBlur: _react.PropTypes.bool,
closeTimeoutMS: _react.PropTypes.number,
children: _react.PropTypes.any,
maintainFocus: _react.PropTypes.bool,
getElementToFocus: _react.PropTypes.func,
getAriaHideElement: _react.PropTypes.func
},
getInitialState: function getInitialState() {
return {
afterOpen: false,
beforeClose: false
};
},
componentDidMount: function componentDidMount() {
if (this.props.isOpen) {
this.setFocusAfterRender(true);
this.open();
}
},
componentWillReceiveProps: function componentWillReceiveProps(props) {
if (props.isOpen) {
this.setFocusAfterRender(true);
this.open();
} else if (this.props.isOpen && !props.isOpen) {
this.close();
}
},
componentDidUpdate: function componentDidUpdate() {
if (this.focusAfterRender) {
if (this.props.getElementToFocus) {
this.props.getElementToFocus().focus();
} else {
this.focusContent();
}
this.setFocusAfterRender(false);
}
},
setFocusAfterRender: function setFocusAfterRender(focus) {
this.focusAfterRender = focus;
},
focusContent: function focusContent() {
this.refs.content.focus();
},
applyAriaHidden: function applyAriaHidden(element) {
element.setAttribute('aria-hidden', true);
},
removeAriaHidden: function removeAriaHidden(element) {
element.removeAttribute('aria-hidden');
},
handleOverlayClick: function handleOverlayClick(e) {
if (!isChild(this.refs.content, e.target)) {
this.props.onBlur();
}
},
handleContentKeyDown: function handleContentKeyDown(e) {
// Treat ESC as blur/close
if (e.keyCode === 27) {
this.props.onBlur();
}
// Keep focus inside the tray if maintainFocus is true
if (e.keyCode === 9 && this.props.maintainFocus && (0, _helpersIsLeavingNode2['default'])(this.refs.content, e)) {
e.preventDefault();
var tabbable = (0, _helpersTabbable2['default'])(this.refs.content);
var target = tabbable[e.shiftKey ? tabbable.length - 1 : 0];
target.focus();
return;
}
// Treat tabbing away from content as blur/close if closeOnBlur
if (e.keyCode === 9 && this.props.closeOnBlur && (0, _helpersIsLeavingNode2['default'])(this.refs.content, e)) {
e.preventDefault();
this.props.onBlur();
}
},
open: function open() {
var _this = this;
_helpersFocusManager2['default'].markForFocusLater();
this.setState({ isOpen: true }, function () {
if (_this.props.onOpen) {
_this.props.onOpen();
}
if (_this.props.getAriaHideElement) {
_this.applyAriaHidden(_this.props.getAriaHideElement());
}
_this.setState({ afterOpen: true });
});
},
close: function close() {
if (this.props.closeTimeoutMS > 0) {
this.closeWithTimeout();
} else {
this.closeWithoutTimeout();
}
if (this.props.getAriaHideElement) {
this.removeAriaHidden(this.props.getAriaHideElement());
}
},
closeWithTimeout: function closeWithTimeout() {
var _this2 = this;
this.setState({ beforeClose: true }, function () {
setTimeout(_this2.closeWithoutTimeout, _this2.props.closeTimeoutMS);
});
},
closeWithoutTimeout: function closeWithoutTimeout() {
this.setState({
afterOpen: false,
beforeClose: false
}, this.afterClose);
},
afterClose: function afterClose() {
_helpersFocusManager2['default'].returnFocus();
},
shouldBeClosed: function shouldBeClosed() {
return !this.props.isOpen && !this.state.beforeClose;
},
buildClassName: function buildClassName(which) {
var className = CLASS_NAMES[which].base;
if (this.state.afterOpen) {
className += ' ' + CLASS_NAMES[which].afterOpen;
}
if (this.state.beforeClose) {
className += ' ' + CLASS_NAMES[which].beforeClose;
}
return className;
},
render: function render() {
return this.shouldBeClosed() ? _react2['default'].createElement('div', null) : _react2['default'].createElement(
'div',
{
ref: 'overlay',
style: styles.overlay,
className: (0, _classnames2['default'])(this.buildClassName('overlay'), this.props.overlayClassName),
onClick: this.handleOverlayClick
},
_react2['default'].createElement(
'div',
{
ref: 'content',
style: styles.content,
className: (0, _classnames2['default'])(this.buildClassName('content'), this.props.className),
onKeyDown: this.handleContentKeyDown,
tabIndex: '-1'
},
this.props.children
)
);
}
});
module.exports = exports['default'];
/***/ },
/* 5 */
/***/ function(module, exports, __webpack_require__) {
var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/*!
Copyright (c) 2016 Jed Watson.
Licensed under the MIT License (MIT), see
http://jedwatson.github.io/classnames
*/
/* global define */
(function () {
'use strict';
var hasOwn = {}.hasOwnProperty;
function classNames () {
var classes = [];
for (var i = 0; i < arguments.length; i++) {
var arg = arguments[i];
if (!arg) continue;
var argType = typeof arg;
if (argType === 'string' || argType === 'number') {
classes.push(arg);
} else if (Array.isArray(arg)) {
classes.push(classNames.apply(null, arg));
} else if (argType === 'object') {
for (var key in arg) {
if (hasOwn.call(arg, key) && arg[key]) {
classes.push(key);
}
}
}
}
return classes.join(' ');
}
if (typeof module !== 'undefined' && module.exports) {
module.exports = classNames;
} else if (true) {
// register as 'classnames', consistent with npm package name
!(__WEBPACK_AMD_DEFINE_ARRAY__ = [], __WEBPACK_AMD_DEFINE_RESULT__ = function () {
return classNames;
}.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
} else {
window.classNames = classNames;
}
}());
/***/ },
/* 6 */
/***/ function(module, exports) {
'use strict';
var focusLaterElement = null;
exports.markForFocusLater = function markForFocusLater() {
focusLaterElement = document.activeElement;
};
exports.returnFocus = function returnFocus() {
try {
focusLaterElement.focus();
} catch (e) {
/* eslint no-console:0 */
console.warn('You tried to return focus to ' + focusLaterElement + ' but it is not in the DOM anymore');
}
focusLaterElement = null;
};
/***/ },
/* 7 */
/***/ function(module, exports, __webpack_require__) {
'use strict';
Object.defineProperty(exports, '__esModule', {
value: true
});
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
var _helpersTabbable = __webpack_require__(8);
var _helpersTabbable2 = _interopRequireDefault(_helpersTabbable);
exports['default'] = function (node, event) {
var tabbable = (0, _helpersTabbable2['default'])(node);
var finalTabbable = tabbable[event.shiftKey ? 0 : tabbable.length - 1];
var isLeavingNode = finalTabbable === document.activeElement;
return isLeavingNode;
};
module.exports = exports['default'];
/***/ },
/* 8 */
/***/ function(module, exports) {
/*!
* Adapted from jQuery UI core
*
* http://jqueryui.com
*
* Copyright 2014 jQuery Foundation and other contributors
* Released under the MIT license.
* http://jquery.org/license
*
* http://api.jqueryui.com/category/ui-core/
*/
'use strict';
Object.defineProperty(exports, '__esModule', {
value: true
});
function hidden(el) {
return el.offsetWidth <= 0 && el.offsetHeight <= 0 || el.style.display === 'none';
}
function visible(element) {
var el = element;
while (el) {
if (el === document.body) break;
if (hidden(el)) return false;
el = el.parentNode;
}
return true;
}
function focusable(element, isTabIndexNotNaN) {
var nodeName = element.nodeName.toLowerCase();
/* eslint no-nested-ternary:0 */
return (/input|select|textarea|button|object/.test(nodeName) ? !element.disabled : nodeName === 'a' ? element.href || isTabIndexNotNaN : isTabIndexNotNaN) && visible(element);
}
function tabbable(element) {
var tabIndex = element.getAttribute('tabindex');
if (tabIndex === null) tabIndex = undefined;
var isTabIndexNaN = isNaN(tabIndex);
return (isTabIndexNaN || tabIndex >= 0) && focusable(element, !isTabIndexNaN);
}
function findTabbableDescendants(element) {
return [].slice.call(element.querySelectorAll('*'), 0).filter(function (el) {
return tabbable(el);
});
}
exports['default'] = findTabbableDescendants;
module.exports = exports['default'];
/***/ },
/* 9 */
/***/ function(module, exports) {
// Adapted from https://github.com/react-bootstrap/react-prop-types/blob/master/src/isRequiredForA11y.js
'use strict';
Object.defineProperty(exports, '__esModule', {
value: true
});
exports.a11yFunction = a11yFunction;
function a11yFunction(props, propName, componentName) {
if (!props[propName] || typeof props[propName] !== 'function') {
return new Error('The prop \'' + propName + '\' is required to make \'' + componentName + '\' fully accessible. ' + 'This will greatly improve the experience for users of assistive technologies. ' + 'You should provide a function that returns a DOM node.');
}
}
/***/ }
/******/ ])
});
;
//# sourceMappingURL=react-tray.js.map