Fix/input file a11y (#2304)
* fix(input-file): improve accessibility * chore: update the message * chore: add the translations * chore: fix linting * chore: add changeset
This commit is contained in:
parent
3dbee0c9b0
commit
b2d7d9b4a4
18 changed files with 173 additions and 21 deletions
5
.changeset/stupid-falcons-drop.md
Normal file
5
.changeset/stupid-falcons-drop.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'@lion/ui': patch
|
||||
---
|
||||
|
||||
[input-file] improve a11y labels
|
||||
|
|
@ -77,6 +77,7 @@ export class LionInputFile extends ScopedElementsMixin(LocalizeMixin(LionField))
|
|||
>
|
||||
${this.buttonLabel}
|
||||
</button>`,
|
||||
after: () => html`<div data-description></div>`,
|
||||
'selected-file-list': () => ({
|
||||
template: html`
|
||||
<lion-selected-file-list
|
||||
|
|
@ -105,8 +106,7 @@ export class LionInputFile extends ScopedElementsMixin(LocalizeMixin(LionField))
|
|||
}
|
||||
|
||||
/**
|
||||
* The helpt text for the input node.
|
||||
* When no light dom defined via [slot=help-text], this value will be used
|
||||
* The label of the button
|
||||
* @type {string}
|
||||
*/
|
||||
get buttonLabel() {
|
||||
|
|
@ -332,8 +332,6 @@ export class LionInputFile extends ScopedElementsMixin(LocalizeMixin(LionField))
|
|||
super.firstUpdated(changedProperties);
|
||||
|
||||
this.__setupFileValidators();
|
||||
// We need to update our light dom
|
||||
this._enhanceSelectedList();
|
||||
|
||||
// TODO: if there's no inputNode by now, we should either throw an error or add a slot change listener
|
||||
if (this._inputNode) {
|
||||
|
|
@ -443,6 +441,7 @@ export class LionInputFile extends ScopedElementsMixin(LocalizeMixin(LionField))
|
|||
this._selectedFilesMetaData = [...this._selectedFilesMetaData];
|
||||
}
|
||||
});
|
||||
this._updateUploadButtonDescription();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -521,20 +520,6 @@ export class LionInputFile extends ScopedElementsMixin(LocalizeMixin(LionField))
|
|||
ev.target.value = ''; // eslint-disable-line no-param-reassign
|
||||
}
|
||||
|
||||
/**
|
||||
* @protected
|
||||
*/
|
||||
_enhanceSelectedList() {
|
||||
/**
|
||||
* @type {LionSelectedFileList | null}
|
||||
*/
|
||||
const selectedFileList = this.querySelector('[slot="selected-file-list"]');
|
||||
if (selectedFileList) {
|
||||
selectedFileList.setAttribute('id', `selected-file-list-${this._inputId}`);
|
||||
this.addToAriaDescribedBy(selectedFileList, { idPrefix: 'selected-file-list' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @protected
|
||||
*/
|
||||
|
|
@ -739,6 +724,46 @@ export class LionInputFile extends ScopedElementsMixin(LocalizeMixin(LionField))
|
|||
fileObj.validationFeedback?.push(errorObj);
|
||||
}
|
||||
|
||||
/**
|
||||
* Description for screen readers connected to the button about how many files have been updated
|
||||
* @protected
|
||||
*/
|
||||
_updateUploadButtonDescription() {
|
||||
const erroneousFilesNames = /** @type {string[]} */ ([]);
|
||||
let errorMessage;
|
||||
|
||||
this._selectedFilesMetaData.forEach(file => {
|
||||
if (file.status === 'FAIL') {
|
||||
errorMessage = file.validationFeedback ? file.validationFeedback[0].message.toString() : '';
|
||||
erroneousFilesNames.push(/** @type {string} */ (file.systemFile.name));
|
||||
}
|
||||
});
|
||||
|
||||
const selectedFiles = this.querySelector('[slot="after"]');
|
||||
if (selectedFiles) {
|
||||
if (!this._selectedFilesMetaData || this._selectedFilesMetaData.length === 0) {
|
||||
selectedFiles.textContent = /** @type {string} */ (
|
||||
this.msgLit('lion-input-file:noFilesSelected')
|
||||
);
|
||||
} else if (this._selectedFilesMetaData.length === 1) {
|
||||
selectedFiles.textContent = /** @type {string} */ (
|
||||
errorMessage || this._selectedFilesMetaData[0].systemFile.name
|
||||
);
|
||||
} else {
|
||||
selectedFiles.textContent = `${this.msgLit('lion-input-file:numberOfFiles', {
|
||||
numberOfFiles: this._selectedFilesMetaData.length,
|
||||
})} ${
|
||||
errorMessage
|
||||
? this.msgLit('lion-input-file:generalValidatorMessage', {
|
||||
validatorMessage: errorMessage,
|
||||
listOfErroneousFiles: erroneousFilesNames.join(', '),
|
||||
})
|
||||
: ''
|
||||
}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {InputFile} removedFile
|
||||
|
|
@ -755,6 +780,7 @@ export class LionInputFile extends ScopedElementsMixin(LocalizeMixin(LionField))
|
|||
}
|
||||
this._inputNode.value = '';
|
||||
this._handleErrors();
|
||||
this._updateUploadButtonDescription();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -847,6 +873,7 @@ export class LionInputFile extends ScopedElementsMixin(LocalizeMixin(LionField))
|
|||
_inputGroupInputTemplate() {
|
||||
return html`
|
||||
<slot name="input"> </slot>
|
||||
<slot name="after"> </slot>
|
||||
${this.enableDropZone && this._isDragAndDropSupported
|
||||
? this._dropZoneTemplate()
|
||||
: html`
|
||||
|
|
@ -891,6 +918,19 @@ export class LionInputFile extends ScopedElementsMixin(LocalizeMixin(LionField))
|
|||
border: dashed 2px black;
|
||||
padding: 24px 0;
|
||||
}
|
||||
|
||||
.input-group__container ::slotted([slot='after']) {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
clip-path: inset(100%);
|
||||
clip: rect(1px, 1px, 1px, 1px);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1133,7 +1133,7 @@ describe('lion-input-file', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('select-button has aria-describedby set to the help-text, selected list and the feedback message', async () => {
|
||||
it('select-button has aria-describedby set to the help-text, after and the feedback message', async () => {
|
||||
const uploadResponse = [
|
||||
{
|
||||
name: 'file1.txt',
|
||||
|
|
@ -1159,11 +1159,73 @@ describe('lion-input-file', () => {
|
|||
// @ts-expect-error [allow-protected-in-test]
|
||||
`feedback-${el._inputId}`,
|
||||
);
|
||||
await el.updateComplete;
|
||||
// @ts-expect-error [allow-protected-in-test]
|
||||
expect(el._buttonNode?.getAttribute('aria-describedby')).to.contain(
|
||||
// @ts-expect-error [allow-protected-in-test]
|
||||
`selected-file-list-${el._inputId}`,
|
||||
`after-${el._inputId}`,
|
||||
);
|
||||
});
|
||||
|
||||
it('after contains upload name of file when SUCCESS', async () => {
|
||||
const uploadResponse = [
|
||||
{
|
||||
name: 'file1.txt',
|
||||
status: 'SUCCESS',
|
||||
errorMessage: '',
|
||||
downloadUrl: '/downloadFile',
|
||||
},
|
||||
];
|
||||
const el = await fixture(html` <lion-input-file label="Select"></lion-input-file> `);
|
||||
|
||||
expect(el.querySelector('[slot="after"]')?.textContent).to.equal('No files selected.');
|
||||
// @ts-expect-error
|
||||
el.uploadResponse = uploadResponse;
|
||||
await el.updateComplete;
|
||||
expect(el.querySelector('[slot="after"]')?.textContent).to.equal('file1.txt');
|
||||
});
|
||||
|
||||
it('after contains upload validator message of file when FAIL', async () => {
|
||||
const uploadResponse = [
|
||||
{
|
||||
name: 'file1.txt',
|
||||
status: 'FAIL',
|
||||
errorMessage: 'something went wrong',
|
||||
downloadUrl: '/downloadFile',
|
||||
},
|
||||
];
|
||||
const el = await fixture(html` <lion-input-file label="Select"></lion-input-file> `);
|
||||
|
||||
expect(el.querySelector('[slot="after"]')?.textContent).to.equal('No files selected.');
|
||||
// @ts-expect-error
|
||||
el.uploadResponse = uploadResponse;
|
||||
await el.updateComplete;
|
||||
expect(el.querySelector('[slot="after"]')?.textContent).to.equal('something went wrong');
|
||||
});
|
||||
|
||||
it('after contains upload status of files when multiple files have been uploaded', async () => {
|
||||
const uploadResponse = [
|
||||
{
|
||||
name: 'file1.txt',
|
||||
status: 'SUCCESS',
|
||||
errorMessage: '',
|
||||
downloadUrl: '/downloadFile',
|
||||
},
|
||||
{
|
||||
name: 'file2.txt',
|
||||
status: 'FAIL',
|
||||
errorMessage: 'something went wrong',
|
||||
downloadUrl: '/downloadFile',
|
||||
},
|
||||
];
|
||||
const el = await fixture(html`
|
||||
<lion-input-file label="Select" multiple></lion-input-file>
|
||||
`);
|
||||
// @ts-expect-error
|
||||
el.uploadResponse = uploadResponse;
|
||||
|
||||
await el.updateComplete;
|
||||
expect(el.querySelector('[slot="after"]')?.textContent?.trim()).to.equal(
|
||||
'2 files. "something went wrong", for file2.txt.',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@ export default {
|
|||
'Моля, качете файл от тип {allowedTypesArray} или {allowedTypesLastItem} с макс. размер {maxSize}.',
|
||||
dragAndDropText: 'Плъзнете и пуснете Вашите файлове тук или',
|
||||
fileNameDescriptionLabel: 'Име на файл: {fileName}',
|
||||
generalValidatorMessage: '"{validatorMessage}", за {listOfErroneousFiles}.',
|
||||
noFilesSelected: 'Не са избрани файлове.',
|
||||
numberOfFiles: '{numberOfFiles} файла.',
|
||||
removeButtonLabel: 'Отстраняване на файла {fileName}',
|
||||
selectTextDuplicateFileName: 'Файл със същото име на файл вече е налице.',
|
||||
selectTextMultipleFile: 'Избор на файлове',
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@ export default {
|
|||
'Nahrajte soubor typu {allowedTypesArray} nebo {allowedTypesLastItem} s max. velikostí {maxSize}.',
|
||||
dragAndDropText: 'Přetáhněte soubory sem nebo',
|
||||
fileNameDescriptionLabel: 'Název souboru: {fileName}',
|
||||
generalValidatorMessage: '"{validatorMessage}", pro {listOfErroneousFiles}.',
|
||||
noFilesSelected: 'Nebyly vybrány žádné soubory.',
|
||||
numberOfFiles: '{numberOfFiles} soubory/souborů.',
|
||||
removeButtonLabel: 'Odebrat soubor {fileName}',
|
||||
selectTextDuplicateFileName: 'Soubor se stejným názvem byl již přítomen.',
|
||||
selectTextMultipleFile: 'Vybrat soubory',
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@ export default {
|
|||
'Laden Sie eine {allowedTypesArray} oder {allowedTypesLastItem}-Datei mit max. {maxSize} hoch.',
|
||||
dragAndDropText: 'Ziehen Sie Ihre Dateien per Drag & Drop hierher oder',
|
||||
fileNameDescriptionLabel: 'Dateiname: {fileName}',
|
||||
generalValidatorMessage: '"{validatorMessage}", für {listOfErroneousFiles}.',
|
||||
noFilesSelected: 'Keine Dateien ausgewählt.',
|
||||
numberOfFiles: '{numberOfFiles} Dateien.',
|
||||
removeButtonLabel: 'Datei {fileName} entfernen',
|
||||
selectTextDuplicateFileName: 'Eine Datei mit demselben Dateinamen war bereits vorhanden.',
|
||||
selectTextMultipleFile: 'Dateien auswählen',
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@ export default {
|
|||
'Please select a {allowedTypesArray} or {allowedTypesLastItem} file with max {maxSize}.',
|
||||
dragAndDropText: 'Drag & Drop your files here or', // TODO: or what? Why is Drag & Drop capitalized?
|
||||
fileNameDescriptionLabel: 'File name: {fileName}',
|
||||
generalValidatorMessage: '"{validatorMessage}", for {listOfErroneousFiles}.',
|
||||
noFilesSelected: 'No files selected.',
|
||||
numberOfFiles: '{numberOfFiles} files.',
|
||||
removeButtonLabel: 'Remove {fileName} file',
|
||||
selectTextDuplicateFileName: 'A file with same filename was already present.',
|
||||
selectTextMultipleFile: 'Select files',
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@ export default {
|
|||
'Cargue un archivo {allowedTypesArray} o {allowedTypesLastItem} de {maxSize} como máximo.',
|
||||
dragAndDropText: 'Arrastre y suelte los archivos aquí o',
|
||||
fileNameDescriptionLabel: 'Nombre de archivo: {fileName}',
|
||||
generalValidatorMessage: '"{validatorMessage}", para {listOfErroneousFiles}.',
|
||||
noFilesSelected: 'No se han seleccionado archivos.',
|
||||
numberOfFiles: '{numberOfFiles} archivos.',
|
||||
removeButtonLabel: 'Elimine el archivo: {fileName}',
|
||||
selectTextDuplicateFileName: 'Ya había un archivo con el mismo nombre de archivo.',
|
||||
selectTextMultipleFile: 'Seleccionar archivos',
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@ export default {
|
|||
'Veuillez télécharger un fichier {allowedTypesArray} ou {allowedTypesLastItem} avec une taille maximale de {maxSize}.',
|
||||
dragAndDropText: 'Glissez et déposez vos fichiers ici ou',
|
||||
fileNameDescriptionLabel: 'Nom de fichier: {fileName}',
|
||||
generalValidatorMessage: '"{validatorMessage}", pour {listOfErroneousFiles}.',
|
||||
noFilesSelected: 'Aucun fichier sélectionné.',
|
||||
numberOfFiles: '{numberOfFiles} fichiers.',
|
||||
removeButtonLabel: 'Supprimer le fichier {fileName}',
|
||||
selectTextDuplicateFileName: 'Un fichier portant le même nom de fichier était déjà présent.',
|
||||
selectTextMultipleFile: 'Sélectionnez des fichiers',
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@ export default {
|
|||
'Töltsön fel egy legfeljebb {maxSize} méretű {allowedTypesArray} vagy {allowedTypesLastItem} fájlt.',
|
||||
dragAndDropText: 'Húzza át a fájlokat ide vagy',
|
||||
fileNameDescriptionLabel: 'Fájlnév: {fileName}',
|
||||
generalValidatorMessage: '"{validatorMessage}", ehhez: {listOfErroneousFiles}.',
|
||||
noFilesSelected: 'Nincs fájl kiválasztva.',
|
||||
numberOfFiles: '{numberOfFiles} fájl.',
|
||||
removeButtonLabel: 'A(z) {fileName} fájl eltávolítása',
|
||||
selectTextDuplicateFileName: 'Már volt ilyen nevű fájl.',
|
||||
selectTextMultipleFile: 'Fájl(ok) kiválasztása',
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@ export default {
|
|||
'Caricare un file {allowedTypesArray} o {allowedTypesLastItem} di {maxSize} max.',
|
||||
dragAndDropText: 'Trascinare i file qui o',
|
||||
fileNameDescriptionLabel: 'Nome file: {fileName}',
|
||||
generalValidatorMessage: '"{validatorMessage}", per {listOfErroneousFiles}.',
|
||||
noFilesSelected: 'Nessun file selezionato.',
|
||||
numberOfFiles: '{numberOfFiles} file.',
|
||||
removeButtonLabel: 'Rimuovere il file {fileName}',
|
||||
selectTextDuplicateFileName: 'Un file con lo stesso nome file era già presente.',
|
||||
selectTextMultipleFile: 'Seleziona file',
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@ export default {
|
|||
'Upload een {allowedTypesArray} of {allowedTypesLastItem}-bestand van maximaal {maxSize}.',
|
||||
dragAndDropText: 'Sleep uw bestanden hierheen of',
|
||||
fileNameDescriptionLabel: 'Bestandsnaam: {fileName}',
|
||||
generalValidatorMessage: '"{validatorMessage}", voor {listOfErroneousFiles}.',
|
||||
noFilesSelected: 'Geen bestanden geselecteerd.',
|
||||
numberOfFiles: '{numberOfFiles} bestanden.',
|
||||
removeButtonLabel: 'Verwijder het bestand {fileName}',
|
||||
selectTextDuplicateFileName: 'Er bestaat al een bestand met dezelfde bestandsnaam.',
|
||||
selectTextMultipleFile: 'Selecteer bestanden',
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@ export default {
|
|||
'Prześlij plik {allowedTypesArray} lub {allowedTypesLastItem} o maks. rozmiarze {maxSize}.',
|
||||
dragAndDropText: 'Przeciągnij i upuść pliki tutaj lub',
|
||||
fileNameDescriptionLabel: 'Nazwa pliku: {fileName}',
|
||||
generalValidatorMessage: '"{validatorMessage}", dla {listOfErroneousFiles}.',
|
||||
noFilesSelected: 'Nie wybrano żadnych plików.',
|
||||
numberOfFiles: 'Liczba plików: {numberOfFiles}.',
|
||||
removeButtonLabel: 'Usuń plik {fileName}',
|
||||
selectTextDuplicateFileName: 'Plik o tej samej nazwie już istnieje.',
|
||||
selectTextMultipleFile: 'Wybierz pliki',
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@ export default {
|
|||
'Încărcaţi un fişier {allowedTypesArray} sau {allowedTypesLastItem} de max. {maxSize}.',
|
||||
dragAndDropText: 'Glisaţi şi fixaţi fişierele aici sau',
|
||||
fileNameDescriptionLabel: 'Nume fişier: {fileName}',
|
||||
generalValidatorMessage: '"{validatorMessage}", pentru {listOfErroneousFiles}.',
|
||||
noFilesSelected: 'Niciun fișier selectat.',
|
||||
numberOfFiles: '{numberOfFiles} fișiere.',
|
||||
removeButtonLabel: 'Eliminaţi fişierul {filename}',
|
||||
selectTextDuplicateFileName: 'Există deja un fişier cu acelaşi nume de fişier.',
|
||||
selectTextMultipleFile: 'Selectare fișiere',
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@ export default {
|
|||
'Загрузите файл {allowedTypesArray} или {allowedTypesLastItem} размером не более {maxSize}.',
|
||||
dragAndDropText: 'Перетащите файлы сюда или',
|
||||
fileNameDescriptionLabel: 'Название файла: {fileName}',
|
||||
generalValidatorMessage: '"{validatorMessage}", для {listOfErroneousFiles}.',
|
||||
noFilesSelected: 'Файлы не выбраны.',
|
||||
numberOfFiles: 'Файлов: {numberOfFiles}.',
|
||||
removeButtonLabel: 'Удалить файл {fileName}',
|
||||
selectTextDuplicateFileName: 'Файл с таким названием уже существует.',
|
||||
selectTextMultipleFile: 'Выберите файлы',
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@ export default {
|
|||
'Nahrajte súbor {allowedTypesArray} alebo {allowedTypesLastItem} s maximálnou veľkosťou {maxSize}.',
|
||||
dragAndDropText: 'Súbory presuňte sem alebo',
|
||||
fileNameDescriptionLabel: 'Názov súboru: {fileName}',
|
||||
generalValidatorMessage: '"{validatorMessage}", pre {listOfErroneousFiles}.',
|
||||
noFilesSelected: 'Neboli vybrané žiadne súbory.',
|
||||
numberOfFiles: 'Počet súborov: {numberOfFiles}.',
|
||||
removeButtonLabel: 'Odstrániť súbor {fileName}',
|
||||
selectTextDuplicateFileName: 'Súbor s rovnakým názvom súboru už existoval.',
|
||||
selectTextMultipleFile: 'Vybrať súbory',
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@ export default {
|
|||
'Завантажте файл {allowedTypesArray} або {allowedTypesLastItem} розміром до {maxSize}.',
|
||||
dragAndDropText: 'Перетягніть файли сюди або',
|
||||
fileNameDescriptionLabel: 'Ім’я файлу: {fileName}',
|
||||
generalValidatorMessage: '"{validatorMessage}", для {listOfErroneousFiles}.',
|
||||
noFilesSelected: 'Не вибрано жодного файлу.',
|
||||
numberOfFiles: '{numberOfFiles} файли(-ів).',
|
||||
removeButtonLabel: 'Видалення файлу {fileName}',
|
||||
selectTextDuplicateFileName: 'Файл із такою назвою вже існував.',
|
||||
selectTextMultipleFile: 'Виберіть файли',
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@ export default {
|
|||
'请上传最大 {maxSize} 的 {allowedTypesArray} 或 {allowedTypesLastItem} 文件。',
|
||||
dragAndDropText: '将您的文件拖放到此处,或',
|
||||
fileNameDescriptionLabel: '文件名: {fileName}',
|
||||
generalValidatorMessage: '"{validatorMessage}", 例如 {listOfErroneousFiles}。',
|
||||
noFilesSelected: '未选择任何文件。',
|
||||
numberOfFiles: '{numberOfFiles} 个文件。',
|
||||
removeButtonLabel: '删除 {fileName} 文件',
|
||||
selectTextDuplicateFileName: '已存在具有相同文件名的文件。',
|
||||
selectTextMultipleFile: '选择多个文件',
|
||||
|
|
|
|||
Loading…
Reference in a new issue