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

Commit f18cb2b

Browse files
topherfangiokara
authored andcommitted
fix(chips): Add basic accessibility support. (#9650)
Fixes #9391. Fixes #9556. Fixes #8897. Fixes #8867. Fixes #9649.
1 parent 16c2512 commit f18cb2b

File tree

11 files changed

+460
-75
lines changed

11 files changed

+460
-75
lines changed

docs/config/template/index.template.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<!doctype html>
2-
<html ng-app="docsApp" ng-controller="DocsCtrl" lang="en" ng-strict-di >
2+
<html ng-app="docsApp" ng-controller="DocsCtrl" lang="en" ng-strict-di>
33
<head>
44
<base href="/">
55
<title ng-bind="'Angular Material - ' + menu.currentSection.name +
@@ -12,7 +12,7 @@
1212
<link rel="stylesheet" href="angular-material.min.css">
1313
<link rel="stylesheet" href="docs.css">
1414
</head>
15-
<body class="docs-body" layout="row" ng-cloak>
15+
<body class="docs-body" layout="row" ng-cloak aria-label="Angular Material Docs">
1616

1717
<md-sidenav class="site-sidenav md-sidenav-left md-whiteframe-z2"
1818
md-component-id="left" hide-print

src/components/chips/chips.spec.js

Lines changed: 126 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ describe('<md-chips>', function() {
2020
'</md-chips>';
2121
var CHIP_NOT_REMOVABLE_TEMPLATE =
2222
'<md-chips ng-model="items" readonly="true" md-removable="false"></md-chips>';
23+
var CHIP_APPEND_DELAY_TEMPLATE =
24+
'<md-chips ng-model="items" md-chip-append-delay="800"></md-chips>';
2325

2426
afterEach(function() {
2527
attachedElements.forEach(function(element) {
@@ -170,7 +172,7 @@ describe('<md-chips>', function() {
170172

171173
expect(scope.addChip).toHaveBeenCalled();
172174
expect(scope.addChip.calls.mostRecent().args[0]).toBe('Grape'); // Chip
173-
expect(scope.addChip.calls.mostRecent().args[1]).toBe(4); // Index
175+
expect(scope.addChip.calls.mostRecent().args[1]).toBe(3); // Index
174176
});
175177

176178

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

427429
var updatedChips = getChipElements(element);
428430

429-
expect(chips.length).not.toBe(updatedChips.length);
431+
expect(updatedChips.length).toBe(chips.length - 1);
430432
}));
431433

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

826828
});
827829

830+
it('utilizes the default chip append delay of 300ms', inject(function($timeout) {
831+
var element = buildChips(BASIC_CHIP_TEMPLATE);
832+
var ctrl = element.controller('mdChips');
833+
834+
// Append element to body
835+
angular.element(document.body).append(element);
836+
837+
// Append a new chips which will fire the delay
838+
ctrl.appendChip('test');
839+
840+
// Before 300ms timeout, focus should be on the chip (i.e. the chip content)
841+
$timeout.flush(299);
842+
expect(document.activeElement).toHaveClass('md-chip-content');
843+
844+
// At/after 300ms timeout, focus should be on the input
845+
$timeout.flush(1);
846+
expect(document.activeElement.tagName.toUpperCase()).toEqual('INPUT');
847+
848+
// cleanup
849+
element.remove();
850+
}));
851+
852+
it('utilizes a custom chip append delay', inject(function($timeout) {
853+
var element = buildChips(CHIP_APPEND_DELAY_TEMPLATE);
854+
var ctrl = element.controller('mdChips');
855+
856+
// Append element to body
857+
angular.element(document.body).append(element);
858+
859+
// Append a new chips which will fire the delay
860+
ctrl.appendChip('test');
861+
862+
// Before custom timeout, focus should be on the chip (i.e. the chip content)
863+
$timeout.flush(ctrl.chipAppendDelay - 1);
864+
expect(document.activeElement).toHaveClass('md-chip-content');
865+
866+
// At/after custom timeout, focus should be on the input
867+
$timeout.flush(1);
868+
expect(document.activeElement.tagName.toUpperCase()).toEqual('INPUT');
869+
870+
// cleanup
871+
element.remove();
872+
}));
873+
828874
});
829875

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

