Skip to content

Commit

Permalink
Add focus trap to app-drawer
Browse files Browse the repository at this point in the history
  • Loading branch information
keanulee committed Apr 20, 2016
1 parent 6118b6e commit 793d6cf
Show file tree
Hide file tree
Showing 3 changed files with 168 additions and 44 deletions.
89 changes: 83 additions & 6 deletions app-drawer/app-drawer.html
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,14 @@
type: Boolean,
value: false,
reflectToAttribute: true
},

/**
* Trap keyboard focus when the drawer is opened and not persistent.
*/
noFocusTrap: {
type: Boolean,
value: false
}
},

Expand All @@ -233,7 +241,13 @@

_drawerState: 0,

_boundKeydownHandler: null,
_boundEscKeydownHandler: null,

_lastFocusedElement: null,

_firstTabStop: null,

_lastTabStop: null,

ready: function() {
// Set the scroll direction so you can vertically scroll inside the drawer.
Expand All @@ -249,16 +263,17 @@
// may need to set the initial opened state which should not be transitioned).
Polymer.RenderStatus.afterNextRender(this, function() {
this._setTransitionDuration('');
this._boundKeydownHandler = this._keydownHandler.bind(this);
this._boundEscKeydownHandler = this._escKeydownHandler.bind(this);
this._resetDrawerState();

this.listen(this, 'track', '_track');
this.addEventListener('transitionend', this._transitionend.bind(this));
this.addEventListener('keydown', this._tabKeydownHandler.bind(this))
});
},

detached: function() {
document.removeEventListener('keydown', this._boundKeydownHandler);
document.removeEventListener('keydown', this._boundEscKeydownHandler);
},

/**
Expand Down Expand Up @@ -313,7 +328,7 @@
this._setPosition(this.align);
},

_keydownHandler: function(event) {
_escKeydownHandler: function(event) {
var ESC_KEYCODE = 27;
if (event.keyCode === ESC_KEYCODE) {
// Prevent any side effects if app-drawer closes.
Expand Down Expand Up @@ -536,10 +551,12 @@

if (oldState !== this._drawerState) {
if (this._drawerState === this._DRAWER_STATE.OPENED) {
document.addEventListener('keydown', this._boundKeydownHandler);
document.addEventListener('keydown', this._boundEscKeydownHandler);
this._addKeyboardFocusTrap();
document.body.style.overflow = 'hidden';
} else {
document.removeEventListener('keydown', this._boundKeydownHandler);
document.removeEventListener('keydown', this._boundEscKeydownHandler);
this._removeKeyboardFocusTrap();
document.body.style.overflow = '';
}

Expand All @@ -550,6 +567,66 @@
}
},

_addKeyboardFocusTrap: function() {
if (this.noFocusTrap) {
return;
}

this._lastFocusedElement = document.activeElement;

// NOTE: Unless we use /deep/ (which we shouldn't since it's deprecated), this will
// not select focusable elements inside shadow roots.
var focusableElementsSelector = [
'a[href]:not([tabindex="-1"])',
'area[href]:not([tabindex="-1"])',
'input:not([disabled]):not([tabindex="-1"])',
'select:not([disabled]):not([tabindex="-1"])',
'textarea:not([disabled]):not([tabindex="-1"])',
'button:not([disabled]):not([tabindex="-1"])',
'iframe:not([tabindex="-1"])',
'[tabindex]:not([tabindex="-1"])',
'[contentEditable=true]:not([tabindex="-1"])'
].join(',');
var focusableElements = Polymer.dom(this).querySelectorAll(focusableElementsSelector);

if (focusableElements.length > 0) {
this._firstTabStop = focusableElements[0];
this._lastTabStop = focusableElements[focusableElements.length - 1];
this._firstTabStop.focus();
} else {
// Reset saved tab stops when there are no focusable elements in the drawer.
this._firstTabStop = null;
this._lastTabStop = null;
}
},

_removeKeyboardFocusTrap: function() {
if (!this.noFocusTrap && this._lastFocusedElement) {
this._lastFocusedElement.focus();
}
},

_tabKeydownHandler: function(event) {
if (this.noFocusTrap) {
return;
}

var TAB_KEYCODE = 9;
if (this._drawerState === this._DRAWER_STATE.OPENED && event.keyCode === TAB_KEYCODE) {
if (event.shiftKey) {
if (this._firstTabStop && document.activeElement === this._firstTabStop) {
event.preventDefault();
this._lastTabStop.focus();
}
} else {
if (this._lastTabStop && document.activeElement === this._lastTabStop) {
event.preventDefault();
this._firstTabStop.focus();
}
}
}
},

_MIN_FLING_THRESHOLD: 0.2,

_MIN_TRANSITION_VELOCITY: 1.2,
Expand Down
17 changes: 1 addition & 16 deletions app-drawer/demo/demo2.html
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@

<sample-content size="100"></sample-content>

<app-drawer id="drawer" align="end" swipe-open on-app-drawer-transitioned="drawerTransitioned">
<app-drawer id="drawer" align="end" swipe-open>
<div class="drawer-contents">
<template is="dom-repeat" id="menu" items="[[items]]">
<paper-icon-item>
Expand All @@ -85,21 +85,6 @@
scope.$.drawer.toggle();
};

scope.drawerTransitioned = function() {
if (scope.$.drawer.opened) {
document.querySelector('paper-icon-item').focus();
document.addEventListener('focus', scope.focusHandler, true /* useCapture */);
} else {
document.removeEventListener('focus', scope.focusHandler, true /* useCapture */);
}
};

