Skip to content

Commit

Permalink
Merge pull request #21 from Brightspace/dbatiste/arrowkey-focusable-b…
Browse files Browse the repository at this point in the history
…ehavior

Dbatiste/arrowkey focusable behavior
  • Loading branch information
dbatiste authored May 24, 2018
2 parents c51fd75 + 5fa8c8a commit c58f284
Show file tree
Hide file tree
Showing 4 changed files with 308 additions and 0 deletions.
46 changes: 46 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,52 @@ D2L.Gestures.Swipe.unregister(element);
D2L.Id.getUniqueId();
```

#### Behaviors

**D2L.PolymerBehaviors.FocusableArrowKeysBehavior**

The `FocusableArrowKeysBehavior` can be used for managing focus with the arrow keys.

* right/down - focuses next element, or first if currently at the end
* left/up - focuses previous element, or last if currently at beginning
* home - focuses first
* end - focuses last

```javascript

// include the behavior
behaviors: [
D2L.PolymerBehaviors.FocusableArrowKeysBehavior
],

attached: function() {
Polymer.RenderStatus.afterNextRender(this, function() {

// indicate the direction (default is leftright)
this.arrowKeyFocusablesDirection = 'updown';

// required container element of focusables (used to listen for key events)
this.arrowKeyFocusablesContainer = container;

// required provider method that can return list of focusables - possible async
this.arrowKeyFocusablesProvider = function() {

// simple case
return Promise.resolve(focusables);

// other cases (ex. check visibility when querying focusables)
return new Promise(function(resolve) {
fastdom.measure(function() {
// ...
resolve(focusables);
});
});

}.bind(this);
});
}
```

### Usage in Production

In production, it's recommended to use a build tool like [Vulcanize](https://github.com/Polymer/vulcanize) to combine all your web components into a single import file. [More from the Polymer Docs: Optimize for Production](https://www.polymer-project.org/1.0/tools/optimize-for-production.html)...
Expand Down
146 changes: 146 additions & 0 deletions d2l-focusable-arrowkeys-behavior.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
<link rel="import" href="../polymer/polymer.html">
<link rel="import" href="../d2l-fastdom-import/fastdom.html">

<script>
window.D2L = window.D2L || {};
window.D2L.PolymerBehaviors = window.D2L.PolymerBehaviors || {};

/** @polymerBehavior */
D2L.PolymerBehaviors.FocusableArrowKeysBehavior = {

properties: {

arrowKeyFocusablesContainer: {
type: Object,
observer: '__handleArrowKeyFocusablesContainer',
},

arrowKeyFocusablesProvider: {
type: Object
},

arrowKeyFocusablesDirection: {
type: String,
value: 'leftright'
}

},

__keyCodes: {
END: 35,
HOME: 36,
LEFT: 37,
UP: 38,
RIGHT: 39,
DOWN: 40
},

ready: function() {
this.__handleKeyDown = this.__handleKeyDown.bind(this);
},

detached: function() {
this.arrowKeyFocusablesContainer = null;
this.arrowKeyFocusablesProvider = null;
},

__focusFirst: function() {
if (!this.arrowKeyFocusablesProvider) {
Promise.reject('No focusables provider.');
}
return this.arrowKeyFocusablesProvider().then(
function(elems) {
if (elems && elems.length > 0) fastdom.mutate(function() { elems[0].focus(); });
}
);
},

__focusLast: function() {
if (!this.arrowKeyFocusablesProvider) {
Promise.reject('No focusables provider.');
}
return this.arrowKeyFocusablesProvider().then(
function(elems) {
if (elems && elems.length > 0) fastdom.mutate(function() { elems[elems.length - 1].focus(); });
}
);
},

__focusNext: function(elem) {
if (!this.arrowKeyFocusablesProvider) {
Promise.reject('No focusables provider.');
}
return this.arrowKeyFocusablesProvider().then(
function(elems) {
var next = this.__tryGetNextFocusable(elems, elem);
if (next) fastdom.mutate(function() { next.focus(); });
}.bind(this)
);
},

__focusPrevious: function(elem) {
if (!this.arrowKeyFocusablesProvider) {
Promise.reject('No focusables provider.');
}
return this.arrowKeyFocusablesProvider().then(
function(elems) {
var previous = this.__tryGetPreviousFocusable(elems, elem);
if (previous) fastdom.mutate(function() { previous.focus(); });
}.bind(this)
);
},

__handleArrowKeyFocusablesContainer: function(newElem, oldElem) {
if (oldElem) {
oldElem.removeEventListener('keydown', this.__handleKeyDown);
}
if (!newElem) {
return;
}
newElem.addEventListener('keydown', this.__handleKeyDown);
},

__handleKeyDown: function(e) {
if (this.arrowKeyFocusablesDirection === 'leftright' && e.keyCode === this.__keyCodes.LEFT) {
this.__focusPrevious(e.target);
} else if (this.arrowKeyFocusablesDirection === 'leftright' && e.keyCode === this.__keyCodes.RIGHT) {
this.__focusNext(e.target);
} else if (this.arrowKeyFocusablesDirection === 'updown' && e.keyCode === this.__keyCodes.UP) {
this.__focusPrevious(e.target);
} else if (this.arrowKeyFocusablesDirection === 'updown' && e.keyCode === this.__keyCodes.DOWN) {
this.__focusNext(e.target);
} else if (e.keyCode === this.__keyCodes.HOME) {
this.__focusFirst();
} else if (e.keyCode === this.__keyCodes.END) {
this.__focusLast();
} else {
return;
}
e.preventDefault();
},

__tryGetNextFocusable: function(elems, elem) {
if (!elems || elems.length === 0) {
return;
}
var index = elems.indexOf(elem);
if (index === elems.length - 1) {
return elems[0];
}
return elems[index + 1];
},

__tryGetPreviousFocusable: function(elems, elem) {
if (!elems || elems.length === 0) {
return;
}
var index = elems.indexOf(elem);
if (index === 0) {
return elems[elems.length - 1];
}
return elems[index - 1];
}

};

</script>
115 changes: 115 additions & 0 deletions test/focusable-arrowkeys-behavior.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>d2l-focusable-arrowkeys-behavior tests</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
<script src="../../webcomponentsjs/webcomponents-lite.js"></script>
<script src="../../web-component-tester/browser.js"></script>
<link rel="import" href="../../iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="../../polymer/polymer.html">
<link rel="import" href="../d2l-focusable-arrowkeys-behavior.html">
<link rel="import" href="../d2l-dom-focus.html">
</head>
<body>
<dom-module id="d2l-focusable-arrowkeys-test">
<template>
<div id="arrowFocusables">
<div tabindex="-1"></div>
<div tabindex="-1"></div>
<div tabindex="-1"></div>
<div tabindex="-1"></div>
<div tabindex="-1"></div>
</div>
</template>
<script>
Polymer({
is: 'd2l-focusable-arrowkeys-test',
behaviors: [D2L.PolymerBehaviors.FocusableArrowKeysBehavior],
attached: function() {
this.arrowKeyFocusablesContainer = this.$.arrowFocusables;
this.arrowKeyFocusablesProvider = function() {
return Promise.resolve(Array.prototype.slice.call(Polymer.dom(this.root).querySelectorAll('[tabindex]')));
}.bind(this);
}
});
</script>
</dom-module>

<test-fixture id="simpleFixture">
<template>
<d2l-focusable-arrowkeys-test></d2l-focusable-arrowkeys-test>
</template>
</test-fixture>

<script>

describe('d2l-focusable-arrowkeys-behavior', function() {

var simpleFixture, focusables;

beforeEach(function(done) {
simpleFixture = fixture('simpleFixture');
simpleFixture.arrowKeyFocusablesProvider().then(function(result) {
focusables = result;
done();
});
});

var testKeyInteractions = function(keyInteractions) {

for (var i=0; i<keyInteractions.length; i++) {

(function(i) {

it(keyInteractions[i].name, function(done) {
focusables[keyInteractions[i].startIndex].focus();
focusables[keyInteractions[i].endIndex].addEventListener('focus', function() {
expect(D2L.Dom.Focus.getComposedActiveElement()).to.equal(focusables[keyInteractions[i].endIndex]);
done();
});
MockInteractions.keyDownOn(focusables[keyInteractions[i].startIndex], keyInteractions[i].keyCode);
});

}(i));

}

};

describe('left-right', function() {

testKeyInteractions([
{ name: 'focuses on next focusable when Right arrow key is pressed', startIndex: 2, endIndex: 3, keyCode: 39 },
{ name: 'focuses on previous focusable when Left arrow key is pressed', startIndex: 2, endIndex: 1, keyCode: 37 },
{ name: 'focuses on first focusable when Right arrow key is pressed on last focusable', startIndex: 4, endIndex: 0, keyCode: 39 },
{ name: 'focuses on last focusable when Left arrow key is pressed on first focusable', startIndex: 0, endIndex: 4, keyCode: 37 },
{ name: 'focuses on first focusable when Home key is pressed', startIndex: 2, endIndex: 0, keyCode: 36 },
{ name: 'focuses on last focusable when End key is pressed', startIndex: 2, endIndex: 4, keyCode: 35 }
]);

});

describe('up-down', function() {

beforeEach(function(done) {
simpleFixture.arrowKeyFocusablesDirection = 'updown';
done();
});

testKeyInteractions([
{ name: 'focuses on next focusable when Down arrow key is pressed', startIndex: 2, endIndex: 3, keyCode: 40 },
{ name: 'focuses on previous focusable when Up arrow key is pressed', startIndex: 2, endIndex: 1, keyCode: 38 },
{ name: 'focuses on first focusable when Down arrow key is pressed on last focusable', startIndex: 4, endIndex: 0, keyCode: 40 },
{ name: 'focuses on last focusable when Up arrow key is pressed on first focusable', startIndex: 0, endIndex: 4, keyCode: 38 },
{ name: 'focuses on first focusable when Home key is pressed', startIndex: 2, endIndex: 0, keyCode: 36 },
{ name: 'focuses on last focusable when End key is pressed', startIndex: 2, endIndex: 4, keyCode: 35 }
]);

});

});

</script>
</body>
</html>
1 change: 1 addition & 0 deletions test/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
'dom-visibility.html',
'dom-focus.html',
'focusable-behavior.html',
'focusable-arrowkeys-behavior.html',
'id.html'
];

Expand Down

0 comments on commit c58f284

Please sign in to comment.