Skip to content

feat(file-input): expose the ClearEvent #476

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
May 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/tired-words-buy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@tapsioss/react-components": minor
"@tapsioss/web-components": minor
---

Expose File Input's Clear Event

16 changes: 14 additions & 2 deletions packages/react-components/src/FileInput/FileInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
import * as React from "react";

import {
FileInputClearEvent,
FileInput as FileInputElement,
FileInputRetryEvent,
FileInputSlots,
Expand All @@ -16,16 +17,27 @@ registerFileInput();

export const FileInput: ReactWebComponent<
FileInputElement,
{ onRetry: EventName<FileInputRetryEvent>; onChange: string; onInput: string }
{
onRetry: EventName<FileInputRetryEvent>;
onClear: EventName<FileInputClearEvent>;
onChange: string;
onInput: string;
}
> = createComponent({
tagName: "tapsi-file-input",
elementClass: FileInputElement,
react: React,
events: {
onRetry: "retry" as EventName<FileInputRetryEvent>,
onClear: "clear" as EventName<FileInputClearEvent>,
onChange: "input",
onInput: "input",
},
});

export { FileInputElement, FileInputRetryEvent, FileInputSlots };
export {
FileInputClearEvent,
FileInputElement,
FileInputRetryEvent,
FileInputSlots,
};
12 changes: 12 additions & 0 deletions packages/web-components/src/file-input/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,15 @@ export class RetryEvent extends BaseEvent<null> {
});
}
}

