Skip to content

Commit

Permalink
fix(#249): [Masonry][Accessibility] Files: Main Navigation (Card View…
Browse files Browse the repository at this point in the history
…) - Selected state of the folder is not announced to the screen reader (#250)

* fix(#244): [Masonry][Accessibility] add aria-multiselectable to parent element with role="grid"

* fix(#249): [Masonry][Accessibility] Files: Main Navigation (Card View) - Selected state of the folder is not announced to the screen reader
  • Loading branch information
majornista committed Nov 2, 2022
1 parent bc53116 commit ff33df1
Show file tree
Hide file tree
Showing 5 changed files with 88 additions and 30 deletions.
7 changes: 2 additions & 5 deletions coral-component-masonry/examples/index.html
Expand Up @@ -145,7 +145,7 @@ <h2 class="coral-Heading--M">Usage notes</h2>
<article>
<span coral-masonry-draghandle></span>
With image
<img src="http://via.placeholder.com/600x300" alt="600 × 300" />
<img src="https://via.placeholder.com/600x300" alt="600 × 300" />
</article>
</coral-masonry-item>
<coral-masonry-item class="coral-Well">
Expand Down Expand Up @@ -234,7 +234,6 @@ <h2 class="coral-Heading--M">Usage notes</h2>
item.id = getUID();
}
article.id = article.id || item.id + '-content';
item.setAttribute('aria-labelledby', article.id);
});

const ariaGridSelector = document.getElementById('ariaGrid');
Expand Down Expand Up @@ -301,11 +300,10 @@ <h2 class="coral-Heading--M">Usage notes</h2>
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 = '<img id="' + contentId + '" alt="' + width + ' × ' + height + '" src="' + url + '" style="width: ' + (width / 2) + 'px; height: ' + (height / 2) + 'px;">';

Expand All @@ -320,7 +318,6 @@ <h2 class="coral-Heading--M">Usage notes</h2>

const item = document.createElement('coral-masonry-item');
item.id = id;
item.setAttribute('aria-labelledby', contentId);
item.classList.add('coral-Well');
item.content.innerHTML = '<article role="presentation" id="' + contentId + '">' + html + '</article>';
return item;
Expand Down
34 changes: 29 additions & 5 deletions coral-component-masonry/src/scripts/Masonry.js
Expand Up @@ -226,12 +226,18 @@ const Masonry = Decorator(class extends BaseComponent(HTMLElement) {
this._selectionMode = validate.enumeration(selectionMode)(value) && value || selectionMode.NONE;
this._reflectAttribute('selectionmode', this._selectionMode);

const isGrid = this.ariaGrid === ariaGrid.ON && this.parentElement;

if (this._selectionMode === selectionMode.NONE) {
this.classList.remove('is-selectable');
this.removeAttribute('aria-multiselectable');
if (isGrid) {
this.parentElement.removeAttribute('aria-multiselectable');
}
} else {
this.classList.add('is-selectable');
this.setAttribute('aria-multiselectable', this._selectionMode === selectionMode.MULTIPLE);
if (isGrid) {
this.parentElement.setAttribute('aria-multiselectable', this._selectionMode === selectionMode.MULTIPLE);
}
}

this._validateSelection();
Expand Down Expand Up @@ -295,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 = '';
}
Expand Down Expand Up @@ -543,6 +549,12 @@ const Masonry = Decorator(class extends BaseComponent(HTMLElement) {
this._preservedParentAriaLabelledby = this.parentElement.getAttribute('aria-labelledby');
this.parentElement.setAttribute('aria-labelledby', this.ariaLabelledby);
}

if (this._selectionMode === selectionMode.NONE) {
this.parentElement.removeAttribute('aria-multiselectable');
} else {
this.parentElement.setAttribute('aria-multiselectable', this._selectionMode === selectionMode.MULTIPLE);
}
} else {
// Restore/remove role of the parent element
if (this._preservedParentAriaRole) {
Expand All @@ -567,6 +579,9 @@ const Masonry = Decorator(class extends BaseComponent(HTMLElement) {

// Remove aria-colcount
this.parentElement.removeAttribute('aria-colcount');

// Remove aria-multiselectable
this.parentElement.removeAttribute('aria-multiselectable');
}
}

Expand All @@ -583,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');
}
}

Expand All @@ -606,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
Expand Down
17 changes: 9 additions & 8 deletions coral-component-masonry/src/scripts/MasonryItem.js
Expand Up @@ -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) {
Expand All @@ -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 */
Expand Down
55 changes: 43 additions & 12 deletions coral-component-masonry/src/tests/test.Masonry.js
Expand Up @@ -484,11 +484,18 @@ describe('Masonry', function () {
it('should have an aria attribute for single/multiple selection', function () {
const el = helpers.build(new Masonry());

// aria-multiselectable should only apply when ariaGrid="on".
el.ariaGrid = 'on';
expect(el.parentElement.hasAttribute('aria-multiselectable')).to.be.false;

el.selectionMode = 'single';
expect(el.getAttribute('aria-multiselectable')).to.equal('false');
expect(el.parentElement.getAttribute('aria-multiselectable')).to.equal('false');

el.selectionMode = 'multiple';
expect(el.getAttribute('aria-multiselectable')).to.equal('true');
expect(el.parentElement.getAttribute('aria-multiselectable')).to.equal('true');

el.selectionMode = 'none';
expect(el.parentElement.hasAttribute('aria-multiselectable')).to.be.false;
});

it('should announce "checked" when item becomes selected', function(done) {
Expand All @@ -506,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);
});
});

Expand All @@ -534,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);
});
});
});
Expand All @@ -564,6 +571,8 @@ describe('Masonry', function () {
.to.equal('gridcell', '<coral-masonry-item> should have role="gridcell"');
expect(el.items.last().getAttribute('aria-colindex'))
.to.equal('3', 'last <coral-masonry-item> should have aria-colindex="3"');
expect(el.items.first().hasAttribute('aria-selected'))
.to.equal(false, '<coral-masonry-item> should not have aria-selected when selectionMode="none"');

// Disable aria grid dynamically
el.ariaGrid = "off";
Expand All @@ -588,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 <coral-masonry-item> should have aria-selected="true" when selectionMode="single"');
expect(el.items.last().getAttribute('aria-selected'))
.to.equal('false', 'not selected <coral-masonry-item> should have aria-selected="false" when selectionMode="single"');

el.selectionMode = 'multiple';
expect(el.items.first().getAttribute('aria-selected'))
.to.equal('true', 'selected <coral-masonry-item> should have aria-selected="true" when selectionMode="multiple"');
expect(el.items.last().getAttribute('aria-selected'))
.to.equal('false', 'not selected <coral-masonry-item> should have aria-selected="false" when selectionMode="multiple"');

el.selectionMode = 'none';
expect(el.items.first().hasAttribute('aria-selected'))
.to.equal(false, 'selected <coral-masonry-item> should not have aria-selected when selectionMode="none"');
expect(el.items.last().hasAttribute('aria-selected'))
.to.equal(false, 'not selected <coral-masonry-item> 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");
Expand Down
5 changes: 5 additions & 0 deletions coral-gulp/configs/karma.conf.js
Expand Up @@ -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
} */
}
});
};

0 comments on commit ff33df1

Please sign in to comment.