Skip to content

Commit

Permalink
feat: announce file upload events (#3389)
Browse files Browse the repository at this point in the history
  • Loading branch information
tomivirkki committed Feb 3, 2022
1 parent 262e47c commit 529cad5
Show file tree
Hide file tree
Showing 7 changed files with 165 additions and 5 deletions.
29 changes: 29 additions & 0 deletions dev/upload.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,28 @@

<script type="module">
import '@vaadin/upload';
import '@vaadin/radio-group';
import { xhrCreator } from '@vaadin/upload/test/common.js'

const nextUpload = document.querySelector('#next-upload');

const upload = document.querySelector('vaadin-upload');
// Use a fake xhr for testing
upload._createXhr = () => {
if (nextUpload.value === 'successful') {
return xhrCreator({ size: 512, uploadTime: 3000, stepTime: 1000 })();
} else if (nextUpload.value === 'rejected') {
return new XMLHttpRequest();
} else if (nextUpload.value === 'error') {
return xhrCreator({ serverValidation: () => {
return {
statusText: 'Error'
};
}})();
}
}


upload.files = [
{ name: 'Annual Report.docx', complete: true },
{
Expand All @@ -20,10 +40,19 @@
},
{ name: 'Financials.xlsx', error: 'An error occurred' }
];

</script>
</head>

<body>
<vaadin-upload target="/api/fileupload"></vaadin-upload>

<br>

<vaadin-radio-group label="Next file upload" theme="vertical" id="next-upload">
<vaadin-radio-button value="successful" checked label="Successful"></vaadin-radio-button>
<vaadin-radio-button value="rejected" label="Rejected"></vaadin-radio-button>
<vaadin-radio-button value="error" label="Server error"></vaadin-radio-button>
</vaadin-radio-group>
</body>
</html>
2 changes: 1 addition & 1 deletion packages/component-base/src/a11y-announcer.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@
/**
* Cause a text string to be announced by screen readers.
*/
export function announce(text: string, options?: { mode?: 'polite' | 'assertive'; timeout?: number }): void;
export function announce(text: string, options?: { mode?: 'polite' | 'assertive' | 'alert'; timeout?: number }): void;
18 changes: 17 additions & 1 deletion packages/component-base/src/a11y-announcer.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
*/

import { animationFrame } from '@vaadin/component-base/src/async.js';
import { Debouncer } from '@vaadin/component-base/src/debounce.js';

const region = document.createElement('div');

region.style.position = 'fixed';
Expand All @@ -12,6 +15,7 @@ region.setAttribute('aria-live', 'polite');

document.body.appendChild(region);

let alertDebouncer;
/**
* Cause a text string to be announced by screen readers.
*
Expand All @@ -22,7 +26,19 @@ export function announce(text, options = {}) {
const mode = options.mode || 'polite';
const timeout = options.timeout === undefined ? 150 : options.timeout;

region.setAttribute('aria-live', mode);
if (mode === 'alert') {
region.removeAttribute('aria-live');
region.removeAttribute('role');
alertDebouncer = Debouncer.debounce(alertDebouncer, animationFrame, () => {
region.setAttribute('role', 'alert');
});
} else {
if (alertDebouncer) {
alertDebouncer.cancel();
}
region.removeAttribute('role');
region.setAttribute('aria-live', mode);
}

region.textContent = '';

Expand Down
38 changes: 37 additions & 1 deletion packages/component-base/test/a11y-announcer.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { announce } from '../src/a11y-announcer.js';
describe('a11y announcer', () => {
let region;

beforeEach(() => {
before(() => {
region = document.querySelector('[aria-live]');
});

Expand Down Expand Up @@ -65,5 +65,41 @@ describe('a11y announcer', () => {
announce('Test', { mode: 'assertive' });
expect(region.getAttribute('aria-live')).to.equal('assertive');
});

it('should clear region aria-live attribute when mode is alert', () => {
announce('Test', { mode: 'alert' });
expect(region.hasAttribute('aria-live')).to.be.false;
});

it('should update region role attribute when mode is alert', async () => {
announce('Test', { mode: 'alert' });
clock.tick(100);
expect(region.getAttribute('role')).to.equal('alert');
});

it('should not update region role attribute synchronously when mode is alert', async () => {
announce('Test', { mode: 'alert' });
expect(region.hasAttribute('role')).to.be.false;
});

it('should restore region aria-live attribute', async () => {
announce('Test', { mode: 'alert' });
announce('Test', { mode: 'assertive' });
expect(region.getAttribute('aria-live')).to.equal('assertive');
});

it('should clear region role attribute', async () => {
announce('Test', { mode: 'alert' });
clock.tick(100);
announce('Test', { mode: 'assertive' });
expect(region.hasAttribute('role')).to.be.false;
});

it('should not set region role back to alert', async () => {
announce('Test', { mode: 'alert' });
announce('Test', { mode: 'assertive' });
clock.tick(100);
expect(region.hasAttribute('role')).to.be.false;
});
});
});
10 changes: 8 additions & 2 deletions packages/component-base/test/virtualizer-unlimited-size.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,10 @@ describe('unlimited size', () => {
it('should have a last visible index at start', () => {
const item = elementsContainer.querySelector(`#item-${virtualizer.lastVisibleIndex}`);
const itemRect = item.getBoundingClientRect();
expect(scrollTarget.getBoundingClientRect().bottom).to.be.within(itemRect.top, itemRect.bottom);
expect(scrollTarget.getBoundingClientRect().bottom).to.be.within(
Math.round(itemRect.top),
Math.round(itemRect.bottom)
);
});

it('should have a first visible index at end', () => {
Expand All @@ -194,6 +197,9 @@ describe('unlimited size', () => {

const item = elementsContainer.querySelector(`#item-${virtualizer.lastVisibleIndex}`);
const itemRect = item.getBoundingClientRect();
expect(scrollTarget.getBoundingClientRect().bottom).to.be.within(itemRect.top, itemRect.bottom);
expect(scrollTarget.getBoundingClientRect().bottom).to.be.within(
Math.round(itemRect.top),
Math.round(itemRect.bottom)
);
});
});
25 changes: 25 additions & 0 deletions packages/upload/src/vaadin-upload.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import '@vaadin/button/src/vaadin-button.js';
import './vaadin-upload-icons.js';
import './vaadin-upload-file.js';
import { html, PolymerElement } from '@polymer/polymer/polymer-element.js';
import { announce } from '@vaadin/component-base/src/a11y-announcer.js';
import { isTouch } from '@vaadin/component-base/src/browser-utils.js';
import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
Expand Down Expand Up @@ -443,6 +444,10 @@ class Upload extends ElementMixin(ThemableMixin(PolymerElement)) {
this.addEventListener('file-abort', this._onFileAbort.bind(this));
this.addEventListener('file-remove', this._onFileRemove.bind(this));
this.addEventListener('file-start', this._onFileStart.bind(this));
this.addEventListener('file-reject', this._onFileReject.bind(this));
this.addEventListener('upload-start', this._onUploadStart.bind(this));
this.addEventListener('upload-success', this._onUploadSuccess.bind(this));
this.addEventListener('upload-error', this._onUploadError.bind(this));
}

/** @private */
Expand Down Expand Up @@ -857,6 +862,26 @@ class Upload extends ElementMixin(ThemableMixin(PolymerElement)) {
this._removeFile(event.detail.file);
}

/** @private */
_onFileReject(event) {
announce(`${event.detail.file.name}: ${event.detail.file.error}`, { mode: 'alert' });
}

/** @private */
_onUploadStart(event) {
announce(`${event.detail.file.name}: 0%`, { mode: 'alert' });
}

/** @private */
_onUploadSuccess(event) {
announce(`${event.detail.file.name}: 100%`, { mode: 'alert' });
}

/** @private */
_onUploadError(event) {
announce(`${event.detail.file.name}: ${event.detail.file.error}`, { mode: 'alert' });
}

/** @private */
_dragoverChanged(dragover) {
dragover ? this.setAttribute('dragover', dragover) : this.removeAttribute('dragover');
Expand Down
48 changes: 48 additions & 0 deletions packages/upload/test/a11y.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { expect } from '@esm-bundle/chai';
import { fixtureSync } from '@vaadin/testing-helpers';
import sinon from 'sinon';
import '../vaadin-upload.js';
import { createFile } from './common.js';

Expand Down Expand Up @@ -66,4 +67,51 @@ describe('a11y', () => {
});
});
});