14051451
});
14061452
});
1453+
1454+
describe('keyboard navigation', function() {
1455+
var leftEvent, rightEvent;
1456+
1457+
beforeEach(inject(function($mdConstant) {
1458+
leftEvent = {
1459+
type: 'keydown',
1460+
keyCode: $mdConstant.KEY_CODE.LEFT_ARROW,
1461+
which: $mdConstant.KEY_CODE.LEFT_ARROW
1462+
};
1463+
rightEvent = {
1464+
type: 'keydown',
1465+
keyCode: $mdConstant.KEY_CODE.RIGHT_ARROW,
1466+
which: $mdConstant.KEY_CODE.RIGHT_ARROW
1467+
};
1468+
}));
1469+
1470+
describe('when readonly', function() {
1471+
// TODO: Add readonly specific tests
1472+
});
1473+
1474+
describe('when we have an input', function() {
1475+
it('clears the selected chip when the input is focused', inject(function($timeout) {
1476+
var element = buildChips(BASIC_CHIP_TEMPLATE);
1477+
var ctrl = element.controller('mdChips');
1478+
1479+
// Focus the input
1480+
ctrl.focusInput();
1481+
$timeout.flush();
1482+
1483+
// Expect no chip to be selected
1484+
expect(ctrl.selectedChip).toBe(-1);
1485+
}));
1486+
1487+
it('selects the previous chip', inject(function($timeout) {
1488+
var element = buildChips(BASIC_CHIP_TEMPLATE);
1489+
var ctrl = element.controller('mdChips');
1490+
var chips = getChipElements(element);
1491+
1492+
// Select the second chip
1493+
ctrl.selectAndFocusChipSafe(1);
1494+
$timeout.flush();
1495+
1496+
expect(ctrl.selectedChip).toBe(1);
1497+
1498+
// Select the 1st chip
1499+
element.find('md-chips-wrap').triggerHandler(angular.copy(leftEvent));
1500+
$timeout.flush();
1501+
1502+
expect(ctrl.selectedChip).toBe(0);
1503+
}));
1504+
1505+
it('and the first chip is selected, selects the input', inject(function($timeout) {
1506+
var element = buildChips(BASIC_CHIP_TEMPLATE);
1507+
var ctrl = element.controller('mdChips');
1508+
var chips = getChipElements(element);
1509+
1510+
// Append so we can focus the input
1511+
angular.element(document.body).append(element);
1512+
1513+
// Select the second chip
1514+
ctrl.selectAndFocusChipSafe(0);
1515+
$timeout.flush();
1516+
1517+
expect(ctrl.selectedChip).toBe(0);
1518+
1519+
// Selecting past the first should wrap back to the input
1520+
element.find('md-chips-wrap').triggerHandler(angular.copy(leftEvent));
1521+
$timeout.flush();
1522+
1523+
expect(ctrl.selectedChip).toBe(-1);
1524+
expect(document.activeElement).toBe(element.find('input')[0]);
1525+
1526+
// Cleanup after ourselves
1527+
element.remove();
1528+
}));
1529+
});
1530+
});
14071531
});
14081532