scope.focusHandler = function(event) {
if (Polymer.dom(event).path.indexOf(scope.$.drawer) === -1) {
document.querySelector('paper-icon-item').focus();
}
};

var icons = ['inbox', 'favorite', 'polymer', 'question-answer', 'send', 'archive', 'backup', 'dashboard'];
scope.items = icons.concat(icons).concat(icons);

Expand Down
106 changes: 84 additions & 22 deletions app-drawer/test/app-drawer.html
Original file line number Diff line number Diff line change
Expand Up @@ -39,23 +39,35 @@
</template>
</test-fixture>

<dom-module id="x-button">
<test-fixture id="focusDrawer">
<template>
<button id="btn">Shadow</button>
<div>
<button>Button</button>
<app-drawer>
<input type="text">
<div tabindex="0">Div</div>
<span>Not focusable</span>
</app-drawer>
</div>
</template>

<script>
HTMLImports.whenReady(function() {
Polymer({ is: 'x-button' });
});
</script>
</dom-module>
</test-fixture>

<script>

suite('basic features', function() {
var drawer, scrim, contentContainer, transformSpy;

function fireKeydownEvent(target, keyCode, shiftKey) {
var e = new CustomEvent('keydown', {
bubbles: true,
cancelable: true
});
e.keyCode = keyCode;
e.shiftKey = !!shiftKey;
target.dispatchEvent(e);
return e;
}

function assertDrawerStyles(translateX, opacity, desc) {
assert.equal(transformSpy.lastCall.args[0], 'translate3d(' + translateX + 'px,0,0)', desc);
assert.equal(parseFloat(scrim.style.opacity).toFixed(4), opacity.toFixed(4), desc);
Expand Down Expand Up @@ -93,6 +105,7 @@
assert.isFalse(drawer.persistent);
assert.equal(drawer.align, 'left');
assert.isFalse(drawer.swipeOpen);
assert.isFalse(drawer.noFocusTrap);
});

test('set scroll direction', function() {
Expand Down Expand Up @@ -579,22 +592,71 @@
}, 100);
});

test('focus trap', function(done) {
var container = fixture('focusDrawer');
var button = container.querySelector('button');
var drawer = container.querySelector('app-drawer');
var input = Polymer.dom(drawer).querySelector('input');
var div = Polymer.dom(drawer).querySelector('div[tabindex]');
var buttonFocusSpy = sinon.spy(button, 'focus');
var inputFocusSpy = sinon.spy(input, 'focus');
var divFocusSpy = sinon.spy(div, 'focus');
button.focus();
drawer.opened = true;

window.setTimeout(function() {
assert.isTrue(inputFocusSpy.called);

var e = fireKeydownEvent(input, 9);

assert.isFalse(e.defaultPrevented, 'should not prevent default');

input.focus();
inputFocusSpy.reset();
e = fireKeydownEvent(input, 9, true /* shiftKey */);

assert.isTrue(divFocusSpy.called);
assert.isTrue(e.defaultPrevented, 'should prevent default');

e = fireKeydownEvent(div, 9, true /* shiftKey */);

assert.isFalse(e.defaultPrevented, 'should not prevent default');

div.focus();
e = fireKeydownEvent(div, 9);

assert.isTrue(inputFocusSpy.called);
assert.isTrue(e.defaultPrevented, 'should prevent default');
done();
}, 350);
});

test('no focus trap', function(done) {
var container = fixture('focusDrawer');
var button = container.querySelector('button');
var drawer = container.querySelector('app-drawer');
var input = Polymer.dom(drawer).querySelector('input');
var inputFocusSpy = sinon.spy(input, 'focus');
drawer.noFocusTrap = true;
button.focus();
drawer.opened = true;

window.setTimeout(function() {
assert.isFalse(inputFocusSpy.called);
done();
}, 350);
});

test('esc key handler', function(done) {
drawer.opened = true;

window.setTimeout(function() {
drawer.opened = true;
var e = fireKeydownEvent(document, 27);

window.setTimeout(function() {
var e = new CustomEvent('keydown', {
cancelable: true
});
e.keyCode = 27;
document.dispatchEvent(e);

assert.isFalse(drawer.opened, 'should close drawer on esc');
assert.isTrue(e.defaultPrevented, 'should prevent default');
done();
}, 350);
}, 100);
assert.isFalse(drawer.opened, 'should close drawer on esc');
assert.isTrue(e.defaultPrevented, 'should prevent default');
done();
}, 350);
});

test('scrim', function() {
Expand Down

0 comments on commit 793d6cf

Please sign in to comment.