describe('upload announcements', () => {
let clock, upload, announceRegion;

before(() => {
announceRegion = document.querySelector('[aria-live]');
});

beforeEach(() => {
upload = fixtureSync(`<vaadin-upload></vaadin-upload>`);
clock = sinon.useFakeTimers();
});

afterEach(() => {
clock.restore();
});

it('should announce upload start', async () => {
upload.dispatchEvent(new CustomEvent('upload-start', { detail: { file: { name: 'file.js' } } }));
clock.tick(200);
expect(announceRegion.textContent).to.equal('file.js: 0%');
expect(announceRegion.getAttribute('role')).to.equal('alert');
});

it('should announce upload success', async () => {
upload.dispatchEvent(new CustomEvent('upload-success', { detail: { file: { name: 'file.js' } } }));
clock.tick(200);
expect(announceRegion.textContent).to.equal('file.js: 100%');
expect(announceRegion.getAttribute('role')).to.equal('alert');
});

it('should announce file reject', async () => {
upload.dispatchEvent(
new CustomEvent('file-reject', { detail: { file: { name: 'file.js', error: 'rejected' } } })
);
clock.tick(200);
expect(announceRegion.textContent).to.equal('file.js: rejected');
expect(announceRegion.getAttribute('role')).to.equal('alert');
});

it('should announce upload error', async () => {
upload.dispatchEvent(new CustomEvent('upload-error', { detail: { file: { name: 'file.js', error: 'error' } } }));
clock.tick(200);
expect(announceRegion.textContent).to.equal('file.js: error');
expect(announceRegion.getAttribute('role')).to.equal('alert');
});
});
});

0 comments on commit 529cad5

Please sign in to comment.