14091533
describe('with $interpolate.start/endSymbol override', function() {

src/components/chips/contact-chips.spec.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ describe('<md-contact-chips>', function() {
99
md-contact-email="email"\
1010
md-highlight-flags="i"\
1111
md-min-length="1"\
12+
md-chip-append-delay="2000"\
1213
placeholder="To">\
1314
</md-contact-chips>';
1415

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

68+
it('forwards the md-chips-append-delay attribute to the md-chips', function() {
69+
var element = buildChips(CONTACT_CHIPS_TEMPLATE);
70+
var chipsCtrl = element.find('md-chips').controller('mdChips');
71+
72+
expect(chipsCtrl.chipAppendDelay).toEqual(2000);
73+
});
74+
6775
it('renders an image element for contacts with an image property', function() {
6876
scope.contacts.push(scope.allContacts[2]);
6977

src/components/chips/demoBasicUsage/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ <h2 class="md-title">Use a custom chip template.</h2>
55

66
<form name="fruitForm">
77
<md-chips ng-model="ctrl.roFruitNames" name="fruitName" readonly="ctrl.readonly"
8-
md-removable="ctrl.removable" md-max-chips="5">
8+
md-removable="ctrl.removable" md-max-chips="5" placeholder="Enter a fruit...">
99
<md-chip-template>
1010
<strong>{{$chip}}</strong>
1111
<em>(fruit)</em>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<p>
2+
<b>Note:</b> Version 1.1.2 drastically improves keyboard and screen reader accessibility for the
3+
<code>md-chips</code> component. In order to achieve this, the behavior has changed to also select
4+
and highlight the newly appended chip for <code>300ms</code> before re-focusing the text input.
5+
</p>
6+
7+
<p>
8+
Please see the <a href="api/directive/mdChips">documentation</a> for more information and for
9+
the new <code>md-chip-append-delay</code> option which allows you to customize this delay.
10+
</p>

src/components/chips/demoContactChips/script.js

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,22 @@
11
(function () {
22
'use strict';
3+
4+
// If we do not have CryptoJS defined; import it
5+
if (typeof CryptoJS == 'undefined') {
6+
var cryptoSrc = '//cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.2/rollups/md5.js';
7+
var scriptTag = document.createElement('script');
8+
scriptTag.setAttribute('src', cryptoSrc);
9+
document.body.appendChild(scriptTag);
10+
}
11+
312
angular
413
.module('contactChipsDemo', ['ngMaterial'])
514
.controller('ContactChipDemoCtrl', DemoCtrl);
615

716
function DemoCtrl ($q, $timeout) {
817
var self = this;
918
var pendingSearch, cancelSearch = angular.noop;
10-
var cachedQuery, lastSearch;
19+
var lastSearch;
1120

1221
self.allContacts = loadContacts();
1322
self.contacts = [self.allContacts[0]];
@@ -21,16 +30,14 @@
2130
* Search for contacts; use a random delay to simulate a remote call
2231
*/
2332
function querySearch (criteria) {
24-
cachedQuery = cachedQuery || criteria;
25-
return cachedQuery ? self.allContacts.filter(createFilterFor(cachedQuery)) : [];
33+
return criteria ? self.allContacts.filter(createFilterFor(criteria)) : [];
2634
}
2735

2836
/**
2937
* Async search for contacts
3038
* Also debounce the queries; since the md-contact-chips does not support this
3139
*/
3240
function delayedQuerySearch(criteria) {
33-
cachedQuery = criteria;
3441
if ( !pendingSearch || !debounceSearch() ) {
3542
cancelSearch();
3643

@@ -39,7 +46,7 @@
3946
cancelSearch = reject;
4047
$timeout(function() {
4148

42-
resolve( self.querySearch() );
49+
resolve( self.querySearch(criteria) );
4350

4451
refreshDebounce();
4552
}, Math.random() * 500, true)
@@ -72,7 +79,7 @@
7279
var lowercaseQuery = angular.lowercase(query);
7380

7481
return function filterFn(contact) {
75-
return (contact._lowername.indexOf(lowercaseQuery) != -1);;
82+
return (contact._lowername.indexOf(lowercaseQuery) != -1);
7683
};
7784

7885
}
@@ -92,10 +99,13 @@
9299

93100
return contacts.map(function (c, index) {
94101
var cParts = c.split(' ');
102+
var email = cParts[0][0].toLowerCase() + '.' + cParts[1].toLowerCase() + '@example.com';
103+
var hash = CryptoJS.MD5(email);
104+
95105
var contact = {
96106
name: c,
97-
email: cParts[0][0].toLowerCase() + '.' + cParts[1].toLowerCase() + '@example.com',
98-
image: 'http://lorempixel.com/50/50/people?' + index
107+
email: email,
108+
image: '//www.gravatar.com/avatar/' + hash + '?s=50&d=retro'
99109
};
100110
contact._lowername = contact.name.toLowerCase();
101111
return contact;

src/components/chips/js/chipDirective.js

Lines changed: 32 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
angular
2-
.module('material.components.chips')
3-
.directive('mdChip', MdChip);
2+
.module('material.components.chips')
3+
.directive('mdChip', MdChip);
44

55
/**
66
* @ngdoc directive
@@ -33,36 +33,46 @@ var DELETE_HINT_TEMPLATE = '\
3333
* @param $mdUtil
3434
* @ngInject
3535
*/
36-
function MdChip($mdTheming, $mdUtil) {
37-
var hintTemplate = $mdUtil.processTemplate(DELETE_HINT_TEMPLATE);
36+
function MdChip($mdTheming, $mdUtil, $compile, $timeout) {
37+
var deleteHintTemplate = $mdUtil.processTemplate(DELETE_HINT_TEMPLATE);
3838

3939
return {
4040
restrict: 'E',
4141
require: ['^?mdChips', 'mdChip'],
42-
compile: compile,
42+
link: postLink,
4343
controller: 'MdChipCtrl'
4444
};
4545

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

50-
return function postLink(scope, element, attr, ctrls) {
51-
var chipsController = ctrls.shift();
52-
var chipController = ctrls.shift();
53-
$mdTheming(element);
51+
$mdTheming(element);
5452

55-
if (chipsController) {
56-
chipController.init(chipsController);
53+
if (chipsController) {
54+
chipController.init(chipsController);
5755

58-
angular
59-
.element(element[0]
60-
.querySelector('.md-chip-content'))
61-
.on('blur', function () {
62-
chipsController.resetSelectedChip();
63-
chipsController.$scope.$applyAsync();
64-
});
56+
// Append our delete hint to the div.md-chip-content (which does not exist at compile time)
57+
chipContentElement.append($compile(deleteHintTemplate)(scope));
58+
59+
// When a chip is blurred, make sure to unset (or reset) the selected chip so that tabbing
60+
// through elements works properly
61+
chipContentElement.on('blur', function() {
62+
chipsController.resetSelectedChip();
63+
chipsController.$scope.$applyAsync();
64+
});
65+
}
66+
67+
// Use $timeout to ensure we run AFTER the element has been added to the DOM so we can focus it.
68+
$timeout(function() {
69+
if (!chipsController) {
70+
return;
71+
}
72+
73+
if (chipsController.shouldFocusLastChip) {
74+
chipsController.focusLastChipThenInput();
6575
}
66-
};
76+
});
6777
}
6878
}

0 commit comments

Comments
 (0)