export class ClearEvent extends BaseEvent<null> {
public static readonly type = "clear";

constructor() {
super(ClearEvent.type, {
bubbles: true,
composed: true,
details: null,
});
}
}
111 changes: 86 additions & 25 deletions packages/web-components/src/file-input/file-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import {
withOnReportValidity,
} from "../utils/index.ts";
import { ErrorMessages, scope, Slots } from "./constants.ts";
import { RetryEvent } from "./events.ts";
import { ClearEvent, RetryEvent } from "./events.ts";
import styles from "./file-input.style.ts";
import { clear, error, image } from "./icons.ts";
import {
Expand Down Expand Up @@ -61,6 +61,7 @@ const BaseClass = withOnReportValidity(
* @slot [placeholder-icon] - The slot for icon placeholder.
*
* @fires {RetryEvent} retry - Fires when the retry button is clicked. (bubbles)
* @fires {ClearEvent} clear - Fires when user clicks on the clear button for reseting the input value (bubbles)
* @fires {Event} change -
* Fires when the user modifies the element's value.
* Unlike the `input` event, the change event is not necessarily fired for each
Expand Down Expand Up @@ -593,6 +594,21 @@ export class FileInput extends BaseClass {
return this.loading.toString() !== "false";
}

private _handleClear() {
this.reset();
const event = new ClearEvent();

this.dispatchEvent(event);
event.stopPropagation();
}

private _handleRetry() {
const event = new RetryEvent();

this.dispatchEvent(event);
event.stopPropagation();
}

private _renderHelperText() {
const text = this._getSupportingOrErrorText();

Expand Down Expand Up @@ -647,13 +663,20 @@ export class FileInput extends BaseClass {
src=${this._previewSrc}
alt="preview"
class="preview"
part="preview"
/>`;
}

return html`<span class="text">${file.name}</span>`;
return html`<span
class="text"
part="text"
>${file.name}</span
>`;
}

return html`<span class="text"
return html`<span
class="text"
part="text"
>${toFaNumber(this.files.length.toString())} فایل انتخاب شده</span
>`;
}
Expand All @@ -664,20 +687,28 @@ export class FileInput extends BaseClass {
let icon;

if (!isNumber) {
icon = html`<div class="spinner">
icon = html`<div
class="spinner"
part="spinner"
>
<tapsi-spinner></tapsi-spinner>
</div>`;
} else {
const { offset, progressSize, progressStroke, circumference, radius } =
getProgressUiParams(parseInt(this.loading.toString()));

icon = html`<div class="progress-wrapper">
icon = html`<div
class="progress-wrapper"
part="progress-wrapper"
>
<svg
class="progress"
part="progress"
viewBox="0 0 48 48"
>
<circle
class="background-circle"
part="background-circle"
cx=${progressSize}
cy=${progressSize}
r=${radius}
Expand All @@ -686,6 +717,7 @@ export class FileInput extends BaseClass {
/>
<circle
class="foreground-circle"
part="foreground-circle"
cx=${progressSize}
cy=${progressSize}
r=${radius}
Expand All @@ -696,13 +728,24 @@ export class FileInput extends BaseClass {
stroke-dashoffset="${offset}"
/>
</svg>
<span class="percentage">${toFaNumber(this.loading.toString())}٪</span>
<span
class="percentage"
part="percentage"
>${toFaNumber(this.loading.toString())}٪</span
>
</div>`;
}

return html`<div class="loading-state">
return html`<div
class="loading-state"
part="loading-state"
>
${icon}
<span class="text">${this.loadingText}</span>
<span
class="text"
part="text"
>${this.loadingText}</span
>
</div>`;
}

Expand All @@ -713,24 +756,37 @@ export class FileInput extends BaseClass {
size="sm"
variant="ghost"
class="error-action"
@click=${() => this.dispatchEvent(new RetryEvent())}
part="error-action"
@click=${this._handleRetry}
>
تلاش مجدد
</tapsi-button>`;
}

private _renderErrorState() {
return html` <div class="error-state">
<div class="icon">${error}</div>
return html` <div
class="error-state"
part="error-state"
>
<div
class="icon"
part="icon"
>
${error}
</div>
${this._renderRetry()}
</div>`;
}

private _renderEmptyState() {
return html`
<div class="empty-state">
<div
class="empty-state"
part="empty-state"
>
<div
class="icon"
part="icon"
?hidden=${!this._hasPlaceholderIconSlot}
>
<slot
Expand All @@ -741,11 +797,16 @@ export class FileInput extends BaseClass {

<div
class="icon"
part="icon"
?hidden=${this._hasPlaceholderIconSlot}
>
${image}
</div>
<span class="placeholder">${this.placeholder}</span>
<span
class="placeholder"
part="placeholder"
>${this.placeholder}</span
>
</div>
`;
}
Expand All @@ -762,16 +823,16 @@ export class FileInput extends BaseClass {

private _renderClearIcon() {
if (this._value)
return html` <div class="clear-button">
<tapsi-icon-button
@click=${this.reset}
label="clear"
variant="elevated"
size="sm"
>
${clear}
</tapsi-icon-button>
</div>`;
return html`<tapsi-icon-button
class="clear-button"
part="clear-button"
@click=${this._handleClear}
label="clear"
variant="elevated"
size="sm"
>
${clear}
</tapsi-icon-button>`;

return null;
}
Expand Down Expand Up @@ -803,10 +864,10 @@ export class FileInput extends BaseClass {
part="control"
>
<input
class="input"
part="input"
type="file"
id="input"
class="input"
aria-invalid=${this._hasError()}
aria-label=${ariaLabel}
aria-labelledby=${ariaLabelledBy}
Expand All @@ -822,8 +883,8 @@ export class FileInput extends BaseClass {
.value=${live(this._value)}
/>
<div
part="file-input"
class="file-input"
part="file-input"
>
${this._renderFileInputContent()}${this._renderClearIcon()}
</div>
Expand Down
2 changes: 2 additions & 0 deletions packages/web-components/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ import {
} from "./empty-state/index.ts";
import {
FileInput,
ClearEvent as FileInputClearEvent,
RetryEvent as FileInputRetryEvent,
Slots as FileInputSlots,
register as registerFileInput,
Expand Down Expand Up @@ -219,6 +220,7 @@ export {
EmptyState,
EmptyStateSlots,
FileInput,
FileInputClearEvent,
FileInputRetryEvent,
FileInputSlots,
IconButton,
Expand Down