From 4c1923e2de9c4ace2de96b4c177478f53dde1f35 Mon Sep 17 00:00:00 2001 From: Segun Adebayo Date: Thu, 5 Sep 2024 17:18:06 +0100 Subject: [PATCH] refactor(file-upload): add invalid prop --- .changeset/plenty-toys-behave.md | 6 +++ .xstate/file-upload.js | 30 ++++--------- .../file-upload/src/file-upload.connect.ts | 4 +- .../file-upload/src/file-upload.machine.ts | 44 +++++-------------- .../file-upload/src/file-upload.props.ts | 1 + .../file-upload/src/file-upload.types.ts | 14 +++--- 6 files changed, 36 insertions(+), 63 deletions(-) create mode 100644 .changeset/plenty-toys-behave.md diff --git a/.changeset/plenty-toys-behave.md b/.changeset/plenty-toys-behave.md new file mode 100644 index 0000000000..547278c5c0 --- /dev/null +++ b/.changeset/plenty-toys-behave.md @@ -0,0 +1,6 @@ +--- +"@zag-js/file-upload": minor +--- + +Add support for `invalid` prop in file upload to explicitly mark upload operation as invalid. This could be paired with +the `rejectedFiles` to show an error message. diff --git a/.xstate/file-upload.js b/.xstate/file-upload.js index 5d0cb11195..87510cbb92 100644 --- a/.xstate/file-upload.js +++ b/.xstate/file-upload.js @@ -12,10 +12,7 @@ const { const fetchMachine = createMachine({ id: "fileupload", initial: "idle", - context: { - "!isWithinRange": false, - "!isWithinRange": false - }, + context: {}, on: { "FILES.SET": { actions: ["setFilesFromEvent"] @@ -45,13 +42,9 @@ const fetchMachine = createMachine({ actions: ["openFilePicker"] }, "DROPZONE.FOCUS": "focused", - "DROPZONE.DRAG_OVER": [{ - cond: "!isWithinRange", - target: "dragging", - actions: ["setInvalid"] - }, { + "DROPZONE.DRAG_OVER": { target: "dragging" - }] + } } }, focused: { @@ -63,24 +56,19 @@ const fetchMachine = createMachine({ "DROPZONE.CLICK": { actions: ["openFilePicker"] }, - "DROPZONE.DRAG_OVER": [{ - cond: "!isWithinRange", - target: "dragging", - actions: ["setInvalid"] - }, { + "DROPZONE.DRAG_OVER": { target: "dragging" - }] + } } }, dragging: { on: { "DROPZONE.DROP": { target: "idle", - actions: ["clearInvalid", "setFilesFromEvent", "syncInputElement"] + actions: ["setFilesFromEvent", "syncInputElement"] }, "DROPZONE.DRAG_LEAVE": { - target: "idle", - actions: ["clearInvalid"] + target: "idle" } } } @@ -93,7 +81,5 @@ const fetchMachine = createMachine({ }; }) }, - guards: { - "!isWithinRange": ctx => ctx["!isWithinRange"] - } + guards: {} }); \ No newline at end of file diff --git a/packages/machines/file-upload/src/file-upload.connect.ts b/packages/machines/file-upload/src/file-upload.connect.ts index ffb8130943..03f6b3c7d1 100644 --- a/packages/machines/file-upload/src/file-upload.connect.ts +++ b/packages/machines/file-upload/src/file-upload.connect.ts @@ -61,8 +61,9 @@ export function connect(state: State, send: Send, normalize dir: state.context.dir, id: dom.getDropzoneId(state.context), tabIndex: disabled ? undefined : 0, + role: "button", + "aria-label": translations.dropzone, "aria-disabled": disabled, - "aria-invalid": state.context.invalid, "data-invalid": dataAttr(state.context.invalid), "data-disabled": dataAttr(disabled), "data-dragging": dataAttr(dragging), @@ -124,6 +125,7 @@ export function connect(state: State, send: Send, normalize id: dom.getTriggerId(state.context), disabled, "data-disabled": dataAttr(disabled), + "data-invalid": dataAttr(state.context.invalid), type: "button", onClick(event) { if (disabled) return diff --git a/packages/machines/file-upload/src/file-upload.machine.ts b/packages/machines/file-upload/src/file-upload.machine.ts index 2d5c26a277..1cebf55132 100644 --- a/packages/machines/file-upload/src/file-upload.machine.ts +++ b/packages/machines/file-upload/src/file-upload.machine.ts @@ -1,12 +1,10 @@ -import { createMachine, guards, ref } from "@zag-js/core" +import { createMachine, ref } from "@zag-js/core" import { raf } from "@zag-js/dom-query" import { getAcceptAttrString, isFileEqual } from "@zag-js/file-utils" import { compact } from "@zag-js/utils" import { dom } from "./file-upload.dom" -import type { MachineContext, MachineState, FileRejection, UserDefinedContext } from "./file-upload.types" -import { getFilesFromEvent, isFilesWithinRange } from "./file-upload.utils" - -const { not } = guards +import type { FileRejection, MachineContext, MachineState, UserDefinedContext } from "./file-upload.types" +import { getFilesFromEvent } from "./file-upload.utils" export function machine(userContext: UserDefinedContext) { const ctx = compact(userContext) @@ -22,8 +20,8 @@ export function machine(userContext: UserDefinedContext) { ...ctx, acceptedFiles: ref([]), rejectedFiles: ref([]), - invalid: false, translations: { + dropzone: "dropzone", itemPreview: (file) => `preview of ${file.name}`, deleteFile: (file) => `delete file ${file.name}`, ...ctx.translations, @@ -57,14 +55,9 @@ export function machine(userContext: UserDefinedContext) { actions: ["openFilePicker"], }, "DROPZONE.FOCUS": "focused", - "DROPZONE.DRAG_OVER": [ - { - guard: not("isWithinRange"), - target: "dragging", - actions: ["setInvalid"], - }, - { target: "dragging" }, - ], + "DROPZONE.DRAG_OVER": { + target: "dragging", + }, }, }, focused: { @@ -76,34 +69,25 @@ export function machine(userContext: UserDefinedContext) { "DROPZONE.CLICK": { actions: ["openFilePicker"], }, - "DROPZONE.DRAG_OVER": [ - { - guard: not("isWithinRange"), - target: "dragging", - actions: ["setInvalid"], - }, - { target: "dragging" }, - ], + "DROPZONE.DRAG_OVER": { + target: "dragging", + }, }, }, dragging: { on: { "DROPZONE.DROP": { target: "idle", - actions: ["clearInvalid", "setFilesFromEvent", "syncInputElement"], + actions: ["setFilesFromEvent", "syncInputElement"], }, "DROPZONE.DRAG_LEAVE": { target: "idle", - actions: ["clearInvalid"], }, }, }, }, }, { - guards: { - isWithinRange: (ctx, evt) => isFilesWithinRange(ctx, evt.count), - }, actions: { syncInputElement(ctx) { const inputEl = dom.getHiddenInputEl(ctx) @@ -123,12 +107,6 @@ export function machine(userContext: UserDefinedContext) { dom.getHiddenInputEl(ctx)?.click() }) }, - setInvalid(ctx) { - ctx.invalid = true - }, - clearInvalid(ctx) { - ctx.invalid = false - }, setFilesFromEvent(ctx, evt) { const result = getFilesFromEvent(ctx, evt.files) const { acceptedFiles, rejectedFiles } = result diff --git a/packages/machines/file-upload/src/file-upload.props.ts b/packages/machines/file-upload/src/file-upload.props.ts index a110121e87..30fe16659c 100644 --- a/packages/machines/file-upload/src/file-upload.props.ts +++ b/packages/machines/file-upload/src/file-upload.props.ts @@ -17,6 +17,7 @@ export const props = createProps()([ "maxFileSize", "minFileSize", "name", + "invalid", "onFileAccept", "onFileReject", "onFileChange", diff --git a/packages/machines/file-upload/src/file-upload.types.ts b/packages/machines/file-upload/src/file-upload.types.ts index 767cd322ee..c37c5b48f5 100644 --- a/packages/machines/file-upload/src/file-upload.types.ts +++ b/packages/machines/file-upload/src/file-upload.types.ts @@ -43,8 +43,9 @@ export type ElementIds = Partial<{ }> export interface IntlTranslations { - itemPreview(file: File): string - deleteFile(file: File): string + dropzone?: string + itemPreview?(file: File): string + deleteFile?(file: File): string } interface PublicContext extends LocaleProperties, CommonProperties { @@ -118,14 +119,13 @@ interface PublicContext extends LocaleProperties, CommonProperties { * Whether to accept directories, only works in webkit browsers */ directory?: boolean + /** + * Whether the file input is invalid + */ + invalid?: boolean } interface PrivateContext { - /** - * @internal - * Whether the files includes any rejection - */ - invalid: boolean /** * @internal * The rejected files