lion/packages/ui/components/input-file/src/LionSelectedFileList.js
Danny Moerkerke 259e0dd468
Fix/remove file (#2018)
* fix: lion-selected-filelist, restored composed: true and bubbles: true for 'file-remove-requested' event, otherwise removing files for subclassers won't work

* fix: lion-selected-filelist, restored composed: true and bubbles: true for 'file-remove-requested' event, otherwise removing files for subclassers won't work

* fix: removed composed: true and bubbles: true and set eventlistener directly on lion-selected-file-list

* Update packages/ui/components/input-file/src/LionInputFile.js

* Update packages/ui/components/input-file/src/LionInputFile.js

* Update packages/ui/components/input-file/src/LionInputFile.js

* fix: fixed test

* Update packages/ui/components/input-file/test/lion-input-file.test.js

* chore: format

* fix

---------

Co-authored-by: Thijs Louisse <thijs.louisse@ing.com>
2023-06-20 17:32:05 +02:00

229 lines
6.5 KiB
JavaScript

import { uuid } from '@lion/ui/core.js';
import { LionValidationFeedback } from '@lion/ui/form-core.js';
import { LocalizeMixin } from '@lion/ui/localize.js';
import { ScopedElementsMixin } from '@open-wc/scoped-elements';
import { css, html, LitElement, nothing } from 'lit';
import { ifDefined } from 'lit/directives/if-defined.js';
import { repeat } from 'lit/directives/repeat.js';
import { localizeNamespaceLoader } from './localizeNamespaceLoader.js';
/**
* @typedef {import('lit').TemplateResult} TemplateResult
* @typedef {import('../types/input-file.js').InputFile} InputFile
* @typedef {import('../types/input-file.js').SystemFile} SystemFile
*/
export class LionSelectedFileList extends LocalizeMixin(ScopedElementsMixin(LitElement)) {
static get scopedElements() {
return {
// @ts-expect-error [external] fix types scopedElements
...super.scopedElements,
'lion-validation-feedback': LionValidationFeedback,
};
}
static get properties() {
return {
fileList: { type: Array },
multiple: { type: Boolean },
};
}
static localizeNamespaces = [
{ 'lion-input-file': localizeNamespaceLoader },
...super.localizeNamespaces,
];
constructor() {
super();
/**
* @type {InputFile[]}
*/
this.fileList = [];
this.multiple = false;
}
/**
* @param {import('lit').PropertyValues} changedProperties
*/
updated(changedProperties) {
super.updated(changedProperties);
if (changedProperties.has('fileList')) {
this._enhanceLightDomA11y();
}
}
_enhanceLightDomA11y() {
const fileFeedbackElementList = this.shadowRoot?.querySelectorAll('[id^="file-feedback"]');
// TODO: this will break as soon as a Subclasser changes the template ...
const inputFileNode = this.parentNode?.parentNode;
fileFeedbackElementList?.forEach(feedbackEl => {
// Generic focus/blur handling that works for both Fields/FormGroups
inputFileNode?.addEventListener('focusin', () => {
feedbackEl.setAttribute('aria-live', 'polite');
});
inputFileNode?.addEventListener('focusout', () => {
feedbackEl.setAttribute('aria-live', 'assertive');
});
});
}
/**
* @protected
* @param {InputFile} removedFile
*/
_removeFile(removedFile) {
this.dispatchEvent(
new CustomEvent('file-remove-requested', {
detail: {
removedFile,
status: removedFile.status,
uploadResponse: removedFile.response,
},
}),
);
}
/**
* @protected
* @param {Array<import('../../form-core/types/validate/ValidateMixinTypes.js').FeedbackMessage>} validationFeedback
* @param {string} fileUuid
* @return {TemplateResult}
*/
// eslint-disable-next-line class-methods-use-this
_validationFeedbackTemplate(validationFeedback, fileUuid) {
return html`
<lion-validation-feedback
id="file-feedback-${fileUuid}"
.feedbackData="${validationFeedback}"
aria-live="assertive"
></lion-validation-feedback>
`;
}
/**
* @protected
* @param {InputFile} file
* @return {TemplateResult|nothing}
*/
// eslint-disable-next-line class-methods-use-this, no-unused-vars
_listItemBeforeTemplate(file) {
return nothing;
}
/**
* @protected
* @param {InputFile} file
* @param {string} fileUuid
* @return {TemplateResult}
*/
// eslint-disable-next-line no-unused-vars
_listItemAfterTemplate(file, fileUuid) {
return html`
<button
class="selected__list__item__remove-button"
aria-label="${this.msgLit('lion-input-file:removeButtonLabel', {
fileName: file.systemFile.name,
})}"
@click=${() => this._removeFile(file)}
>
${this._removeButtonContentTemplate()}
</button>
`;
}
/**
* @protected
* @return {TemplateResult}
*/
// eslint-disable-next-line class-methods-use-this
_removeButtonContentTemplate() {
return html`✖️`;
}
/**
* @protected
* @param {InputFile} file
* @return {TemplateResult}
*/
_selectedListItemTemplate(file) {
const fileUuid = uuid();
return html`
<div class="selected__list__item" status="${file.status ? file.status.toLowerCase() : ''}">
<div class="selected__list__item__label">
${this._listItemBeforeTemplate(file)}
<span id="selected-list-item-label-${fileUuid}" class="selected__list__item__label__text">
<span class="sr-only">${this.msgLit('lion-input-file:fileNameDescriptionLabel')}</span>
${file.downloadUrl && file.status !== 'LOADING'
? html`
<a
class="selected__list__item__label__link"
href="${file.downloadUrl}"
target="${file.downloadUrl.startsWith('blob') ? '_blank' : ''}"
rel="${ifDefined(
file.downloadUrl.startsWith('blob') ? 'noopener noreferrer' : undefined,
)}"
>${file.systemFile?.name}</a
>
`
: file.systemFile?.name}
</span>
${this._listItemAfterTemplate(file, fileUuid)}
</div>
${file.status === 'FAIL' && file.validationFeedback
? html`
${repeat(
file.validationFeedback,
validationFeedback => html`
${this._validationFeedbackTemplate([validationFeedback], fileUuid)}
`,
)}
`
: nothing}
</div>
`;
}
render() {
return this.fileList?.length
? html`
${this.multiple
? html`
<ul class="selected__list">
${this.fileList.map(
file => html` <li>${this._selectedListItemTemplate(file)}</li> `,
)}
</ul>
`
: html` ${this._selectedListItemTemplate(this.fileList[0])} `}
`
: nothing;
}
static get styles() {
return [
css`
.selected__list {
list-style-type: none;
margin-block-start: 0;
margin-block-end: 0;
padding-inline-start: 0;
}
.sr-only {
position: absolute;
top: 0;
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;
}
`,
];
}
}