├── .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 |
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 |
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 |
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
--------------------------------------------------------------------------------