From 11f064ab58dd4fe43fbcc171f28a7a09658377d7 Mon Sep 17 00:00:00 2001 From: Michael Jordan Date: Tue, 11 Oct 2022 17:18:19 -0400 Subject: [PATCH] fix(#249): [Masonry][Accessibility] Files: Main Navigation (Card View) - Selected state of the folder is not announced to the screen reader --- coral-component-masonry/examples/index.html | 7 +-- .../src/scripts/Masonry.js | 15 +++++-- .../src/scripts/MasonryItem.js | 17 +++---- .../src/tests/test.Masonry.js | 44 ++++++++++++++----- coral-gulp/configs/karma.conf.js | 5 +++ 5 files changed, 62 insertions(+), 26 deletions(-) diff --git a/coral-component-masonry/examples/index.html b/coral-component-masonry/examples/index.html index c1b9f69c73..fcc2262461 100644 --- a/coral-component-masonry/examples/index.html +++ b/coral-component-masonry/examples/index.html @@ -145,7 +145,7 @@

Usage notes

With image - 600 × 300 + 600 × 300
@@ -234,7 +234,6 @@

Usage notes

item.id = getUID(); } article.id = article.id || item.id + '-content'; - item.setAttribute('aria-labelledby', article.id); }); const ariaGridSelector = document.getElementById('ariaGrid'); @@ -301,11 +300,10 @@

Usage notes

const contentId = id + '-content'; const width = randomSize(); const height = randomSize(); - const url = 'http://via.placeholder.com/' + width + 'x' + height; + const url = 'https://via.placeholder.com/' + width + 'x' + height; const item = document.createElement('coral-masonry-item'); item.id = id; - item.setAttribute('aria-labelledby', contentId); item.classList.add('coral-Well'); item.content.innerHTML = '' + width + ' × ' + height + ''; @@ -320,7 +318,6 @@

Usage notes

