react-tray
Version:
An accessible tray component useful for navigation menus
249 lines (209 loc) • 6.53 kB
JavaScript
'use strict';
Object.defineProperty(exports, '__esModule', {
value: true
});
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
var _react = require('react');
var _react2 = _interopRequireDefault(_react);
var _classnames = require('classnames');
var _classnames2 = _interopRequireDefault(_classnames);
var _helpersFocusManager = require('../helpers/focusManager');
var _helpersFocusManager2 = _interopRequireDefault(_helpersFocusManager);
var _helpersIsLeavingNode = require('../helpers/isLeavingNode');
var _helpersIsLeavingNode2 = _interopRequireDefault(_helpersIsLeavingNode);
var _helpersTabbable = require('../helpers/tabbable');
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'];