Skip to content
This repository has been archived by the owner on Sep 5, 2024. It is now read-only.

Commit

Permalink
fix(chips): Add basic accessibility support. (#9650)
Browse files Browse the repository at this point in the history
Fixes #9391. Fixes #9556. Fixes #8897. Fixes #8867. Fixes #9649.
  • Loading branch information
topherfangio authored and kara committed Jan 4, 2017
1 parent 16c2512 commit f18cb2b
Show file tree
Hide file tree
Showing 11 changed files with 460 additions and 75 deletions.
4 changes: 2 additions & 2 deletions docs/config/template/index.template.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!doctype html>
<html ng-app="docsApp" ng-controller="DocsCtrl" lang="en" ng-strict-di >
<html ng-app="docsApp" ng-controller="DocsCtrl" lang="en" ng-strict-di>
<head>
<base href="/">
<title ng-bind="'Angular Material - ' + menu.currentSection.name +
Expand All @@ -12,7 +12,7 @@
<link rel="stylesheet" href="angular-material.min.css">
<link rel="stylesheet" href="docs.css">
</head>
<body class="docs-body" layout="row" ng-cloak>
<body class="docs-body" layout="row" ng-cloak aria-label="Angular Material Docs">

<md-sidenav class="site-sidenav md-sidenav-left md-whiteframe-z2"
md-component-id="left" hide-print
Expand Down
128 changes: 126 additions & 2 deletions src/components/chips/chips.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ describe('<md-chips>', function() {
'</md-chips>';
var CHIP_NOT_REMOVABLE_TEMPLATE =
'<md-chips ng-model="items" readonly="true" md-removable="false"></md-chips>';
var CHIP_APPEND_DELAY_TEMPLATE =
'<md-chips ng-model="items" md-chip-append-delay="800"></md-chips>';

afterEach(function() {
attachedElements.forEach(function(element) {
Expand Down Expand Up @@ -170,7 +172,7 @@ describe('<md-chips>', function() {

expect(scope.addChip).toHaveBeenCalled();
expect(scope.addChip.calls.mostRecent().args[0]).toBe('Grape'); // Chip
expect(scope.addChip.calls.mostRecent().args[1]).toBe(4); // Index
expect(scope.addChip.calls.mostRecent().args[1]).toBe(3); // Index
});


Expand Down Expand Up @@ -426,7 +428,7 @@ describe('<md-chips>', function() {

var updatedChips = getChipElements(element);

expect(chips.length).not.toBe(updatedChips.length);
expect(updatedChips.length).toBe(chips.length - 1);
}));

it('should set removable to true by default', function() {
Expand Down Expand Up @@ -825,6 +827,50 @@ describe('<md-chips>', function() {

});

it('utilizes the default chip append delay of 300ms', inject(function($timeout) {
var element = buildChips(BASIC_CHIP_TEMPLATE);
var ctrl = element.controller('mdChips');

// Append element to body
angular.element(document.body).append(element);

// Append a new chips which will fire the delay
ctrl.appendChip('test');

// Before 300ms timeout, focus should be on the chip (i.e. the chip content)
$timeout.flush(299);
expect(document.activeElement).toHaveClass('md-chip-content');

// At/after 300ms timeout, focus should be on the input
$timeout.flush(1);
expect(document.activeElement.tagName.toUpperCase()).toEqual('INPUT');

// cleanup
element.remove();
}));

it('utilizes a custom chip append delay', inject(function($timeout) {
var element = buildChips(CHIP_APPEND_DELAY_TEMPLATE);
var ctrl = element.controller('mdChips');

// Append element to body
angular.element(document.body).append(element);

// Append a new chips which will fire the delay
ctrl.appendChip('test');

// Before custom timeout, focus should be on the chip (i.e. the chip content)
$timeout.flush(ctrl.chipAppendDelay - 1);
expect(document.activeElement).toHaveClass('md-chip-content');

// At/after custom timeout, focus should be on the input
$timeout.flush(1);
expect(document.activeElement.tagName.toUpperCase()).toEqual('INPUT');

// cleanup
element.remove();
}));

});

describe('custom inputs', function() {
Expand Down Expand Up @@ -1404,6 +1450,84 @@ describe('<md-chips>', function() {

});
});

describe('keyboard navigation', function() {
var leftEvent, rightEvent;

beforeEach(inject(function($mdConstant) {
leftEvent = {
type: 'keydown',
keyCode: $mdConstant.KEY_CODE.LEFT_ARROW,
which: $mdConstant.KEY_CODE.LEFT_ARROW
};
rightEvent = {
type: 'keydown',
keyCode: $mdConstant.KEY_CODE.RIGHT_ARROW,
which: $mdConstant.KEY_CODE.RIGHT_ARROW
};
}));

describe('when readonly', function() {
// TODO: Add readonly specific tests
});

describe('when we have an input', function() {
it('clears the selected chip when the input is focused', inject(function($timeout) {
var element = buildChips(BASIC_CHIP_TEMPLATE);
var ctrl = element.controller('mdChips');

// Focus the input
ctrl.focusInput();
$timeout.flush();

// Expect no chip to be selected
expect(ctrl.selectedChip).toBe(-1);
}));

it('selects the previous chip', inject(function($timeout) {
var element = buildChips(BASIC_CHIP_TEMPLATE);
var ctrl = element.controller('mdChips');
var chips = getChipElements(element);

// Select the second chip
ctrl.selectAndFocusChipSafe(1);
$timeout.flush();

expect(ctrl.selectedChip).toBe(1);

// Select the 1st chip
element.find('md-chips-wrap').triggerHandler(angular.copy(leftEvent));
$timeout.flush();

expect(ctrl.selectedChip).toBe(0);
}));

it('and the first chip is selected, selects the input', inject(function($timeout) {
var element = buildChips(BASIC_CHIP_TEMPLATE);
var ctrl = element.controller('mdChips');
var chips = getChipElements(element);

// Append so we can focus the input
angular.element(document.body).append(element);

// Select the second chip
ctrl.selectAndFocusChipSafe(0);
$timeout.flush();

expect(ctrl.selectedChip).toBe(0);

// Selecting past the first should wrap back to the input
element.find('md-chips-wrap').triggerHandler(angular.copy(leftEvent));
$timeout.flush();

expect(ctrl.selectedChip).toBe(-1);
expect(document.activeElement).toBe(element.find('input')[0]);

// Cleanup after ourselves
element.remove();
}));
});
});
});

describe('with $interpolate.start/endSymbol override', function() {
Expand Down
8 changes: 8 additions & 0 deletions src/components/chips/contact-chips.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ describe('<md-contact-chips>', function() {
md-contact-email="email"\
md-highlight-flags="i"\
md-min-length="1"\
md-chip-append-delay="2000"\
placeholder="To">\
</md-contact-chips>';

Expand Down Expand Up @@ -64,6 +65,13 @@ describe('<md-contact-chips>', function() {
expect(ctrl.highlightFlags).toEqual('i');
});

it('forwards the md-chips-append-delay attribute to the md-chips', function() {
var element = buildChips(CONTACT_CHIPS_TEMPLATE);
var chipsCtrl = element.find('md-chips').controller('mdChips');

expect(chipsCtrl.chipAppendDelay).toEqual(2000);
});

it('renders an image element for contacts with an image property', function() {
scope.contacts.push(scope.allContacts[2]);

Expand Down
2 changes: 1 addition & 1 deletion src/components/chips/demoBasicUsage/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ <h2 class="md-title">Use a custom chip template.</h2>

<form name="fruitForm">
<md-chips ng-model="ctrl.roFruitNames" name="fruitName" readonly="ctrl.readonly"
md-removable="ctrl.removable" md-max-chips="5">
md-removable="ctrl.removable" md-max-chips="5" placeholder="Enter a fruit...">
<md-chip-template>
<strong>{{$chip}}</strong>
<em>(fruit)</em>
Expand Down
10 changes: 10 additions & 0 deletions src/components/chips/demoBasicUsage/readme.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<p>
<b>Note:</b> Version 1.1.2 drastically improves keyboard and screen reader accessibility for the
<code>md-chips</code> component. In order to achieve this, the behavior has changed to also select
and highlight the newly appended chip for <code>300ms</code> before re-focusing the text input.
</p>

<p>
Please see the <a href="api/directive/mdChips">documentation</a> for more information and for
the new <code>md-chip-append-delay</code> option which allows you to customize this delay.
</p>
26 changes: 18 additions & 8 deletions src/components/chips/demoContactChips/script.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
(function () {
'use strict';

// If we do not have CryptoJS defined; import it
if (typeof CryptoJS == 'undefined') {
var cryptoSrc = '//cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.2/rollups/md5.js';
var scriptTag = document.createElement('script');
scriptTag.setAttribute('src', cryptoSrc);
document.body.appendChild(scriptTag);
}

angular
.module('contactChipsDemo', ['ngMaterial'])
.controller('ContactChipDemoCtrl', DemoCtrl);

function DemoCtrl ($q, $timeout) {
var self = this;
var pendingSearch, cancelSearch = angular.noop;
var cachedQuery, lastSearch;
var lastSearch;

self.allContacts = loadContacts();
self.contacts = [self.allContacts[0]];
Expand All @@ -21,16 +30,14 @@
* Search for contacts; use a random delay to simulate a remote call
*/
function querySearch (criteria) {
cachedQuery = cachedQuery || criteria;
return cachedQuery ? self.allContacts.filter(createFilterFor(cachedQuery)) : [];
return criteria ? self.allContacts.filter(createFilterFor(criteria)) : [];
}

/**
* Async search for contacts
* Also debounce the queries; since the md-contact-chips does not support this
*/
function delayedQuerySearch(criteria) {
cachedQuery = criteria;
if ( !pendingSearch || !debounceSearch() ) {
cancelSearch();

Expand All @@ -39,7 +46,7 @@
cancelSearch = reject;
$timeout(function() {

resolve( self.querySearch() );
resolve( self.querySearch(criteria) );

refreshDebounce();
}, Math.random() * 500, true)
Expand Down Expand Up @@ -72,7 +79,7 @@
var lowercaseQuery = angular.lowercase(query);

return function filterFn(contact) {
return (contact._lowername.indexOf(lowercaseQuery) != -1);;
return (contact._lowername.indexOf(lowercaseQuery) != -1);
};

}
Expand All @@ -92,10 +99,13 @@

return contacts.map(function (c, index) {
var cParts = c.split(' ');
var email = cParts[0][0].toLowerCase() + '.' + cParts[1].toLowerCase() + '@example.com';
var hash = CryptoJS.MD5(email);

var contact = {
name: c,
email: cParts[0][0].toLowerCase() + '.' + cParts[1].toLowerCase() + '@example.com',
image: 'http://lorempixel.com/50/50/people?' + index
email: email,
image: '//www.gravatar.com/avatar/' + hash + '?s=50&d=retro'
};
contact._lowername = contact.name.toLowerCase();
return contact;
Expand Down
54 changes: 32 additions & 22 deletions src/components/chips/js/chipDirective.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
angular
.module('material.components.chips')
.directive('mdChip', MdChip);
.module('material.components.chips')
.directive('mdChip', MdChip);

/**
* @ngdoc directive
Expand Down Expand Up @@ -33,36 +33,46 @@ var DELETE_HINT_TEMPLATE = '\
* @param $mdUtil
* @ngInject
*/
function MdChip($mdTheming, $mdUtil) {
var hintTemplate = $mdUtil.processTemplate(DELETE_HINT_TEMPLATE);
function MdChip($mdTheming, $mdUtil, $compile, $timeout) {
var deleteHintTemplate = $mdUtil.processTemplate(DELETE_HINT_TEMPLATE);

return {
restrict: 'E',
require: ['^?mdChips', 'mdChip'],
compile: compile,
link: postLink,
controller: 'MdChipCtrl'
};

function compile(element, attr) {
// Append the delete template
element.append($mdUtil.processTemplate(hintTemplate));
function postLink(scope, element, attr, ctrls) {
var chipsController = ctrls.shift();
var chipController = ctrls.shift();
var chipContentElement = angular.element(element[0].querySelector('.md-chip-content'));

return function postLink(scope, element, attr, ctrls) {
var chipsController = ctrls.shift();
var chipController = ctrls.shift();
$mdTheming(element);
$mdTheming(element);

if (chipsController) {
chipController.init(chipsController);
if (chipsController) {
chipController.init(chipsController);

angular
.element(element[0]
.querySelector('.md-chip-content'))
.on('blur', function () {
chipsController.resetSelectedChip();
chipsController.$scope.$applyAsync();
});
// Append our delete hint to the div.md-chip-content (which does not exist at compile time)
chipContentElement.append($compile(deleteHintTemplate)(scope));

// When a chip is blurred, make sure to unset (or reset) the selected chip so that tabbing
// through elements works properly
chipContentElement.on('blur', function() {
chipsController.resetSelectedChip();
chipsController.$scope.$applyAsync();
});
}

// Use $timeout to ensure we run AFTER the element has been added to the DOM so we can focus it.
$timeout(function() {
if (!chipsController) {
return;
}

if (chipsController.shouldFocusLastChip) {
chipsController.focusLastChipThenInput();
}
};
});
}
}
Loading

0 comments on commit f18cb2b

Please sign in to comment.