Skip to content

Commit 51bc865

Browse files
MAGETWO-55364: [WCAG 2.0 AA] Add Aria-Labels for Color Swatches
1 parent 0a03ae5 commit 51bc865

File tree

3 files changed

+108
-27
lines changed

3 files changed

+108
-27
lines changed

app/code/Magento/Swatches/view/frontend/templates/product/layered/renderer.phtml

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,19 @@
1010
?>
1111
<?php $swatchData = $block->getSwatchData(); ?>
1212
<div class="swatch-attribute swatch-layered <?php /* @escapeNotVerified */ echo $swatchData['attribute_code'] ?>"
13-
attribute-code="<?php /* @escapeNotVerified */ echo $swatchData['attribute_code'] ?>" attribute-id="<?php /* @escapeNotVerified */ echo $swatchData['attribute_id'] ?>">
13+
attribute-code="<?php /* @escapeNotVerified */ echo $swatchData['attribute_code'] ?>"
14+
attribute-id="<?php /* @escapeNotVerified */ echo $swatchData['attribute_id'] ?>">
1415
<div class="swatch-attribute-options clearfix">
1516
<?php foreach ($swatchData['options'] as $option => $label): ?>
16-
<a href="<?php /* @escapeNotVerified */ echo $label['link'] ?>" class="swatch-option-link-layered">
17+
<a href="<?php /* @escapeNotVerified */ echo $label['link'] ?>"
18+
aria-label="<?php /* @escapeNotVerified */ echo $label['label'] ?>"
19+
class="swatch-option-link-layered">
1720
<?php if (isset($swatchData['swatches'][$option]['type'])) { ?>
1821
<?php switch ($swatchData['swatches'][$option]['type']) {
1922
case '3':
2023
?>
2124
<div class="swatch-option <?php /* @escapeNotVerified */ echo $label['custom_style'] ?>"
25+
tabindex="-1"
2226
option-type="3"
2327
option-id="<?php /* @escapeNotVerified */ echo $option ?>"
2428
option-label="<?php /* @escapeNotVerified */ echo $label['label'] ?>"
@@ -33,6 +37,7 @@
3337
<?php $swatchImagePath = $block->getSwatchPath('swatch_image',
3438
$swatchData['swatches'][$option]['value']); ?>
3539
<div class="swatch-option image <?php /* @escapeNotVerified */ echo $label['custom_style'] ?>"
40+
tabindex="-1"
3641
option-type="2"
3742
option-id="<?php /* @escapeNotVerified */ echo $option ?>"
3843
option-label="<?php /* @escapeNotVerified */ echo $label['label'] ?>"
@@ -43,6 +48,7 @@
4348
case '1':
4449
?>
4550
<div class="swatch-option color <?php /* @escapeNotVerified */ echo $label['custom_style'] ?>"
51+
tabindex="-1"
4652
option-type="1"
4753
option-id="<?php /* @escapeNotVerified */ echo $option ?>"
4854
option-label="<?php /* @escapeNotVerified */ echo $label['label'] ?>"
@@ -54,6 +60,7 @@
5460
default:
5561
?>
5662
<div class="swatch-option text <?php /* @escapeNotVerified */ echo $label['custom_style'] ?>"
63+
tabindex="-1"
5764
option-type="0"
5865
option-id="<?php /* @escapeNotVerified */ echo $option ?>"
5966
option-label="<?php /* @escapeNotVerified */ echo $label['label'] ?>"

app/code/Magento/Swatches/view/frontend/web/css/swatches.css

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,10 @@
210210
padding: 0 !important;
211211
}
212212

213+
.swatch-option-link-layered:focus > div {
214+
box-shadow: 0 0 3px 1px #68a8e0;
215+
}
216+
213217
.swatch-option-tooltip-layered {
214218
width: 140px;
215219
position: absolute;
@@ -276,3 +280,9 @@
276280
.swatch-option-loading {
277281
content: url("../images/loader-2.gif");
278282
}
283+
284+
.swatch-input {
285+
left: -1000px;
286+
position: absolute;
287+
visibility: hidden;
288+
}

app/code/Magento/Swatches/view/frontend/web/js/swatch-renderer.js

Lines changed: 89 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ define([
117117
$element.hide();
118118
clearTimeout(timer);
119119
});
120+
120121
$(document).on('tap', function () {
121122
$element.hide();
122123
clearTimeout(timer);
@@ -169,6 +170,9 @@ define([
169170
//selector of product images gallery wrapper
170171
mediaGallerySelector: '[data-gallery-role=gallery-placeholder]',
171172

173+
// selector of category product tile wrapper
174+
selectorProductTile: '.product-item',
175+
172176
// number of controls to show (false or zero = show all)
173177
numberToShow: false,
174178

@@ -178,6 +182,9 @@ define([
178182
// enable label for control
179183
enableControlLabel: true,
180184

185+
// control label id
186+
controlLabelId: '',
187+
181188
// text for more button
182189
moreButtonText: 'More',
183190

@@ -191,7 +198,10 @@ define([
191198
mediaGalleryInitial: [{}],
192199

193200
//
194-
onlyMainImg: false
201+
onlyMainImg: false,
202+
203+
// whether swatches are rendered in product list or on product page
204+
inProductList: false
195205
},
196206

197207
/**
@@ -246,7 +256,9 @@ define([
246256
'img': $main.find('.product-image-photo').attr('src')
247257
}];
248258
}
249-
this.productForm = this.element.parents(this.options.selectorProduct).find('form:first');
259+
260+
this.productForm = this.element.parents(this.options.selectorProductTile).find('form:first');
261+
this.inProductList = this.productForm.length > 0;
250262
},
251263

252264
/**
@@ -264,9 +276,11 @@ define([
264276

265277
$.each(this.options.jsonConfig.attributes, function () {
266278
var item = this,
267-
options = $widget._RenderSwatchOptions(item),
279+
controlLabelId = 'option-label-' + item.code + '-' + item.id,
280+
options = $widget._RenderSwatchOptions(item, controlLabelId),
268281
select = $widget._RenderSwatchSelect(item, chooseText),
269282
input = $widget._RenderFormInput(item),
283+
listLabel ='',
270284
label = '';
271285

272286
// Show only swatch controls
@@ -276,22 +290,28 @@ define([
276290

277291
if ($widget.options.enableControlLabel) {
278292
label +=
279-
'<span class="' + classes.attributeLabelClass + '">' + item.label + '</span>' +
293+
'<span id="' + controlLabelId + '" class="' + classes.attributeLabelClass + '">' + item.label + '</span>' +
280294
'<span class="' + classes.attributeSelectedOptionLabelClass + '"></span>';
281295
}
282296

283-
if ($widget.productForm) {
297+
if ($widget.inProductList) {
284298
$widget.productForm.append(input);
285299
input = '';
300+
listLabel = 'aria-label="' + item.label + '"';
301+
} else {
302+
listLabel = 'aria-labelledby="' + controlLabelId + '"';
286303
}
287304

288305
// Create new control
289306
container.append(
290-
'<div class="' + classes.attributeClass + ' ' + item.code +
291-
'" attribute-code="' + item.code +
292-
'" attribute-id="' + item.id + '">' +
293-
label +
294-
'<div class="' + classes.attributeOptionsWrapper + ' clearfix">' +
307+
'<div class="' + classes.attributeClass + ' ' + item.code + '" ' +
308+
'attribute-code="' + item.code + '" ' +
309+
'attribute-id="' + item.id + '">' +
310+
label +
311+
'<div aria-activedescendant="" ' +
312+
'tabindex="0" ' +
313+
'role="listbox" ' + listLabel +
314+
'class="' + classes.attributeOptionsWrapper + ' clearfix">' +
295315
options + select +
296316
'</div>' + input +
297317
'</div>'
@@ -336,10 +356,11 @@ define([
336356
* Render swatch options by part of config
337357
*
338358
* @param {Object} config
359+
* @param {String} controlId
339360
* @returns {String}
340361
* @private
341362
*/
342-
_RenderSwatchOptions: function (config) {
363+
_RenderSwatchOptions: function (config, controlId) {
343364
var optionConfig = this.options.jsonSwatchConfig[config.id],
344365
optionClass = this.options.classes.optionClass,
345366
moreLimit = parseInt(this.options.numberToShow, 10),
@@ -375,11 +396,17 @@ define([
375396
thumb = optionConfig[id].hasOwnProperty('thumb') ? optionConfig[id].thumb : '';
376397
label = this.label ? this.label : '';
377398
attr =
399+
' id="' + controlId + '-item-' + id + '"' +
400+
' aria-checked="false"' +
401+
' aria-describedby="' + controlId + '"' +
402+
' tabindex="0"' +
378403
' option-type="' + type + '"' +
379404
' option-id="' + id + '"' +
380405
' option-label="' + label + '"' +
406+
' aria-label="' + label + '"' +
381407
' option-tooltip-thumb="' + thumb + '"' +
382-
' option-tooltip-value="' + value + '"';
408+
' option-tooltip-value="' + value + '"' +
409+
' role="option"';
383410

384411
if (!this.hasOwnProperty('products') || this.products.length <= 0) {
385412
attr += ' option-empty="true"';
@@ -392,19 +419,19 @@ define([
392419
} else if (type === 1) {
393420
// Color
394421
html += '<div class="' + optionClass + ' color" ' + attr +
395-
'" style="background: ' + value +
422+
' style="background: ' + value +
396423
' no-repeat center; background-size: initial;">' + '' +
397424
'</div>';
398425
} else if (type === 2) {
399426
// Image
400427
html += '<div class="' + optionClass + ' image" ' + attr +
401-
'" style="background: url(' + value + ') no-repeat center; background-size: initial;">' + '' +
428+
' style="background: url(' + value + ') no-repeat center; background-size: initial;">' + '' +
402429
'</div>';
403430
} else if (type === 3) {
404431
// Clear
405432
html += '<div class="' + optionClass + '" ' + attr + '></div>';
406433
} else {
407-
// Defaualt
434+
// Default
408435
html += '<div class="' + optionClass + '" ' + attr + '>' + label + '</div>';
409436
}
410437
});
@@ -460,10 +487,9 @@ define([
460487
'type="text" ' +
461488
'value="" ' +
462489
'data-selector="super_attribute[' + config.id + ']" ' +
463-
'data-validate="{required:true}" ' +
490+
'data-validate="{required: true}" ' +
464491
'aria-required="true" ' +
465-
'aria-invalid="true" ' +
466-
'style="visibility: hidden; position:absolute; left:-1000px">';
492+
'aria-invalid="false">';
467493
},
468494

469495
/**
@@ -472,22 +498,38 @@ define([
472498
* @private
473499
*/
474500
_EventListener: function () {
501+
var $widget = this,
502+
options = this.options.classes;
475503

476-
var $widget = this;
477-
478-
$widget.element.on('click', '.' + this.options.classes.optionClass, function () {
504+
$widget.element.on('click', '.' + options.optionClass, function () {
479505
return $widget._OnClick($(this), $widget);
480506
});
481507

482-
$widget.element.on('change', '.' + this.options.classes.selectClass, function () {
508+
$widget.element.on('change', '.' + options.selectClass, function () {
483509
return $widget._OnChange($(this), $widget);
484510
});
485511

486-
$widget.element.on('click', '.' + this.options.classes.moreButton, function (e) {
512+
$widget.element.on('click', '.' + options.moreButton, function (e) {
487513
e.preventDefault();
488514

489515
return $widget._OnMoreClick($(this));
490516
});
517+
518+
$widget.element.on('keydown', function (e) {
519+
if (e.which == 13) {
520+
var target = $(e.target);
521+
522+
if (target.is('.' + options.optionClass)) {
523+
return $widget._OnClick(target, $widget);
524+
} else if (target.is('.' + options.selectClass)) {
525+
return $widget._OnChange(target, $widget);
526+
} else if (target.is('.' + options.moreButton)) {
527+
e.preventDefault();
528+
529+
return $widget._OnMoreClick(target);
530+
}
531+
}
532+
});
491533
},
492534

493535
/**
@@ -498,13 +540,17 @@ define([
498540
* @private
499541
*/
500542
_OnClick: function ($this, $widget) {
501-
502543
var $parent = $this.parents('.' + $widget.options.classes.attributeClass),
544+
$wrapper = $this.parents('.' + $widget.options.classes.attributeOptionsWrapper),
503545
$label = $parent.find('.' + $widget.options.classes.attributeSelectedOptionLabelClass),
504546
attributeId = $parent.attr('attribute-id'),
547+
$input = $parent.find('.' + $widget.options.classes.attributeInput);
548+
549+
if ($widget.inProductList) {
505550
$input = $widget.productForm.find(
506551
'.' + $widget.options.classes.attributeInput + '[name="super_attribute[' + attributeId + ']"]'
507552
);
553+
}
508554

509555
if ($this.hasClass('disabled')) {
510556
return;
@@ -514,11 +560,13 @@ define([
514560
$parent.removeAttr('option-selected').find('.selected').removeClass('selected');
515561
$input.val('');
516562
$label.text('');
563+
$this.attr('aria-checked', false);
517564
} else {
518565
$parent.attr('option-selected', $this.attr('option-id')).find('.selected').removeClass('selected');
519566
$label.text($this.attr('option-label'));
520567
$input.val($this.attr('option-id'));
521568
$this.addClass('selected');
569+
$widget._toggleCheckedAttributes($this, $wrapper);
522570
}
523571

524572
$widget._Rebuild();
@@ -533,6 +581,19 @@ define([
533581
$input.trigger('change');
534582
},
535583

584+
/**
585+
* Toggle accessibility attributes
586+
*
587+
* @param {Object} $this
588+
* @param {Object} $wrapper
589+
* @private
590+
*/
591+
_toggleCheckedAttributes: function ($this, $wrapper) {
592+
$wrapper.attr('aria-activedescendant', $this.attr('id'))
593+
.find('.' + this.options.classes.optionClass).attr('aria-checked', false);
594+
$this.attr('aria-checked', true);
595+
},
596+
536597
/**
537598
* Event for select
538599
*
@@ -543,9 +604,13 @@ define([
543604
_OnChange: function ($this, $widget) {
544605
var $parent = $this.parents('.' + $widget.options.classes.attributeClass),
545606
attributeId = $parent.attr('attribute-id'),
607+
$input = $parent.find('.' + $widget.options.classes.attributeInput);
608+
609+
if ($widget.productForm.length > 0) {
546610
$input = $widget.productForm.find(
547611
'.' + $widget.options.classes.attributeInput + '[name="super_attribute[' + attributeId + ']"]'
548612
);
613+
}
549614

550615
if ($this.val() > 0) {
551616
$parent.attr('option-selected', $this.val());
@@ -687,7 +752,6 @@ define([
687752
'prices': $widget._getPrices(result, $productPrice.priceBox('option').prices)
688753
}
689754
);
690-
691755
},
692756

693757
/**

0 commit comments

Comments
 (0)