├── .npmignore ├── .travis.yml ├── .gitignore ├── lib ├── main.js ├── helpers │ ├── isLeavingNode.js │ ├── focusManager.js │ ├── customPropTypes.js │ └── tabbable.js └── components │ ├── Tray.js │ ├── TrayPortal.js │ └── __tests__ │ └── Tray-test.js ├── examples ├── index.html └── basic │ ├── index.html │ └── app.js ├── package.json ├── README.md ├── CHANGELOG.md └── dist ├── react-tray.min.js └── react-tray.js /.npmignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /lib/main.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./components/Tray'); 2 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /lib/helpers/isLeavingNode.js: -------------------------------------------------------------------------------- 1 | import findTabbable from '../helpers/tabbable'; 2 | 3 | export default function(node, event) { 4 | const tabbable = findTabbable(node); 5 | const finalTabbable = tabbable[event.shiftKey ? 0 : tabbable.length - 1]; 6 | const isLeavingNode = ( 7 | finalTabbable === document.activeElement 8 | ); 9 | return isLeavingNode; 10 | } 11 | -------------------------------------------------------------------------------- /lib/helpers/focusManager.js: -------------------------------------------------------------------------------- 1 | let focusLaterElement = null; 2 | 3 | exports.markForFocusLater = function markForFocusLater() { 4 | focusLaterElement = document.activeElement; 5 | }; 6 | 7 | exports.returnFocus = function returnFocus() { 8 | try { 9 | focusLaterElement.focus(); 10 | } catch (e) { 11 | /* eslint no-console:0 */ 12 | console.warn('You tried to return focus to ' + focusLaterElement + ' but it is not in the DOM anymore'); 13 | } 14 | focusLaterElement = null; 15 | }; 16 | -------------------------------------------------------------------------------- /lib/helpers/customPropTypes.js: -------------------------------------------------------------------------------- 1 | // Adapted from https://github.com/react-bootstrap/react-prop-types/blob/master/src/isRequiredForA11y.js 2 | export function a11yFunction(props, propName, componentName) { 3 | if ((!props[propName]) || (typeof props[propName] !== 'function')) { 4 | return new Error( 5 | `The prop '${propName}' is required to make '${componentName}' fully accessible. ` + 6 | `This will greatly improve the experience for users of assistive technologies. ` + 7 | `You should provide a function that returns a DOM node.` 8 | ); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-tray", 3 | "version": "2.0.4", 4 | "description": "An accessible tray component useful for navigation menus", 5 | "main": "lib/main.js", 6 | "directories": { 7 | "example": "examples" 8 | }, 9 | "scripts": { 10 | "test": "rackt test --single-run --browsers Firefox", 11 | "start": "rackt server" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/instructure-react/react-tray.git" 16 | }, 17 | "keywords": [ 18 | "react", 19 | "tray", 20 | "react-component" 21 | ], 22 | "author": "Matt Zabriskie", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/instructure-react/react-tray/issues" 26 | }, 27 | "homepage": "https://github.com/instructure-react/react-tray", 28 | "dependencies": { 29 | "classnames": "^2.2.0" 30 | }, 31 | "peerDependencies": { 32 | "react": "^0.14.0", 33 | "react-dom": "^0.14.0" 34 | }, 35 | "devDependencies": { 36 | "rackt-cli": "^0.8.0", 37 | "react": "^0.14.0", 38 | "react-addons-test-utils": "^0.14.0", 39 | "react-dom": "^0.14.0" 40 | } 41 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-tray 2 | 3 | An accessible tray component useful for navigation menus 4 | See example at [http://instructure-react.github.io/react-tray](http://instructure-react.github.io/react-tray) 5 | 6 | ## Usage 7 | 8 | ```js 9 | var React = require('react'); 10 | var Tray = require('react-tray'); 11 | 12 | var App = React.createClass({ 13 | getInitialState: function () { 14 | return { 15 | isTrayOpen: false 16 | }; 17 | }, 18 | 19 | openTray: function () { 20 | this.setState({ 21 | isTrayOpen: true 22 | }); 23 | }, 24 | 25 | closeTray: function () { 26 | this.setState({ 27 | isTrayOpen: false 28 | }); 29 | }, 30 | 31 | 32 | render: function () { 33 | return ( 34 |
35 | 43 | 47 |

Tray Content