const item = document.createElement('coral-masonry-item'); item.id = id; - item.setAttribute('aria-labelledby', contentId); item.classList.add('coral-Well'); item.content.innerHTML = ''; return item; diff --git a/coral-component-masonry/src/scripts/Masonry.js b/coral-component-masonry/src/scripts/Masonry.js index 5f2f1dc51f..def1a92eb7 100644 --- a/coral-component-masonry/src/scripts/Masonry.js +++ b/coral-component-masonry/src/scripts/Masonry.js @@ -301,7 +301,7 @@ const Masonry = Decorator(class extends BaseComponent(HTMLElement) { // @a11y only persist the checked state on macOS, // where VoiceOver does not announce the selected state for a gridcell. - accessibilityState.hidden = true; + accessibilityState.hidden = !isMacLike || !self.selected; if (!isMacLike || !self.selected) { accessibilityState.innerHTML = ''; } @@ -598,9 +598,15 @@ const Masonry = Decorator(class extends BaseComponent(HTMLElement) { if (activateAriaGrid === ariaGrid.ON) { item.setAttribute('role', 'gridcell'); item.setAttribute('aria-colindex', columnIndex); + + // communicate aria-selected state of all cells + if (this.selectionMode !== selectionMode.NONE || this.parentElement.hasAttribute('aria-multiselectable')) { + item.setAttribute('aria-selected', item.selected); + } } else { item.removeAttribute('role'); item.removeAttribute('aria-colindex'); + item.removeAttribute('aria-selected'); } } @@ -621,10 +627,13 @@ const Masonry = Decorator(class extends BaseComponent(HTMLElement) { const selectedItems = this.selectedItems; if (this.selectionMode === selectionMode.NONE) { - selectedItems.forEach((selectedItem) => { + this.items.getAll().forEach((item) => { // Don't trigger change events this._preventTriggeringEvents = true; - selectedItem.removeAttribute('selected'); + if (item.selected) { + item.removeAttribute('selected'); + } + item.removeAttribute('aria-selected'); }); } else if (this.selectionMode === selectionMode.SINGLE) { // Last selected item wins if multiple selection while not allowed diff --git a/coral-component-masonry/src/scripts/MasonryItem.js b/coral-component-masonry/src/scripts/MasonryItem.js index 56e27e19a5..fc63794a0b 100644 --- a/coral-component-masonry/src/scripts/MasonryItem.js +++ b/coral-component-masonry/src/scripts/MasonryItem.js @@ -246,18 +246,18 @@ const MasonryItem = Decorator(class extends BaseComponent(HTMLElement) { if (!accessibilityState.parentNode) { this.appendChild(accessibilityState); } + + // @a11y Item should be labelled by accessibility state. + if (isMacLike) { + const ariaLabelledby = this.getAttribute('aria-labelledby'); + if (ariaLabelledby) { + this.setAttribute('aria-labelledby', ariaLabelledby + ' ' + accessibilityState.id); + } + } }); this._elements.accessibilityState = accessibilityState; - // @a11y Item should be labelled by accessibility state. - if (isMacLike) { - const ariaLabelledby = this.getAttribute('aria-labelledby'); - if (ariaLabelledby) { - this.setAttribute('aria-labelledby', ariaLabelledby + ' ' + accessibilityState.id); - } - } - // Support cloneNode const template = this.querySelector('._coral-Masonry-item-quickActions'); if (template) { @@ -266,6 +266,7 @@ const MasonryItem = Decorator(class extends BaseComponent(HTMLElement) { this.insertBefore(this._elements.quickactions, this.firstChild); // todo workaround to not give user possibility to tab into checkbox this._elements.check._labellableElement.tabIndex = -1; + this._elements.check.setAttribute('aria-hidden', 'true'); } /** @ignore */ diff --git a/coral-component-masonry/src/tests/test.Masonry.js b/coral-component-masonry/src/tests/test.Masonry.js index 0d67493038..a9721c91e8 100644 --- a/coral-component-masonry/src/tests/test.Masonry.js +++ b/coral-component-masonry/src/tests/test.Masonry.js @@ -513,16 +513,16 @@ describe('Masonry', function () { item.selected = true; setTimeout(function() { - expect(a11yState.textContent).to.equal(i18n.get('checked')); + expect(a11yState.textContent).to.equal(i18n.get('checked'), 'after ~275ms accessibilityState should read "checked"'); expect(a11yState.hidden).to.be.false; expect(a11yState.hasAttribute('aria-live')).to.be.false; setTimeout(function() { - expect(a11yState.textContent).to.equal(isMacLike ? i18n.get('checked') : ''); - expect(a11yState.hidden).to.be.true; + expect(a11yState.textContent).to.equal(isMacLike ? i18n.get('checked') : '', 'after 1600ms accessibilityState should read "" or "checked" on macOS'); + expect(a11yState.hidden).to.equal(!isMacLike, 'on macOS, the "checked" accessibilityState should not be hidden'); expect(a11yState.getAttribute('aria-live')).equal('off'); done(); - }, 1650); - }, 220); + }, 1600); + }, 275); }); }); @@ -541,16 +541,16 @@ describe('Masonry', function () { item.selected = true; item.selected = false; setTimeout(function() { - expect(a11yState.textContent).to.equal(i18n.get('not checked')); + expect(a11yState.textContent).to.equal(i18n.get('not checked'), 'after ~275ms accessibilityState should read "not checked"'); expect(a11yState.hidden).to.be.false; expect(a11yState.hasAttribute('aria-live')).to.be.false; setTimeout(function() { - expect(a11yState.textContent).to.equal(''); + expect(a11yState.textContent).to.equal('', 'after 1600ms accessibilityState should read ""'); expect(a11yState.hidden).to.be.true; expect(a11yState.getAttribute('aria-live')).to.equal('off'); done(); - }, 1650); - }, 210); + }, 1600); + }, 275); }); }); }); @@ -571,6 +571,8 @@ describe('Masonry', function () { .to.equal('gridcell', ' should have role="gridcell"'); expect(el.items.last().getAttribute('aria-colindex')) .to.equal('3', 'last should have aria-colindex="3"'); + expect(el.items.first().hasAttribute('aria-selected')) + .to.equal(false, ' should not have aria-selected when selectionMode="none"'); // Disable aria grid dynamically el.ariaGrid = "off"; @@ -595,9 +597,31 @@ describe('Masonry', function () { expect(el.parentElement.getAttribute('aria-label')).to.equal('Masonry Label', 'Masonry parent element should receive same aria-label as Masonry'); expect(el.parentElement.getAttribute('aria-labelledby')).to.equal('Masonry Labelledby', 'Masonry parent element should receive same aria-labelledby as Masonry'); }); + it('masonry elements should have aria-selected when selectionMode is not "none"', function() { + const el = helpers.build(window.__html__['Masonry.items.selected.html']); + + el.ariaGrid = "on"; + + expect(el.items.first().getAttribute('aria-selected')) + .to.equal('true', 'selected should have aria-selected="true" when selectionMode="single"'); + expect(el.items.last().getAttribute('aria-selected')) + .to.equal('false', 'not selected should have aria-selected="false" when selectionMode="single"'); + + el.selectionMode = 'multiple'; + expect(el.items.first().getAttribute('aria-selected')) + .to.equal('true', 'selected should have aria-selected="true" when selectionMode="multiple"'); + expect(el.items.last().getAttribute('aria-selected')) + .to.equal('false', 'not selected should have aria-selected="false" when selectionMode="multiple"'); + + el.selectionMode = 'none'; + expect(el.items.first().hasAttribute('aria-selected')) + .to.equal(false, 'selected should not have aria-selected when selectionMode="none"'); + expect(el.items.last().hasAttribute('aria-selected')) + .to.equal(false, 'not selected should note have aria-selected="false" when selectionMode="none"'); + }) }); - describe('Attach/Detch', function () { + describe('Attach/Detach', function () { it('changing masonry parent should keep child intact', function (done) { const el = helpers.build(window.__html__['Masonry.with.div.wrapper.html']); const masonry = el.querySelector("coral-masonry"); diff --git a/coral-gulp/configs/karma.conf.js b/coral-gulp/configs/karma.conf.js index 97815299fd..3f4bcf8bbc 100644 --- a/coral-gulp/configs/karma.conf.js +++ b/coral-gulp/configs/karma.conf.js @@ -159,6 +159,11 @@ module.exports = function (config) { client: { // Set to true for debugging via e.g console.debug captureConsole: false + /* , + // Override the timeout, should tests fail due to timeout errors. + mocha: { + timeout: 2500 + } */ } }); };