From 720c1cea1cef362d993bcae4b015201ff14777a4 Mon Sep 17 00:00:00 2001 From: Clay Diffrient Date: Thu, 7 Jul 2016 10:24:17 -0600 Subject: [PATCH] [changed] Maintain focus inside of tray This adds a new prop `maintainFocus` which defaults to `true`. This makes the tray "modal" in that it will not allow focus to escape from within it until it is explicitly closed via close button, escape key, or clicking in the overlay area. Previous behavior can be achieved by setting the `maintainFocus` prop to `false`. This defaults to `true` so that it can achieve a greater level of accessibility support which I felt was valuable to everyone so it is opt-out rather than opt-in. --- lib/components/Tray.js | 6 ++++-- lib/components/TrayPortal.js | 13 ++++++++++- lib/components/__tests__/Tray-test.js | 31 +++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 3 deletions(-) diff --git a/lib/components/Tray.js b/lib/components/Tray.js index 041ca0f..d0e5002 100644 --- a/lib/components/Tray.js +++ b/lib/components/Tray.js @@ -10,14 +10,16 @@ export default React.createClass({ isOpen: React.PropTypes.bool, onBlur: React.PropTypes.func, closeTimeoutMS: React.PropTypes.number, - closeOnBlur: React.PropTypes.bool + closeOnBlur: React.PropTypes.bool, + maintainFocus: React.PropTypes.bool }, getDefaultProps() { return { isOpen: false, closeTimeoutMS: 0, - closeOnBlur: true + closeOnBlur: true, + maintainFocus: false }; }, diff --git a/lib/components/TrayPortal.js b/lib/components/TrayPortal.js index 151ddde..c4b93d5 100644 --- a/lib/components/TrayPortal.js +++ b/lib/components/TrayPortal.js @@ -2,6 +2,7 @@ import React, { PropTypes } from 'react'; import cx from 'classnames'; import focusManager from '../helpers/focusManager'; import isLeavingNode from '../helpers/isLeavingNode'; +import findTabbable from '../helpers/tabbable'; const styles = { overlay: { @@ -55,7 +56,8 @@ export default React.createClass({ onBlur: PropTypes.func, closeOnBlur: PropTypes.bool, closeTimeoutMS: PropTypes.number, - children: PropTypes.any + children: PropTypes.any, + maintainFocus: PropTypes.bool }, getInitialState() { @@ -108,6 +110,15 @@ export default React.createClass({ this.props.onBlur(); } + // Keep focus inside the tray if maintainFocus is true + if (e.keyCode === 9 && this.props.maintainFocus && isLeavingNode(this.refs.content, e)) { + e.preventDefault(); + const tabbable = findTabbable(this.refs.content); + const target = tabbable[e.shiftKey ? tabbable.length - 1 : 0]; + target.focus(); + return; + } + // Treat tabbing away from content as blur/close if closeOnBlur if (e.keyCode === 9 && this.props.closeOnBlur && isLeavingNode(this.refs.content, e)) { e.preventDefault(); diff --git a/lib/components/__tests__/Tray-test.js b/lib/components/__tests__/Tray-test.js index ffa8d3e..c9ae247 100644 --- a/lib/components/__tests__/Tray-test.js +++ b/lib/components/__tests__/Tray-test.js @@ -79,4 +79,35 @@ describe('react-tray', function() { equal(document.querySelectorAll('.ReactTray__Content').length, 1); }, 0); }); + + describe('maintainFocus prop', function() { + this.timeout(0); + beforeEach(function(done) { + const props = {isOpen: true, onBlur: function() {}, closeTimeoutMS: 0, maintainFocus: true}; + const children = ( +
+ One + Two + Three +
+ ); + renderTray(props, children, () => done()); + }); + + it('sends focus to the first item if tabbing away from the last element', function() { + const firstItem = document.querySelector('#one'); + const lastItem = document.querySelector('#three'); + lastItem.focus(); + TestUtils.Simulate.keyDown(document.querySelector('.ReactTray__Content'), {keyCode: 9}); + equal(document.activeElement, firstItem); + }); + + it('sends focus to the last item if shift + tabbing from the first item', function() { + const firstItem = document.querySelector('#one'); + const lastItem = document.querySelector('#three'); + firstItem.focus(); + TestUtils.Simulate.keyDown(document.querySelector('.ReactTray__Content'), {keyCode: 9, shiftKey: true}); + equal(document.activeElement, lastItem); + }); + }); });