48 |
Learn to drive and everything.
49 |
50 |
51 | ); 52 | } 53 | }); 54 | 55 | React.render(, document.getElementById('content')); 56 | ``` 57 | 58 | -------------------------------------------------------------------------------- /lib/helpers/tabbable.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Adapted from jQuery UI core 3 | * 4 | * http://jqueryui.com 5 | * 6 | * Copyright 2014 jQuery Foundation and other contributors 7 | * Released under the MIT license. 8 | * http://jquery.org/license 9 | * 10 | * http://api.jqueryui.com/category/ui-core/ 11 | */ 12 | 13 | function hidden(el) { 14 | return (el.offsetWidth <= 0 && el.offsetHeight <= 0) || 15 | el.style.display === 'none'; 16 | } 17 | 18 | function visible(element) { 19 | let el = element; 20 | while (el) { 21 | if (el === document.body) break; 22 | if (hidden(el)) return false; 23 | el = el.parentNode; 24 | } 25 | return true; 26 | } 27 | 28 | function focusable(element, isTabIndexNotNaN) { 29 | const nodeName = element.nodeName.toLowerCase(); 30 | /* eslint no-nested-ternary:0 */ 31 | return (/input|select|textarea|button|object/.test(nodeName) ? 32 | !element.disabled : 33 | nodeName === 'a' ? 34 | element.href || isTabIndexNotNaN : 35 | isTabIndexNotNaN) && visible(element); 36 | } 37 | 38 | function tabbable(element) { 39 | let tabIndex = element.getAttribute('tabindex'); 40 | if (tabIndex === null) tabIndex = undefined; 41 | const isTabIndexNaN = isNaN(tabIndex); 42 | return (isTabIndexNaN || tabIndex >= 0) && focusable(element, !isTabIndexNaN); 43 | } 44 | 45 | function findTabbableDescendants(element) { 46 | return [].slice.call(element.querySelectorAll('*'), 0).filter((el) => { 47 | return tabbable(el); 48 | }); 49 | } 50 | 51 | export default findTabbableDescendants; 52 | -------------------------------------------------------------------------------- /lib/components/Tray.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import TrayPortal from './TrayPortal'; 4 | import { a11yFunction } from '../helpers/customPropTypes'; 5 | const renderSubtreeIntoContainer = ReactDOM.unstable_renderSubtreeIntoContainer; 6 | 7 | export default React.createClass({ 8 | displayName: 'Tray', 9 | 10 | propTypes: { 11 | isOpen: React.PropTypes.bool, 12 | onBlur: React.PropTypes.func, 13 | onOpen: React.PropTypes.func, 14 | closeTimeoutMS: React.PropTypes.number, 15 | closeOnBlur: React.PropTypes.bool, 16 | maintainFocus: React.PropTypes.bool, 17 | getElementToFocus: a11yFunction, 18 | getAriaHideElement: a11yFunction 19 | }, 20 | 21 | getDefaultProps() { 22 | return { 23 | isOpen: false, 24 | closeTimeoutMS: 0, 25 | closeOnBlur: true, 26 | maintainFocus: true 27 | }; 28 | }, 29 | 30 | componentDidMount() { 31 | this.node = document.createElement('div'); 32 | this.node.className = 'ReactTrayPortal'; 33 | document.body.appendChild(this.node); 34 | this.renderPortal(this.props); 35 | }, 36 | 37 | componentWillReceiveProps(props) { 38 | this.renderPortal(props); 39 | }, 40 | 41 | componentWillUnmount() { 42 | ReactDOM.unmountComponentAtNode(this.node); 43 | document.body.removeChild(this.node); 44 | }, 45 | 46 | renderPortal(props) { 47 | delete props.ref; 48 | 49 | renderSubtreeIntoContainer(this, , this.node); 50 | }, 51 | 52 | render() { 53 | return null; 54 | } 55 | }); 56 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | v2.0.4 - Tue, 25 Oct 2016 20:21:50 GMT 2 | -------------------------------------- 3 | 4 | - 5 | 6 | 7 | v2.0.3 - Tue, 25 Oct 2016 20:11:21 GMT 8 | -------------------------------------- 9 | 10 | - 11 | 12 | 13 | v2.0.2 - Mon, 17 Oct 2016 15:55:11 GMT 14 | -------------------------------------- 15 | This commit mostly addresses the `dist/` versions for use in AMD builds. They are now named properly fixing the issue with https://github.com/instructure-react/react-tray/pull/10 16 | - [c0a6782](../../commit/c0a6782) Updated rackt-cli build/release tooling version 17 | 18 | 19 | v2.0.1 - Wed, 13 Jul 2016 22:55:21 GMT 20 | -------------------------------------- 21 | 22 | - [36e3e55](../../commit/36e3e55) [fixed] Split toggleAriaHidden internal method to two methods 23 | 24 | 25 | v2.0.0 - Wed, 13 Jul 2016 14:42:45 GMT 26 | -------------------------------------- 27 | 28 | - [f3b4731](../../commit/f3b4731) [changed] Move elementToFocus prop to getElementToFocus (#9) 29 | - [7a316ff](../../commit/7a316ff) [added] Prop for adding aria-hidden to application element (#8) 30 | 31 | 32 | v1.1.0 - Mon, 11 Jul 2016 22:24:30 GMT 33 | -------------------------------------- 34 | 35 | - [d860b32](../../commit/d860b32) [fixed] Failing test for elementToFocus 36 | - [3300f2c](../../commit/3300f2c) [added] Add elementToFocus prop (#5) 37 | - [afe8d85](../../commit/afe8d85) [added] onOpen callback 38 | 39 | 40 | v1.0.1 - Fri, 08 Jul 2016 22:00:48 GMT 41 | -------------------------------------- 42 | 43 | - [a374410](../../commit/a374410) [fixed] make default maintainFocus prob actually be true 44 | 45 | 46 | v1.0.0 - Fri, 08 Jul 2016 21:20:29 GMT 47 | -------------------------------------- 48 | 49 | - [720c1ce](../../commit/720c1ce) [changed] Maintain focus inside of tray 50 | 51 | 52 | v0.3.0 - Thu, 05 Nov 2015 22:29:26 GMT 53 | -------------------------------------- 54 | 55 | - 56 | 57 | 58 | v0.2.1 - Fri, 23 Oct 2015 23:00:10 GMT 59 | -------------------------------------- 60 | 61 | - [9f6d417](../../commit/9f6d417) [fixed] Removing ReactDOM from bundle 62 | 63 | 64 | v0.2.0 - Thu, 22 Oct 2015 09:32:14 GMT 65 | -------------------------------------- 66 | 67 | - [a478e8d](../../commit/a478e8d) [changed] support for React 0.14.0 68 | 69 | 70 | v0.1.2 - Wed, 13 May 2015 20:57:15 GMT 71 | -------------------------------------- 72 | 73 | - [db92743](../../commit/db92743) [fixed] don't include React in dist 74 | 75 | 76 | v0.1.1 - Wed, 13 May 2015 20:11:02 GMT 77 | -------------------------------------- 78 | 79 | - [de9f58c](../../commit/de9f58c) [fixed] empty dist files 80 | 81 | 82 | v0.1.0 - Tue, 28 Apr 2015 22:56:46 GMT 83 | -------------------------------------- 84 | 85 | - 86 | 87 | 88 | -------------------------------------------------------------------------------- /examples/basic/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | React Tray 4 | 5 | 84 | 85 |
86 |

react-tray

87 |

React tray component

88 |
89 |
90 | Fork me on GitHub 91 | 92 | 93 | -------------------------------------------------------------------------------- /examples/basic/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import Tray from '../../lib/main'; 4 | import cx from 'classnames'; 5 | 6 | const App = React.createClass({ 7 | getInitialState() { 8 | return { 9 | orientation: 'left', 10 | isTrayOpen: false 11 | }; 12 | }, 13 | 14 | handleNavClick(e) { 15 | const type = e.target.getAttribute('data-type'); 16 | this.openTray(type); 17 | }, 18 | 19 | handleNavKeyPress(e) { 20 | if (e.which === 13 || e.which === 32) { 21 | const type = e.target.getAttribute('data-type'); 22 | this.openTray(type); 23 | } 24 | }, 25 | 26 | handleOrientationChange(e) { 27 | this.setState({ 28 | orientation: e.target.value 29 | }); 30 | }, 31 | 32 | renderTrayContent() { 33 | switch (this.state.type) { 34 | case 'foo': 35 | return ( 36 |
37 |

Foo

38 |
Content for foo
39 | 44 |
45 | ); 46 | case 'bar': 47 | return ( 48 |
49 |

Bar

50 |
Lorem Ipsum
51 | 56 |
57 | ); 58 | case 'baz': 59 | return ( 60 |
61 |

Baz

62 |
Other stuff here
63 | 68 |
69 | ); 70 | default: 71 | return ( 72 |

You shouldn't see me

73 | ); 74 | } 75 | }, 76 | 77 | render() { 78 | return ( 79 |
80 |
    86 |
  • 89 | Foo 95 |
  • 96 |
  • 99 | Bar 105 |
  • 106 |
  • 109 | Baz 115 |
  • 116 |
117 | 125 | {this.renderTrayContent()} 126 | 127 |
128 | 132 |
133 |
134 | ); 135 | }, 136 | 137 | openTray(type) { 138 | this.setState({ 139 | type: type, 140 | isTrayOpen: true 141 | }); 142 | }, 143 | 144 | closeTray() { 145 | this.setState({ 146 | isTrayOpen: false 147 | }, () => { 148 | setTimeout(() => { 149 | this.setState({ 150 | type: null 151 | }); 152 | }, 150); 153 | }); 154 | } 155 | }); 156 | 157 | ReactDOM.render(, document.getElementById('example')); 158 | -------------------------------------------------------------------------------- /lib/components/TrayPortal.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import cx from 'classnames'; 3 | import focusManager from '../helpers/focusManager'; 4 | import isLeavingNode from '../helpers/isLeavingNode'; 5 | import findTabbable from '../helpers/tabbable'; 6 | 7 | const styles = { 8 | overlay: { 9 | position: 'fixed', 10 | top: 0, 11 | left: 0, 12 | right: 0, 13 | bottom: 0 14 | }, 15 | content: { 16 | position: 'absolute', 17 | background: '#fff' 18 | } 19 | }; 20 | 21 | const CLASS_NAMES = { 22 | overlay: { 23 | base: 'ReactTray__Overlay', 24 | afterOpen: 'ReactTray__Overlay--after-open', 25 | beforeClose: 'ReactTray__Overlay--before-close' 26 | }, 27 | content: { 28 | base: 'ReactTray__Content', 29 | afterOpen: 'ReactTray__Content--after-open', 30 | beforeClose: 'ReactTray__Content--before-close' 31 | } 32 | }; 33 | 34 | function isChild(parent, child) { 35 | if (parent === child) { 36 | return true; 37 | } 38 | 39 | let node = child; 40 | /* eslint no-cond-assign:0 */ 41 | while (node = node.parentNode) { 42 | if (node === parent) { 43 | return true; 44 | } 45 | } 46 | return false; 47 | } 48 | 49 | export default React.createClass({ 50 | displayName: 'TrayPortal', 51 | 52 | propTypes: { 53 | className: PropTypes.string, 54 | overlayClassName: PropTypes.string, 55 | isOpen: PropTypes.bool, 56 | onBlur: PropTypes.func, 57 | onOpen: PropTypes.func, 58 | closeOnBlur: PropTypes.bool, 59 | closeTimeoutMS: PropTypes.number, 60 | children: PropTypes.any, 61 | maintainFocus: PropTypes.bool, 62 | getElementToFocus: PropTypes.func, 63 | getAriaHideElement: PropTypes.func 64 | }, 65 | 66 | getInitialState() { 67 | return { 68 | afterOpen: false, 69 | beforeClose: false 70 | }; 71 | }, 72 | 73 | componentDidMount() { 74 | if (this.props.isOpen) { 75 | this.setFocusAfterRender(true); 76 | this.open(); 77 | } 78 | }, 79 | 80 | componentWillReceiveProps(props) { 81 | if (props.isOpen) { 82 | this.setFocusAfterRender(true); 83 | this.open(); 84 | } else if (this.props.isOpen && !props.isOpen) { 85 | this.close(); 86 | } 87 | }, 88 | 89 | componentDidUpdate() { 90 | if (this.focusAfterRender) { 91 | if (this.props.getElementToFocus) { 92 | this.props.getElementToFocus().focus(); 93 | } else { 94 | this.focusContent(); 95 | } 96 | this.setFocusAfterRender(false); 97 | } 98 | }, 99 | 100 | setFocusAfterRender(focus) { 101 | this.focusAfterRender = focus; 102 | }, 103 | 104 | focusContent() { 105 | this.refs.content.focus(); 106 | }, 107 | 108 | applyAriaHidden(element) { 109 | element.setAttribute('aria-hidden', true); 110 | }, 111 | 112 | removeAriaHidden(element) { 113 | element.removeAttribute('aria-hidden'); 114 | }, 115 | 116 | handleOverlayClick(e) { 117 | if (!isChild(this.refs.content, e.target)) { 118 | this.props.onBlur(); 119 | } 120 | }, 121 | 122 | handleContentKeyDown(e) { 123 | // Treat ESC as blur/close 124 | if (e.keyCode === 27) { 125 | this.props.onBlur(); 126 | } 127 | 128 | // Keep focus inside the tray if maintainFocus is true 129 | if (e.keyCode === 9 && this.props.maintainFocus && isLeavingNode(this.refs.content, e)) { 130 | e.preventDefault(); 131 | const tabbable = findTabbable(this.refs.content); 132 | const target = tabbable[e.shiftKey ? tabbable.length - 1 : 0]; 133 | target.focus(); 134 | return; 135 | } 136 | 137 | // Treat tabbing away from content as blur/close if closeOnBlur 138 | if (e.keyCode === 9 && this.props.closeOnBlur && isLeavingNode(this.refs.content, e)) { 139 | e.preventDefault(); 140 | this.props.onBlur(); 141 | } 142 | }, 143 | 144 | open() { 145 | focusManager.markForFocusLater(); 146 | this.setState({isOpen: true}, () => { 147 | if (this.props.onOpen) { 148 | this.props.onOpen(); 149 | } 150 | if (this.props.getAriaHideElement) { 151 | this.applyAriaHidden(this.props.getAriaHideElement()); 152 | } 153 | this.setState({afterOpen: true}); 154 | }); 155 | }, 156 | 157 | close() { 158 | if (this.props.closeTimeoutMS > 0) { 159 | this.closeWithTimeout(); 160 | } else { 161 | this.closeWithoutTimeout(); 162 | } 163 | if (this.props.getAriaHideElement) { 164 | this.removeAriaHidden(this.props.getAriaHideElement()); 165 | } 166 | }, 167 | 168 | closeWithTimeout() { 169 | this.setState({beforeClose: true}, () => { 170 | setTimeout(this.closeWithoutTimeout, this.props.closeTimeoutMS); 171 | }); 172 | }, 173 | 174 | closeWithoutTimeout() { 175 | this.setState({ 176 | afterOpen: false, 177 | beforeClose: false 178 | }, this.afterClose); 179 | }, 180 | 181 | afterClose() { 182 | focusManager.returnFocus(); 183 | }, 184 | 185 | shouldBeClosed() { 186 | return !this.props.isOpen && !this.state.beforeClose; 187 | }, 188 | 189 | buildClassName(which) { 190 | let className = CLASS_NAMES[which].base; 191 | if (this.state.afterOpen) { 192 | className += ' ' + CLASS_NAMES[which].afterOpen; 193 | } 194 | if (this.state.beforeClose) { 195 | className += ' ' + CLASS_NAMES[which].beforeClose; 196 | } 197 | return className; 198 | }, 199 | 200 | render() { 201 | return this.shouldBeClosed() ?
: ( 202 |
211 |
221 | {this.props.children} 222 |
223 |
224 | ); 225 | } 226 | }); 227 | -------------------------------------------------------------------------------- /lib/components/__tests__/Tray-test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import TestUtils from 'react-addons-test-utils'; 4 | import { equal } from 'assert'; 5 | import ReactTray from '../../main'; 6 | 7 | let _currentDiv = null; 8 | 9 | function renderTray(props, children, callback) { 10 | _currentDiv = document.createElement('div'); 11 | document.body.appendChild(_currentDiv); 12 | return ReactDOM.render({children}, _currentDiv, callback); 13 | } 14 | 15 | function unmountTray() { 16 | ReactDOM.unmountComponentAtNode(_currentDiv); 17 | document.body.removeChild(_currentDiv); 18 | _currentDiv = null; 19 | } 20 | 21 | /* eslint func-names:0 */ 22 | describe('react-tray', function() { 23 | afterEach(function() { 24 | unmountTray(); 25 | }); 26 | 27 | it('should not be visible when isOpen is false', function() { 28 | renderTray(); 29 | equal(document.querySelectorAll('.ReactTray__Content').length, 0); 30 | }); 31 | 32 | it('should be visible when isOpen is true', function() { 33 | renderTray({isOpen: true}); 34 | equal(document.querySelectorAll('.ReactTray__Content').length, 1); 35 | }); 36 | 37 | it('should receive focus when opened', function() { 38 | renderTray({isOpen: true}); 39 | equal(document.querySelector('.ReactTray__Content'), document.activeElement); 40 | }); 41 | 42 | it('should call onBlur when closed', function() { 43 | const blurred = false; 44 | renderTray({isOpen: true, onBlur: function() { blurred: true; }, closeTimeoutMS: 0}); 45 | TestUtils.Simulate.click(document.querySelector('.ReactTray__Overlay')); 46 | setTimeout(function() { 47 | equal(blurred, true); 48 | }, 0); 49 | }); 50 | 51 | it('should call onOpen when it opens', function() { 52 | let calledOpen = false; 53 | renderTray({isOpen: true, onOpen: function() { calledOpen = true;}}); 54 | equal(calledOpen, true); 55 | }); 56 | 57 | it('should close on overlay click', function() { 58 | renderTray({isOpen: true, onBlur: function() {}, closeTimeoutMS: 0}); 59 | TestUtils.Simulate.click(document.querySelector('.ReactTray__Overlay')); 60 | setTimeout(function() { 61 | equal(document.querySelectorAll('.ReactTray__Content').length, 0); 62 | }, 0); 63 | }); 64 | 65 | it('should close on ESC key', function() { 66 | renderTray({isOpen: true, onBlur: function() {}, closeTimeoutMS: 0}); 67 | TestUtils.Simulate.keyDown(document.querySelector('.ReactTray__Content'), {key: 'Esc'}); 68 | setTimeout(function() { 69 | equal(document.querySelectorAll('.ReactTray__Content').length, 0); 70 | }, 0); 71 | }); 72 | 73 | it('should close on blur by default', function() { 74 | renderTray({isOpen: true, onBlur: function() {}, closeTimeoutMS: 0}); 75 | TestUtils.Simulate.keyDown(document.querySelector('.ReactTray__Content'), {key: 'Tab'}); 76 | setTimeout(function() { 77 | equal(document.querySelectorAll('.ReactTray__Content').length, 0); 78 | }, 0); 79 | }); 80 | 81 | it('should not close on blur', function() { 82 | renderTray({isOpen: true, onBlur: function() {}, closeTimeoutMS: 0, closeOnBlur: false}); 83 | TestUtils.Simulate.keyDown(document.querySelector('.ReactTray__Content'), {key: 'Tab'}); 84 | setTimeout(function() { 85 | equal(document.querySelectorAll('.ReactTray__Content').length, 1); 86 | }, 0); 87 | }); 88 | 89 | describe('maintainFocus prop', function() { 90 | this.timeout(0); 91 | beforeEach(function(done) { 92 | const props = {isOpen: true, onBlur: function() {}, closeTimeoutMS: 0, maintainFocus: true}; 93 | const children = ( 94 |
95 | One 96 | Two 97 | Three 98 |
99 | ); 100 | renderTray(props, children, () => done()); 101 | }); 102 | 103 | it('sends focus to the first item if tabbing away from the last element', function() { 104 | const firstItem = document.querySelector('#one'); 105 | const lastItem = document.querySelector('#three'); 106 | lastItem.focus(); 107 | TestUtils.Simulate.keyDown(document.querySelector('.ReactTray__Content'), {keyCode: 9}); 108 | equal(document.activeElement, firstItem); 109 | }); 110 | 111 | it('sends focus to the last item if shift + tabbing from the first item', function() { 112 | const firstItem = document.querySelector('#one'); 113 | const lastItem = document.querySelector('#three'); 114 | firstItem.focus(); 115 | TestUtils.Simulate.keyDown(document.querySelector('.ReactTray__Content'), {keyCode: 9, shiftKey: true}); 116 | equal(document.activeElement, lastItem); 117 | }); 118 | }); 119 | 120 | describe('getElementToFocus prop', function() { 121 | const getElementToFocus = () => { 122 | return document.getElementById('two'); 123 | }; 124 | 125 | beforeEach(function(done) { 126 | const props = {isOpen: true, onBlur: function() {}, closeTimeoutMS: 0, maintainFocus: true, getElementToFocus: getElementToFocus}; 127 | const children = ( 128 |
129 | One 130 | Two 131 | Three 132 |
133 | ); 134 | renderTray(props, children, () => done()); 135 | }); 136 | 137 | it('sends focus to the DOM node found via the selector passed in the prop', function() { 138 | const secondItem = document.querySelector('#two'); 139 | equal(document.activeElement, secondItem); 140 | }); 141 | }); 142 | 143 | describe('getAriaHideElement prop', function() { 144 | const getAriaHideElement = () => { 145 | return document.getElementById('main_application_div'); 146 | }; 147 | 148 | beforeEach(function() { 149 | const mainDiv = document.createElement('div'); 150 | mainDiv.id = 'main_application_div'; 151 | document.body.appendChild(mainDiv); 152 | }); 153 | 154 | it('adds aria-hidden to the given element when open', function() { 155 | renderTray({isOpen: true, getAriaHideElement: getAriaHideElement}); 156 | const el = document.getElementById('main_application_div'); 157 | equal(el.getAttribute('aria-hidden'), 'true'); 158 | }); 159 | 160 | it('removes aria-hidden from the given element when closed', function() { 161 | renderTray({isOpen: true, onBlur: function() {}, closeTimeoutMS: 0, getAriaHideElement: getAriaHideElement}); 162 | TestUtils.Simulate.keyDown(document.querySelector('.ReactTray__Content'), {key: 'Esc'}); 163 | setTimeout(function() { 164 | const el = document.getElementById('main_application_div'); 165 | equal(el.getAttribute('aria-hidden'), null); 166 | }, 0); 167 | }); 168 | }); 169 | }); 170 | -------------------------------------------------------------------------------- /dist/react-tray.min.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(require("react"),require("react-dom")):"function"==typeof define&&define.amd?define(["react","react-dom"],t):"object"==typeof exports?exports.ReactTray=t(require("react"),require("react-dom")):e.ReactTray=t(e.React,e.ReactDOM)}(this,function(e,t){return function(e){function t(n){if(o[n])return o[n].exports;var r=o[n]={exports:{},id:n,loaded:!1};return e[n].call(r.exports,r,r.exports,t),r.loaded=!0,r.exports}var o={};return t.m=e,t.c=o,t.p="",t(0)}([function(e,t,o){"use strict";e.exports=o(1)},function(e,t,o){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(t,"__esModule",{value:!0});var r=o(2),s=n(r),i=o(3),u=n(i),a=o(4),l=n(a),c=o(9),f=u["default"].unstable_renderSubtreeIntoContainer;t["default"]=s["default"].createClass({displayName:"Tray",propTypes:{isOpen:s["default"].PropTypes.bool,onBlur:s["default"].PropTypes.func,onOpen:s["default"].PropTypes.func,closeTimeoutMS:s["default"].PropTypes.number,closeOnBlur:s["default"].PropTypes.bool,maintainFocus:s["default"].PropTypes.bool,getElementToFocus:c.a11yFunction,getAriaHideElement:c.a11yFunction},getDefaultProps:function(){return{isOpen:!1,closeTimeoutMS:0,closeOnBlur:!0,maintainFocus:!0}},componentDidMount:function(){this.node=document.createElement("div"),this.node.className="ReactTrayPortal",document.body.appendChild(this.node),this.renderPortal(this.props)},componentWillReceiveProps:function(e){this.renderPortal(e)},componentWillUnmount:function(){u["default"].unmountComponentAtNode(this.node),document.body.removeChild(this.node)},renderPortal:function(e){delete e.ref,f(this,s["default"].createElement(l["default"],e),this.node)},render:function(){return null}}),e.exports=t["default"]},function(t,o){t.exports=e},function(e,o){e.exports=t},function(e,t,o){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(e===t)return!0;for(var o=t;o=o.parentNode;)if(o===e)return!0;return!1}Object.defineProperty(t,"__esModule",{value:!0});var s=o(2),i=n(s),u=o(5),a=n(u),l=o(6),c=n(l),f=o(7),p=n(f),d=o(8),h=n(d),y={overlay:{position:"fixed",top:0,left:0,right:0,bottom:0},content:{position:"absolute",background:"#fff"}},m={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"}};t["default"]=i["default"].createClass({displayName:"TrayPortal",propTypes:{className:s.PropTypes.string,overlayClassName:s.PropTypes.string,isOpen:s.PropTypes.bool,onBlur:s.PropTypes.func,onOpen:s.PropTypes.func,closeOnBlur:s.PropTypes.bool,closeTimeoutMS:s.PropTypes.number,children:s.PropTypes.any,maintainFocus:s.PropTypes.bool,getElementToFocus:s.PropTypes.func,getAriaHideElement:s.PropTypes.func},getInitialState:function(){return{afterOpen:!1,beforeClose:!1}},componentDidMount:function(){this.props.isOpen&&(this.setFocusAfterRender(!0),this.open())},componentWillReceiveProps:function(e){e.isOpen?(this.setFocusAfterRender(!0),this.open()):this.props.isOpen&&!e.isOpen&&this.close()},componentDidUpdate:function(){this.focusAfterRender&&(this.props.getElementToFocus?this.props.getElementToFocus().focus():this.focusContent(),this.setFocusAfterRender(!1))},setFocusAfterRender:function(e){this.focusAfterRender=e},focusContent:function(){this.refs.content.focus()},applyAriaHidden:function(e){e.setAttribute("aria-hidden",!0)},removeAriaHidden:function(e){e.removeAttribute("aria-hidden")},handleOverlayClick:function(e){r(this.refs.content,e.target)||this.props.onBlur()},handleContentKeyDown:function(e){if(27===e.keyCode&&this.props.onBlur(),9===e.keyCode&&this.props.maintainFocus&&p["default"](this.refs.content,e)){e.preventDefault();var t=h["default"](this.refs.content),o=t[e.shiftKey?t.length-1:0];return void o.focus()}9===e.keyCode&&this.props.closeOnBlur&&p["default"](this.refs.content,e)&&(e.preventDefault(),this.props.onBlur())},open:function(){var e=this;c["default"].markForFocusLater(),this.setState({isOpen:!0},function(){e.props.onOpen&&e.props.onOpen(),e.props.getAriaHideElement&&e.applyAriaHidden(e.props.getAriaHideElement()),e.setState({afterOpen:!0})})},close:function(){this.props.closeTimeoutMS>0?this.closeWithTimeout():this.closeWithoutTimeout(),this.props.getAriaHideElement&&this.removeAriaHidden(this.props.getAriaHideElement())},closeWithTimeout:function(){var e=this;this.setState({beforeClose:!0},function(){setTimeout(e.closeWithoutTimeout,e.props.closeTimeoutMS)})},closeWithoutTimeout:function(){this.setState({afterOpen:!1,beforeClose:!1},this.afterClose)},afterClose:function(){c["default"].returnFocus()},shouldBeClosed:function(){return!this.props.isOpen&&!this.state.beforeClose},buildClassName:function(e){var t=m[e].base;return this.state.afterOpen&&(t+=" "+m[e].afterOpen),this.state.beforeClose&&(t+=" "+m[e].beforeClose),t},render:function(){return this.shouldBeClosed()?i["default"].createElement("div",null):i["default"].createElement("div",{ref:"overlay",style:y.overlay,className:a["default"](this.buildClassName("overlay"),this.props.overlayClassName),onClick:this.handleOverlayClick},i["default"].createElement("div",{ref:"content",style:y.content,className:a["default"](this.buildClassName("content"),this.props.className),onKeyDown:this.handleContentKeyDown,tabIndex:"-1"},this.props.children))}}),e.exports=t["default"]},function(e,t,o){var n,r;/*! 2 | Copyright (c) 2016 Jed Watson. 3 | Licensed under the MIT License (MIT), see 4 | http://jedwatson.github.io/classnames 5 | */ 6 | !function(){"use strict";function o(){for(var e=[],t=0;t=0)&&r(e,!o)}function i(e){return[].slice.call(e.querySelectorAll("*"),0).filter(function(e){return s(e)})}Object.defineProperty(t,"__esModule",{value:!0}),t["default"]=i,e.exports=t["default"]},function(e,t){"use strict";function o(e,t,o){return e[t]&&"function"==typeof e[t]?void 0:new Error("The prop '"+t+"' is required to make '"+o+"' fully accessible. This will greatly improve the experience for users of assistive technologies. You should provide a function that returns a DOM node.")}Object.defineProperty(t,"__esModule",{value:!0}),t.a11yFunction=o}])}); 18 | //# sourceMappingURL=react-tray.min.js.map -------------------------------------------------------------------------------- /dist/react-tray.js: -------------------------------------------------------------------------------- 1 | (function webpackUniversalModuleDefinition(root, factory) { 2 | if(typeof exports === 'object' && typeof module === 'object') 3 | module.exports = factory(require("react"), require("react-dom")); 4 | else if(typeof define === 'function' && define.amd) 5 | define(["react", "react-dom"], factory); 6 | else if(typeof exports === 'object') 7 | exports["ReactTray"] = factory(require("react"), require("react-dom")); 8 | else 9 | root["ReactTray"] = factory(root["React"], root["ReactDOM"]); 10 | })(this, function(__WEBPACK_EXTERNAL_MODULE_2__, __WEBPACK_EXTERNAL_MODULE_3__) { 11 | return /******/ (function(modules) { // webpackBootstrap 12 | /******/ // The module cache 13 | /******/ var installedModules = {}; 14 | /******/ 15 | /******/ // The require function 16 | /******/ function __webpack_require__(moduleId) { 17 | /******/ 18 | /******/ // Check if module is in cache 19 | /******/ if(installedModules[moduleId]) 20 | /******/ return installedModules[moduleId].exports; 21 | /******/ 22 | /******/ // Create a new module (and put it into the cache) 23 | /******/ var module = installedModules[moduleId] = { 24 | /******/ exports: {}, 25 | /******/ id: moduleId, 26 | /******/ loaded: false 27 | /******/ }; 28 | /******/ 29 | /******/ // Execute the module function 30 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 31 | /******/ 32 | /******/ // Flag the module as loaded 33 | /******/ module.loaded = true; 34 | /******/ 35 | /******/ // Return the exports of the module 36 | /******/ return module.exports; 37 | /******/ } 38 | /******/ 39 | /******/ 40 | /******/ // expose the modules object (__webpack_modules__) 41 | /******/ __webpack_require__.m = modules; 42 | /******/ 43 | /******/ // expose the module cache 44 | /******/ __webpack_require__.c = installedModules; 45 | /******/ 46 | /******/ // __webpack_public_path__ 47 | /******/ __webpack_require__.p = ""; 48 | /******/ 49 | /******/ // Load entry module and return exports 50 | /******/ return __webpack_require__(0); 51 | /******/ }) 52 | /************************************************************************/ 53 | /******/ ([ 54 | /* 0 */ 55 | /***/ function(module, exports, __webpack_require__) { 56 | 57 | 'use strict'; 58 | 59 | module.exports = __webpack_require__(1); 60 | 61 | /***/ }, 62 | /* 1 */ 63 | /***/ function(module, exports, __webpack_require__) { 64 | 65 | 'use strict'; 66 | 67 | Object.defineProperty(exports, '__esModule', { 68 | value: true 69 | }); 70 | 71 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } 72 | 73 | var _react = __webpack_require__(2); 74 | 75 | var _react2 = _interopRequireDefault(_react); 76 | 77 | var _reactDom = __webpack_require__(3); 78 | 79 | var _reactDom2 = _interopRequireDefault(_reactDom); 80 | 81 | var _TrayPortal = __webpack_require__(4); 82 | 83 | var _TrayPortal2 = _interopRequireDefault(_TrayPortal); 84 | 85 | var _helpersCustomPropTypes = __webpack_require__(9); 86 | 87 | var renderSubtreeIntoContainer = _reactDom2['default'].unstable_renderSubtreeIntoContainer; 88 | 89 | exports['default'] = _react2['default'].createClass({ 90 | displayName: 'Tray', 91 | 92 | propTypes: { 93 | isOpen: _react2['default'].PropTypes.bool, 94 | onBlur: _react2['default'].PropTypes.func, 95 | onOpen: _react2['default'].PropTypes.func, 96 | closeTimeoutMS: _react2['default'].PropTypes.number, 97 | closeOnBlur: _react2['default'].PropTypes.bool, 98 | maintainFocus: _react2['default'].PropTypes.bool, 99 | getElementToFocus: _helpersCustomPropTypes.a11yFunction, 100 | getAriaHideElement: _helpersCustomPropTypes.a11yFunction 101 | }, 102 | 103 | getDefaultProps: function getDefaultProps() { 104 | return { 105 | isOpen: false, 106 | closeTimeoutMS: 0, 107 | closeOnBlur: true, 108 | maintainFocus: true 109 | }; 110 | }, 111 | 112 | componentDidMount: function componentDidMount() { 113 | this.node = document.createElement('div'); 114 | this.node.className = 'ReactTrayPortal'; 115 | document.body.appendChild(this.node); 116 | this.renderPortal(this.props); 117 | }, 118 | 119 | componentWillReceiveProps: function componentWillReceiveProps(props) { 120 | this.renderPortal(props); 121 | }, 122 | 123 | componentWillUnmount: function componentWillUnmount() { 124 | _reactDom2['default'].unmountComponentAtNode(this.node); 125 | document.body.removeChild(this.node); 126 | }, 127 | 128 | renderPortal: function renderPortal(props) { 129 | delete props.ref; 130 | 131 | renderSubtreeIntoContainer(this, _react2['default'].createElement(_TrayPortal2['default'], props), this.node); 132 | }, 133 | 134 | render: function render() { 135 | return null; 136 | } 137 | }); 138 | module.exports = exports['default']; 139 | 140 | /***/ }, 141 | /* 2 */ 142 | /***/ function(module, exports) { 143 | 144 | module.exports = __WEBPACK_EXTERNAL_MODULE_2__; 145 | 146 | /***/ }, 147 | /* 3 */ 148 | /***/ function(module, exports) { 149 | 150 | module.exports = __WEBPACK_EXTERNAL_MODULE_3__; 151 | 152 | /***/ }, 153 | /* 4 */ 154 | /***/ function(module, exports, __webpack_require__) { 155 | 156 | 'use strict'; 157 | 158 | Object.defineProperty(exports, '__esModule', { 159 | value: true 160 | }); 161 | 162 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } 163 | 164 | var _react = __webpack_require__(2); 165 | 166 | var _react2 = _interopRequireDefault(_react); 167 | 168 | var _classnames = __webpack_require__(5); 169 | 170 | var _classnames2 = _interopRequireDefault(_classnames); 171 | 172 | var _helpersFocusManager = __webpack_require__(6); 173 | 174 | var _helpersFocusManager2 = _interopRequireDefault(_helpersFocusManager); 175 | 176 | var _helpersIsLeavingNode = __webpack_require__(7); 177 | 178 | var _helpersIsLeavingNode2 = _interopRequireDefault(_helpersIsLeavingNode); 179 | 180 | var _helpersTabbable = __webpack_require__(8); 181 | 182 | var _helpersTabbable2 = _interopRequireDefault(_helpersTabbable); 183 | 184 | var styles = { 185 | overlay: { 186 | position: 'fixed', 187 | top: 0, 188 | left: 0, 189 | right: 0, 190 | bottom: 0 191 | }, 192 | content: { 193 | position: 'absolute', 194 | background: '#fff' 195 | } 196 | }; 197 | 198 | var CLASS_NAMES = { 199 | overlay: { 200 | base: 'ReactTray__Overlay', 201 | afterOpen: 'ReactTray__Overlay--after-open', 202 | beforeClose: 'ReactTray__Overlay--before-close' 203 | }, 204 | content: { 205 | base: 'ReactTray__Content', 206 | afterOpen: 'ReactTray__Content--after-open', 207 | beforeClose: 'ReactTray__Content--before-close' 208 | } 209 | }; 210 | 211 | function isChild(parent, child) { 212 | if (parent === child) { 213 | return true; 214 | } 215 | 216 | var node = child; 217 | /* eslint no-cond-assign:0 */ 218 | while (node = node.parentNode) { 219 | if (node === parent) { 220 | return true; 221 | } 222 | } 223 | return false; 224 | } 225 | 226 | exports['default'] = _react2['default'].createClass({ 227 | displayName: 'TrayPortal', 228 | 229 | propTypes: { 230 | className: _react.PropTypes.string, 231 | overlayClassName: _react.PropTypes.string, 232 | isOpen: _react.PropTypes.bool, 233 | onBlur: _react.PropTypes.func, 234 | onOpen: _react.PropTypes.func, 235 | closeOnBlur: _react.PropTypes.bool, 236 | closeTimeoutMS: _react.PropTypes.number, 237 | children: _react.PropTypes.any, 238 | maintainFocus: _react.PropTypes.bool, 239 | getElementToFocus: _react.PropTypes.func, 240 | getAriaHideElement: _react.PropTypes.func 241 | }, 242 | 243 | getInitialState: function getInitialState() { 244 | return { 245 | afterOpen: false, 246 | beforeClose: false 247 | }; 248 | }, 249 | 250 | componentDidMount: function componentDidMount() { 251 | if (this.props.isOpen) { 252 | this.setFocusAfterRender(true); 253 | this.open(); 254 | } 255 | }, 256 | 257 | componentWillReceiveProps: function componentWillReceiveProps(props) { 258 | if (props.isOpen) { 259 | this.setFocusAfterRender(true); 260 | this.open(); 261 | } else if (this.props.isOpen && !props.isOpen) { 262 | this.close(); 263 | } 264 | }, 265 | 266 | componentDidUpdate: function componentDidUpdate() { 267 | if (this.focusAfterRender) { 268 | if (this.props.getElementToFocus) { 269 | this.props.getElementToFocus().focus(); 270 | } else { 271 | this.focusContent(); 272 | } 273 | this.setFocusAfterRender(false); 274 | } 275 | }, 276 | 277 | setFocusAfterRender: function setFocusAfterRender(focus) { 278 | this.focusAfterRender = focus; 279 | }, 280 | 281 | focusContent: function focusContent() { 282 | this.refs.content.focus(); 283 | }, 284 | 285 | applyAriaHidden: function applyAriaHidden(element) { 286 | element.setAttribute('aria-hidden', true); 287 | }, 288 | 289 | removeAriaHidden: function removeAriaHidden(element) { 290 | element.removeAttribute('aria-hidden'); 291 | }, 292 | 293 | handleOverlayClick: function handleOverlayClick(e) { 294 | if (!isChild(this.refs.content, e.target)) { 295 | this.props.onBlur(); 296 | } 297 | }, 298 | 299 | handleContentKeyDown: function handleContentKeyDown(e) { 300 | // Treat ESC as blur/close 301 | if (e.keyCode === 27) { 302 | this.props.onBlur(); 303 | } 304 | 305 | // Keep focus inside the tray if maintainFocus is true 306 | if (e.keyCode === 9 && this.props.maintainFocus && (0, _helpersIsLeavingNode2['default'])(this.refs.content, e)) { 307 | e.preventDefault(); 308 | var tabbable = (0, _helpersTabbable2['default'])(this.refs.content); 309 | var target = tabbable[e.shiftKey ? tabbable.length - 1 : 0]; 310 | target.focus(); 311 | return; 312 | } 313 | 314 | // Treat tabbing away from content as blur/close if closeOnBlur 315 | if (e.keyCode === 9 && this.props.closeOnBlur && (0, _helpersIsLeavingNode2['default'])(this.refs.content, e)) { 316 | e.preventDefault(); 317 | this.props.onBlur(); 318 | } 319 | }, 320 | 321 | open: function open() { 322 | var _this = this; 323 | 324 | _helpersFocusManager2['default'].markForFocusLater(); 325 | this.setState({ isOpen: true }, function () { 326 | if (_this.props.onOpen) { 327 | _this.props.onOpen(); 328 | } 329 | if (_this.props.getAriaHideElement) { 330 | _this.applyAriaHidden(_this.props.getAriaHideElement()); 331 | } 332 | _this.setState({ afterOpen: true }); 333 | }); 334 | }, 335 | 336 | close: function close() { 337 | if (this.props.closeTimeoutMS > 0) { 338 | this.closeWithTimeout(); 339 | } else { 340 | this.closeWithoutTimeout(); 341 | } 342 | if (this.props.getAriaHideElement) { 343 | this.removeAriaHidden(this.props.getAriaHideElement()); 344 | } 345 | }, 346 | 347 | closeWithTimeout: function closeWithTimeout() { 348 | var _this2 = this; 349 | 350 | this.setState({ beforeClose: true }, function () { 351 | setTimeout(_this2.closeWithoutTimeout, _this2.props.closeTimeoutMS); 352 | }); 353 | }, 354 | 355 | closeWithoutTimeout: function closeWithoutTimeout() { 356 | this.setState({ 357 | afterOpen: false, 358 | beforeClose: false 359 | }, this.afterClose); 360 | }, 361 | 362 | afterClose: function afterClose() { 363 | _helpersFocusManager2['default'].returnFocus(); 364 | }, 365 | 366 | shouldBeClosed: function shouldBeClosed() { 367 | return !this.props.isOpen && !this.state.beforeClose; 368 | }, 369 | 370 | buildClassName: function buildClassName(which) { 371 | var className = CLASS_NAMES[which].base; 372 | if (this.state.afterOpen) { 373 | className += ' ' + CLASS_NAMES[which].afterOpen; 374 | } 375 | if (this.state.beforeClose) { 376 | className += ' ' + CLASS_NAMES[which].beforeClose; 377 | } 378 | return className; 379 | }, 380 | 381 | render: function render() { 382 | return this.shouldBeClosed() ? _react2['default'].createElement('div', null) : _react2['default'].createElement( 383 | 'div', 384 | { 385 | ref: 'overlay', 386 | style: styles.overlay, 387 | className: (0, _classnames2['default'])(this.buildClassName('overlay'), this.props.overlayClassName), 388 | onClick: this.handleOverlayClick 389 | }, 390 | _react2['default'].createElement( 391 | 'div', 392 | { 393 | ref: 'content', 394 | style: styles.content, 395 | className: (0, _classnames2['default'])(this.buildClassName('content'), this.props.className), 396 | onKeyDown: this.handleContentKeyDown, 397 | tabIndex: '-1' 398 | }, 399 | this.props.children 400 | ) 401 | ); 402 | } 403 | }); 404 | module.exports = exports['default']; 405 | 406 | /***/ }, 407 | /* 5 */ 408 | /***/ function(module, exports, __webpack_require__) { 409 | 410 | var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/*! 411 | Copyright (c) 2016 Jed Watson. 412 | Licensed under the MIT License (MIT), see 413 | http://jedwatson.github.io/classnames 414 | */ 415 | /* global define */ 416 | 417 | (function () { 418 | 'use strict'; 419 | 420 | var hasOwn = {}.hasOwnProperty; 421 | 422 | function classNames () { 423 | var classes = []; 424 | 425 | for (var i = 0; i < arguments.length; i++) { 426 | var arg = arguments[i]; 427 | if (!arg) continue; 428 | 429 | var argType = typeof arg; 430 | 431 | if (argType === 'string' || argType === 'number') { 432 | classes.push(arg); 433 | } else if (Array.isArray(arg)) { 434 | classes.push(classNames.apply(null, arg)); 435 | } else if (argType === 'object') { 436 | for (var key in arg) { 437 | if (hasOwn.call(arg, key) && arg[key]) { 438 | classes.push(key); 439 | } 440 | } 441 | } 442 | } 443 | 444 | return classes.join(' '); 445 | } 446 | 447 | if (typeof module !== 'undefined' && module.exports) { 448 | module.exports = classNames; 449 | } else if (true) { 450 | // register as 'classnames', consistent with npm package name 451 | !(__WEBPACK_AMD_DEFINE_ARRAY__ = [], __WEBPACK_AMD_DEFINE_RESULT__ = function () { 452 | return classNames; 453 | }.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); 454 | } else { 455 | window.classNames = classNames; 456 | } 457 | }()); 458 | 459 | 460 | /***/ }, 461 | /* 6 */ 462 | /***/ function(module, exports) { 463 | 464 | 'use strict'; 465 | 466 | var focusLaterElement = null; 467 | 468 | exports.markForFocusLater = function markForFocusLater() { 469 | focusLaterElement = document.activeElement; 470 | }; 471 | 472 | exports.returnFocus = function returnFocus() { 473 | try { 474 | focusLaterElement.focus(); 475 | } catch (e) { 476 | /* eslint no-console:0 */ 477 | console.warn('You tried to return focus to ' + focusLaterElement + ' but it is not in the DOM anymore'); 478 | } 479 | focusLaterElement = null; 480 | }; 481 | 482 | /***/ }, 483 | /* 7 */ 484 | /***/ function(module, exports, __webpack_require__) { 485 | 486 | 'use strict'; 487 | 488 | Object.defineProperty(exports, '__esModule', { 489 | value: true 490 | }); 491 | 492 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } 493 | 494 | var _helpersTabbable = __webpack_require__(8); 495 | 496 | var _helpersTabbable2 = _interopRequireDefault(_helpersTabbable); 497 | 498 | exports['default'] = function (node, event) { 499 | var tabbable = (0, _helpersTabbable2['default'])(node); 500 | var finalTabbable = tabbable[event.shiftKey ? 0 : tabbable.length - 1]; 501 | var isLeavingNode = finalTabbable === document.activeElement; 502 | return isLeavingNode; 503 | }; 504 | 505 | module.exports = exports['default']; 506 | 507 | /***/ }, 508 | /* 8 */ 509 | /***/ function(module, exports) { 510 | 511 | /*! 512 | * Adapted from jQuery UI core 513 | * 514 | * http://jqueryui.com 515 | * 516 | * Copyright 2014 jQuery Foundation and other contributors 517 | * Released under the MIT license. 518 | * http://jquery.org/license 519 | * 520 | * http://api.jqueryui.com/category/ui-core/ 521 | */ 522 | 523 | 'use strict'; 524 | 525 | Object.defineProperty(exports, '__esModule', { 526 | value: true 527 | }); 528 | function hidden(el) { 529 | return el.offsetWidth <= 0 && el.offsetHeight <= 0 || el.style.display === 'none'; 530 | } 531 | 532 | function visible(element) { 533 | var el = element; 534 | while (el) { 535 | if (el === document.body) break; 536 | if (hidden(el)) return false; 537 | el = el.parentNode; 538 | } 539 | return true; 540 | } 541 | 542 | function focusable(element, isTabIndexNotNaN) { 543 | var nodeName = element.nodeName.toLowerCase(); 544 | /* eslint no-nested-ternary:0 */ 545 | return (/input|select|textarea|button|object/.test(nodeName) ? !element.disabled : nodeName === 'a' ? element.href || isTabIndexNotNaN : isTabIndexNotNaN) && visible(element); 546 | } 547 | 548 | function tabbable(element) { 549 | var tabIndex = element.getAttribute('tabindex'); 550 | if (tabIndex === null) tabIndex = undefined; 551 | var isTabIndexNaN = isNaN(tabIndex); 552 | return (isTabIndexNaN || tabIndex >= 0) && focusable(element, !isTabIndexNaN); 553 | } 554 | 555 | function findTabbableDescendants(element) { 556 | return [].slice.call(element.querySelectorAll('*'), 0).filter(function (el) { 557 | return tabbable(el); 558 | }); 559 | } 560 | 561 | exports['default'] = findTabbableDescendants; 562 | module.exports = exports['default']; 563 | 564 | /***/ }, 565 | /* 9 */ 566 | /***/ function(module, exports) { 567 | 568 | // Adapted from https://github.com/react-bootstrap/react-prop-types/blob/master/src/isRequiredForA11y.js 569 | 'use strict'; 570 | 571 | Object.defineProperty(exports, '__esModule', { 572 | value: true 573 | }); 574 | exports.a11yFunction = a11yFunction; 575 | 576 | function a11yFunction(props, propName, componentName) { 577 | if (!props[propName] || typeof props[propName] !== 'function') { 578 | 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.'); 579 | } 580 | } 581 | 582 | /***/ } 583 | /******/ ]) 584 | }); 585 | ; 586 | //# sourceMappingURL=react-tray.js.map --------------------------------------------------------------------------------