diff --git a/.changeset/honest-keys-push.md b/.changeset/honest-keys-push.md new file mode 100644 index 00000000..67614dad --- /dev/null +++ b/.changeset/honest-keys-push.md @@ -0,0 +1,6 @@ +--- +"@tapsioss/web-components": patch +--- + +Make the file input's preview remain visible on loading and error state. + \ No newline at end of file diff --git a/packages/web-components/src/file-input/file-input.style.ts b/packages/web-components/src/file-input/file-input.style.ts index 5e839562..998dfe68 100644 --- a/packages/web-components/src/file-input/file-input.style.ts +++ b/packages/web-components/src/file-input/file-input.style.ts @@ -18,6 +18,13 @@ const styles: CSSResult = css` --input-support-color: var(--tapsi-color-content-secondary); --input-control-color: var(--tapsi-color-content-on-inverse); --input-label-color: var(--tapsi-color-content-primary); + --input-loading-state-text-color: var(--tapsi-color-content-secondary); + --input-loading-state-progress-foreground-color: var( + --tapsi-color-content-accent + ); + --input-loading-state-progress-background-color: var( + --tapsi-color-surface-tertiary + ); display: inline-block; vertical-align: middle; @@ -140,15 +147,42 @@ const styles: CSSResult = css` flex-direction: column; align-items: center; justify-content: center; - color: var(--tapsi-color-content-accent); + + color: var(--input-loading-state-progress-foreground-color); + + position: absolute; + } + + .loading-state.on-overlay { + --input-loading-state-text-color: var(--tapsi-color-content-on-inverse); + --input-loading-state-progress-foreground-color: var( + --tapsi-color-content-on-inverse + ); + --input-loading-state-progress-background-color: transparent; } .preview { + position: absolute; + width: calc(100% - var(--tapsi-spacing-6)); height: calc(100% - var(--tapsi-spacing-6)); object-fit: contain; } + .overlay { + position: absolute; + bottom: var(--tapsi-spacing-4); + left: var(--tapsi-spacing-4); + top: var(--tapsi-spacing-4); + right: var(--tapsi-spacing-4); + + border-radius: var(--tapsi-spacing-3); + + background-color: var(--tapsi-color-surface-overlay-dark); + + backdrop-filter: blur(2px); + } + .clear-button { position: absolute; bottom: var(--tapsi-spacing-6); @@ -160,6 +194,8 @@ const styles: CSSResult = css` align-items: center; justify-content: center; flex-direction: column; + + position: absolute; } .error-state .icon { @@ -194,15 +230,11 @@ const styles: CSSResult = css` font-weight: var(--tapsi-typography-body-sm-weight); } - .progress-circle { - transform: rotate(-90deg); - transform-origin: center; - } .background-circle { - stroke: var(--tapsi-color-surface-tertiary); + stroke: var(--input-loading-state-progress-background-color); } .foreground-circle { - stroke: var(--tapsi-color-content-accent); + stroke: var(--input-loading-state-progress-foreground-color); transition: stroke-dashoffset 0.3s ease; } @@ -219,6 +251,8 @@ const styles: CSSResult = css` .progress-wrapper .progress { width: 100%; height: 100%; + + transform: rotate(-90deg); } .progress-wrapper .percentage { @@ -231,7 +265,7 @@ const styles: CSSResult = css` } .loading-state .text { - color: var(--tapsi-color-content-secondary); + color: var(--input-loading-state-text-color); margin-top: var(--tapsi-spacing-4); @@ -244,6 +278,8 @@ const styles: CSSResult = css` .loading-state .spinner { height: 2rem; width: 2rem; + + color: inherit; } .sr-only { diff --git a/packages/web-components/src/file-input/file-input.ts b/packages/web-components/src/file-input/file-input.ts index b3d50ea1..116a93da 100644 --- a/packages/web-components/src/file-input/file-input.ts +++ b/packages/web-components/src/file-input/file-input.ts @@ -297,7 +297,7 @@ export class FileInput extends BaseClass { * The list of selected files. */ public get files(): FileList | null { - return this._input?.files ?? null; + return this._files; } @state() @@ -317,6 +317,9 @@ export class FileInput extends BaseClass { @state() private _nativeErrorText = ""; + @state() + private _files: FileList | null = null; + @queryAssignedNodes({ slot: Slots.PLACEHOLDER_ICON }) private _placeholderIconSlotNodes!: Node[]; @@ -401,7 +404,8 @@ export class FileInput extends BaseClass { private _handleInput() { if (!this._isInteractable) return; - this.requestUpdate(); + // this.requestUpdate(); + this._files = this._input?.files ?? null; } private _handleChange(event: Event) { @@ -433,6 +437,7 @@ export class FileInput extends BaseClass { if (!input) return; input.files = files; + this._files = files; this.dispatchEvent(new Event("change", { bubbles: true })); this.dispatchEvent(new Event("input", { bubbles: true, composed: true })); @@ -575,6 +580,10 @@ export class FileInput extends BaseClass { return !this.disabled && !this._isLoading; } + private get _shouldShowOverlay(): boolean { + return !!this.value && (this._hasError() || this._isLoading); + } + private _handleClick() { if (!this._isInteractable) return; @@ -644,7 +653,10 @@ export class FileInput extends BaseClass { private _renderPreview() { const files = this.files; - if (!files) return null; + if (!files || files.length === 0 || !this.value) { + if (this._hasError() || this._isLoading) return null; + else return this._renderEmptyState(); + } if (files.length === 1) { const file = files[0]!; @@ -729,8 +741,13 @@ export class FileInput extends BaseClass { `; } + const loadingClasses = classMap({ + ["loading-state"]: true, + ["on-overlay"]: this._shouldShowOverlay, + }); + return html`