From 9a3a82f0b3738beda59c313fafd51360e6b0322f Mon Sep 17 00:00:00 2001 From: Segun Adebayo Date: Wed, 13 Sep 2023 20:35:27 +0100 Subject: [PATCH] refactor: unify callbacks (#862) --- .changeset/selfish-windows-warn.md | 33 + .xstate/date-picker.js | 23 +- .xstate/editable.js | 1 + .xstate/tags-input.js | 63 +- examples/next-ts/package.json | 3 +- examples/next-ts/pages/combobox.tsx | 5 +- examples/next-ts/pages/hover-card.tsx | 1 + examples/next-ts/pages/pagination.tsx | 2 +- examples/next-ts/pages/popper.tsx | 2 +- examples/next-ts/pages/radio-group.tsx | 3 - examples/next-ts/pages/select.tsx | 10 +- examples/nuxt-ts/package.json | 3 +- examples/nuxt-ts/pages/combobox.vue | 5 +- examples/nuxt-ts/pages/radio-group.vue | 1 - examples/shadow-dom/package.json | 3 +- examples/solid-ts/package.json | 3 +- examples/solid-ts/src/pages/combobox.tsx | 5 +- examples/solid-ts/src/pages/radio-group.tsx | 3 - examples/vue-ts/package.json | 3 +- examples/vue-ts/src/pages/combobox.tsx | 5 +- examples/vue-ts/src/pages/pagination.tsx | 2 +- examples/vue-ts/src/pages/radio-group.tsx | 3 - packages/docs/api.json | 1163 ++++++++--------- .../accordion/src/accordion.connect.ts | 18 +- .../accordion/src/accordion.machine.ts | 2 +- .../machines/accordion/src/accordion.types.ts | 107 +- .../machines/avatar/src/avatar.machine.ts | 4 +- packages/machines/avatar/src/avatar.types.ts | 31 +- .../machines/carousel/src/carousel.types.ts | 125 +- .../machines/checkbox/src/checkbox.machine.ts | 2 +- .../machines/checkbox/src/checkbox.types.ts | 105 +- .../color-picker/src/color-picker.machine.ts | 17 +- .../color-picker/src/color-picker.types.ts | 77 +- .../machines/combobox/src/combobox.machine.ts | 16 +- .../machines/combobox/src/combobox.types.ts | 306 ++--- .../date-picker/src/date-picker.connect.ts | 3 - .../date-picker/src/date-picker.machine.ts | 44 +- .../date-picker/src/date-picker.types.ts | 474 ++++--- .../date-picker/src/date-picker.utils.ts | 15 +- .../machines/dialog/src/dialog.machine.ts | 4 +- packages/machines/dialog/src/dialog.types.ts | 154 ++- .../machines/editable/src/editable.machine.ts | 15 +- .../machines/editable/src/editable.types.ts | 222 ++-- packages/machines/editable/src/index.ts | 2 +- .../file-upload/src/file-upload.connect.ts | 4 +- .../file-upload/src/file-upload.machine.ts | 11 +- .../file-upload/src/file-upload.types.ts | 59 +- .../hover-card/src/hover-card.machine.ts | 4 +- .../hover-card/src/hover-card.types.ts | 109 +- packages/machines/menu/src/menu.machine.ts | 4 +- packages/machines/menu/src/menu.types.ts | 139 +- .../number-input/src/number-input.machine.ts | 34 +- .../number-input/src/number-input.types.ts | 270 ++-- .../pagination/src/pagination.anatomy.ts | 1 + .../pagination/src/pagination.connect.ts | 16 +- .../pagination/src/pagination.machine.ts | 2 +- .../pagination/src/pagination.types.ts | 127 +- .../pagination/src/pagination.utils.ts | 96 +- .../pagination/tests/pagination.utils.test.ts | 2 +- .../pin-input/src/pin-input.machine.ts | 15 +- .../machines/pin-input/src/pin-input.types.ts | 179 +-- .../machines/popover/src/popover.machine.ts | 4 +- .../machines/popover/src/popover.types.ts | 135 +- .../machines/presence/src/presence.types.ts | 38 +- .../machines/pressable/src/pressable.types.ts | 82 +- .../radio-group/src/radio-group.machine.ts | 2 +- .../radio-group/src/radio-group.types.ts | 100 +- .../range-slider/src/range-slider.machine.ts | 13 +- .../range-slider/src/range-slider.types.ts | 210 +-- .../rating-group/src/rating-group.connect.ts | 49 +- .../rating-group/src/rating-group.machine.ts | 4 +- .../rating-group/src/rating-group.types.ts | 148 ++- .../machines/select/src/select.machine.ts | 14 +- packages/machines/select/src/select.types.ts | 225 ++-- .../machines/slider/src/slider.machine.ts | 6 +- packages/machines/slider/src/slider.types.ts | 247 ++-- .../machines/splitter/src/splitter.connect.ts | 50 +- .../machines/splitter/src/splitter.machine.ts | 6 +- .../machines/splitter/src/splitter.types.ts | 99 +- .../machines/switch/src/switch.machine.ts | 2 +- packages/machines/switch/src/switch.types.ts | 119 +- packages/machines/tabs/src/tabs.connect.ts | 35 +- packages/machines/tabs/src/tabs.machine.ts | 4 +- packages/machines/tabs/src/tabs.types.ts | 145 +- packages/machines/tags-input/src/index.ts | 2 +- .../tags-input/src/tags-input.connect.ts | 60 +- .../machines/tags-input/src/tags-input.dom.ts | 21 +- .../tags-input/src/tags-input.machine.ts | 150 ++- .../tags-input/src/tags-input.types.ts | 259 ++-- packages/machines/toast/src/toast.types.ts | 139 +- .../toggle-group/src/toggle-group.machine.ts | 2 +- .../toggle-group/src/toggle-group.types.ts | 98 +- .../machines/tooltip/src/tooltip.machine.ts | 4 +- .../machines/tooltip/src/tooltip.types.ts | 37 +- packages/types/src/index.ts | 14 +- packages/utilities/aria-hidden/src/index.ts | 2 +- packages/utilities/collection/src/types.ts | 8 +- .../dismissable/src/dismissable-layer.ts | 7 +- packages/utilities/dom-event/src/types.ts | 2 +- .../dom-query/src/get-by-typeahead.ts | 4 +- packages/utilities/element-rect/src/index.ts | 4 +- .../utilities/element-size/src/track-size.ts | 2 +- .../utilities/element-size/src/track-sizes.ts | 2 +- packages/utilities/file-utils/README.md | 19 + packages/utilities/file-utils/package.json | 36 + .../utilities/file-utils/src/format-bytes.ts | 11 + .../file-utils/src/get-file-data-url.ts | 20 + .../file-utils/src/get-total-file-size.ts | 3 + packages/utilities/file-utils/src/index.ts | 4 + .../utilities/file-utils/src/is-file-equal.ts | 3 + packages/utilities/file-utils/tsconfig.json | 7 + .../focus-scope/src/focus-containment.ts | 2 +- .../utilities/focus-scope/src/focus-move.ts | 2 +- .../utilities/focus-scope/src/focus-stack.ts | 2 +- packages/utilities/focus-scope/src/index.ts | 9 +- .../utilities/form-utils/src/input-event.ts | 2 +- .../utilities/interact-outside/src/index.ts | 15 +- packages/utilities/live-region/src/index.ts | 2 +- packages/utilities/popper/src/auto-update.ts | 2 +- packages/utilities/popper/src/get-styles.ts | 2 +- packages/utilities/popper/src/types.ts | 2 +- .../utilities/tabbable/src/proxy-tab-focus.ts | 2 +- .../utilities/text-selection/src/index.ts | 2 +- pnpm-lock.yaml | 22 + scripts/typedocs.ts | 16 +- website/components/machines/combobox.tsx | 5 +- website/data/components/accordion.mdx | 15 + website/data/components/avatar.mdx | 14 +- website/data/components/checkbox.mdx | 8 +- website/data/components/combobox.mdx | 12 +- website/data/components/dialog.mdx | 17 +- website/data/components/editable.mdx | 36 +- website/data/components/file-upload.mdx | 15 +- website/data/components/hover-card.mdx | 8 +- website/data/components/menu.mdx | 28 +- website/data/components/number-input.mdx | 15 + website/data/components/pin-input.mdx | 17 +- website/data/components/popover.mdx | 13 +- website/data/components/radio-group.mdx | 8 +- website/data/components/range-slider.mdx | 17 +- website/data/components/rating-group.mdx | 4 +- website/data/components/segmented-control.mdx | 9 +- website/data/components/select.mdx | 27 +- website/data/components/slider.mdx | 15 +- website/data/components/switch.mdx | 7 +- website/data/components/tabs.mdx | 14 +- website/data/components/tags-input.mdx | 22 +- website/data/components/toggle-group.mdx | 10 +- website/data/components/tooltip.mdx | 12 +- .../data/overview/programmatic-control.mdx | 2 +- .../data/snippets/react/combobox/usage.mdx | 5 +- .../data/snippets/solid/combobox/usage.mdx | 5 +- .../data/snippets/vue-jsx/combobox/usage.mdx | 5 +- .../data/snippets/vue-sfc/combobox/usage.mdx | 5 +- website/lib/use-search.ts | 4 +- 155 files changed, 4066 insertions(+), 3207 deletions(-) create mode 100644 .changeset/selfish-windows-warn.md create mode 100644 packages/utilities/file-utils/README.md create mode 100644 packages/utilities/file-utils/package.json create mode 100644 packages/utilities/file-utils/src/format-bytes.ts create mode 100644 packages/utilities/file-utils/src/get-file-data-url.ts create mode 100644 packages/utilities/file-utils/src/get-total-file-size.ts create mode 100644 packages/utilities/file-utils/src/index.ts create mode 100644 packages/utilities/file-utils/src/is-file-equal.ts create mode 100644 packages/utilities/file-utils/tsconfig.json diff --git a/.changeset/selfish-windows-warn.md b/.changeset/selfish-windows-warn.md new file mode 100644 index 0000000000..744af4b1ef --- /dev/null +++ b/.changeset/selfish-windows-warn.md @@ -0,0 +1,33 @@ +--- +"@zag-js/color-picker": minor +"@zag-js/number-input": minor +"@zag-js/range-slider": minor +"@zag-js/rating-group": minor +"@zag-js/toggle-group": minor +"@zag-js/date-picker": minor +"@zag-js/file-upload": minor +"@zag-js/radio-group": minor +"@zag-js/hover-card": minor +"@zag-js/pagination": minor +"@zag-js/tags-input": minor +"@zag-js/accordion": minor +"@zag-js/pin-input": minor +"@zag-js/carousel": minor +"@zag-js/checkbox": minor +"@zag-js/combobox": minor +"@zag-js/editable": minor +"@zag-js/presence": minor +"@zag-js/splitter": minor +"@zag-js/popover": minor +"@zag-js/tooltip": minor +"@zag-js/avatar": minor +"@zag-js/dialog": minor +"@zag-js/select": minor +"@zag-js/slider": minor +"@zag-js/switch": minor +"@zag-js/menu": minor +"@zag-js/tabs": minor +"@zag-js/types": minor +--- + +BREAKING: Unify all callbacks to follow a consistent naming convention diff --git a/.xstate/date-picker.js b/.xstate/date-picker.js index 1a5d13005c..171dc2b3b8 100644 --- a/.xstate/date-picker.js +++ b/.xstate/date-picker.js @@ -101,7 +101,7 @@ const fetchMachine = createMachine({ }, "TRIGGER.CLICK": { target: "open", - actions: ["focusFirstSelectedDate"] + actions: ["focusFirstSelectedDate", "invokeOnOpen"] } } }, @@ -110,7 +110,7 @@ const fetchMachine = createMachine({ on: { "TRIGGER.CLICK": { target: "open", - actions: ["setViewToDay", "focusFirstSelectedDate"] + actions: ["setViewToDay", "focusFirstSelectedDate", "invokeOnOpen"] }, "INPUT.CHANGE": { actions: ["focusParsedDate"] @@ -123,7 +123,7 @@ const fetchMachine = createMachine({ }, "CELL.FOCUS": { target: "open", - actions: ["setView"] + actions: ["setView", "invokeOnOpen"] } } }, @@ -153,7 +153,7 @@ const fetchMachine = createMachine({ }, { target: "focused", cond: "isRangePicker && isSelectingEndDate", - actions: ["setFocusedDate", "setSelectedDate", "setStartIndex", "clearHoveredDate", "focusInputElement"] + actions: ["setFocusedDate", "setSelectedDate", "setStartIndex", "clearHoveredDate", "focusInputElement", "invokeOnClose"] }, // === { @@ -169,7 +169,7 @@ const fetchMachine = createMachine({ actions: ["setFocusedDate", "setSelectedDate"] }, { target: "focused", - actions: ["setFocusedDate", "setSelectedDate", "focusInputElement"] + actions: ["setFocusedDate", "setSelectedDate", "focusInputElement", "invokeOnClose"] } // === ], @@ -193,7 +193,7 @@ const fetchMachine = createMachine({ "GRID.ESCAPE": { cond: "!isInline", target: "focused", - actions: ["setViewToDay", "focusFirstSelectedDate", "focusTriggerElement"] + actions: ["setViewToDay", "focusFirstSelectedDate", "focusTriggerElement", "invokeOnClose"] }, "GRID.ENTER": [{ cond: "isMonthView", @@ -212,7 +212,7 @@ const fetchMachine = createMachine({ }, { target: "focused", cond: "isRangePicker && isSelectingEndDate", - actions: ["setSelectedDate", "setStartIndex", "focusInputElement"] + actions: ["setSelectedDate", "setStartIndex", "focusInputElement", "invokeOnClose"] }, // === { @@ -228,7 +228,7 @@ const fetchMachine = createMachine({ actions: ["selectFocusedDate"] }, { target: "focused", - actions: ["selectFocusedDate", "focusInputElement"] + actions: ["selectFocusedDate", "focusInputElement", "invokeOnClose"] } // === ], @@ -294,7 +294,8 @@ const fetchMachine = createMachine({ actions: ["focusSectionEnd"] }], "TRIGGER.CLICK": { - target: "focused" + target: "focused", + actions: ["invokeOnClose"] }, "VIEW.CHANGE": [{ cond: "isDayView", @@ -306,10 +307,10 @@ const fetchMachine = createMachine({ DISMISS: [{ cond: "isTargetFocusable", target: "idle", - actions: ["setStartIndex"] + actions: ["setStartIndex", "invokeOnClose"] }, { target: "focused", - actions: ["focusTriggerElement", "setStartIndex"] + actions: ["focusTriggerElement", "setStartIndex", "invokeOnClose"] }] } } diff --git a/.xstate/editable.js b/.xstate/editable.js index e4bfee2d9b..06b64ea467 100644 --- a/.xstate/editable.js +++ b/.xstate/editable.js @@ -12,6 +12,7 @@ const { const fetchMachine = createMachine({ id: "editable", initial: ctx.startWithEditView ? "edit" : "preview", + entry: ctx.startWithEditView ? ["focusInput"] : undefined, context: { "activateOnDblClick": false, "activateOnFocus": false, diff --git a/.xstate/tags-input.js b/.xstate/tags-input.js index 40ecd32a78..403de88624 100644 --- a/.xstate/tags-input.js +++ b/.xstate/tags-input.js @@ -14,24 +14,23 @@ const fetchMachine = createMachine({ initial: ctx.autoFocus ? "focused:input" : "idle", context: { "allowEditTag": false, - "!isTagFocused": false, + "!isTagHighlighted": false, "(!isAtMax || allowOverflow) && !isInputValueEmpty": false, "addOnBlur": false, "clearOnBlur": false, - "!hasFocusedId": false, + "!hasHighlightedTag": false, "addOnBlur": false, "clearOnBlur": false, "hasTags && isInputCaretAtStart": false, "hasTags && isInputCaretAtStart": false, "addOnPaste": false, - "hasTags && isInputCaretAtStart && !isLastTagFocused": false, - "allowEditTag && hasFocusedId": false, - "isFirstTagFocused": false, + "hasTags && isInputCaretAtStart && !isLastTagHighlighted": false, + "allowEditTag && hasHighlightedTag": false, + "isFirstTagHighlighted": false, "isInputRelatedTarget": false }, - activities: ["trackFormControlState"], - entry: ["setupLiveRegion"], - exit: ["removeLiveRegion", "clearLog"], + activities: ["trackLiveRegion", "trackFormControlState"], + exit: ["clearLog"], on: { DOUBLE_CLICK_TAG: { internal: true, @@ -41,9 +40,9 @@ const fetchMachine = createMachine({ }, POINTER_DOWN_TAG: { internal: true, - cond: "!isTagFocused", + cond: "!isTagHighlighted", target: "navigating:tag", - actions: ["focusTag", "focusInput"] + actions: ["highlightTag", "focusInput"] }, SET_INPUT_VALUE: { actions: ["setInputValue"] @@ -83,14 +82,14 @@ const fetchMachine = createMachine({ on: { FOCUS: "focused:input", POINTER_DOWN: { - cond: "!hasFocusedId", + cond: "!hasHighlightedTag", target: "focused:input" } } }, "focused:input": { tags: ["focused"], - entry: ["focusInput", "clearFocusedId"], + entry: ["focusInput", "clearHighlightedId"], activities: ["trackInteractOutside"], on: { TYPE: { @@ -116,12 +115,12 @@ const fetchMachine = createMachine({ ARROW_LEFT: { cond: "hasTags && isInputCaretAtStart", target: "navigating:tag", - actions: "focusLastTag" + actions: "highlightLastTag" }, BACKSPACE: { target: "navigating:tag", cond: "hasTags && isInputCaretAtStart", - actions: "focusLastTag" + actions: "highlightLastTag" }, PASTE: { cond: "addOnPaste", @@ -134,20 +133,20 @@ const fetchMachine = createMachine({ activities: ["trackInteractOutside"], on: { ARROW_RIGHT: [{ - cond: "hasTags && isInputCaretAtStart && !isLastTagFocused", - actions: "focusNextTag" + cond: "hasTags && isInputCaretAtStart && !isLastTagHighlighted", + actions: "highlightNextTag" }, { target: "focused:input" }], ARROW_LEFT: { - actions: "focusPrevTag" + actions: "highlightPrevTag" }, BLUR: { target: "idle", - actions: "clearFocusedId" + actions: "clearHighlightedId" }, ENTER: { - cond: "allowEditTag && hasFocusedId", + cond: "allowEditTag && hasHighlightedTag", target: "editing:tag", actions: ["setEditedId", "initializeEditedTagValue", "focusEditedTagInput"] }, @@ -158,13 +157,13 @@ const fetchMachine = createMachine({ actions: "setInputValue" }, BACKSPACE: [{ - cond: "isFirstTagFocused", - actions: ["deleteFocusedTag", "focusFirstTag"] + cond: "isFirstTagHighlighted", + actions: ["deleteHighlightedTag", "highlightFirstTag"] }, { - actions: ["deleteFocusedTag", "focusPrevTag"] + actions: ["deleteHighlightedTag", "highlightPrevTag"] }], DELETE: { - actions: ["deleteFocusedTag", "focusTagAtIndex"] + actions: ["deleteHighlightedTag", "highlightTagAtIndex"] } } }, @@ -178,19 +177,19 @@ const fetchMachine = createMachine({ }, TAG_INPUT_ESCAPE: { target: "navigating:tag", - actions: ["clearEditedTagValue", "focusInput", "clearEditedId", "focusTagAtIndex"] + actions: ["clearEditedTagValue", "focusInput", "clearEditedId", "highlightTagAtIndex"] }, TAG_INPUT_BLUR: [{ cond: "isInputRelatedTarget", target: "navigating:tag", - actions: ["clearEditedTagValue", "clearFocusedId", "clearEditedId"] + actions: ["clearEditedTagValue", "clearHighlightedId", "clearEditedId"] }, { target: "idle", - actions: ["clearEditedTagValue", "clearFocusedId", "clearEditedId", "raiseExternalBlurEvent"] + actions: ["clearEditedTagValue", "clearHighlightedId", "clearEditedId", "raiseExternalBlurEvent"] }], TAG_INPUT_ENTER: { target: "navigating:tag", - actions: ["submitEditedTagValue", "focusInput", "clearEditedId", "focusTagAtIndex"] + actions: ["submitEditedTagValue", "focusInput", "clearEditedId", "highlightTagAtIndex"] } } } @@ -205,16 +204,16 @@ const fetchMachine = createMachine({ }, guards: { "allowEditTag": ctx => ctx["allowEditTag"], - "!isTagFocused": ctx => ctx["!isTagFocused"], + "!isTagHighlighted": ctx => ctx["!isTagHighlighted"], "(!isAtMax || allowOverflow) && !isInputValueEmpty": ctx => ctx["(!isAtMax || allowOverflow) && !isInputValueEmpty"], "addOnBlur": ctx => ctx["addOnBlur"], "clearOnBlur": ctx => ctx["clearOnBlur"], - "!hasFocusedId": ctx => ctx["!hasFocusedId"], + "!hasHighlightedTag": ctx => ctx["!hasHighlightedTag"], "hasTags && isInputCaretAtStart": ctx => ctx["hasTags && isInputCaretAtStart"], "addOnPaste": ctx => ctx["addOnPaste"], - "hasTags && isInputCaretAtStart && !isLastTagFocused": ctx => ctx["hasTags && isInputCaretAtStart && !isLastTagFocused"], - "allowEditTag && hasFocusedId": ctx => ctx["allowEditTag && hasFocusedId"], - "isFirstTagFocused": ctx => ctx["isFirstTagFocused"], + "hasTags && isInputCaretAtStart && !isLastTagHighlighted": ctx => ctx["hasTags && isInputCaretAtStart && !isLastTagHighlighted"], + "allowEditTag && hasHighlightedTag": ctx => ctx["allowEditTag && hasHighlightedTag"], + "isFirstTagHighlighted": ctx => ctx["isFirstTagHighlighted"], "isInputRelatedTarget": ctx => ctx["isInputRelatedTarget"] } }); \ No newline at end of file diff --git a/examples/next-ts/package.json b/examples/next-ts/package.json index 3ebb316cfb..e8cb03b47f 100644 --- a/examples/next-ts/package.json +++ b/examples/next-ts/package.json @@ -35,6 +35,7 @@ "@zag-js/element-rect": "workspace:*", "@zag-js/element-size": "workspace:*", "@zag-js/file-upload": "workspace:*", + "@zag-js/file-utils": "workspace:*", "@zag-js/focus-scope": "workspace:*", "@zag-js/focus-visible": "workspace:*", "@zag-js/form-utils": "workspace:*", @@ -90,4 +91,4 @@ "typescript": "5.2.2" }, "license": "MIT" -} +} \ No newline at end of file diff --git a/examples/next-ts/pages/combobox.tsx b/examples/next-ts/pages/combobox.tsx index c9d010e175..b1af34e789 100644 --- a/examples/next-ts/pages/combobox.tsx +++ b/examples/next-ts/pages/combobox.tsx @@ -21,10 +21,11 @@ export default function Page() { combobox.machine({ id: useId(), collection, - onOpen() { + onOpenChange(details) { + if (!details.open) return setOptions(comboboxData) }, - onInputChange({ value }) { + onInputValueChange({ value }) { const filtered = comboboxData.filter((item) => item.label.toLowerCase().includes(value.toLowerCase())) setOptions(filtered.length > 0 ? filtered : comboboxData) }, diff --git a/examples/next-ts/pages/hover-card.tsx b/examples/next-ts/pages/hover-card.tsx index bfdf2b9199..f73edac1ac 100644 --- a/examples/next-ts/pages/hover-card.tsx +++ b/examples/next-ts/pages/hover-card.tsx @@ -1,3 +1,4 @@ +/* eslint-disable react/jsx-no-target-blank */ import * as hoverCard from "@zag-js/hover-card" import { normalizeProps, Portal, useMachine } from "@zag-js/react" import { hoverCardControls } from "@zag-js/shared" diff --git a/examples/next-ts/pages/pagination.tsx b/examples/next-ts/pages/pagination.tsx index 13c819a103..65199aba17 100644 --- a/examples/next-ts/pages/pagination.tsx +++ b/examples/next-ts/pages/pagination.tsx @@ -14,7 +14,7 @@ export default function Page() { pagination.machine({ id: useId(), count: paginationData.length, - onChange: console.log, + onPageChange: console.log, }), { context: controls.context, diff --git a/examples/next-ts/pages/popper.tsx b/examples/next-ts/pages/popper.tsx index 7e65654803..a1e03511ab 100644 --- a/examples/next-ts/pages/popper.tsx +++ b/examples/next-ts/pages/popper.tsx @@ -14,7 +14,7 @@ export default function App() { }) }, []) - const styles = getPlacementStyles({ measured: true }) + const styles = getPlacementStyles({ placement: "bottom" }) return (
diff --git a/examples/next-ts/pages/radio-group.tsx b/examples/next-ts/pages/radio-group.tsx index 7e39c88c5b..7313db72e8 100644 --- a/examples/next-ts/pages/radio-group.tsx +++ b/examples/next-ts/pages/radio-group.tsx @@ -50,9 +50,6 @@ export default function Page() { - diff --git a/examples/next-ts/pages/select.tsx b/examples/next-ts/pages/select.tsx index b337c6eb4d..294f1716a1 100644 --- a/examples/next-ts/pages/select.tsx +++ b/examples/next-ts/pages/select.tsx @@ -15,14 +15,14 @@ export default function Page() { collection: select.collection({ items: selectData }), id: useId(), name: "country", - onHighlight(details) { - console.log("onHighlight", details) + onHighlightChange(details) { + console.log("onHighlightChange", details) }, - onChange(details) { + onValueChange(details) { console.log("onChange", details) }, - onOpen() { - console.log("onOpen") + onOpenChange(details) { + console.log("onOpenChange", details) }, }), { diff --git a/examples/nuxt-ts/package.json b/examples/nuxt-ts/package.json index 10150907d5..aa41760266 100644 --- a/examples/nuxt-ts/package.json +++ b/examples/nuxt-ts/package.json @@ -34,6 +34,7 @@ "@zag-js/element-rect": "workspace:*", "@zag-js/element-size": "workspace:*", "@zag-js/file-upload": "workspace:*", + "@zag-js/file-utils": "workspace:*", "@zag-js/focus-scope": "workspace:*", "@zag-js/focus-visible": "workspace:*", "@zag-js/form-utils": "workspace:*", @@ -81,4 +82,4 @@ "@types/node": "^20.6.0", "nuxt": "^3.7.1" } -} +} \ No newline at end of file diff --git a/examples/nuxt-ts/pages/combobox.vue b/examples/nuxt-ts/pages/combobox.vue index e5da5ab0be..771437f2ba 100644 --- a/examples/nuxt-ts/pages/combobox.vue +++ b/examples/nuxt-ts/pages/combobox.vue @@ -18,10 +18,11 @@ const [state, send] = useMachine( combobox.machine({ id: "1", collection: collectionRef.value, - onOpen() { + onOpenChange(details) { + if (!details.open) return options.value = comboboxData }, - onInputChange({ value }) { + onInputValueChange({ value }) { const filtered = comboboxData.filter((item) => item.label.toLowerCase().includes(value.toLowerCase())) options.value = filtered.length > 0 ? filtered : comboboxData }, diff --git a/examples/nuxt-ts/pages/radio-group.vue b/examples/nuxt-ts/pages/radio-group.vue index d9bf378675..7c27c476c3 100644 --- a/examples/nuxt-ts/pages/radio-group.vue +++ b/examples/nuxt-ts/pages/radio-group.vue @@ -44,7 +44,6 @@ const api = computed(() => radio.connect(state.value, send, normalizeProps)) - diff --git a/examples/shadow-dom/package.json b/examples/shadow-dom/package.json index 9d760b88c0..cfc304f0ba 100644 --- a/examples/shadow-dom/package.json +++ b/examples/shadow-dom/package.json @@ -33,6 +33,7 @@ "@zag-js/element-rect": "workspace:*", "@zag-js/element-size": "workspace:*", "@zag-js/file-upload": "workspace:*", + "@zag-js/file-utils": "workspace:*", "@zag-js/focus-scope": "workspace:*", "@zag-js/focus-visible": "workspace:*", "@zag-js/form-utils": "workspace:*", @@ -81,4 +82,4 @@ "typescript": "^5.2.2", "vite": "^4.4.9" } -} +} \ No newline at end of file diff --git a/examples/solid-ts/package.json b/examples/solid-ts/package.json index e16dcae6f9..31fea555d2 100644 --- a/examples/solid-ts/package.json +++ b/examples/solid-ts/package.json @@ -43,6 +43,7 @@ "@zag-js/element-rect": "workspace:*", "@zag-js/element-size": "workspace:*", "@zag-js/file-upload": "workspace:*", + "@zag-js/file-utils": "workspace:*", "@zag-js/focus-scope": "workspace:*", "@zag-js/focus-visible": "workspace:*", "@zag-js/form-utils": "workspace:*", @@ -85,4 +86,4 @@ "form-serialize": "0.7.2", "solid-js": "1.7.11" } -} +} \ No newline at end of file diff --git a/examples/solid-ts/src/pages/combobox.tsx b/examples/solid-ts/src/pages/combobox.tsx index a8d0b3cdf7..2e8b998976 100644 --- a/examples/solid-ts/src/pages/combobox.tsx +++ b/examples/solid-ts/src/pages/combobox.tsx @@ -28,10 +28,11 @@ export default function Page() { combobox.machine({ id: createUniqueId(), collection: collection(), - onOpen() { + onOpenChange(details) { + if (!details.open) return setOptions(comboboxData) }, - onInputChange({ value }) { + onInputValueChange({ value }) { const filtered = comboboxData.filter((item) => item.label.toLowerCase().includes(value.toLowerCase())) setOptions(filtered.length > 0 ? filtered : comboboxData) }, diff --git a/examples/solid-ts/src/pages/radio-group.tsx b/examples/solid-ts/src/pages/radio-group.tsx index 6f80836a9b..882527e667 100644 --- a/examples/solid-ts/src/pages/radio-group.tsx +++ b/examples/solid-ts/src/pages/radio-group.tsx @@ -53,9 +53,6 @@ export default function Page() { - diff --git a/examples/vue-ts/package.json b/examples/vue-ts/package.json index 19e6e9a084..d6c3a0a86a 100644 --- a/examples/vue-ts/package.json +++ b/examples/vue-ts/package.json @@ -35,6 +35,7 @@ "@zag-js/element-rect": "workspace:*", "@zag-js/element-size": "workspace:*", "@zag-js/file-upload": "workspace:*", + "@zag-js/file-utils": "workspace:*", "@zag-js/focus-scope": "workspace:*", "@zag-js/focus-visible": "workspace:*", "@zag-js/form-utils": "workspace:*", @@ -95,4 +96,4 @@ "vue-tsc": "1.8.10" }, "license": "MIT" -} +} \ No newline at end of file diff --git a/examples/vue-ts/src/pages/combobox.tsx b/examples/vue-ts/src/pages/combobox.tsx index ac3f7a8d18..9d0edb98c2 100644 --- a/examples/vue-ts/src/pages/combobox.tsx +++ b/examples/vue-ts/src/pages/combobox.tsx @@ -23,10 +23,11 @@ export default defineComponent({ combobox.machine({ collection: collectionRef.value, id: "1", - onOpen() { + onOpenChange(details) { + if (!details.open) return options.value = comboboxData }, - onInputChange({ value }) { + onInputValueChange({ value }) { const filtered = comboboxData.filter((item) => item.label.toLowerCase().includes(value.toLowerCase())) options.value = filtered.length > 0 ? filtered : comboboxData }, diff --git a/examples/vue-ts/src/pages/pagination.tsx b/examples/vue-ts/src/pages/pagination.tsx index 212b89abd6..b415a2ba15 100644 --- a/examples/vue-ts/src/pages/pagination.tsx +++ b/examples/vue-ts/src/pages/pagination.tsx @@ -16,7 +16,7 @@ export default defineComponent({ pagination.machine({ id: "1", count: paginationData.length, - onChange: console.log, + onPageChange: console.log, }), { context: controls.context, diff --git a/examples/vue-ts/src/pages/radio-group.tsx b/examples/vue-ts/src/pages/radio-group.tsx index b0bd473794..bfca8302ab 100644 --- a/examples/vue-ts/src/pages/radio-group.tsx +++ b/examples/vue-ts/src/pages/radio-group.tsx @@ -52,9 +52,6 @@ export default defineComponent({ - diff --git a/packages/docs/api.json b/packages/docs/api.json index f9811155b2..98a04469a5 100644 --- a/packages/docs/api.json +++ b/packages/docs/api.json @@ -19,19 +19,6 @@ } }, "context": { - "dir": { - "type": "\"ltr\" | \"rtl\"", - "description": "The document's text/writing direction.", - "defaultValue": "\"ltr\"" - }, - "id": { - "type": "string", - "description": "The unique identifier of the machine." - }, - "getRootNode": { - "type": "() => ShadowRoot | Node | Document", - "description": "A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron." - }, "ids": { "type": "Partial<{ root: string; item(value: string): string; content(value: string): string; trigger(value: string): string; }>", "description": "The ids of the elements in the accordion. Useful for composition." @@ -54,8 +41,8 @@ "type": "boolean", "description": "Whether the accordion items are disabled" }, - "onChange": { - "type": "(details: ChangeDetails) => void", + "onValueChange": { + "type": "(details: ValueChangeDetails) => void", "description": "The callback fired when the state of opened/closed accordion items changes." }, "onFocusChange": { @@ -65,6 +52,19 @@ "orientation": { "type": "\"horizontal\" | \"vertical\"", "description": "The orientation of the accordion items." + }, + "dir": { + "type": "\"ltr\" | \"rtl\"", + "description": "The document's text/writing direction.", + "defaultValue": "\"ltr\"" + }, + "id": { + "type": "string", + "description": "The unique identifier of the machine." + }, + "getRootNode": { + "type": "() => ShadowRoot | Node | Document", + "description": "A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron." } } }, @@ -92,6 +92,10 @@ } }, "context": { + "onLoadingStatusChange": { + "type": "(details: StatusChangeDetails) => void", + "description": "Functional called when the image loading status changes." + }, "id": { "type": "string", "description": "The unique identifier of the machine." @@ -137,7 +141,7 @@ "description": "Function to scroll to the previous slide" }, "getSlideState": { - "type": "(props: SlideProps) => { valueText: string; isCurrent: boolean; isNext: boolean; isPrevious: boolean; isInView: boolean; }", + "type": "(props: SlideState", "description": "Returns the state of a specific slide" }, "play": { @@ -150,19 +154,6 @@ } }, "context": { - "dir": { - "type": "\"ltr\" | \"rtl\"", - "description": "The document's text/writing direction.", - "defaultValue": "\"ltr\"" - }, - "id": { - "type": "string", - "description": "The unique identifier of the machine." - }, - "getRootNode": { - "type": "() => ShadowRoot | Node | Document", - "description": "A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron." - }, "orientation": { "type": "\"horizontal\" | \"vertical\"", "description": "The orientation of the carousel.", @@ -189,12 +180,25 @@ "description": "The amount of space between slides." }, "onSlideChange": { - "type": "(details: ChangeDetails) => void", + "type": "(details: SlideChangeDetails) => void", "description": "Function called when the slide changes." }, "ids": { "type": "Partial<{ root: string; viewport: string; slide(index: number): string; slideGroup: string; nextSlideTrigger: string; prevSlideTrigger: string; indicatorGroup: string; indicator(index: number): string; }>", "description": "The ids of the elements in the carousel. Useful for composition." + }, + "dir": { + "type": "\"ltr\" | \"rtl\"", + "description": "The document's text/writing direction.", + "defaultValue": "\"ltr\"" + }, + "id": { + "type": "string", + "description": "The unique identifier of the machine." + }, + "getRootNode": { + "type": "() => ShadowRoot | Node | Document", + "description": "A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron." } } }, @@ -230,19 +234,6 @@ } }, "context": { - "dir": { - "type": "\"ltr\" | \"rtl\"", - "description": "The document's text/writing direction.", - "defaultValue": "\"ltr\"" - }, - "id": { - "type": "string", - "description": "The unique identifier of the machine." - }, - "getRootNode": { - "type": "() => ShadowRoot | Node | Document", - "description": "A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron." - }, "ids": { "type": "Partial<{ root: string; hiddenInput: string; control: string; label: string; }>", "description": "The ids of the elements in the checkbox. Useful for composition." @@ -263,8 +254,8 @@ "type": "CheckedState", "description": "If `true`, the checkbox will be checked." }, - "onChange": { - "type": "(details: { checked: CheckedState; }) => void", + "onCheckedChange": { + "type": "(details: CheckedChangeDetails) => void", "description": "The callback invoked when the checked state of the `Checkbox` changes." }, "name": { @@ -279,6 +270,19 @@ "type": "string", "description": "The value of checkbox input. Useful for form submission.", "defaultValue": "\"on\"" + }, + "dir": { + "type": "\"ltr\" | \"rtl\"", + "description": "The document's text/writing direction.", + "defaultValue": "\"ltr\"" + }, + "id": { + "type": "string", + "description": "The unique identifier of the machine." + }, + "getRootNode": { + "type": "() => ShadowRoot | Node | Document", + "description": "A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron." } } }, @@ -322,14 +326,6 @@ } }, "context": { - "id": { - "type": "string", - "description": "The unique identifier of the machine." - }, - "getRootNode": { - "type": "() => ShadowRoot | Node | Document", - "description": "A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron." - }, "ids": { "type": "Partial<{ content: string; area: string; areaGradient: string; areaThumb: string; channelInput(id: string): string; channelSliderTrack(id: ColorChannel): string; }>", "description": "The ids of the elements in the color picker. Useful for composition." @@ -350,17 +346,25 @@ "type": "boolean", "description": "Whether the color picker is read-only" }, - "onChange": { - "type": "(details: ChangeDetails) => void", + "onValueChange": { + "type": "(details: ValueChangeDetails) => void", "description": "Handler that is called when the value changes, as the user drags." }, - "onChangeEnd": { - "type": "(details: ChangeDetails) => void", + "onValueChangeEnd": { + "type": "(details: ValueChangeDetails) => void", "description": "Handler that is called when the user stops dragging." }, "name": { "type": "string", "description": "The name for the form input" + }, + "id": { + "type": "string", + "description": "The unique identifier of the machine." + }, + "getRootNode": { + "type": "() => ShadowRoot | Node | Document", + "description": "A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron." } } }, @@ -387,7 +391,7 @@ "description": "The value of the highlighted item" }, "highlightedItem": { - "type": "CollectionItem", + "type": "V", "description": "The highlighted item" }, "highlightValue": { @@ -395,7 +399,7 @@ "description": "The value of the combobox input" }, "selectedItems": { - "type": "CollectionItem[]", + "type": "V[]", "description": "The selected items" }, "hasSelectedItems": { @@ -443,24 +447,11 @@ "description": "Function to close the combobox" }, "setCollection": { - "type": "(collection: Collection) => void", + "type": "(collection: Collection) => void", "description": "Function to set the collection of items" } }, "context": { - "dir": { - "type": "\"ltr\" | \"rtl\"", - "description": "The document's text/writing direction.", - "defaultValue": "\"ltr\"" - }, - "id": { - "type": "string", - "description": "The unique identifier of the machine." - }, - "getRootNode": { - "type": "() => ShadowRoot | Node | Document", - "description": "A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron." - }, "ids": { "type": "Partial<{ root: string; label: string; control: string; input: string; content: string; trigger: string; clearTrigger: string; item(id: string, index?: number): string; positioner: string; itemGroup(id: string | number): string; itemGroupLabel(id: string | number): string; }>", "description": "The ids of the elements in the combobox. Useful for composition." @@ -533,42 +524,26 @@ "type": "PositioningOptions", "description": "The positioning options to dynamically position the menu" }, - "onInputChange": { - "type": "(details: { value: string; }) => void", + "onInputValueChange": { + "type": "(details: InputValueChangeDetails) => void", "description": "Function called when the input's value changes" }, - "onChange": { - "type": "(details: { value: string[]; items: CollectionItem[]; }) => void", + "onValueChange": { + "type": "(details: ValueChangeDetails) => void", "description": "Function called when a new item is selected" }, - "onHighlight": { - "type": "(details: { value: string; item: CollectionItem; }) => void", + "onHighlightChange": { + "type": "(details: HighlightChangeDetails) => void", "description": "Function called when an item is highlighted using the pointer\nor keyboard navigation." }, - "onOpen": { - "type": "VoidFunction", + "onOpenChange": { + "type": "(details: OpenChangeDetails) => void", "description": "Function called when the popup is opened" }, - "onClose": { - "type": "VoidFunction", - "description": "Function called when the popup is closed" - }, "translations": { "type": "IntlTranslations", "description": "Specifies the localized strings that identifies the accessibility elements and their states" }, - "onPointerDownOutside": { - "type": "(event: PointerDownOutsideEvent) => void", - "description": "Function called when the pointer is pressed down outside the combobox" - }, - "onFocusOutside": { - "type": "(event: FocusOutsideEvent) => void", - "description": "Function called when the focus is moved outside the combobox" - }, - "onInteractOutside": { - "type": "(event: InteractOutsideEvent) => void", - "description": "Function called when an interaction happens outside the combobox" - }, "collection": { "type": "Collection", "description": "The collection of items" @@ -580,6 +555,31 @@ "closeOnSelect": { "type": "boolean", "description": "Whether to close the combobox when an item is selected." + }, + "dir": { + "type": "\"ltr\" | \"rtl\"", + "description": "The document's text/writing direction.", + "defaultValue": "\"ltr\"" + }, + "id": { + "type": "string", + "description": "The unique identifier of the machine." + }, + "getRootNode": { + "type": "() => ShadowRoot | Node | Document", + "description": "A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron." + }, + "onPointerDownOutside": { + "type": "(event: PointerDownOutsideEvent) => void", + "description": "Function called when the pointer is pressed down outside the combobox" + }, + "onFocusOutside": { + "type": "(event: FocusOutsideEvent) => void", + "description": "Function called when the focus is moved outside the combobox" + }, + "onInteractOutside": { + "type": "(event: InteractOutsideEvent) => void", + "description": "Function called when an interaction happens outside the combobox" } } }, @@ -597,10 +597,6 @@ "type": "DateView", "description": "The current view of the date picker" }, - "matchView": { - "type": "typeof matchView", - "description": "Matcher for the current view of the date picker" - }, "getDaysInWeek": { "type": "(weekIndex: number, from?: DateValue[]", "description": "Returns an array of days in the week index counted from the provided start date, or the first visible date if not given." @@ -690,11 +686,11 @@ "description": "The visible range of dates." }, "getYears": { - "type": "() => { label: string; value: number; }[]", + "type": "() => GridItem[]", "description": "Returns the months of the year" }, "getYearsGrid": { - "type": "(props?: { columns?: number; }) => { label: string; value: number; }[][]", + "type": "(props?: YearGridValue", "description": "Returns the years of the decade based on the columns.\nRepresented as an array of arrays of years." }, "getDecade": { @@ -702,11 +698,11 @@ "description": "Returns the start and end years of the decade." }, "getMonths": { - "type": "(props?: { format?: \"short\" | \"long\"; }) => { label: string; value: number; }[]", + "type": "(props?: { format?: \"short\" | \"long\"; }) => GridItem[]", "description": "Returns the months of the year" }, "getMonthsGrid": { - "type": "(props?: { columns?: number; format?: \"short\" | \"long\"; }) => { label: string; value: number; }[][]", + "type": "(props?: MonthGridValue", "description": "Returns the months of the year based on the columns.\nRepresented as an array of arrays of months." }, "format": { @@ -726,24 +722,15 @@ "description": "Goes to the previous month/year/decade." }, "getDayCellState": { - "type": "(props: DayCellProps) => { isInvalid: boolean; isDisabled: boolean; isSelected: boolean; isUnavailable: boolean; isOutsideRange: boolean; isInRange: boolean; isFirstInRange: boolean; isLastInRange: boolean; isToday: boolean; isWeekend: boolean; formattedDate: string; readonly isFocused: boolean; readonly ariaLabel: string; readonly isSelectable: boolean; }", + "type": "(props: DayCellState", "description": "Returns the state details for a given cell." + }, + "getMonthCellState": { + "type": "(props: CellState", + "description": "Returns the state details for a given month cell." } }, "context": { - "dir": { - "type": "\"ltr\" | \"rtl\"", - "description": "The document's text/writing direction.", - "defaultValue": "\"ltr\"" - }, - "id": { - "type": "string", - "description": "The unique identifier of the machine." - }, - "getRootNode": { - "type": "() => ShadowRoot | Node | Document", - "description": "A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron." - }, "messages": { "type": "IntlMessages", "description": "The localized messages to use." @@ -804,18 +791,22 @@ "type": "boolean", "description": "Whether the calendar should have a fixed number of weeks.\nThis renders the calendar with 6 weeks instead of 5 or 6." }, - "onChange": { - "type": "(details: { value: DateValue[]; }) => void", + "onValueChange": { + "type": "(details: ValueChangeDetails) => void", "description": "Function called when the value changes." }, "onFocusChange": { - "type": "(details: ChangeDetails<{ focusedValue: DateView; }>) => void", + "type": "(details: FocusChangeDetails) => void", "description": "Function called when the focused date changes." }, "onViewChange": { - "type": "(details: { view: DateView; }) => void", + "type": "(details: ViewChangeDetails) => void", "description": "Function called when the view changes." }, + "onOpenChange": { + "type": "(details: OpenChangeDetails) => void", + "description": "Function called when the calendar opens or closes." + }, "isDateUnavailable": { "type": "(date: DateValue, locale: string) => boolean", "description": "Returns whether a date of the calendar is available." @@ -844,6 +835,19 @@ "positioning": { "type": "PositioningOptions", "description": "The user provided options used to position the date picker content" + }, + "dir": { + "type": "\"ltr\" | \"rtl\"", + "description": "The document's text/writing direction.", + "defaultValue": "\"ltr\"" + }, + "id": { + "type": "string", + "description": "The unique identifier of the machine." + }, + "getRootNode": { + "type": "() => ShadowRoot | Node | Document", + "description": "A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron." } } }, @@ -863,19 +867,6 @@ } }, "context": { - "dir": { - "type": "\"ltr\" | \"rtl\"", - "description": "The document's text/writing direction.", - "defaultValue": "\"ltr\"" - }, - "id": { - "type": "string", - "description": "The unique identifier of the machine." - }, - "getRootNode": { - "type": "() => ShadowRoot | Node | Document", - "description": "A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron." - }, "ids": { "type": "Partial<{ trigger: string; container: string; backdrop: string; content: string; closeTrigger: string; title: string; description: string; }>", "description": "The ids of the elements in the dialog. Useful for composition." @@ -904,20 +895,16 @@ "type": "boolean", "description": "Whether to restore focus to the element that had focus before the dialog was opened" }, - "onClose": { - "type": "() => void", - "description": "Callback to be invoked when the dialog is closed" - }, - "onOpen": { - "type": "() => void", - "description": "Callback to be invoked when the dialog is opened" + "onOpenChange": { + "type": "(details: OpenChangeDetails) => void", + "description": "Callback to be invoked when the dialog is opened or closed" }, "closeOnOutsideClick": { "type": "boolean", "description": "Whether to close the dialog when the outside is clicked" }, "onOutsideClick": { - "type": "() => void", + "type": "VoidFunction", "description": "Callback to be invoked when the outside is clicked" }, "closeOnEsc": { @@ -925,7 +912,7 @@ "description": "Whether to close the dialog when the escape key is pressed" }, "onEsc": { - "type": "() => void", + "type": "VoidFunction", "description": "Callback to be invoked when the escape key is pressed" }, "aria-label": { @@ -940,6 +927,19 @@ "open": { "type": "boolean", "description": "Whether the dialog is open" + }, + "dir": { + "type": "\"ltr\" | \"rtl\"", + "description": "The document's text/writing direction.", + "defaultValue": "\"ltr\"" + }, + "id": { + "type": "string", + "description": "The unique identifier of the machine." + }, + "getRootNode": { + "type": "() => Node | ShadowRoot | Document", + "description": "A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron." } } }, @@ -979,19 +979,6 @@ } }, "context": { - "dir": { - "type": "\"ltr\" | \"rtl\"", - "description": "The document's text/writing direction.", - "defaultValue": "\"ltr\"" - }, - "id": { - "type": "string", - "description": "The unique identifier of the machine." - }, - "getRootNode": { - "type": "() => ShadowRoot | Node | Document", - "description": "A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron." - }, "ids": { "type": "Partial<{ root: string; area: string; label: string; preview: string; input: string; controls: string; submitTrigger: string; cancelTrigger: string; editTrigger: string; }>", "description": "The ids of the elements in the editable. Useful for composition." @@ -1046,16 +1033,16 @@ "type": "boolean", "description": "Whether the editable is readonly" }, - "onChange": { - "type": "(details: { value: string; }) => void", + "onValueChange": { + "type": "(details: ValueChangeDetails) => void", "description": "The callback that is called when the editable's value is changed" }, - "onCancel": { - "type": "(details: { value: string; }) => void", + "onValueRevert": { + "type": "(details: ValueChangeDetails) => void", "description": "The callback that is called when the esc key is pressed or the cancel button is clicked" }, - "onSubmit": { - "type": "(details: { value: string; }) => void", + "onValueCommit": { + "type": "(details: ValueChangeDetails) => void", "description": "The callback that is called when the editable's value is submitted." }, "onEdit": { @@ -1073,22 +1060,47 @@ "finalFocusEl": { "type": "() => HTMLElement", "description": "The element that should receive focus when the editable is closed.\nBy default, it will focus on the trigger element." - } - } - }, - "file-upload": { - "api": { - "isDragging": { - "type": "boolean", - "description": "Whether the user is dragging something over the root element" }, - "isFocused": { - "type": "boolean", - "description": "Whether the user is focused on the root element" + "dir": { + "type": "\"ltr\" | \"rtl\"", + "description": "The document's text/writing direction.", + "defaultValue": "\"ltr\"" }, - "open": { - "type": "() => void", - "description": "Function to open the file dialog" + "id": { + "type": "string", + "description": "The unique identifier of the machine." + }, + "getRootNode": { + "type": "() => Node | ShadowRoot | Document", + "description": "A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron." + }, + "onPointerDownOutside": { + "type": "(event: PointerDownOutsideEvent) => void", + "description": "Function called when the pointer is pressed down outside the combobox" + }, + "onFocusOutside": { + "type": "(event: FocusOutsideEvent) => void", + "description": "Function called when the focus is moved outside the combobox" + }, + "onInteractOutside": { + "type": "(event: InteractOutsideEvent) => void", + "description": "Function called when an interaction happens outside the combobox" + } + } + }, + "file-upload": { + "api": { + "isDragging": { + "type": "boolean", + "description": "Whether the user is dragging something over the root element" + }, + "isFocused": { + "type": "boolean", + "description": "Whether the user is focused on the root element" + }, + "open": { + "type": "() => void", + "description": "Function to open the file dialog" }, "deleteFile": { "type": "(file: File) => void", @@ -1098,24 +1110,16 @@ "type": "File[]", "description": "The files that have been dropped or selected" }, - "setValue": { + "setFiles": { "type": "(files: File[]) => void", "description": "Function to set the value" }, - "clearValue": { + "clearFiles": { "type": "() => void", "description": "Function to clear the value" } }, "context": { - "id": { - "type": "string", - "description": "The unique identifier of the machine." - }, - "getRootNode": { - "type": "() => ShadowRoot | Node | Document", - "description": "A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron." - }, "name": { "type": "string", "description": "The name of the underlying file input" @@ -1152,9 +1156,17 @@ "type": "File[]", "description": "The current value of the file input" }, - "onChange": { - "type": "(details: ChangeDetails) => void", + "onFilesChange": { + "type": "(details: FileChangeDetails) => void", "description": "Function called when the value changes" + }, + "id": { + "type": "string", + "description": "The unique identifier of the machine." + }, + "getRootNode": { + "type": "() => ShadowRoot | Node | Document", + "description": "A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron." } } }, @@ -1178,30 +1190,13 @@ } }, "context": { - "dir": { - "type": "\"ltr\" | \"rtl\"", - "description": "The document's text/writing direction.", - "defaultValue": "\"ltr\"" - }, - "id": { - "type": "string", - "description": "The unique identifier of the machine." - }, - "getRootNode": { - "type": "() => ShadowRoot | Node | Document", - "description": "A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron." - }, "ids": { "type": "Partial<{ trigger: string; content: string; positioner: string; arrow: string; }>", "description": "The ids of the elements in the popover. Useful for composition." }, - "onOpen": { - "type": "VoidFunction", - "description": "Function invoked when the hover card is opened." - }, - "onClose": { - "type": "VoidFunction", - "description": "Function invoked when the hover card is closed." + "onOpenChange": { + "type": "(details: OpenChangeDetails) => void", + "description": "Function called when the hover card opens or closes." }, "openDelay": { "type": "number", @@ -1218,6 +1213,19 @@ "positioning": { "type": "PositioningOptions", "description": "The user provided options used to position the popover content" + }, + "dir": { + "type": "\"ltr\" | \"rtl\"", + "description": "The document's text/writing direction.", + "defaultValue": "\"ltr\"" + }, + "id": { + "type": "string", + "description": "The unique identifier of the machine." + }, + "getRootNode": { + "type": "() => ShadowRoot | Node | Document", + "description": "A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron." } } }, @@ -1269,19 +1277,6 @@ } }, "context": { - "dir": { - "type": "\"ltr\" | \"rtl\"", - "description": "The document's text/writing direction.", - "defaultValue": "\"ltr\"" - }, - "id": { - "type": "string", - "description": "The unique identifier of the machine." - }, - "getRootNode": { - "type": "() => ShadowRoot | Node | Document", - "description": "A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron." - }, "ids": { "type": "Partial<{ trigger: string; contextTrigger: string; content: string; label(id: string): string; group(id: string): string; positioner: string; arrow: string; }>", "description": "The ids of the elements in the menu. Useful for composition." @@ -1291,7 +1286,7 @@ "description": "The values of radios and checkboxes in the menu." }, "onValueChange": { - "type": "(details: { name: string; value: string | string[]; }) => void", + "type": "(details: ValueChangeDetails) => void", "description": "Callback to be called when the menu values change (for radios and checkboxes)." }, "highlightedId": { @@ -1299,7 +1294,7 @@ "description": "The `id` of the active menu item." }, "onSelect": { - "type": "(details: { value: string; }) => void", + "type": "(details: SelectionDetails) => void", "description": "Function called when a menu item is selected." }, "anchorPoint": { @@ -1322,13 +1317,34 @@ "type": "string", "description": "The accessibility label for the menu" }, - "onOpen": { - "type": "() => void", - "description": "Function called when the menu is opened" + "onOpenChange": { + "type": "(details: OpenChangeDetails) => void", + "description": "Function called when the menu opens or closes" }, - "onClose": { - "type": "() => void", - "description": "Function called when the menu is closed" + "dir": { + "type": "\"ltr\" | \"rtl\"", + "description": "The document's text/writing direction.", + "defaultValue": "\"ltr\"" + }, + "id": { + "type": "string", + "description": "The unique identifier of the machine." + }, + "getRootNode": { + "type": "() => ShadowRoot | Node | Document", + "description": "A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron." + }, + "onPointerDownOutside": { + "type": "(event: PointerDownOutsideEvent) => void", + "description": "Function called when the pointer is pressed down outside the combobox" + }, + "onFocusOutside": { + "type": "(event: FocusOutsideEvent) => void", + "description": "Function called when the focus is moved outside the combobox" + }, + "onInteractOutside": { + "type": "(event: InteractOutsideEvent) => void", + "description": "Function called when an interaction happens outside the combobox" } } }, @@ -1388,19 +1404,6 @@ } }, "context": { - "dir": { - "type": "\"ltr\" | \"rtl\"", - "description": "The document's text/writing direction.", - "defaultValue": "\"ltr\"" - }, - "id": { - "type": "string", - "description": "The unique identifier of the machine." - }, - "getRootNode": { - "type": "() => ShadowRoot | Node | Document", - "description": "A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron." - }, "ids": { "type": "Partial<{ root: string; label: string; input: string; incrementTrigger: string; decrementTrigger: string; scrubber: string; }>", "description": "The ids of the elements in the number input. Useful for composition." @@ -1482,26 +1485,22 @@ "description": "If using a custom display format, this converts the default format to the custom format." }, "inputMode": { - "type": "\"text\" | \"tel\" | \"numeric\" | \"decimal\"", + "type": "InputMode", "description": "Hints at the type of data that might be entered by the user. It also determines\nthe type of keyboard shown to the user on mobile devices", "defaultValue": "\"decimal\"" }, - "onChange": { - "type": "(details: Value) => void", + "onValueChange": { + "type": "(details: ValueChangeDetails) => void", "description": "Function invoked when the value changes" }, - "onInvalid": { - "type": "(details: Value & { reason: ValidityState; }) => void", + "onValueInvalid": { + "type": "(details: ValueInvalidDetails) => void", "description": "Function invoked when the value overflows or underflows the min/max range" }, - "onFocus": { - "type": "(details: Value & { srcElement: HTMLElement; }) => void", + "onFocusChange": { + "type": "(details: FocusChangeDetails) => void", "description": "Function invoked when the number input is focused" }, - "onBlur": { - "type": "(details: Value) => void", - "description": "The value of the input when it is blurred" - }, "minFractionDigits": { "type": "number", "description": "The minimum number of fraction digits to use. Possible values are from 0 to 20" @@ -1513,6 +1512,19 @@ "spinOnPress": { "type": "boolean", "description": "Whether to spin the value when the increment/decrement button is pressed" + }, + "dir": { + "type": "\"ltr\" | \"rtl\"", + "description": "The document's text/writing direction.", + "defaultValue": "\"ltr\"" + }, + "id": { + "type": "string", + "description": "The unique identifier of the machine." + }, + "getRootNode": { + "type": "() => ShadowRoot | Node | Document", + "description": "A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron." } } }, @@ -1527,7 +1539,7 @@ "description": "The total number of pages." }, "pages": { - "type": "PaginationRange", + "type": "Pages", "description": "The page range. Represented as an array of page numbers (including ellipsis)" }, "previousPage": { @@ -1539,11 +1551,11 @@ "description": "The next page." }, "pageRange": { - "type": "{ start: number; end: number; }", + "type": "PageRange", "description": "The page range. Represented as an object with `start` and `end` properties." }, "slice": { - "type": "(data: T_1[]) => T_1[]", + "type": "(data: V[]) => V[]", "description": "Function to slice an array of data based on the current page." }, "isFirstPage": { @@ -1568,19 +1580,6 @@ } }, "context": { - "dir": { - "type": "\"ltr\" | \"rtl\"", - "description": "The document's text/writing direction.", - "defaultValue": "\"ltr\"" - }, - "id": { - "type": "string", - "description": "The unique identifier of the machine." - }, - "getRootNode": { - "type": "() => ShadowRoot | Node | Document", - "description": "A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron." - }, "ids": { "type": "Partial<{ root: string; ellipsis(index: number): string; prevPageTrigger: string; nextPageTrigger: string; pageTrigger(page: number): string; }>", "description": "The ids of the elements in the accordion. Useful for composition." @@ -1605,14 +1604,27 @@ "type": "number", "description": "The active page" }, - "onChange": { - "type": "(details: ChangeDetails) => void", + "onPageChange": { + "type": "(details: PageChangeDetails) => void", "description": "Called when the page number is changed, and it takes the resulting page number argument" }, "type": { "type": "\"button\" | \"link\"", "description": "The type of the trigger element", "defaultValue": "\"button\"" + }, + "dir": { + "type": "\"ltr\" | \"rtl\"", + "description": "The document's text/writing direction.", + "defaultValue": "\"ltr\"" + }, + "id": { + "type": "string", + "description": "The unique identifier of the machine." + }, + "getRootNode": { + "type": "() => ShadowRoot | Node | Document", + "description": "A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron." } } }, @@ -1648,19 +1660,6 @@ } }, "context": { - "dir": { - "type": "\"ltr\" | \"rtl\"", - "description": "The document's text/writing direction.", - "defaultValue": "\"ltr\"" - }, - "id": { - "type": "string", - "description": "The unique identifier of the machine." - }, - "getRootNode": { - "type": "() => ShadowRoot | Node | Document", - "description": "A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron." - }, "name": { "type": "string", "description": "The name of the input element. Useful for form submission." @@ -1705,16 +1704,16 @@ "type": "\"alphanumeric\" | \"numeric\" | \"alphabetic\"", "description": "The type of value the pin-input should allow" }, - "onComplete": { - "type": "(details: { value: string[]; valueAsString: string; }) => void", + "onValueComplete": { + "type": "(details: ValueChangeDetails) => void", "description": "Function called when all inputs have valid values" }, - "onChange": { - "type": "(details: { value: string[]; }) => void", + "onValueChange": { + "type": "(details: ValueChangeDetails) => void", "description": "Function called on input change" }, - "onInvalid": { - "type": "(details: { value: string; index: number; }) => void", + "onValueInvalid": { + "type": "(details: ValueInvalidDetails) => void", "description": "Function called when an invalid value is entered" }, "mask": { @@ -1732,6 +1731,19 @@ "translations": { "type": "IntlTranslations", "description": "Specifies the localized strings that identifies the accessibility elements and their states" + }, + "dir": { + "type": "\"ltr\" | \"rtl\"", + "description": "The document's text/writing direction.", + "defaultValue": "\"ltr\"" + }, + "id": { + "type": "string", + "description": "The unique identifier of the machine." + }, + "getRootNode": { + "type": "() => ShadowRoot | Node | Document", + "description": "A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron." } } }, @@ -1759,14 +1771,6 @@ } }, "context": { - "id": { - "type": "string", - "description": "The unique identifier of the machine." - }, - "getRootNode": { - "type": "() => ShadowRoot | Node | Document", - "description": "A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron." - }, "ids": { "type": "Partial<{ anchor: string; trigger: string; content: string; title: string; description: string; closeTrigger: string; positioner: string; arrow: string; }>", "description": "The ids of the elements in the popover. Useful for composition." @@ -1797,13 +1801,9 @@ "type": "boolean", "description": "Whether to close the popover when the escape key is pressed." }, - "onClose": { - "type": "VoidFunction", - "description": "Function invoked when the popover is closed" - }, - "onOpen": { - "type": "VoidFunction", - "description": "Function invoked when the popover is opened" + "onOpenChange": { + "type": "(details: OpenChangeDetails) => void", + "description": "Function invoked when the popover opens or closes" }, "positioning": { "type": "PositioningOptions", @@ -1812,6 +1812,30 @@ "open": { "type": "boolean", "description": "Whether the popover is open" + }, + "onEscapeKeyDown": { + "type": "(event: KeyboardEvent) => void", + "description": "Function called when the escape key is pressed" + }, + "onPointerDownOutside": { + "type": "(event: PointerDownOutsideEvent) => void", + "description": "Function called when the pointer is pressed down outside the combobox" + }, + "onFocusOutside": { + "type": "(event: FocusOutsideEvent) => void", + "description": "Function called when the focus is moved outside the combobox" + }, + "onInteractOutside": { + "type": "(event: InteractOutsideEvent) => void", + "description": "Function called when an interaction happens outside the combobox" + }, + "id": { + "type": "string", + "description": "The unique identifier of the machine." + }, + "getRootNode": { + "type": "() => Node | ShadowRoot | Document", + "description": "A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron." } } }, @@ -1836,6 +1860,26 @@ } }, "context": { + "disabled": { + "type": "boolean", + "description": "Whether the element is disabled" + }, + "preventFocusOnPress": { + "type": "boolean", + "description": "Whether the target should not receive focus on press." + }, + "cancelOnPointerExit": { + "type": "boolean", + "description": "Whether press events should be canceled when the pointer leaves the target while pressed.\n\nBy default, this is `false`, which means if the pointer returns back over the target while\nstill pressed, onPressStart will be fired again.\n\nIf set to `true`, the press is canceled when the pointer leaves the target and\nonPressStart will not be fired if the pointer returns." + }, + "allowTextSelectionOnPress": { + "type": "boolean", + "description": "Whether text selection should be enabled on the pressable element." + }, + "longPressDelay": { + "type": "number", + "description": "The amount of time (in milliseconds) to wait before firing the `onLongPress` event." + }, "dir": { "type": "\"ltr\" | \"rtl\"", "description": "The document's text/writing direction.", @@ -1868,26 +1912,6 @@ "onLongPress": { "type": "(event: PressEvent) => void", "description": "Handler that is called when the element has been pressed for 500 milliseconds" - }, - "disabled": { - "type": "boolean", - "description": "Whether the element is disabled" - }, - "preventFocusOnPress": { - "type": "boolean", - "description": "Whether the target should not receive focus on press." - }, - "cancelOnPointerExit": { - "type": "boolean", - "description": "Whether press events should be canceled when the pointer leaves the target while pressed.\n\nBy default, this is `false`, which means if the pointer returns back over the target while\nstill pressed, onPressStart will be fired again.\n\nIf set to `true`, the press is canceled when the pointer leaves the target and\nonPressStart will not be fired if the pointer returns." - }, - "allowTextSelectionOnPress": { - "type": "boolean", - "description": "Whether text selection should be enabled on the pressable element." - }, - "longPressDelay": { - "type": "number", - "description": "The amount of time (in milliseconds) to wait before firing the `onLongPress` event." } } }, @@ -1909,29 +1933,12 @@ "type": "() => void", "description": "Function to focus the radio group" }, - "blur": { - "type": "() => void", - "description": "Function to blur the currently focused radio input in the radio group" - }, "getRadioState": { - "type": "(props: T_1) => { isInteractive: boolean; isInvalid: boolean; isDisabled: boolean; isChecked: boolean; isFocused: boolean; isHovered: boolean; isActive: boolean; }", + "type": "(props: RadioState", "description": "Returns the state details of a radio input" } }, "context": { - "dir": { - "type": "\"ltr\" | \"rtl\"", - "description": "The document's text/writing direction.", - "defaultValue": "\"ltr\"" - }, - "id": { - "type": "string", - "description": "The unique identifier of the machine." - }, - "getRootNode": { - "type": "() => ShadowRoot | Node | Document", - "description": "A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron." - }, "ids": { "type": "Partial<{ root: string; label: string; indicator: string; radio(value: string): string; radioLabel(value: string): string; radioControl(value: string): string; radioHiddenInput(value: string): string; }>", "description": "The ids of the elements in the radio. Useful for composition." @@ -1952,13 +1959,26 @@ "type": "boolean", "description": "If `true`, the radio group will be disabled" }, - "onChange": { - "type": "(details: { value: string; }) => void", + "onValueChange": { + "type": "(details: ValueChangeDetails) => void", "description": "Function called once a radio is checked" }, "orientation": { "type": "\"horizontal\" | \"vertical\"", "description": "Orientation of the radio group" + }, + "dir": { + "type": "\"ltr\" | \"rtl\"", + "description": "The document's text/writing direction.", + "defaultValue": "\"ltr\"" + }, + "id": { + "type": "string", + "description": "The unique identifier of the machine." + }, + "getRootNode": { + "type": "() => ShadowRoot | Node | Document", + "description": "A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron." } } }, @@ -2026,19 +2046,6 @@ } }, "context": { - "dir": { - "type": "\"ltr\" | \"rtl\"", - "description": "The document's text/writing direction.", - "defaultValue": "\"ltr\"" - }, - "id": { - "type": "string", - "description": "The unique identifier of the machine." - }, - "getRootNode": { - "type": "() => ShadowRoot | Node | Document", - "description": "A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron." - }, "ids": { "type": "Partial<{ root: string; thumb(index: number): string; control: string; track: string; range: string; label: string; output: string; marker(index: number): string; }>", "description": "The ids of the elements in the range slider. Useful for composition." @@ -2075,20 +2082,20 @@ "type": "boolean", "description": "Whether the slider is invalid" }, - "onChange": { - "type": "(details: { value: number[]; }) => void", + "onValueChange": { + "type": "(details: ValueChangeDetails) => void", "description": "Function invoked when the value of the slider changes" }, - "onChangeStart": { - "type": "(details: { value: number[]; }) => void", + "onValueChangeStart": { + "type": "(details: ValueChangeDetails) => void", "description": "Function invoked when the slider value change is started" }, - "onChangeEnd": { - "type": "(details: { value: number[]; }) => void", + "onValueChangeEnd": { + "type": "(details: ValueChangeDetails) => void", "description": "Function invoked when the slider value change is done" }, "onFocusChange": { - "type": "(details: { index: number; value: number[]; }) => void", + "type": "(details: FocusChangeDetails) => void", "description": "Function invoked when the slider's focused index changes" }, "getAriaValueText": { @@ -2122,6 +2129,19 @@ "thumbSize": { "type": "{ width: number; height: number; }", "description": "The slider thumbs dimensions" + }, + "dir": { + "type": "\"ltr\" | \"rtl\"", + "description": "The document's text/writing direction.", + "defaultValue": "\"ltr\"" + }, + "id": { + "type": "string", + "description": "The unique identifier of the machine." + }, + "getRootNode": { + "type": "() => ShadowRoot | Node | Document", + "description": "A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron." } } }, @@ -2161,19 +2181,6 @@ } }, "context": { - "dir": { - "type": "\"ltr\" | \"rtl\"", - "description": "The document's text/writing direction.", - "defaultValue": "\"ltr\"" - }, - "id": { - "type": "string", - "description": "The unique identifier of the machine." - }, - "getRootNode": { - "type": "() => ShadowRoot | Node | Document", - "description": "A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron." - }, "ids": { "type": "Partial<{ root: string; label: string; hiddenInput: string; control: string; rating(id: string): string; }>", "description": "The ids of the elements in the rating. Useful for composition." @@ -2214,13 +2221,26 @@ "type": "boolean", "description": "Whether to autofocus the rating." }, - "onChange": { - "type": "(details: { value: number; }) => void", + "onValueChange": { + "type": "(details: ValueChangeDetails) => void", "description": "Function to be called when the rating value changes." }, - "onHover": { - "type": "(details: { value: number; }) => void", + "onHoverChange": { + "type": "(details: HoverChangeDetails) => void", "description": "Function to be called when the rating value is hovered." + }, + "dir": { + "type": "\"ltr\" | \"rtl\"", + "description": "The document's text/writing direction.", + "defaultValue": "\"ltr\"" + }, + "id": { + "type": "string", + "description": "The unique identifier of the machine." + }, + "getRootNode": { + "type": "() => ShadowRoot | Node | Document", + "description": "A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron." } } }, @@ -2239,7 +2259,7 @@ "description": "The value of the highlighted item" }, "highlightedItem": { - "type": "CollectionItem", + "type": "V", "description": "The highlighted item" }, "highlightValue": { @@ -2247,7 +2267,7 @@ "description": "The value of the combobox input" }, "selectedItems": { - "type": "CollectionItem[]", + "type": "V[]", "description": "The selected items" }, "hasSelectedItems": { @@ -2291,30 +2311,17 @@ "description": "Function to close the combobox" }, "setCollection": { - "type": "(collection: Collection) => void", + "type": "(collection: Collection) => void", "description": "Function to set the collection of items" } }, "context": { - "dir": { - "type": "\"ltr\" | \"rtl\"", - "description": "The document's text/writing direction.", - "defaultValue": "\"ltr\"" - }, - "id": { - "type": "string", - "description": "The unique identifier of the machine." - }, - "getRootNode": { - "type": "() => ShadowRoot | Node | Document", - "description": "A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron." - }, "collection": { "type": "Collection", "description": "The item collection" }, "ids": { - "type": "Partial<{ content: string; trigger: string; clearTrigger: string; label: string; hiddenSelect: string; positioner: string; item(id: string | number): string; itemGroup(id: string | number): string; itemGroupLabel(id: string | number): string; }>", + "type": "Partial<{ root: string; content: string; control: string; trigger: string; clearTrigger: string; label: string; hiddenSelect: string; positioner: string; item(id: string | number): string; itemGroup(id: string | number): string; itemGroupLabel(id: string | number): string; }>", "description": "The ids of the elements in the select. Useful for composition." }, "name": { @@ -2345,22 +2352,18 @@ "type": "boolean", "description": "Whether to select the highlighted item when the user presses Tab,\nand the menu is open." }, - "onHighlight": { - "type": "(details: HighlightChangeDetails) => void", + "onHighlightChange": { + "type": "(details: HighlightChangeDetails) => void", "description": "The callback fired when the highlighted item changes." }, - "onChange": { - "type": "(details: ValueChangeDetails) => void", + "onValueChange": { + "type": "(details: ValueChangeDetails) => void", "description": "The callback fired when the selected item changes." }, - "onOpen": { - "type": "VoidFunction", + "onOpenChange": { + "type": "(details: OpenChangeDetails) => void", "description": "Function called when the popup is opened" }, - "onClose": { - "type": "VoidFunction", - "description": "Function called when the popup is closed" - }, "positioning": { "type": "PositioningOptions", "description": "The positioning options of the menu." @@ -2384,6 +2387,31 @@ "open": { "type": "boolean", "description": "Whether the select menu is open" + }, + "dir": { + "type": "\"ltr\" | \"rtl\"", + "description": "The document's text/writing direction.", + "defaultValue": "\"ltr\"" + }, + "id": { + "type": "string", + "description": "The unique identifier of the machine." + }, + "getRootNode": { + "type": "() => ShadowRoot | Node | Document", + "description": "A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron." + }, + "onPointerDownOutside": { + "type": "(event: PointerDownOutsideEvent) => void", + "description": "Function called when the pointer is pressed down outside the combobox" + }, + "onFocusOutside": { + "type": "(event: FocusOutsideEvent) => void", + "description": "Function called when the focus is moved outside the combobox" + }, + "onInteractOutside": { + "type": "(event: InteractOutsideEvent) => void", + "description": "Function called when an interaction happens outside the combobox" } } }, @@ -2431,19 +2459,6 @@ } }, "context": { - "dir": { - "type": "\"ltr\" | \"rtl\"", - "description": "The document's text/writing direction.", - "defaultValue": "\"ltr\"" - }, - "id": { - "type": "string", - "description": "The unique identifier of the machine." - }, - "getRootNode": { - "type": "() => ShadowRoot | Node | Document", - "description": "A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron." - }, "ids": { "type": "Partial<{ root: string; thumb: string; control: string; track: string; range: string; label: string; output: string; hiddenInput: string; }>", "description": "The ids of the elements in the slider. Useful for composition." @@ -2508,16 +2523,16 @@ "type": "(value: number) => string", "description": "Function that returns a human readable value for the slider" }, - "onChange": { - "type": "(details: { value: number; }) => void", + "onValueChange": { + "type": "(details: ValueChangeDetails) => void", "description": "Function invoked when the value of the slider changes" }, - "onChangeEnd": { - "type": "(details: { value: number; }) => void", + "onValueChangeEnd": { + "type": "(details: ValueChangeDetails) => void", "description": "Function invoked when the slider value change is done" }, - "onChangeStart": { - "type": "(details: { value: number; }) => void", + "onValueChangeStart": { + "type": "(details: ValueChangeDetails) => void", "description": "Function invoked when the slider value change is started" }, "thumbAlignment": { @@ -2525,8 +2540,21 @@ "description": "The alignment of the slider thumb relative to the track\n- `center`: the thumb will extend beyond the bounds of the slider track.\n- `contain`: the thumb will be contained within the bounds of the track." }, "thumbSize": { - "type": "{ width: number; height: number; }", + "type": "Size", "description": "The slider thumb dimensions.If not provided, the thumb size will be measured automatically." + }, + "dir": { + "type": "\"ltr\" | \"rtl\"", + "description": "The document's text/writing direction.", + "defaultValue": "\"ltr\"" + }, + "id": { + "type": "string", + "description": "The unique identifier of the machine." + }, + "getRootNode": { + "type": "() => ShadowRoot | Node | Document", + "description": "A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron." } } }, @@ -2557,24 +2585,11 @@ "description": "Function to set the size of a panel." }, "getResizeTriggerState": { - "type": "(props: ResizeTriggerProps) => { isDisabled: boolean; isFocused: boolean; panelIds: string[]; min: number; max: number; value: number; }", + "type": "(props: ResizeTriggerState", "description": "Returns the state details for a resize trigger." } }, "context": { - "dir": { - "type": "\"ltr\" | \"rtl\"", - "description": "The document's text/writing direction.", - "defaultValue": "\"ltr\"" - }, - "id": { - "type": "string", - "description": "The unique identifier of the machine." - }, - "getRootNode": { - "type": "() => ShadowRoot | Node | Document", - "description": "A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron." - }, "orientation": { "type": "\"horizontal\" | \"vertical\"", "description": "The orientation of the splitter. Can be `horizontal` or `vertical`" @@ -2583,21 +2598,34 @@ "type": "PanelSizeData[]", "description": "The size data of the panels" }, - "onResize": { - "type": "(details: ResizeDetails) => void", + "onSizeChange": { + "type": "(details: SizeChangeDetails) => void", "description": "Function called when the splitter is resized." }, - "onResizeStart": { - "type": "(details: ResizeDetails) => void", + "onSizeChangeStart": { + "type": "(details: SizeChangeDetails) => void", "description": "Function called when the splitter resize starts." }, - "onResizeEnd": { - "type": "(details: ResizeDetails) => void", + "onSizeChangeEnd": { + "type": "(details: SizeChangeDetails) => void", "description": "Function called when the splitter resize ends." }, "ids": { "type": "Partial<{ root: string; resizeTrigger(id: string): string; label(id: string): string; panel(id: string | number): string; }>", "description": "The ids of the elements in the splitter. Useful for composition." + }, + "dir": { + "type": "\"ltr\" | \"rtl\"", + "description": "The document's text/writing direction.", + "defaultValue": "\"ltr\"" + }, + "id": { + "type": "string", + "description": "The unique identifier of the machine." + }, + "getRootNode": { + "type": "() => ShadowRoot | Node | Document", + "description": "A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron." } } }, @@ -2625,19 +2653,6 @@ } }, "context": { - "dir": { - "type": "\"ltr\" | \"rtl\"", - "description": "The document's text/writing direction.", - "defaultValue": "\"ltr\"" - }, - "id": { - "type": "string", - "description": "The unique identifier of the machine." - }, - "getRootNode": { - "type": "() => ShadowRoot | Node | Document", - "description": "A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron." - }, "ids": { "type": "Partial<{ root: string; input: string; control: string; label: string; thumb: string; }>", "description": "The ids of the elements in the switch. Useful for composition." @@ -2650,10 +2665,6 @@ "type": "boolean", "description": "Whether the switch is disabled." }, - "focusable": { - "type": "boolean", - "description": "If `true` and `disabled` is passed, the switch will\nremain tabbable but not interactive" - }, "readOnly": { "type": "boolean", "description": "If `true`, the switch will be readonly" @@ -2666,8 +2677,8 @@ "type": "boolean", "description": "If `true`, the switch input is marked as required," }, - "onChange": { - "type": "(details: { checked: boolean; }) => void", + "onCheckedChange": { + "type": "(details: CheckedChangeDetails) => void", "description": "Function to call when the switch is clicked." }, "checked": { @@ -2686,6 +2697,19 @@ "type": "string | number", "description": "The value of switch input. Useful for form submission.", "defaultValue": "\"on\"" + }, + "dir": { + "type": "\"ltr\" | \"rtl\"", + "description": "The document's text/writing direction.", + "defaultValue": "\"ltr\"" + }, + "id": { + "type": "string", + "description": "The unique identifier of the machine." + }, + "getRootNode": { + "type": "() => ShadowRoot | Node | Document", + "description": "A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron." } } }, @@ -2712,24 +2736,15 @@ "description": "Clears the value of the tabs." }, "setIndicatorRect": { - "type": "(id: string) => void", - "description": "Sets the indicator rect to the tab with the given id." + "type": "(value: string) => void", + "description": "Sets the indicator rect to the tab with the given value" + }, + "getTriggerState": { + "type": "(props: TriggerState", + "description": "Returns the state of the trigger with the given props" } }, "context": { - "dir": { - "type": "\"ltr\" | \"rtl\"", - "description": "The document's text/writing direction.", - "defaultValue": "\"ltr\"" - }, - "id": { - "type": "string", - "description": "The unique identifier of the machine." - }, - "getRootNode": { - "type": "() => ShadowRoot | Node | Document", - "description": "A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron." - }, "ids": { "type": "Partial<{ root: string; trigger: string; tablist: string; content: string; indicator: string; }>", "description": "The ids of the elements in the tabs. Useful for composition." @@ -2757,17 +2772,26 @@ "description": "The activation mode of the tabs. Can be `manual` or `automatic`\n- `manual`: Tabs are activated when clicked or press `enter` key.\n- `automatic`: Tabs are activated when receiving focus", "defaultValue": "\"automatic\"" }, - "onChange": { - "type": "(details: { value: string; }) => void", + "onValueChange": { + "type": "(details: ValueChangeDetails) => void", "description": "Callback to be called when the selected/active tab changes" }, - "onFocus": { - "type": "(details: { value: string; }) => void", + "onFocusChange": { + "type": "(details: FocusChangeDetails) => void", "description": "Callback to be called when the focused tab changes" }, - "onDelete": { - "type": "(details: { value: string; }) => void", - "description": "Callback to be called when a tab's close button is clicked" + "dir": { + "type": "\"ltr\" | \"rtl\"", + "description": "The document's text/writing direction.", + "defaultValue": "\"ltr\"" + }, + "id": { + "type": "string", + "description": "The unique identifier of the machine." + }, + "getRootNode": { + "type": "() => ShadowRoot | Node | Document", + "description": "A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron." } } }, @@ -2824,22 +2848,13 @@ "focus": { "type": "() => void", "description": "Function to focus the tags entry input." + }, + "getTagState": { + "type": "(props: TagState", + "description": "Returns the state of a tag" } }, "context": { - "dir": { - "type": "\"ltr\" | \"rtl\"", - "description": "The document's text/writing direction.", - "defaultValue": "\"ltr\"" - }, - "id": { - "type": "string", - "description": "The unique identifier of the machine." - }, - "getRootNode": { - "type": "() => ShadowRoot | Node | Document", - "description": "A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron." - }, "ids": { "type": "Partial<{ root: string; input: string; clearBtn: string; label: string; control: string; tag(opts: TagProps): string; }>", "description": "The ids of the elements in the tags input. Useful for composition." @@ -2885,20 +2900,20 @@ "type": "string[]", "description": "The tag values" }, - "onChange": { - "type": "(details: { values: string[]; }) => void", + "onValueChange": { + "type": "(details: ValueChangeDetails) => void", "description": "Callback fired when the tag values is updated" }, - "onFocusChange": { - "type": "(details: { value: string; }) => void", - "description": "Callback fired when a tag is focused by pointer or keyboard navigation" + "onHighlightChange": { + "type": "(details: HighlightChangeDetails) => void", + "description": "Callback fired when a tag is highlighted by pointer or keyboard navigation" }, - "onInvalid": { - "type": "(details: { reason: ValidityState; }) => void", + "onValueInvalid": { + "type": "(details: ValidityChangeDetails) => void", "description": "Callback fired when the max tag count is reached or the `validateTag` function returns `false`" }, "validate": { - "type": "(details: { inputValue: string; values: string[]; }) => boolean", + "type": "(details: ValidateArgs) => boolean", "description": "Returns a boolean that determines whether a tag can be added.\nUseful for preventing duplicates or invalid tag values." }, "blurBehavior": { @@ -2925,6 +2940,31 @@ "form": { "type": "string", "description": "The associate form of the underlying input element." + }, + "dir": { + "type": "\"ltr\" | \"rtl\"", + "description": "The document's text/writing direction.", + "defaultValue": "\"ltr\"" + }, + "id": { + "type": "string", + "description": "The unique identifier of the machine." + }, + "getRootNode": { + "type": "() => ShadowRoot | Node | Document", + "description": "A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron." + }, + "onPointerDownOutside": { + "type": "(event: PointerDownOutsideEvent) => void", + "description": "Function called when the pointer is pressed down outside the combobox" + }, + "onFocusOutside": { + "type": "(event: FocusOutsideEvent) => void", + "description": "Function called when the focus is moved outside the combobox" + }, + "onInteractOutside": { + "type": "(event: InteractOutsideEvent) => void", + "description": "Function called when an interaction happens outside the combobox" } } }, @@ -2977,53 +3017,6 @@ }, "context": {} }, - "toggle": { - "api": { - "isPressed": { - "type": "boolean", - "description": "Whether the toggle is pressed." - }, - "setPressed": { - "type": "(pressed: boolean) => void", - "description": "Function to set the pressed state of the toggle." - } - }, - "context": { - "dir": { - "type": "\"ltr\" | \"rtl\"", - "description": "The document's text/writing direction.", - "defaultValue": "\"ltr\"" - }, - "id": { - "type": "string", - "description": "The unique identifier of the machine." - }, - "getRootNode": { - "type": "() => ShadowRoot | Node | Document", - "description": "A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron." - }, - "ids": { - "type": "Partial<{ button: string; }>", - "description": "The ids of the elements in the toggle. Useful for composition." - }, - "aria-label": { - "type": "string", - "description": "Specifies the localized strings that identifies the accessibility elements and their states" - }, - "disabled": { - "type": "boolean", - "description": "Whether the toggle is disabled." - }, - "onChange": { - "type": "(details: { pressed: boolean; }) => void", - "description": "Function to call when the toggle is clicked." - }, - "pressed": { - "type": "boolean", - "description": "Whether the toggle is initially pressed." - } - } - }, "toggle-group": { "api": { "value": { @@ -3036,19 +3029,6 @@ } }, "context": { - "dir": { - "type": "\"ltr\" | \"rtl\"", - "description": "The document's text/writing direction.", - "defaultValue": "\"ltr\"" - }, - "id": { - "type": "string", - "description": "The unique identifier of the machine." - }, - "getRootNode": { - "type": "() => ShadowRoot | Node | Document", - "description": "A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron." - }, "ids": { "type": "Partial<{ root: string; toggle(value: string): string; }>", "description": "The ids of the elements in the toggle. Useful for composition." @@ -3061,8 +3041,8 @@ "type": "string[]", "description": "The values of the toggles in the group." }, - "onChange": { - "type": "(details: { value: string[]; }) => void", + "onValueChange": { + "type": "(details: ValueChangeDetails) => void", "description": "Function to call when the toggle is clicked." }, "loop": { @@ -3080,6 +3060,19 @@ "multiple": { "type": "boolean", "description": "Whether to allow multiple toggles to be selected." + }, + "dir": { + "type": "\"ltr\" | \"rtl\"", + "description": "The document's text/writing direction.", + "defaultValue": "\"ltr\"" + }, + "id": { + "type": "string", + "description": "The unique identifier of the machine." + }, + "getRootNode": { + "type": "() => ShadowRoot | Node | Document", + "description": "A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron." } } }, @@ -3103,14 +3096,14 @@ } }, "context": { - "getRootNode": { - "type": "() => ShadowRoot | Node | Document", - "description": "A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron." - }, "ids": { "type": "Partial<{ trigger: string; content: string; arrow: string; positioner: string; }>", "description": "The ids of the elements in the tooltip. Useful for composition." }, + "id": { + "type": "string", + "description": "The `id` of the tooltip." + }, "openDelay": { "type": "number", "description": "The open delay of the tooltip." @@ -3131,14 +3124,10 @@ "type": "boolean", "description": "Whether the tooltip's content is interactive.\nIn this mode, the tooltip will remain open when user hovers over the content." }, - "onOpen": { - "type": "VoidFunction", + "onOpenChange": { + "type": "(details: OpenChangeDetails) => void", "description": "Function called when the tooltip is opened." }, - "onClose": { - "type": "VoidFunction", - "description": "Function called when the tooltip is closed." - }, "aria-label": { "type": "string", "description": "Custom label for the tooltip." @@ -3154,6 +3143,10 @@ "open": { "type": "boolean", "description": "Whether the tooltip is open" + }, + "getRootNode": { + "type": "() => ShadowRoot | Node | Document", + "description": "A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron." } } } diff --git a/packages/machines/accordion/src/accordion.connect.ts b/packages/machines/accordion/src/accordion.connect.ts index 2c45777db7..f8c58bc906 100644 --- a/packages/machines/accordion/src/accordion.connect.ts +++ b/packages/machines/accordion/src/accordion.connect.ts @@ -39,28 +39,28 @@ export function connect(state: State, send: Send, normalize }), getItemProps(props: ItemProps) { - const { isOpen, isFocused, isDisabled } = getItemState(props) + const itemState = getItemState(props) return normalize.element({ ...parts.item.attrs, id: dom.getItemId(state.context, props.value), - "data-state": isOpen ? "open" : "closed", - "data-focus": dataAttr(isFocused), - "data-disabled": dataAttr(isDisabled), + "data-state": itemState.isOpen ? "open" : "closed", + "data-focus": dataAttr(itemState.isFocused), + "data-disabled": dataAttr(itemState.isDisabled), "data-orientation": state.context.orientation, }) }, getContentProps(props: ItemProps) { - const { isOpen, isFocused, isDisabled } = getItemState(props) + const itemState = getItemState(props) return normalize.element({ ...parts.content.attrs, role: "region", id: dom.getContentId(state.context, props.value), "aria-labelledby": dom.getTriggerId(state.context, props.value), - hidden: !isOpen, - "data-state": isOpen ? "open" : "closed", - "data-disabled": dataAttr(isDisabled), - "data-focus": dataAttr(isFocused), + hidden: !itemState.isOpen, + "data-state": itemState.isOpen ? "open" : "closed", + "data-disabled": dataAttr(itemState.isDisabled), + "data-focus": dataAttr(itemState.isFocused), "data-orientation": state.context.orientation, }) }, diff --git a/packages/machines/accordion/src/accordion.machine.ts b/packages/machines/accordion/src/accordion.machine.ts index 5a363c4eb3..1f65903385 100644 --- a/packages/machines/accordion/src/accordion.machine.ts +++ b/packages/machines/accordion/src/accordion.machine.ts @@ -131,7 +131,7 @@ export function machine(userContext: UserDefinedContext) { const invoke = { change(ctx: MachineContext) { - ctx.onChange?.({ value: Array.from(ctx.value) }) + ctx.onValueChange?.({ value: Array.from(ctx.value) }) }, focusChange(ctx: MachineContext) { ctx.onFocusChange?.({ value: ctx.focusedValue }) diff --git a/packages/machines/accordion/src/accordion.types.ts b/packages/machines/accordion/src/accordion.types.ts index 8741cf49f7..15718f6298 100644 --- a/packages/machines/accordion/src/accordion.types.ts +++ b/packages/machines/accordion/src/accordion.types.ts @@ -1,6 +1,22 @@ import type { StateMachine as S } from "@zag-js/core" import type { CommonProperties, Context, DirectionProperty, PropTypes, RequiredBy } from "@zag-js/types" +/* ----------------------------------------------------------------------------- + * Callback details + * -----------------------------------------------------------------------------*/ + +export interface ValueChangeDetails { + value: string[] +} + +export interface FocusChangeDetails { + value: string | null +} + +/* ----------------------------------------------------------------------------- + * Machine context + * -----------------------------------------------------------------------------*/ + type ElementIds = Partial<{ root: string item(value: string): string @@ -8,52 +24,43 @@ type ElementIds = Partial<{ trigger(value: string): string }> -export type ChangeDetails = { +interface PublicContext extends DirectionProperty, CommonProperties { + /** + * The ids of the elements in the accordion. Useful for composition. + */ + ids?: ElementIds + /** + * Whether multple accordion items can be open at the same time. + * @default false + */ + multiple?: boolean + /** + * Whether an accordion item can be collapsed after it has been opened. + * @default false + */ + collapsible?: boolean + /** + * The `id` of the accordion item that is currently being opened. + */ value: string[] + /** + * Whether the accordion items are disabled + */ + disabled?: boolean + /** + * The callback fired when the state of opened/closed accordion items changes. + */ + onValueChange?: (details: ValueChangeDetails) => void + /** + * The callback fired when the focused accordion item changes. + */ + onFocusChange?: (details: FocusChangeDetails) => void + /** + * The orientation of the accordion items. + */ + orientation?: "horizontal" | "vertical" } -export type FocusChangeDetails = { - value: string | null -} - -type PublicContext = DirectionProperty & - CommonProperties & { - /** - * The ids of the elements in the accordion. Useful for composition. - */ - ids?: ElementIds - /** - * Whether multple accordion items can be open at the same time. - * @default false - */ - multiple?: boolean - /** - * Whether an accordion item can be collapsed after it has been opened. - * @default false - */ - collapsible?: boolean - /** - * The `id` of the accordion item that is currently being opened. - */ - value: string[] - /** - * Whether the accordion items are disabled - */ - disabled?: boolean - /** - * The callback fired when the state of opened/closed accordion items changes. - */ - onChange?: (details: ChangeDetails) => void - /** - * The callback fired when the focused accordion item changes. - */ - onFocusChange?: (details: FocusChangeDetails) => void - /** - * The orientation of the accordion items. - */ - orientation?: "horizontal" | "vertical" - } - export type UserDefinedContext = RequiredBy type ComputedContext = Readonly<{ @@ -72,9 +79,9 @@ type PrivateContext = Context<{ focusedValue: string | null }> -export type MachineContext = PublicContext & PrivateContext & ComputedContext +export interface MachineContext extends PublicContext, PrivateContext, ComputedContext {} -export type MachineState = { +export interface MachineState { value: "idle" | "focused" } @@ -82,18 +89,22 @@ export type State = S.State export type Send = S.Send -export type ItemProps = { +/* ----------------------------------------------------------------------------- + * Component API + * -----------------------------------------------------------------------------*/ + +export interface ItemProps { value: string disabled?: boolean } -export type ItemState = { +export interface ItemState { isOpen: boolean isFocused: boolean isDisabled: boolean } -export type MachineApi = { +export interface MachineApi { /** * The value of the focused accordion item. */ diff --git a/packages/machines/avatar/src/avatar.machine.ts b/packages/machines/avatar/src/avatar.machine.ts index 2866f89097..f41f311662 100644 --- a/packages/machines/avatar/src/avatar.machine.ts +++ b/packages/machines/avatar/src/avatar.machine.ts @@ -79,10 +79,10 @@ export function machine(userContext: UserDefinedContext) { }, actions: { invokeOnLoad(ctx) { - ctx.onLoad?.() + ctx.onLoadingStatusChange?.({ status: "loaded" }) }, invokeOnError(ctx) { - ctx.onError?.() + ctx.onLoadingStatusChange?.({ status: "error" }) }, checkImgStatus(ctx, _evt, { send }) { const img = dom.getImageEl(ctx) diff --git a/packages/machines/avatar/src/avatar.types.ts b/packages/machines/avatar/src/avatar.types.ts index 7b0a827c95..70140a7fcd 100644 --- a/packages/machines/avatar/src/avatar.types.ts +++ b/packages/machines/avatar/src/avatar.types.ts @@ -1,9 +1,23 @@ import type { StateMachine as S } from "@zag-js/core" import type { CommonProperties, Context, PropTypes, RequiredBy } from "@zag-js/types" -type PublicContext = CommonProperties & { - onLoad?: () => void - onError?: () => void +/* ----------------------------------------------------------------------------- + * Callback details + * -----------------------------------------------------------------------------*/ + +export interface StatusChangeDetails { + status: "loaded" | "error" +} + +/* ----------------------------------------------------------------------------- + * Machine context + * -----------------------------------------------------------------------------*/ + +interface PublicContext extends CommonProperties { + /** + * Functional called when the image loading status changes. + */ + onLoadingStatusChange?: (details: StatusChangeDetails) => void } type PrivateContext = Context<{}> @@ -12,9 +26,9 @@ type ComputedContext = Readonly<{}> export type UserDefinedContext = RequiredBy -export type MachineContext = PublicContext & PrivateContext & ComputedContext +export interface MachineContext extends PublicContext, PrivateContext, ComputedContext {} -export type MachineState = { +export interface MachineState { value: "loading" | "error" | "loaded" } @@ -22,7 +36,11 @@ export type State = S.State export type Send = S.Send -export type MachineApi = { +/* ----------------------------------------------------------------------------- + * Component API + * -----------------------------------------------------------------------------*/ + +export interface MachineApi { /** * Whether the image is loaded. */ @@ -43,6 +61,7 @@ export type MachineApi = { * Function to set error state. */ setError(): void + rootProps: T["element"] imageProps: T["img"] fallbackProps: T["element"] diff --git a/packages/machines/carousel/src/carousel.types.ts b/packages/machines/carousel/src/carousel.types.ts index b4828c60ee..0a656228aa 100644 --- a/packages/machines/carousel/src/carousel.types.ts +++ b/packages/machines/carousel/src/carousel.types.ts @@ -1,15 +1,20 @@ import type { StateMachine as S } from "@zag-js/core" import type { CommonProperties, Context, DirectionProperty, PropTypes, RequiredBy } from "@zag-js/types" -export type SlideProps = { - index: number -} +/* ----------------------------------------------------------------------------- + * Callback details + * -----------------------------------------------------------------------------*/ -export type SlideIndicatorProps = { +export interface SlideChangeDetails { index: number - readOnly?: boolean } +/* ----------------------------------------------------------------------------- + * Machine context + * -----------------------------------------------------------------------------*/ + +type RectEdge = "top" | "right" | "bottom" | "left" + type ElementIds = Partial<{ root: string viewport: string @@ -21,44 +26,41 @@ type ElementIds = Partial<{ indicator(index: number): string }> -type ChangeDetails = { index: number } - -type PublicContext = DirectionProperty & - CommonProperties & { - /** - * The orientation of the carousel. - * @default "horizontal" - */ - orientation: "horizontal" | "vertical" - /** - * The alignment of the slides in the carousel. - */ - align: "start" | "center" | "end" - /** - * The number of slides to show at a time. - */ - slidesPerView: number | "auto" - /** - * Whether the carousel should loop around. - */ - loop: boolean - /** - * The current slide index. - */ - index: number - /** - * The amount of space between slides. - */ - spacing: string - /** - * Function called when the slide changes. - */ - onSlideChange?: (details: ChangeDetails) => void - /** - * The ids of the elements in the carousel. Useful for composition. - */ - ids?: ElementIds - } +interface PublicContext extends DirectionProperty, CommonProperties { + /** + * The orientation of the carousel. + * @default "horizontal" + */ + orientation: "horizontal" | "vertical" + /** + * The alignment of the slides in the carousel. + */ + align: "start" | "center" | "end" + /** + * The number of slides to show at a time. + */ + slidesPerView: number | "auto" + /** + * Whether the carousel should loop around. + */ + loop: boolean + /** + * The current slide index. + */ + index: number + /** + * The amount of space between slides. + */ + spacing: string + /** + * Function called when the slide changes. + */ + onSlideChange?: (details: SlideChangeDetails) => void + /** + * The ids of the elements in the carousel. Useful for composition. + */ + ids?: ElementIds +} type PrivateContext = Context<{ slideRects: DOMRect[] @@ -68,8 +70,6 @@ type PrivateContext = Context<{ scrollProgress: number }> -type RectEdge = "top" | "right" | "bottom" | "left" - type ComputedContext = Readonly<{ isRtl: boolean isHorizontal: boolean @@ -83,9 +83,9 @@ type ComputedContext = Readonly<{ export type UserDefinedContext = RequiredBy -export type MachineContext = PublicContext & PrivateContext & ComputedContext +export interface MachineContext extends PublicContext, PrivateContext, ComputedContext {} -export type MachineState = { +export interface MachineState { value: "idle" | "dragging" | "autoplay" } @@ -93,7 +93,28 @@ export type State = S.State export type Send = S.Send -export type MachineApi = { +/* ----------------------------------------------------------------------------- + * Component API + * -----------------------------------------------------------------------------*/ + +export interface SlideProps { + index: number +} + +export interface SlideState { + valueText: string + isCurrent: boolean + isNext: boolean + isPrevious: boolean + isInView: boolean +} + +export interface SlideIndicatorProps { + index: number + readOnly?: boolean +} + +export interface MachineApi { /** * The current index of the carousel */ @@ -129,13 +150,7 @@ export type MachineApi = { /** * Returns the state of a specific slide */ - getSlideState: (props: SlideProps) => { - valueText: string - isCurrent: boolean - isNext: boolean - isPrevious: boolean - isInView: boolean - } + getSlideState(props: SlideProps): SlideState /** * Function to start/resume autoplay */ diff --git a/packages/machines/checkbox/src/checkbox.machine.ts b/packages/machines/checkbox/src/checkbox.machine.ts index 38b8f49267..8d9a0d3529 100644 --- a/packages/machines/checkbox/src/checkbox.machine.ts +++ b/packages/machines/checkbox/src/checkbox.machine.ts @@ -120,7 +120,7 @@ function isChecked(checked?: CheckedState): checked is boolean { const invoke = { change: (ctx: MachineContext) => { - ctx.onChange?.({ checked: ctx.checked }) + ctx.onCheckedChange?.({ checked: ctx.checked }) }, } diff --git a/packages/machines/checkbox/src/checkbox.types.ts b/packages/machines/checkbox/src/checkbox.types.ts index a539e28582..0e05514096 100644 --- a/packages/machines/checkbox/src/checkbox.types.ts +++ b/packages/machines/checkbox/src/checkbox.types.ts @@ -1,6 +1,20 @@ import type { StateMachine as S } from "@zag-js/core" import type { CommonProperties, Context, DirectionProperty, PropTypes, RequiredBy } from "@zag-js/types" +/* ----------------------------------------------------------------------------- + * Callback details + * -----------------------------------------------------------------------------*/ + +export type CheckedState = boolean | "indeterminate" + +export interface CheckedChangeDetails { + checked: CheckedState +} + +/* ----------------------------------------------------------------------------- + * Machine context + * -----------------------------------------------------------------------------*/ + type ElementIds = Partial<{ root: string hiddenInput: string @@ -8,48 +22,45 @@ type ElementIds = Partial<{ label: string }> -export type CheckedState = boolean | "indeterminate" - -type PublicContext = DirectionProperty & - CommonProperties & { - /** - * The ids of the elements in the checkbox. Useful for composition. - */ - ids?: ElementIds - /** - * If `true`, the checkbox will be disabled - */ - disabled?: boolean - /** - * If `true`, the checkbox is marked as invalid. - */ - invalid?: boolean - /** - * If `true`, the checkbox input is marked as required, - */ - required?: boolean - /** - * If `true`, the checkbox will be checked. - */ - checked: CheckedState - /** - * The callback invoked when the checked state of the `Checkbox` changes. - */ - onChange?: (details: { checked: CheckedState }) => void - /** - * The name of the input field in a checkbox. Useful for form submission. - */ - name?: string - /** - * The id of the form that the checkbox belongs to. - */ - form?: string - /** - * The value of checkbox input. Useful for form submission. - * @default "on" - */ - value: string - } +interface PublicContext extends DirectionProperty, CommonProperties { + /** + * The ids of the elements in the checkbox. Useful for composition. + */ + ids?: ElementIds + /** + * If `true`, the checkbox will be disabled + */ + disabled?: boolean + /** + * If `true`, the checkbox is marked as invalid. + */ + invalid?: boolean + /** + * If `true`, the checkbox input is marked as required, + */ + required?: boolean + /** + * If `true`, the checkbox will be checked. + */ + checked: CheckedState + /** + * The callback invoked when the checked state of the `Checkbox` changes. + */ + onCheckedChange?(details: CheckedChangeDetails): void + /** + * The name of the input field in a checkbox. Useful for form submission. + */ + name?: string + /** + * The id of the form that the checkbox belongs to. + */ + form?: string + /** + * The value of checkbox input. Useful for form submission. + * @default "on" + */ + value: string +} export type UserDefinedContext = RequiredBy @@ -91,9 +102,9 @@ type PrivateContext = Context<{ fieldsetDisabled: boolean }> -export type MachineContext = PublicContext & PrivateContext & ComputedContext +export interface MachineContext extends PublicContext, PrivateContext, ComputedContext {} -export type MachineState = { +export interface MachineState { value: "ready" } @@ -101,7 +112,11 @@ export type State = S.State export type Send = S.Send -export type MachineApi = { +/* ----------------------------------------------------------------------------- + * Component API + * -----------------------------------------------------------------------------*/ + +export interface MachineApi { /** * Whether the checkbox is checked */ diff --git a/packages/machines/color-picker/src/color-picker.machine.ts b/packages/machines/color-picker/src/color-picker.machine.ts index 00c0d94011..acff89ecd9 100644 --- a/packages/machines/color-picker/src/color-picker.machine.ts +++ b/packages/machines/color-picker/src/color-picker.machine.ts @@ -220,7 +220,7 @@ export function machine(userContext: UserDefinedContext) { const format = ctx.valueAsColor.getColorSpace() const color = parseColor(sRGBHex).toFormat(format) set.value(ctx, color) - ctx.onChangeEnd?.({ value: ctx.value, valueAsColor: color }) + ctx.onValueChangeEnd?.({ value: ctx.value, valueAsColor: color }) }) .catch(() => void 0) }, @@ -368,17 +368,18 @@ export function machine(userContext: UserDefinedContext) { ) } -const getDetails = (ctx: MachineContext) => ({ - value: ctx.value, - valueAsColor: ctx.valueAsColor, -}) - const invoke = { changeEnd(ctx: MachineContext) { - ctx.onChangeEnd?.(getDetails(ctx)) + ctx.onValueChangeEnd?.({ + value: ctx.value, + valueAsColor: ctx.valueAsColor, + }) }, change(ctx: MachineContext) { - ctx.onChange?.(getDetails(ctx)) + ctx.onValueChange?.({ + value: ctx.value, + valueAsColor: ctx.valueAsColor, + }) }, } diff --git a/packages/machines/color-picker/src/color-picker.types.ts b/packages/machines/color-picker/src/color-picker.types.ts index 49553b8378..e76c7637f8 100644 --- a/packages/machines/color-picker/src/color-picker.types.ts +++ b/packages/machines/color-picker/src/color-picker.types.ts @@ -2,33 +2,21 @@ import type { Color, ColorAxes, ColorChannel, ColorFormat, ColorType } from "@za import type { StateMachine as S } from "@zag-js/core" import type { CommonProperties, Context, Orientation, PropTypes, RequiredBy } from "@zag-js/types" -export type ColorChannelProps = { - channel: ColorChannel - orientation?: Orientation -} - -export type ExtendedColorChannel = ColorChannel | "hex" | "css" - -export type ColorChannelInputProps = { - channel: ExtendedColorChannel - orientation?: Orientation -} - -export type ColorAreaProps = { - xChannel: ColorChannel - yChannel: ColorChannel -} - -export type ColorSwatchProps = { - readOnly?: boolean - value: string | Color -} +/* ----------------------------------------------------------------------------- + * Callback details + * -----------------------------------------------------------------------------*/ -type ChangeDetails = { +export interface ValueChangeDetails { value: string valueAsColor: Color } +/* ----------------------------------------------------------------------------- + * Machine context + * -----------------------------------------------------------------------------*/ + +export type ExtendedColorChannel = ColorChannel | "hex" | "css" + type ElementIds = Partial<{ content: string area: string @@ -39,9 +27,7 @@ type ElementIds = Partial<{ channelSliderThumb(id: ColorChannel): string }> -export type { Color, ColorAxes, ColorChannel, ColorFormat, ColorType } - -type PublicContext = CommonProperties & { +interface PublicContext extends CommonProperties { /** * The ids of the elements in the color picker. Useful for composition. */ @@ -65,11 +51,11 @@ type PublicContext = CommonProperties & { /** * Handler that is called when the value changes, as the user drags. */ - onChange?: (details: ChangeDetails) => void + onValueChange?: (details: ValueChangeDetails) => void /** * Handler that is called when the user stops dragging. */ - onChangeEnd?: (details: ChangeDetails) => void + onValueChangeEnd?: (details: ValueChangeDetails) => void /** * The name for the form input */ @@ -124,9 +110,9 @@ type ComputedContext = Readonly<{ export type UserDefinedContext = RequiredBy -export type MachineContext = PublicContext & PrivateContext & ComputedContext +export interface MachineContext extends PublicContext, PrivateContext, ComputedContext {} -export type MachineState = { +export interface MachineState { value: "idle" | "focused" | "dragging" } @@ -134,7 +120,31 @@ export type State = S.State export type Send = S.Send -export type MachineApi = { +/* ----------------------------------------------------------------------------- + * Component API + * -----------------------------------------------------------------------------*/ + +export interface ColorChannelProps { + channel: ColorChannel + orientation?: Orientation +} + +export interface ColorChannelInputProps { + channel: ExtendedColorChannel + orientation?: Orientation +} + +export interface ColorAreaProps { + xChannel: ColorChannel + yChannel: ColorChannel +} + +export interface ColorSwatchProps { + readOnly?: boolean + value: string | Color +} + +export interface MachineApi { /** * Whether the color picker is being dragged */ @@ -171,6 +181,7 @@ export type MachineApi = { * Function to set the color alpha */ setAlpha(value: number): void + contentProps: T["element"] hiddenInputProps: T["input"] getAreaProps(props: ColorAreaProps): T["element"] @@ -184,3 +195,9 @@ export type MachineApi = { getSwatchBackgroundProps(props: ColorSwatchProps): T["element"] getSwatchProps(props: ColorSwatchProps): T["element"] } + +/* ----------------------------------------------------------------------------- + * Re-exported types + * -----------------------------------------------------------------------------*/ + +export type { Color, ColorAxes, ColorChannel, ColorFormat, ColorType } diff --git a/packages/machines/combobox/src/combobox.machine.ts b/packages/machines/combobox/src/combobox.machine.ts index 290a3aff91..21c8e4dedc 100644 --- a/packages/machines/combobox/src/combobox.machine.ts +++ b/packages/machines/combobox/src/combobox.machine.ts @@ -529,10 +529,10 @@ export function machine(userContext: UserDefinedContex contentEl.scrollTop = 0 }, invokeOnOpen(ctx) { - ctx.onOpen?.() + ctx.onOpenChange?.({ open: true }) }, invokeOnClose(ctx) { - ctx.onClose?.() + ctx.onOpenChange?.({ open: false }) }, highlightFirstItem(ctx) { const value = ctx.collection.first() @@ -570,7 +570,10 @@ export function machine(userContext: UserDefinedContex const invoke = { selectionChange: (ctx: MachineContext) => { - ctx.onChange?.({ value: Array.from(ctx.value), items: ctx.selectedItems }) + ctx.onValueChange?.({ + value: Array.from(ctx.value), + items: ctx.selectedItems, + }) // side effect: sync inputValue ctx.inputValue = match(ctx.selectionBehavior, { @@ -580,10 +583,13 @@ const invoke = { }) }, highlightChange: (ctx: MachineContext) => { - ctx.onHighlight?.({ value: ctx.highlightedValue, item: ctx.highlightedItem }) + ctx.onHighlightChange?.({ + highlightedValue: ctx.highlightedValue, + highligtedItem: ctx.highlightedItem, + }) }, inputChange: (ctx: MachineContext) => { - ctx.onInputChange?.({ value: ctx.inputValue }) + ctx.onInputValueChange?.({ value: ctx.inputValue }) }, } diff --git a/packages/machines/combobox/src/combobox.types.ts b/packages/machines/combobox/src/combobox.types.ts index 716ce7262c..ebe9ecd17a 100644 --- a/packages/machines/combobox/src/combobox.types.ts +++ b/packages/machines/combobox/src/combobox.types.ts @@ -4,9 +4,33 @@ import type { InteractOutsideHandlers } from "@zag-js/dismissable" import type { Placement, PositioningOptions } from "@zag-js/popper" import type { CommonProperties, Context, DirectionProperty, PropTypes, RequiredBy } from "@zag-js/types" -export type { CollectionOptions, CollectionItem } +/* ----------------------------------------------------------------------------- + * Callback details + * -----------------------------------------------------------------------------*/ -type IntlTranslations = { +export interface ValueChangeDetails { + value: string[] + items: T[] +} + +export interface HighlightChangeDetails { + highlightedValue: string | null + highligtedItem: T | null +} + +export interface InputValueChangeDetails { + value: string +} + +export interface OpenChangeDetails { + open: boolean +} + +/* ----------------------------------------------------------------------------- + * Machine context + * -----------------------------------------------------------------------------*/ + +interface IntlTranslations { triggerLabel?: string clearTriggerLabel?: string } @@ -25,145 +49,125 @@ type ElementIds = Partial<{ itemGroupLabel(id: string | number): string }> -export type ValueChangeDetails = { +interface PublicContext + extends DirectionProperty, + CommonProperties, + InteractOutsideHandlers { + /** + * The ids of the elements in the combobox. Useful for composition. + */ + ids?: ElementIds + /** + * The current value of the combobox's input + */ + inputValue: string + /** + * The `name` attribute of the combobox's input. Useful for form submission + */ + name?: string + /** + * The associate form of the combobox. + */ + form?: string + /** + * Whether the combobox is disabled + */ + disabled?: boolean + /** + * Whether the combobox is readonly. This puts the combobox in a "non-editable" mode + * but the user can still interact with it + */ + readOnly?: boolean + /** + * Whether the combobox is required + */ + invalid?: boolean + /** + * The placeholder text of the combobox's input + */ + placeholder?: string + /** + * The active item's id. Used to set the `aria-activedescendant` attribute + */ + highlightedValue: string | null + /** + * The keys of the selected items + */ value: string[] - items: T[] -} - -export type HighlightChangeDetails = { - value: string | null - item: T | null -} - -type InputValueChangeDetails = { - value: string + /** + * Defines the auto-completion behavior of the combobox. + * + * - `autohighlight`: The first focused item is highlighted as the user types + * - `autocomplete`: Navigating the listbox with the arrow keys selects the item and the input is updated + */ + inputBehavior: "autohighlight" | "autocomplete" | "none" + /** + * The behavior of the combobox input when an item is selected + * + * - `replace`: The selected item string is set as the input value + * - `clear`: The input value is cleared + * - `preserve`: The input value is preserved + */ + selectionBehavior: "clear" | "replace" | "preserve" + /** + * Whether to select the higlighted item on interaction outside the combobox + */ + selectOnBlur: boolean + /** + * Whether to autofocus the input on mount + */ + autoFocus?: boolean + /** + * Whether to open the combobox popup on initial click on the input + */ + openOnClick?: boolean + /** + * Whether to allow custom values or free values in the input + */ + allowCustomValue?: boolean + /** + * Whether to loop the keyboard navigation through the items + */ + loop?: boolean + /** + * The positioning options to dynamically position the menu + */ + positioning: PositioningOptions + /** + * Function called when the input's value changes + */ + onInputValueChange?: (details: InputValueChangeDetails) => void + /** + * Function called when a new item is selected + */ + onValueChange?: (details: ValueChangeDetails) => void + /** + * Function called when an item is highlighted using the pointer + * or keyboard navigation. + */ + onHighlightChange?: (details: HighlightChangeDetails) => void + /** + * Function called when the popup is opened + */ + onOpenChange?: (details: OpenChangeDetails) => void + /** + * Specifies the localized strings that identifies the accessibility elements and their states + */ + translations: IntlTranslations + /** + * The collection of items + */ + collection: Collection + /** + * Whether to allow multiple selection + */ + multiple?: boolean + /** + * Whether to close the combobox when an item is selected. + */ + closeOnSelect?: boolean } -type PublicContext = DirectionProperty & - CommonProperties & - InteractOutsideHandlers & { - /** - * The ids of the elements in the combobox. Useful for composition. - */ - ids?: ElementIds - /** - * The current value of the combobox's input - */ - inputValue: string - /** - * The `name` attribute of the combobox's input. Useful for form submission - */ - name?: string - /** - * The associate form of the combobox. - */ - form?: string - /** - * Whether the combobox is disabled - */ - disabled?: boolean - /** - * Whether the combobox is readonly. This puts the combobox in a "non-editable" mode - * but the user can still interact with it - */ - readOnly?: boolean - /** - * Whether the combobox is required - */ - invalid?: boolean - /** - * The placeholder text of the combobox's input - */ - placeholder?: string - /** - * The active item's id. Used to set the `aria-activedescendant` attribute - */ - highlightedValue: string | null - /** - * The keys of the selected items - */ - value: string[] - /** - * Defines the auto-completion behavior of the combobox. - * - * - `autohighlight`: The first focused item is highlighted as the user types - * - `autocomplete`: Navigating the listbox with the arrow keys selects the item and the input is updated - */ - inputBehavior: "autohighlight" | "autocomplete" | "none" - /** - * The behavior of the combobox input when an item is selected - * - * - `replace`: The selected item string is set as the input value - * - `clear`: The input value is cleared - * - `preserve`: The input value is preserved - */ - selectionBehavior: "clear" | "replace" | "preserve" - /** - * Whether to select the higlighted item on interaction outside the combobox - */ - selectOnBlur: boolean - /** - * Whether to autofocus the input on mount - */ - autoFocus?: boolean - /** - * Whether to open the combobox popup on initial click on the input - */ - openOnClick?: boolean - /** - * Whether to allow custom values or free values in the input - */ - allowCustomValue?: boolean - /** - * Whether to loop the keyboard navigation through the items - */ - loop?: boolean - /** - * The positioning options to dynamically position the menu - */ - positioning: PositioningOptions - /** - * Function called when the input's value changes - */ - onInputChange?: (details: InputValueChangeDetails) => void - /** - * Function called when a new item is selected - */ - onChange?: (details: ValueChangeDetails) => void - /** - * Function called when an item is highlighted using the pointer - * or keyboard navigation. - */ - onHighlight?: (details: HighlightChangeDetails) => void - /** - * Function called when the popup is opened - */ - onOpen?: VoidFunction - /** - * Function called when the popup is closed - */ - onClose?: VoidFunction - /** - * Specifies the localized strings that identifies the accessibility elements and their states - */ - translations: IntlTranslations - /** - * The collection of items - */ - collection: Collection - /** - * Whether to allow multiple selection - */ - multiple?: boolean - /** - * Whether to close the combobox when an item is selected. - */ - closeOnSelect?: boolean - } - -/** - * This is the actual context exposed to the user. - */ export type UserDefinedContext = RequiredBy< PublicContext, "id" | "collection" @@ -222,9 +226,9 @@ type PrivateContext = Context<{ composing: boolean }> -export type MachineContext = PublicContext & PrivateContext & ComputedContext +export interface MachineContext extends PublicContext, PrivateContext, ComputedContext {} -export type MachineState = { +export interface MachineState { value: "idle" | "focused" | "suggesting" | "interacting" tags: "open" | "focused" | "idle" | "closed" } @@ -233,28 +237,30 @@ export type State = S.State export type Send = S.Send -export type ItemProps = { +/* ----------------------------------------------------------------------------- + * Component API + * -----------------------------------------------------------------------------*/ + +export interface ItemProps { item: CollectionItem } -export type ItemState = { +export interface ItemState { value: string isDisabled: boolean isSelected: boolean isHighlighted: boolean } -export type ItemGroupProps = { +export interface ItemGroupProps { id: string } -export type ItemGroupLabelProps = { +export interface ItemGroupLabelProps { htmlFor: string } -export type { Placement, PositioningOptions } - -export type MachineApi = { +export interface MachineApi { /** * Whether the combobox is focused */ @@ -350,3 +356,9 @@ export type MachineApi value.toString().split("T")[0] @@ -94,8 +93,6 @@ export function connect(state: State, send: Send, normalize isOpen, view: state.context.view, - matchView, - getDaysInWeek(weekIndex: number, from = startValue) { return getDaysInWeek(weekIndex, from, locale, startOfWeek) }, diff --git a/packages/machines/date-picker/src/date-picker.machine.ts b/packages/machines/date-picker/src/date-picker.machine.ts index d027aeda18..1aff9a44cd 100644 --- a/packages/machines/date-picker/src/date-picker.machine.ts +++ b/packages/machines/date-picker/src/date-picker.machine.ts @@ -40,7 +40,7 @@ const getInitialContext = (ctx: Partial): MachineContext => { value = value.map((date) => constrainValue(date, ctx.min, ctx.max)) // get initial focused value - let focusedValue = value.at(0) || ctx.focusedValue || getTodayDate(timeZone) + let focusedValue = value[0] || ctx.focusedValue || getTodayDate(timeZone) focusedValue = constrainValue(focusedValue, ctx.min, ctx.max) // get initial start value for visible range @@ -145,7 +145,7 @@ export function machine(userContext: UserDefinedContext) { }, "TRIGGER.CLICK": { target: "open", - actions: ["focusFirstSelectedDate"], + actions: ["focusFirstSelectedDate", "invokeOnOpen"], }, }, }, @@ -155,7 +155,7 @@ export function machine(userContext: UserDefinedContext) { on: { "TRIGGER.CLICK": { target: "open", - actions: ["setViewToDay", "focusFirstSelectedDate"], + actions: ["setViewToDay", "focusFirstSelectedDate", "invokeOnOpen"], }, "INPUT.CHANGE": { actions: ["focusParsedDate"], @@ -168,7 +168,7 @@ export function machine(userContext: UserDefinedContext) { }, "CELL.FOCUS": { target: "open", - actions: ["setView"], + actions: ["setView", "invokeOnOpen"], }, }, }, @@ -209,6 +209,7 @@ export function machine(userContext: UserDefinedContext) { "setStartIndex", "clearHoveredDate", "focusInputElement", + "invokeOnClose", ], }, // === @@ -227,7 +228,7 @@ export function machine(userContext: UserDefinedContext) { }, { target: "focused", - actions: ["setFocusedDate", "setSelectedDate", "focusInputElement"], + actions: ["setFocusedDate", "setSelectedDate", "focusInputElement", "invokeOnClose"], }, // === ], @@ -250,7 +251,7 @@ export function machine(userContext: UserDefinedContext) { "GRID.ESCAPE": { guard: not("isInline"), target: "focused", - actions: ["setViewToDay", "focusFirstSelectedDate", "focusTriggerElement"], + actions: ["setViewToDay", "focusFirstSelectedDate", "focusTriggerElement", "invokeOnClose"], }, "GRID.ENTER": [ { @@ -273,7 +274,7 @@ export function machine(userContext: UserDefinedContext) { { target: "focused", guard: and("isRangePicker", "isSelectingEndDate"), - actions: ["setSelectedDate", "setStartIndex", "focusInputElement"], + actions: ["setSelectedDate", "setStartIndex", "focusInputElement", "invokeOnClose"], }, // === { @@ -291,7 +292,7 @@ export function machine(userContext: UserDefinedContext) { }, { target: "focused", - actions: ["selectFocusedDate", "focusInputElement"], + actions: ["selectFocusedDate", "focusInputElement", "invokeOnClose"], }, // === ], @@ -333,6 +334,7 @@ export function machine(userContext: UserDefinedContext) { ], "TRIGGER.CLICK": { target: "focused", + actions: ["invokeOnClose"], }, "VIEW.CHANGE": [ { @@ -348,11 +350,11 @@ export function machine(userContext: UserDefinedContext) { { guard: "isTargetFocusable", target: "idle", - actions: ["setStartIndex"], + actions: ["setStartIndex", "invokeOnClose"], }, { target: "focused", - actions: ["focusTriggerElement", "setStartIndex"], + actions: ["focusTriggerElement", "setStartIndex", "invokeOnClose"], }, ], }, @@ -660,6 +662,12 @@ export function machine(userContext: UserDefinedContext) { const startValue = alignDate(ctx.focusedValue, "start", { months: ctx.numOfMonths }, ctx.locale) ctx.startValue = startValue }, + invokeOnOpen(ctx) { + ctx.onOpenChange?.({ open: true }) + }, + invokeOnClose(ctx) { + ctx.onOpenChange?.({ open: false }) + }, }, compareFns: { startValue: (a, b) => a.toString() === b.toString(), @@ -672,16 +680,20 @@ export function machine(userContext: UserDefinedContext) { const invoke = { change(ctx: MachineContext) { - const details = { value: Array.from(ctx.value), view: ctx.view } - ctx.onChange?.(details) + ctx.onValueChange?.({ + value: Array.from(ctx.value), + view: ctx.view, + }) }, focusChange(ctx: MachineContext) { - const details = { focusedValue: ctx.focusedValue, value: Array.from(ctx.value), view: ctx.view } - ctx.onFocusChange?.(details) + ctx.onFocusChange?.({ + focusedValue: ctx.focusedValue, + value: Array.from(ctx.value), + view: ctx.view, + }) }, viewChange(ctx: MachineContext) { - const details = { view: ctx.view } - ctx.onViewChange?.(details) + ctx.onViewChange?.({ view: ctx.view }) }, } diff --git a/packages/machines/date-picker/src/date-picker.types.ts b/packages/machines/date-picker/src/date-picker.types.ts index 509fd72131..9742bad974 100644 --- a/packages/machines/date-picker/src/date-picker.types.ts +++ b/packages/machines/date-picker/src/date-picker.types.ts @@ -11,13 +11,38 @@ import type { Placement, PositioningOptions } from "@zag-js/popper" import type { StateMachine as S } from "@zag-js/core" import type { LiveRegion } from "@zag-js/live-region" import type { CommonProperties, Context, DirectionProperty, PropTypes, RequiredBy } from "@zag-js/types" -import type { matchView } from "./date-picker.utils" -type ChangeDetails = T & { +/* ----------------------------------------------------------------------------- + * Callback details + * -----------------------------------------------------------------------------*/ + +export type DateView = "day" | "month" | "year" + +export interface ValueChangeDetails { value: DateValue[] + view: DateView +} + +export interface FocusChangeDetails extends ValueChangeDetails { + focusedValue: DateValue + view: DateView +} + +export interface ViewChangeDetails { + view: DateView } -type IntlMessages = { +export interface OpenChangeDetails { + open: boolean +} + +/* ----------------------------------------------------------------------------- + * Machine context + * -----------------------------------------------------------------------------*/ + +export type SelectionMode = "single" | "multiple" | "range" + +interface IntlMessages { placeholder: (locale: string) => { year: string; month: string; day: string } } @@ -38,165 +63,154 @@ type ElementIds = Partial<{ positioner: string }> -export type SelectionMode = "single" | "multiple" | "range" - -export type Offset = { - amount: number - visibleRange: { start: DateValue; end: DateValue } -} - -export type DayCellProps = { - value: DateValue - disabled?: boolean - offset?: Offset -} - -export type MachineState = { - tags: "open" | "closed" - value: "idle" | "focused" | "open" -} - -export type GridProps = { - view?: DateView - columns?: number - id?: string -} - -export type CellProps = { +interface PublicContext extends DirectionProperty, CommonProperties { + /** + * The localized messages to use. + */ + messages?: IntlMessages + /** + * The ids of the elements in the date picker. Useful for composition. + */ + ids?: ElementIds + /** + * The `name` attribute of the input element. + */ + name?: string + /** + * The locale (BCP 47 language tag) to use when formatting the date. + */ + locale: string + /** + * The time zone to use + */ + timeZone: string + /** + * Whether the calendar is disabled. + */ disabled?: boolean - value: number -} - -type PublicContext = DirectionProperty & - CommonProperties & { - /** - * The localized messages to use. - */ - messages?: IntlMessages - /** - * The ids of the elements in the date picker. Useful for composition. - */ - ids?: ElementIds - /** - * The `name` attribute of the input element. - */ - name?: string - /** - * The locale (BCP 47 language tag) to use when formatting the date. - */ - locale: string - /** - * The time zone to use - */ - timeZone: string - /** - * Whether the calendar is disabled. - */ - disabled?: boolean - /** - * Whether the calendar is read-only. - */ - readOnly?: boolean - /** - * The minimum date that can be selected. - */ - min?: DateValue - /** - * The maximum date that can be selected. - */ - max?: DateValue - /** - * Whether the calendar should be displayed inline. - */ - inline?: boolean - /** - * The selected date(s). - */ - value: DateValue[] - /** - * The focused date. - */ - focusedValue: DateValue - /** - * The number of months to display. - */ - numOfMonths: number - /** - * The first day of the week. - * `0` - Sunday - * `1` - Monday - * `2` - Tuesday - * `3` - Wednesday - * `4` - Thursday - * `5` - Friday - * `6` - Saturday - */ - startOfWeek?: number - /** - * Whether the calendar should have a fixed number of weeks. - * This renders the calendar with 6 weeks instead of 5 or 6. - */ - fixedWeeks?: boolean - /** - * Function called when the value changes. - */ - onChange?: (details: ChangeDetails) => void - /** - * Function called when the focused date changes. - */ - onFocusChange?: (details: ChangeDetails<{ focusedValue: DateValue; view: DateView }>) => void - /** - * Function called when the view changes. - */ - onViewChange?: (details: { view: DateView }) => void - /** - * Returns whether a date of the calendar is available. - */ - isDateUnavailable?: (date: DateValue, locale: string) => boolean - /** - * The selection mode of the calendar. - * - `single` - only one date can be selected - * - `multiple` - multiple dates can be selected - * - `range` - a range of dates can be selected - */ - selectionMode: SelectionMode - /** - * The format of the date to display in the input. - */ - format?: (date: DateValue[]) => string - /** - * The format of the date to display in the input. - */ - parse?: (value: string) => DateValue[] - /** - * The view of the calendar - * @default "day" - */ - view: DateView - /** - * Whether the calendar should be modal. This means that the calendar will - * block interaction with the rest of the page, and trap focus within it. - */ - modal?: boolean - /** - * The user provided options used to position the date picker content - */ - positioning: PositioningOptions - } - -export type DateView = "day" | "month" | "year" - -export type ViewProps = { - view?: DateView + /** + * Whether the calendar is read-only. + */ + readOnly?: boolean + /** + * The minimum date that can be selected. + */ + min?: DateValue + /** + * The maximum date that can be selected. + */ + max?: DateValue + /** + * Whether the calendar should be displayed inline. + */ + inline?: boolean + /** + * The selected date(s). + */ + value: DateValue[] + /** + * The focused date. + */ + focusedValue: DateValue + /** + * The number of months to display. + */ + numOfMonths: number + /** + * The first day of the week. + * `0` - Sunday + * `1` - Monday + * `2` - Tuesday + * `3` - Wednesday + * `4` - Thursday + * `5` - Friday + * `6` - Saturday + */ + startOfWeek?: number + /** + * Whether the calendar should have a fixed number of weeks. + * This renders the calendar with 6 weeks instead of 5 or 6. + */ + fixedWeeks?: boolean + /** + * Function called when the value changes. + */ + onValueChange?: (details: ValueChangeDetails) => void + /** + * Function called when the focused date changes. + */ + onFocusChange?: (details: FocusChangeDetails) => void + /** + * Function called when the view changes. + */ + onViewChange?: (details: ViewChangeDetails) => void + /** + * Function called when the calendar opens or closes. + */ + onOpenChange?: (details: OpenChangeDetails) => void + /** + * Returns whether a date of the calendar is available. + */ + isDateUnavailable?: (date: DateValue, locale: string) => boolean + /** + * The selection mode of the calendar. + * - `single` - only one date can be selected + * - `multiple` - multiple dates can be selected + * - `range` - a range of dates can be selected + */ + selectionMode: SelectionMode + /** + * The format of the date to display in the input. + */ + format?: (date: DateValue[]) => string + /** + * The format of the date to display in the input. + */ + parse?: (value: string) => DateValue[] + /** + * The view of the calendar + * @default "day" + */ + view: DateView + /** + * Whether the calendar should be modal. This means that the calendar will + * block interaction with the rest of the page, and trap focus within it. + */ + modal?: boolean + /** + * The user provided options used to position the date picker content + */ + positioning: PositioningOptions } type PrivateContext = Context<{ + /** + * @internal + * The start date of the current visible duration. + */ startValue: DateValue + /** + * @internal + * Whether the calendar has focus + */ hasFocus?: boolean + /** + * @internal + * The live region to announce changes + */ announcer?: LiveRegion + /** + * @internal + * The input element's value + */ inputValue: string + /** + * @internal + * The current hovered date. Useful for range selection mode. + */ hoveredValue: DateValue | null /** + * @internal * The index of the currently active date. * Used in range selection mode. */ @@ -253,15 +267,91 @@ type ComputedContext = Readonly<{ export type UserDefinedContext = RequiredBy -export type MachineContext = PublicContext & PrivateContext & ComputedContext +export interface MachineContext extends PublicContext, PrivateContext, ComputedContext {} + +export interface MachineState { + tags: "open" | "closed" + value: "idle" | "focused" | "open" +} export type State = S.State export type Send = S.Send -export type { Calendar, CalendarDate, CalendarDateTime, DateDuration, DateFormatter, DateValue, ZonedDateTime } +/* ----------------------------------------------------------------------------- + * Component API + * -----------------------------------------------------------------------------*/ + +export interface Offset { + amount: number + visibleRange: { start: DateValue; end: DateValue } +} + +export interface CellProps { + disabled?: boolean + value: number +} -export type MachineApi = { +export interface CellState { + isFocused: boolean + isSelectable: boolean + isSelected: boolean + valueText: string + readonly isDisabled: boolean +} + +export interface DayCellProps { + value: DateValue + disabled?: boolean + offset?: Offset +} + +export interface DayCellState { + isInvalid: boolean + isDisabled: boolean + isSelected: boolean + isUnavailable: boolean + isOutsideRange: boolean + isInRange: boolean + isFirstInRange: boolean + isLastInRange: boolean + isToday: boolean + isWeekend: boolean + formattedDate: string + readonly isFocused: boolean + readonly ariaLabel: string + readonly isSelectable: boolean +} + +export interface GridProps { + view?: DateView + columns?: number + id?: string +} + +export interface ViewProps { + view?: DateView +} + +export interface MonthGridProps { + columns?: number + format?: "short" | "long" +} + +interface GridItem { + label: string + value: number +} + +export type MonthGridValue = GridItem[][] + +export interface YearGridProps { + columns?: number +} + +export type YearGridValue = GridItem[][] + +export interface MachineApi { /** * Whether the input is focused */ @@ -274,10 +364,6 @@ export type MachineApi = { * The current view of the date picker */ view: DateView - /** - * Matcher for the current view of the date picker - */ - matchView: typeof matchView /** * Returns an array of days in the week index counted from the provided start date, or the first visible date if not given. */ @@ -388,18 +474,12 @@ export type MachineApi = { /** * Returns the months of the year */ - getYears(): { - label: string - value: number - }[] + getYears(): GridItem[] /** * Returns the years of the decade based on the columns. * Represented as an array of arrays of years. */ - getYearsGrid(props?: { columns?: number }): { - label: string - value: number - }[][] + getYearsGrid(props?: YearGridProps): YearGridValue /** * Returns the start and end years of the decade. */ @@ -410,18 +490,12 @@ export type MachineApi = { /** * Returns the months of the year */ - getMonths(props?: { format?: "short" | "long" }): { - label: string - value: number - }[] + getMonths(props?: { format?: "short" | "long" }): GridItem[] /** * Returns the months of the year based on the columns. * Represented as an array of arrays of months. */ - getMonthsGrid(props?: { columns?: number; format?: "short" | "long" }): { - label: string - value: number - }[][] + getMonthsGrid(props?: MonthGridProps): MonthGridValue /** * Formats the given date value based on the provided options. */ @@ -438,48 +512,24 @@ export type MachineApi = { * Goes to the previous month/year/decade. */ goToPrev(): void + /** + * Returns the state details for a given cell. + */ + getDayCellState(props: DayCellProps): DayCellState + /** + * Returns the state details for a given month cell. + */ + getMonthCellState(props: CellProps): CellState + controlProps: T["element"] contentProps: T["element"] positionerProps: T["element"] - getGridProps(props?: GridProps): T["element"] - /** - * Returns the state details for a given cell. - */ - getDayCellState(props: DayCellProps): { - isInvalid: boolean - isDisabled: boolean - isSelected: boolean - isUnavailable: boolean - isOutsideRange: boolean - isInRange: boolean - isFirstInRange: boolean - isLastInRange: boolean - isToday: boolean - isWeekend: boolean - formattedDate: string - readonly isFocused: boolean - readonly ariaLabel: string - readonly isSelectable: boolean - } getDayCellProps(props: DayCellProps): T["element"] getDayCellTriggerProps(props: DayCellProps): T["element"] - getMonthCellState(props: CellProps): { - isFocused: boolean - isSelectable: boolean - isSelected: boolean - valueText: string - readonly isDisabled: boolean - } getMonthCellProps(props: CellProps): T["element"] getMonthCellTriggerProps(props: CellProps): T["element"] - getYearCellState(props: CellProps): { - isFocused: boolean - isSelectable: boolean - isSelected: boolean - valueText: string - readonly isDisabled: boolean - } + getYearCellState(props: CellProps): CellState getYearCellProps(props: CellProps): T["element"] getYearCellTriggerProps(props: CellProps): T["element"] getNextTriggerProps(props?: ViewProps): T["button"] @@ -492,3 +542,9 @@ export type MachineApi = { monthSelectProps: T["select"] yearSelectProps: T["select"] } + +/* ----------------------------------------------------------------------------- + * Re-exported types + * -----------------------------------------------------------------------------*/ + +export type { Calendar, CalendarDate, CalendarDateTime, DateDuration, DateFormatter, DateValue, ZonedDateTime } diff --git a/packages/machines/date-picker/src/date-picker.utils.ts b/packages/machines/date-picker/src/date-picker.utils.ts index 4027485623..ae1d14ce16 100644 --- a/packages/machines/date-picker/src/date-picker.utils.ts +++ b/packages/machines/date-picker/src/date-picker.utils.ts @@ -1,4 +1,5 @@ import { DateFormatter, type DateValue } from "@internationalized/date" +import { match } from "@zag-js/utils" import type { DateView, MachineContext } from "./date-picker.types" export function adjustStartAndEndDate(value: DateValue[]) { @@ -17,12 +18,6 @@ export function sortDates(values: DateValue[]) { return values.sort((a, b) => a.compare(b)) } -export function matchView(view: DateView, values: { year: T; month: T; day: T }) { - if (view === "year") return values.year - if (view === "month") return values.month - return values.day -} - export function formatValue(ctx: Pick) { const formatter = new DateFormatter(ctx.locale, { timeZone: ctx.timeZone, @@ -47,7 +42,7 @@ export function formatValue(ctx: Pick -type PublicContext = DirectionProperty & - CommonProperties & { - /** - * The ids of the elements in the dialog. Useful for composition. - */ - ids?: ElementIds - /** - * Whether to trap focus inside the dialog when it's opened - */ - trapFocus: boolean - /** - * Whether to prevent scrolling behind the dialog when it's opened - */ - preventScroll: boolean - /** - * Whether to prevent pointer interaction outside the element and hide all content below it - */ - modal?: boolean - /** - * Element to receive focus when the dialog is opened - */ - initialFocusEl?: MaybeElement | (() => MaybeElement) - /** - * Element to receive focus when the dialog is closed - */ - finalFocusEl?: MaybeElement | (() => MaybeElement) - /** - * Whether to restore focus to the element that had focus before the dialog was opened - */ - restoreFocus?: boolean - /** - * Callback to be invoked when the dialog is closed - */ - onClose?: () => void - /** - * Callback to be invoked when the dialog is opened - */ - onOpen?: () => void - /** - * Whether to close the dialog when the outside is clicked - */ - closeOnOutsideClick: boolean - /** - * Callback to be invoked when the outside is clicked - */ - onOutsideClick?: () => void - /** - * Whether to close the dialog when the escape key is pressed - */ - closeOnEsc: boolean - /** - * Callback to be invoked when the escape key is pressed - */ - onEsc?: () => void - /** - * Human readable label for the dialog, in event the dialog title is not rendered - */ - "aria-label"?: string - /** - * The dialog's role - * @default "dialog" - */ - role: "dialog" | "alertdialog" - /** - * Whether the dialog is open - */ - open?: boolean - } +interface PublicContext extends DirectionProperty, CommonProperties { + /** + * The ids of the elements in the dialog. Useful for composition. + */ + ids?: ElementIds + /** + * Whether to trap focus inside the dialog when it's opened + */ + trapFocus: boolean + /** + * Whether to prevent scrolling behind the dialog when it's opened + */ + preventScroll: boolean + /** + * Whether to prevent pointer interaction outside the element and hide all content below it + */ + modal?: boolean + /** + * Element to receive focus when the dialog is opened + */ + initialFocusEl?: MaybeElement | (() => MaybeElement) + /** + * Element to receive focus when the dialog is closed + */ + finalFocusEl?: MaybeElement | (() => MaybeElement) + /** + * Whether to restore focus to the element that had focus before the dialog was opened + */ + restoreFocus?: boolean + /** + * Callback to be invoked when the dialog is opened or closed + */ + onOpenChange?: (details: OpenChangeDetails) => void + /** + * Whether to close the dialog when the outside is clicked + */ + closeOnOutsideClick: boolean + /** + * Callback to be invoked when the outside is clicked + */ + onOutsideClick?: VoidFunction + /** + * Whether to close the dialog when the escape key is pressed + */ + closeOnEsc: boolean + /** + * Callback to be invoked when the escape key is pressed + */ + onEsc?: VoidFunction + /** + * Human readable label for the dialog, in event the dialog title is not rendered + */ + "aria-label"?: string + /** + * The dialog's role + * @default "dialog" + */ + role: "dialog" | "alertdialog" + /** + * Whether the dialog is open + */ + open?: boolean +} export type UserDefinedContext = RequiredBy @@ -95,9 +102,9 @@ type PrivateContext = Context<{ } }> -export type MachineContext = PublicContext & PrivateContext & ComputedContext +export interface MachineContext extends PublicContext, PrivateContext, ComputedContext {} -export type MachineState = { +export interface MachineState { value: "open" | "closed" } @@ -105,7 +112,11 @@ export type State = S.State export type Send = S.Send -export type MachineApi = { +/* ----------------------------------------------------------------------------- + * Component props + * -----------------------------------------------------------------------------*/ + +export interface MachineApi { /** * Whether the dialog is open */ @@ -118,6 +129,7 @@ export type MachineApi = { * Function to close the dialog */ close(): void + triggerProps: T["button"] backdropProps: T["element"] containerProps: T["element"] diff --git a/packages/machines/editable/src/editable.machine.ts b/packages/machines/editable/src/editable.machine.ts index 8183d35ec8..35d4b41113 100644 --- a/packages/machines/editable/src/editable.machine.ts +++ b/packages/machines/editable/src/editable.machine.ts @@ -12,7 +12,10 @@ export function machine(userContext: UserDefinedContext) { return createMachine( { id: "editable", + initial: ctx.startWithEditView ? "edit" : "preview", + entry: ctx.startWithEditView ? ["focusInput"] : undefined, + context: { activationMode: "focus", submitMode: "both", @@ -144,7 +147,6 @@ export function machine(userContext: UserDefinedContext) { raf(() => { const inputEl = dom.getInputEl(ctx) if (!inputEl) return - if (ctx.selectOnFocus) { inputEl.select() } else { @@ -153,18 +155,17 @@ export function machine(userContext: UserDefinedContext) { }) }, invokeOnCancel(ctx) { - ctx.onCancel?.({ value: ctx.previousValue }) + ctx.onValueRevert?.({ value: ctx.previousValue }) }, invokeOnSubmit(ctx) { - ctx.onSubmit?.({ value: ctx.value }) + ctx.onValueCommit?.({ value: ctx.value }) }, invokeOnEdit(ctx) { ctx.onEdit?.() }, syncInputValue(ctx) { - const input = dom.getInputEl(ctx) - if (!input) return - input.value = ctx.value + const inputEl = dom.getInputEl(ctx) + dom.setValue(inputEl, ctx.value) }, setValue(ctx, evt) { set.value(ctx, evt.value) @@ -185,7 +186,7 @@ export function machine(userContext: UserDefinedContext) { const invoke = { change(ctx: MachineContext) { - ctx.onChange?.({ value: ctx.value }) + ctx.onValueChange?.({ value: ctx.value }) }, } diff --git a/packages/machines/editable/src/editable.types.ts b/packages/machines/editable/src/editable.types.ts index d9eb0fa054..a223e4e17c 100644 --- a/packages/machines/editable/src/editable.types.ts +++ b/packages/machines/editable/src/editable.types.ts @@ -1,7 +1,19 @@ import type { StateMachine as S } from "@zag-js/core" -import type { FocusOutsideEvent, InteractOutsideEvent, PointerDownOutsideEvent } from "@zag-js/interact-outside" +import type { InteractOutsideHandlers } from "@zag-js/interact-outside" import type { CommonProperties, Context, DirectionProperty, PropTypes, RequiredBy } from "@zag-js/types" +/* ----------------------------------------------------------------------------- + * Callback details + * -----------------------------------------------------------------------------*/ + +export interface ValueChangeDetails { + value: string +} + +/* ----------------------------------------------------------------------------- + * Machine context + * -----------------------------------------------------------------------------*/ + export type ActivationMode = "focus" | "dblclick" | "none" export type SubmitMode = "enter" | "blur" | "both" | "none" @@ -25,107 +37,102 @@ type ElementIds = Partial<{ editTrigger: string }> -type PublicContext = DirectionProperty & - CommonProperties & { - /** - * The ids of the elements in the editable. Useful for composition. - */ - ids?: ElementIds - /** - * Whether the input's value is invalid. - */ - invalid?: boolean - /** - * The name attribute of the editable component. Used for form submission. - */ - name?: string - /** - * The associate form of the underlying input. - */ - form?: string - /** - * Whether the editable should auto-resize to fit the content. - */ - autoResize?: boolean - /** - * The activation mode for the preview element. - * - * - "focus" - Enter edit mode when the preview element is focused - * - "dblclick" - Enter edit mode when the preview element is double-clicked - * - "none" - No interaction with the preview element will trigger edit mode. - * - * @default "focus" - */ - activationMode: ActivationMode - /** - * The action that triggers submit in the edit mode: - * - * - "enter" - Trigger submit when the enter key is pressed - * - "blur" - Trigger submit when the editable is blurred - * - "none" - No action will trigger submit. You need to use the submit button - * - "both" - Pressing `Enter` and blurring the input will trigger submit - * - * @default "enter" - */ - submitMode: SubmitMode - /** - * Whether to start with the edit mode active. - */ - startWithEditView?: boolean - /** - * Whether to select the text in the input when it is focused. - */ - selectOnFocus?: boolean - /** - * The value of the editable in both edit and preview mode - */ - value: string - /** - * The maximum number of characters allowed in the editable - */ - maxLength?: number - /** - * Whether the editable is disabled - */ - disabled?: boolean - /** - * Whether the editable is readonly - */ - readOnly?: boolean - /** - * The callback that is called when the editable's value is changed - */ - onChange?: (details: { value: string }) => void - /** - * The callback that is called when the esc key is pressed or the cancel button is clicked - */ - onCancel?: (details: { value: string }) => void - /** - * The callback that is called when the editable's value is submitted. - */ - onSubmit?: (details: { value: string }) => void - /** - * The callback that is called when in the edit mode. - */ - onEdit?: () => void - /** - * The placeholder value to show when the `value` is empty - */ - placeholder?: string | { edit: string; preview: string } - /** - * Specifies the localized strings that identifies the accessibility elements and their states - */ - translations: IntlTranslations - /** - * The element that should receive focus when the editable is closed. - * By default, it will focus on the trigger element. - */ - finalFocusEl?: () => HTMLElement | null - - onPointerDownOutside?: (event: PointerDownOutsideEvent) => void - onFocusOutside?: (event: FocusOutsideEvent) => void - onInteractOutside?: (event: InteractOutsideEvent) => void - } +interface PublicContext extends DirectionProperty, CommonProperties, InteractOutsideHandlers { + /** + * The ids of the elements in the editable. Useful for composition. + */ + ids?: ElementIds + /** + * Whether the input's value is invalid. + */ + invalid?: boolean + /** + * The name attribute of the editable component. Used for form submission. + */ + name?: string + /** + * The associate form of the underlying input. + */ + form?: string + /** + * Whether the editable should auto-resize to fit the content. + */ + autoResize?: boolean + /** + * The activation mode for the preview element. + * + * - "focus" - Enter edit mode when the preview element is focused + * - "dblclick" - Enter edit mode when the preview element is double-clicked + * - "none" - No interaction with the preview element will trigger edit mode. + * + * @default "focus" + */ + activationMode: ActivationMode + /** + * The action that triggers submit in the edit mode: + * + * - "enter" - Trigger submit when the enter key is pressed + * - "blur" - Trigger submit when the editable is blurred + * - "none" - No action will trigger submit. You need to use the submit button + * - "both" - Pressing `Enter` and blurring the input will trigger submit + * + * @default "enter" + */ + submitMode: SubmitMode + /** + * Whether to start with the edit mode active. + */ + startWithEditView?: boolean + /** + * Whether to select the text in the input when it is focused. + */ + selectOnFocus?: boolean + /** + * The value of the editable in both edit and preview mode + */ + value: string + /** + * The maximum number of characters allowed in the editable + */ + maxLength?: number + /** + * Whether the editable is disabled + */ + disabled?: boolean + /** + * Whether the editable is readonly + */ + readOnly?: boolean + /** + * The callback that is called when the editable's value is changed + */ + onValueChange?: (details: ValueChangeDetails) => void + /** + * The callback that is called when the esc key is pressed or the cancel button is clicked + */ + onValueRevert?: (details: ValueChangeDetails) => void + /** + * The callback that is called when the editable's value is submitted. + */ + onValueCommit?: (details: ValueChangeDetails) => void + /** + * The callback that is called when in the edit mode. + */ + onEdit?: () => void + /** + * The placeholder value to show when the `value` is empty + */ + placeholder?: string | { edit: string; preview: string } + /** + * Specifies the localized strings that identifies the accessibility elements and their states + */ + translations: IntlTranslations + /** + * The element that should receive focus when the editable is closed. + * By default, it will focus on the trigger element. + */ + finalFocusEl?: () => HTMLElement | null +} export type UserDefinedContext = RequiredBy @@ -165,9 +172,9 @@ type PrivateContext = Context<{ previousValue: string }> -export type MachineContext = PublicContext & PrivateContext & ComputedContext +export interface MachineContext extends PublicContext, PrivateContext, ComputedContext {} -export type MachineState = { +export interface MachineState { value: "preview" | "edit" } @@ -175,9 +182,11 @@ export type State = S.State export type Send = S.Send -export type { InteractOutsideEvent } +/* ----------------------------------------------------------------------------- + * Component API + * -----------------------------------------------------------------------------*/ -export type MachineApi = { +export interface MachineApi { /** * Whether the editable is in edit mode */ @@ -210,6 +219,7 @@ export type MachineApi = { * Function to exit edit mode, and submit any changes */ submit(): void + rootProps: T["element"] areaProps: T["element"] labelProps: T["label"] diff --git a/packages/machines/editable/src/index.ts b/packages/machines/editable/src/index.ts index 09bb1df5d2..7678bd66de 100644 --- a/packages/machines/editable/src/index.ts +++ b/packages/machines/editable/src/index.ts @@ -1,4 +1,4 @@ export { anatomy } from "./editable.anatomy" export { connect } from "./editable.connect" export { machine } from "./editable.machine" -export type { UserDefinedContext as Context, MachineApi as Api, InteractOutsideEvent } from "./editable.types" +export type { UserDefinedContext as Context, MachineApi as Api } from "./editable.types" diff --git a/packages/machines/file-upload/src/file-upload.connect.ts b/packages/machines/file-upload/src/file-upload.connect.ts index 375f4bdbd7..977bc634a1 100644 --- a/packages/machines/file-upload/src/file-upload.connect.ts +++ b/packages/machines/file-upload/src/file-upload.connect.ts @@ -22,11 +22,11 @@ export function connect(state: State, send: Send, normalize send({ type: "FILE.DELETE", file }) }, files: state.context.files, - setValue(files: File[]) { + setFiles(files: File[]) { const count = files.length send({ type: "VALUE.SET", files, count }) }, - clearValue() { + clearFiles() { send({ type: "VALUE.SET", files: [] }) }, diff --git a/packages/machines/file-upload/src/file-upload.machine.ts b/packages/machines/file-upload/src/file-upload.machine.ts index ea679ee10f..b2a025c1ef 100644 --- a/packages/machines/file-upload/src/file-upload.machine.ts +++ b/packages/machines/file-upload/src/file-upload.machine.ts @@ -125,12 +125,6 @@ export function machine(userContext: UserDefinedContext) { const nextFiles = ctx.files.filter((file) => file !== evt.file) set.files(ctx, nextFiles) }, - invokeOnChange(ctx) { - ctx.onChange?.({ - acceptedFiles: ctx.files, - rejectedFiles: ctx.rejectedFiles, - }) - }, }, compareFns: { files: (a, b) => a.length === b.length && a.every((file, i) => file === b[i]), @@ -141,7 +135,10 @@ export function machine(userContext: UserDefinedContext) { const invoke = { change: (ctx: MachineContext) => { - ctx.onChange?.({ acceptedFiles: ctx.files, rejectedFiles: ctx.rejectedFiles }) + ctx.onFilesChange?.({ + acceptedFiles: ctx.files, + rejectedFiles: ctx.rejectedFiles, + }) }, } diff --git a/packages/machines/file-upload/src/file-upload.types.ts b/packages/machines/file-upload/src/file-upload.types.ts index e14160732c..0513cde4a7 100644 --- a/packages/machines/file-upload/src/file-upload.types.ts +++ b/packages/machines/file-upload/src/file-upload.types.ts @@ -1,7 +1,25 @@ import type { StateMachine as S } from "@zag-js/core" import type { CommonProperties, PropTypes, RequiredBy } from "@zag-js/types" -type PublicContext = CommonProperties & { +/* ----------------------------------------------------------------------------- + * Callback details + * -----------------------------------------------------------------------------*/ + +export interface RejectedFile { + file: File + errors: (string | null)[] +} + +export interface FileChangeDetails { + acceptedFiles: File[] + rejectedFiles: RejectedFile[] +} + +/* ----------------------------------------------------------------------------- + * Machine context + * -----------------------------------------------------------------------------*/ + +interface PublicContext extends CommonProperties { /** * The name of the underlying file input */ @@ -41,40 +59,40 @@ type PublicContext = CommonProperties & { /** * Function called when the value changes */ - onChange?: (details: ChangeDetails) => void -} - -export type RejectedFile = { - file: File - errors: (string | null)[] -} - -type ChangeDetails = { - acceptedFiles: File[] - rejectedFiles: RejectedFile[] + onFilesChange?: (details: FileChangeDetails) => void } -type PrivateContext = { +interface PrivateContext { /** + * @internal * Whether the files includes any rejection */ invalid: boolean /** + * @internal * The rejected files */ rejectedFiles: RejectedFile[] } type ComputedContext = Readonly<{ + /** + * @computed + * The accept attribute as a string + */ acceptAttr: string | undefined + /** + * @computed + * Whether the file can select multiple files + */ multiple: boolean }> export type UserDefinedContext = RequiredBy -export type MachineContext = PublicContext & PrivateContext & ComputedContext +export interface MachineContext extends PublicContext, PrivateContext, ComputedContext {} -export type MachineState = { +export interface MachineState { value: "idle" | "focused" | "open" | "dragging" } @@ -82,7 +100,11 @@ export type State = S.State export type Send = S.Send -export type MachineApi = { +/* ----------------------------------------------------------------------------- + * Component API + * -----------------------------------------------------------------------------*/ + +export interface MachineApi { /** * Whether the user is dragging something over the root element */ @@ -106,11 +128,12 @@ export type MachineApi = { /** * Function to set the value */ - setValue(files: File[]): void + setFiles(files: File[]): void /** * Function to clear the value */ - clearValue(): void + clearFiles(): void + rootProps: T["element"] dropzoneProps: T["element"] triggerProps: T["button"] diff --git a/packages/machines/hover-card/src/hover-card.machine.ts b/packages/machines/hover-card/src/hover-card.machine.ts index b8d731239e..bca76bb71e 100644 --- a/packages/machines/hover-card/src/hover-card.machine.ts +++ b/packages/machines/hover-card/src/hover-card.machine.ts @@ -148,10 +148,10 @@ export function machine(userContext: UserDefinedContext) { }, actions: { invokeOnClose(ctx) { - ctx.onClose?.() + ctx.onOpenChange?.({ open: false }) }, invokeOnOpen(ctx) { - ctx.onOpen?.() + ctx.onOpenChange?.({ open: true }) }, setIsPointer(ctx) { ctx.isPointer = true diff --git a/packages/machines/hover-card/src/hover-card.types.ts b/packages/machines/hover-card/src/hover-card.types.ts index 368f2eedc3..f39dcc6515 100644 --- a/packages/machines/hover-card/src/hover-card.types.ts +++ b/packages/machines/hover-card/src/hover-card.types.ts @@ -2,6 +2,18 @@ import type { StateMachine as S } from "@zag-js/core" import type { Placement, PositioningOptions } from "@zag-js/popper" import type { CommonProperties, Context, DirectionProperty, PropTypes, RequiredBy } from "@zag-js/types" +/* ----------------------------------------------------------------------------- + * Callback details + * -----------------------------------------------------------------------------*/ + +export interface OpenChangeDetails { + open: boolean +} + +/* ----------------------------------------------------------------------------- + * Machine context + * -----------------------------------------------------------------------------*/ + type ElementIds = Partial<{ trigger: string content: string @@ -9,60 +21,31 @@ type ElementIds = Partial<{ arrow: string }> -type PublicContext = DirectionProperty & - CommonProperties & { - /** - * The ids of the elements in the popover. Useful for composition. - */ - ids?: ElementIds - /** - * Function invoked when the hover card is opened. - */ - onOpen?: VoidFunction - /** - * Function invoked when the hover card is closed. - */ - onClose?: VoidFunction - /** - * The duration from when the mouse enters the trigger until the hover card opens. - */ - openDelay: number - /** - * The duration from when the mouse leaves the trigger or content until the hover card closes. - */ - closeDelay: number - /** - * Whether the hover card is open - */ - open?: boolean - /** - * The user provided options used to position the popover content - */ - positioning: PositioningOptions - } - -export type MachineApi = { +interface PublicContext extends DirectionProperty, CommonProperties { /** - * Whether the hover card is open + * The ids of the elements in the popover. Useful for composition. */ - isOpen: boolean + ids?: ElementIds /** - * Function to open the hover card + * Function called when the hover card opens or closes. */ - open(): void + onOpenChange?: (details: OpenChangeDetails) => void /** - * Function to close the hover card + * The duration from when the mouse enters the trigger until the hover card opens. */ - close(): void + openDelay: number /** - * Function to reposition the popover + * The duration from when the mouse leaves the trigger or content until the hover card closes. */ - setPositioning(options?: Partial): void - arrowProps: T["element"] - arrowTipProps: T["element"] - triggerProps: T["element"] - positionerProps: T["element"] - contentProps: T["element"] + closeDelay: number + /** + * Whether the hover card is open + */ + open?: boolean + /** + * The user provided options used to position the popover content + */ + positioning: PositioningOptions } type PrivateContext = Context<{ @@ -84,7 +67,7 @@ export type UserDefinedContext = RequiredBy export type MachineContext = PublicContext & PrivateContext & ComputedContext -export type MachineState = { +export interface MachineState { value: "opening" | "open" | "closing" | "closed" tags: "open" | "closed" } @@ -93,4 +76,36 @@ export type State = S.State export type Send = S.Send +/* ----------------------------------------------------------------------------- + * Component API + * -----------------------------------------------------------------------------*/ + +export interface MachineApi { + /** + * Whether the hover card is open + */ + isOpen: boolean + /** + * Function to open the hover card + */ + open(): void + /** + * Function to close the hover card + */ + close(): void + /** + * Function to reposition the popover + */ + setPositioning(options?: Partial): void + arrowProps: T["element"] + arrowTipProps: T["element"] + triggerProps: T["element"] + positionerProps: T["element"] + contentProps: T["element"] +} + +/* ----------------------------------------------------------------------------- + * Re-exported types + * -----------------------------------------------------------------------------*/ + export type { Placement, PositioningOptions } diff --git a/packages/machines/menu/src/menu.machine.ts b/packages/machines/menu/src/menu.machine.ts index e7a78a51eb..1aca6baa31 100644 --- a/packages/machines/menu/src/menu.machine.ts +++ b/packages/machines/menu/src/menu.machine.ts @@ -536,10 +536,10 @@ export function machine(userContext: UserDefinedContext) { ctx.parent?.send("RESTORE_FOCUS") }, invokeOnOpen(ctx) { - ctx.onOpen?.() + ctx.onOpenChange?.({ open: true }) }, invokeOnClose(ctx) { - ctx.onClose?.() + ctx.onOpenChange?.({ open: false }) }, }, }, diff --git a/packages/machines/menu/src/menu.types.ts b/packages/machines/menu/src/menu.types.ts index cbe910d755..f213cb696f 100644 --- a/packages/machines/menu/src/menu.types.ts +++ b/packages/machines/menu/src/menu.types.ts @@ -5,6 +5,27 @@ import type { Placement, PositioningOptions } from "@zag-js/popper" import type { Point } from "@zag-js/rect-utils" import type { CommonProperties, Context, DirectionProperty, PropTypes, RequiredBy } from "@zag-js/types" +/* ----------------------------------------------------------------------------- + * Callback details + * -----------------------------------------------------------------------------*/ + +export interface OpenChangeDetails { + open: boolean +} + +export interface ValueChangeDetails { + name: string + value: string | string[] +} + +export interface SelectionDetails { + value: string +} + +/* ----------------------------------------------------------------------------- + * Machine context + * -----------------------------------------------------------------------------*/ + type ElementIds = Partial<{ trigger: string contextTrigger: string @@ -15,58 +36,52 @@ type ElementIds = Partial<{ arrow: string }> -type PublicContext = DirectionProperty & - CommonProperties & - InteractOutsideHandlers & { - /** - * The ids of the elements in the menu. Useful for composition. - */ - ids?: ElementIds - /** - * The values of radios and checkboxes in the menu. - */ - value?: Record - /** - * Callback to be called when the menu values change (for radios and checkboxes). - */ - onValueChange?: (details: { name: string; value: string | string[] }) => void - /** - * The `id` of the active menu item. - */ - highlightedId: string | null - /** - * Function called when a menu item is selected. - */ - onSelect?: (details: { value: string }) => void - /** - * The positioning point for the menu. Can be set by the context menu trigger or the button trigger. - */ - anchorPoint: Point | null - /** - * Whether to loop the keyboard navigation. - */ - loop: boolean - /** - * The options used to dynamically position the menu - */ - positioning: PositioningOptions - /** - * Whether to close the menu when an option is selected - */ - closeOnSelect: boolean - /** - * The accessibility label for the menu - */ - "aria-label"?: string - /** - * Function called when the menu is opened - */ - onOpen?: () => void - /** - * Function called when the menu is closed - */ - onClose?: () => void - } +interface PublicContext extends DirectionProperty, CommonProperties, InteractOutsideHandlers { + /** + * The ids of the elements in the menu. Useful for composition. + */ + ids?: ElementIds + /** + * The values of radios and checkboxes in the menu. + */ + value?: Record + /** + * Callback to be called when the menu values change (for radios and checkboxes). + */ + onValueChange?: (details: ValueChangeDetails) => void + /** + * The `id` of the active menu item. + */ + highlightedId: string | null + /** + * Function called when a menu item is selected. + */ + onSelect?: (details: SelectionDetails) => void + /** + * The positioning point for the menu. Can be set by the context menu trigger or the button trigger. + */ + anchorPoint: Point | null + /** + * Whether to loop the keyboard navigation. + */ + loop: boolean + /** + * The options used to dynamically position the menu + */ + positioning: PositioningOptions + /** + * Whether to close the menu when an option is selected + */ + closeOnSelect: boolean + /** + * The accessibility label for the menu + */ + "aria-label"?: string + /** + * Function called when the menu opens or closes + */ + onOpenChange?: (details: OpenChangeDetails) => void +} export type UserDefinedContext = RequiredBy @@ -132,9 +147,9 @@ type PrivateContext = Context<{ focusTriggerOnClose?: boolean }> -export type MachineContext = PublicContext & PrivateContext & ComputedContext +export interface MachineContext extends PublicContext, PrivateContext, ComputedContext {} -export type MachineState = { +export interface MachineState { value: "idle" | "open" | "closed" | "opening" | "closing" | "opening:contextmenu" tags: "visible" } @@ -145,12 +160,16 @@ export type Send = S.Send export type Service = Machine -export type Api = { +/* ----------------------------------------------------------------------------- + * Component API + * -----------------------------------------------------------------------------*/ + +export interface Api { getItemProps: (opts: ItemProps) => Record triggerProps: Record } -export type ItemProps = { +export interface ItemProps { /** * The `id` of the menu item option. */ @@ -170,7 +189,7 @@ export type ItemProps = { closeOnSelect?: boolean } -export type OptionItemProps = Partial & { +export interface OptionItemProps extends Partial { /** * The option's name as specified in menu's `context.values` object */ @@ -189,21 +208,21 @@ export type OptionItemProps = Partial & { onCheckedChange?: (checked: boolean) => void } -export type GroupProps = { +export interface GroupProps { /** * The `id` of the element that provides accessibility label to the option group */ id: string } -export type LabelProps = { +export interface LabelProps { /** * The `id` of the group this refers to */ htmlFor: string } -export type MachineApi = { +export interface MachineApi { /** * Whether the menu is open */ diff --git a/packages/machines/number-input/src/number-input.machine.ts b/packages/machines/number-input/src/number-input.machine.ts index b15550efcb..83020fbd09 100644 --- a/packages/machines/number-input/src/number-input.machine.ts +++ b/packages/machines/number-input/src/number-input.machine.ts @@ -3,7 +3,7 @@ import { addDomEvent, requestPointerLock } from "@zag-js/dom-event" import { isSafari, raf } from "@zag-js/dom-query" import { observeAttributes } from "@zag-js/mutation-observer" import { isAtMax, isAtMin, isWithinRange, valueOf } from "@zag-js/number-utils" -import { callAll, compact, isEqual } from "@zag-js/utils" +import { callAll, compact, isEqual, match } from "@zag-js/utils" import { dom } from "./number-input.dom" import type { MachineContext, MachineState, UserDefinedContext } from "./number-input.types" import { utils } from "./number-input.utils" @@ -327,29 +327,29 @@ export function machine(userContext: UserDefinedContext) { ctx.hint = "set" }, invokeOnFocus(ctx, evt) { - let srcElement: HTMLElement | null = null - - if (evt.type === "PRESS_DOWN") { - srcElement = dom.getPressedTriggerEl(ctx, evt.hint) - } else if (evt.type === "FOCUS") { - srcElement = dom.getInputEl(ctx) - } else if (evt.type === "PRESS_DOWN_SCRUBBER") { - srcElement = dom.getScrubberEl(ctx) - } - - ctx.onFocus?.({ - value: ctx.value, - valueAsNumber: ctx.valueAsNumber, + const srcElement: HTMLElement | null = match(evt.type, { + PRESS_DOWN: dom.getPressedTriggerEl(ctx, evt.hint), + FOCUS: dom.getInputEl(ctx), + PRESS_DOWN_SCRUBBER: dom.getScrubberEl(ctx), + }) + ctx.onFocusChange?.({ + focused: true, srcElement, + value: ctx.formattedValue, + valueAsNumber: ctx.valueAsNumber, }) }, invokeOnBlur(ctx) { - ctx.onBlur?.({ value: ctx.value, valueAsNumber: ctx.valueAsNumber }) + ctx.onFocusChange?.({ + focused: false, + value: ctx.formattedValue, + valueAsNumber: ctx.valueAsNumber, + }) }, invokeOnInvalid(ctx) { if (!ctx.isOutOfRange) return const reason = ctx.valueAsNumber > ctx.max ? "rangeOverflow" : "rangeUnderflow" - ctx.onInvalid?.({ + ctx.onValueInvalid?.({ reason, value: ctx.formattedValue, valueAsNumber: ctx.valueAsNumber, @@ -385,7 +385,7 @@ export function machine(userContext: UserDefinedContext) { const invoke = { onChange: (ctx: MachineContext) => { - ctx.onChange?.({ value: ctx.value, valueAsNumber: ctx.valueAsNumber }) + ctx.onValueChange?.({ value: ctx.value, valueAsNumber: ctx.valueAsNumber }) }, } diff --git a/packages/machines/number-input/src/number-input.types.ts b/packages/machines/number-input/src/number-input.types.ts index 4ce9b579b7..206b39143f 100644 --- a/packages/machines/number-input/src/number-input.types.ts +++ b/packages/machines/number-input/src/number-input.types.ts @@ -1,8 +1,32 @@ import type { StateMachine as S } from "@zag-js/core" import type { CommonProperties, Context, DirectionProperty, PropTypes, RequiredBy } from "@zag-js/types" +/* ----------------------------------------------------------------------------- + * Callback details + * -----------------------------------------------------------------------------*/ + +export interface ValueChangeDetails { + value: string + valueAsNumber: number +} + +export interface FocusChangeDetails extends ValueChangeDetails { + focused: boolean + srcElement?: HTMLElement | null +} + type ValidityState = "rangeUnderflow" | "rangeOverflow" +export interface ValueInvalidDetails extends ValueChangeDetails { + reason: ValidityState +} + +/* ----------------------------------------------------------------------------- + * Machine context + * -----------------------------------------------------------------------------*/ + +type InputMode = "text" | "tel" | "numeric" | "decimal" + type ElementIds = Partial<{ root: string label: string @@ -28,132 +52,122 @@ type IntlTranslations = { decrementLabel: string } -type Value = { +interface PublicContext extends DirectionProperty, CommonProperties { + /** + * The ids of the elements in the number input. Useful for composition. + */ + ids?: ElementIds + /** + * The name attribute of the number input. Useful for form submission. + */ + name?: string + /** + * The associate form of the input element. + */ + form?: string + /** + * Whether the number input is disabled. + */ + disabled?: boolean + /** + * Whether the number input is readonly + */ + readOnly?: boolean + /** + * Whether the number input value is invalid. + */ + invalid?: boolean + /** + * The pattern used to check the element's value against + * + * @default + * "[0-9]*(.[0-9]+)?" + */ + pattern: string + /** + * The value of the input + */ value: string - valueAsNumber: number + /** + * The minimum value of the number input + */ + min: number + /** + * The maximum value of the number input + */ + max: number + /** + * The amount to increment or decrement the value by + */ + step: number + /** + * Whether to allow mouse wheel to change the value + */ + allowMouseWheel?: boolean + /** + * Whether to allow the value overflow the min/max range + * @default true + */ + allowOverflow: boolean + /** + * Whether the pressed key should be allowed in the input. + * The default behavior is to allow DOM floating point characters defined by /^[Ee0-9+\-.]$/ + */ + validateCharacter?: (char: string) => boolean + /** + * Whether to clamp the value when the input loses focus (blur) + * @default true + */ + clampValueOnBlur: boolean + /** + * Whether to focus input when the value changes + * @default true + */ + focusInputOnChange: boolean + /** + * Specifies the localized strings that identifies the accessibility elements and their states + */ + translations: IntlTranslations + /** + * If using a custom display format, this converts the custom format to a format `parseFloat` understands. + */ + parse?: (value: string) => string + /** + * If using a custom display format, this converts the default format to the custom format. + */ + format?: (value: string) => string | number + /** + * Hints at the type of data that might be entered by the user. It also determines + * the type of keyboard shown to the user on mobile devices + * @default "decimal" + */ + inputMode: InputMode + /** + * Function invoked when the value changes + */ + onValueChange?: (details: ValueChangeDetails) => void + /** + * Function invoked when the value overflows or underflows the min/max range + */ + onValueInvalid?: (details: ValueInvalidDetails) => void + /** + * Function invoked when the number input is focused + */ + onFocusChange?: (details: FocusChangeDetails) => void + /** + * The minimum number of fraction digits to use. Possible values are from 0 to 20 + */ + minFractionDigits?: number + /** + * The maximum number of fraction digits to use. Possible values are from 0 to 20; + */ + maxFractionDigits?: number + /** + * Whether to spin the value when the increment/decrement button is pressed + */ + spinOnPress?: boolean } -type PublicContext = DirectionProperty & - CommonProperties & { - /** - * The ids of the elements in the number input. Useful for composition. - */ - ids?: ElementIds - /** - * The name attribute of the number input. Useful for form submission. - */ - name?: string - /** - * The associate form of the input element. - */ - form?: string - /** - * Whether the number input is disabled. - */ - disabled?: boolean - /** - * Whether the number input is readonly - */ - readOnly?: boolean - /** - * Whether the number input value is invalid. - */ - invalid?: boolean - /** - * The pattern used to check the element's value against - * - * @default - * "[0-9]*(.[0-9]+)?" - */ - pattern: string - /** - * The value of the input - */ - value: string - /** - * The minimum value of the number input - */ - min: number - /** - * The maximum value of the number input - */ - max: number - /** - * The amount to increment or decrement the value by - */ - step: number - /** - * Whether to allow mouse wheel to change the value - */ - allowMouseWheel?: boolean - /** - * Whether to allow the value overflow the min/max range - * @default true - */ - allowOverflow: boolean - /** - * Whether the pressed key should be allowed in the input. - * The default behavior is to allow DOM floating point characters defined by /^[Ee0-9+\-.]$/ - */ - validateCharacter?: (char: string) => boolean - /** - * Whether to clamp the value when the input loses focus (blur) - * @default true - */ - clampValueOnBlur: boolean - /** - * Whether to focus input when the value changes - * @default true - */ - focusInputOnChange: boolean - /** - * Specifies the localized strings that identifies the accessibility elements and their states - */ - translations: IntlTranslations - /** - * If using a custom display format, this converts the custom format to a format `parseFloat` understands. - */ - parse?: (value: string) => string - /** - * If using a custom display format, this converts the default format to the custom format. - */ - format?: (value: string) => string | number - /** - * Hints at the type of data that might be entered by the user. It also determines - * the type of keyboard shown to the user on mobile devices - * @default "decimal" - */ - inputMode: "text" | "tel" | "numeric" | "decimal" - /** - * Function invoked when the value changes - */ - onChange?: (details: Value) => void - /** - * Function invoked when the value overflows or underflows the min/max range - */ - onInvalid?: (details: Value & { reason: ValidityState }) => void - /** - * Function invoked when the number input is focused - */ - onFocus?: (details: Value & { srcElement: HTMLElement | null }) => void - /** - * The value of the input when it is blurred - */ - onBlur?: (details: Value) => void - /** - * The minimum number of fraction digits to use. Possible values are from 0 to 20 - */ - minFractionDigits?: number - /** - * The maximum number of fraction digits to use. Possible values are from 0 to 20; - */ - maxFractionDigits?: number - /** - * Whether to spin the value when the increment/decrement button is pressed - */ - spinOnPress?: boolean - } - export type UserDefinedContext = RequiredBy type ComputedContext = Readonly<{ @@ -222,9 +236,9 @@ type PrivateContext = Context<{ scrubberCursorPoint: { x: number; y: number } | null }> -export type MachineContext = PublicContext & PrivateContext & ComputedContext +export interface MachineContext extends PublicContext, PrivateContext, ComputedContext {} -export type MachineState = { +export interface MachineState { value: "idle" | "focused" | "spinning" | "before:spin" | "scrubbing" tags: "focus" } @@ -233,7 +247,11 @@ export type State = S.State export type Send = S.Send -export type MachineApi = { +/* ----------------------------------------------------------------------------- + * Component API + * -----------------------------------------------------------------------------*/ + +export interface MachineApi { /** * Whether the input is focused. */ diff --git a/packages/machines/pagination/src/pagination.anatomy.ts b/packages/machines/pagination/src/pagination.anatomy.ts index 52b8523167..80be17e893 100644 --- a/packages/machines/pagination/src/pagination.anatomy.ts +++ b/packages/machines/pagination/src/pagination.anatomy.ts @@ -7,4 +7,5 @@ export const anatomy = createAnatomy("pagination").parts( "prevPageTrigger", "nextPageTrigger", ) + export const parts = anatomy.build() diff --git a/packages/machines/pagination/src/pagination.connect.ts b/packages/machines/pagination/src/pagination.connect.ts index 248bb2e4a8..02228aec92 100644 --- a/packages/machines/pagination/src/pagination.connect.ts +++ b/packages/machines/pagination/src/pagination.connect.ts @@ -2,8 +2,8 @@ import { dataAttr } from "@zag-js/dom-query" import type { NormalizeProps, PropTypes } from "@zag-js/types" import { parts } from "./pagination.anatomy" import { dom } from "./pagination.dom" -import type { EllipsisProps, PageTriggerProps, MachineApi, Send, State } from "./pagination.types" -import { utils } from "./pagination.utils" +import type { MachineApi, Send, State } from "./pagination.types" +import * as utils from "./pagination.utils" export function connect(state: State, send: Send, normalize: NormalizeProps): MachineApi { const totalPages = state.context.totalPages @@ -30,19 +30,19 @@ export function connect(state: State, send: Send, normalize isFirstPage, isLastPage, - slice(data: T[]) { + slice(data) { return data.slice(pageRange.start, pageRange.end) }, - setCount(count: number) { + setCount(count) { send({ type: "SET_COUNT", count }) }, - setPageSize(size: number) { + setPageSize(size) { send({ type: "SET_PAGE_SIZE", size }) }, - setPage(page: number) { + setPage(page) { send({ type: "SET_PAGE", page }) }, @@ -52,14 +52,14 @@ export function connect(state: State, send: Send, normalize "aria-label": translations.rootLabel, }), - getEllipsisProps(props: EllipsisProps) { + getEllipsisProps(props) { return normalize.element({ id: dom.getEllipsisId(state.context, props.index), ...parts.ellipsis.attrs, }) }, - getPageTriggerProps(page: PageTriggerProps) { + getPageTriggerProps(page) { const index = page.value const isCurrentPage = index === state.context.page diff --git a/packages/machines/pagination/src/pagination.machine.ts b/packages/machines/pagination/src/pagination.machine.ts index 01a22f495b..b61fbcacd4 100644 --- a/packages/machines/pagination/src/pagination.machine.ts +++ b/packages/machines/pagination/src/pagination.machine.ts @@ -112,6 +112,6 @@ const set = { page: (ctx: MachineContext, value: number) => { if (isEqual(ctx.page, value)) return ctx.page = value - ctx.onChange?.({ page: ctx.page, pageSize: ctx.pageSize }) + ctx.onPageChange?.({ page: ctx.page, pageSize: ctx.pageSize }) }, } diff --git a/packages/machines/pagination/src/pagination.types.ts b/packages/machines/pagination/src/pagination.types.ts index d309d2961a..3691f4b5e8 100644 --- a/packages/machines/pagination/src/pagination.types.ts +++ b/packages/machines/pagination/src/pagination.types.ts @@ -1,16 +1,20 @@ import type { StateMachine as S } from "@zag-js/core" import type { CommonProperties, Context, DirectionProperty, PropTypes, RequiredBy } from "@zag-js/types" -export type PageTriggerProps = { - type: "page" - value: number -} +/* ----------------------------------------------------------------------------- + * Callback details + * -----------------------------------------------------------------------------*/ -export type EllipsisProps = { - index: number +export interface PageChangeDetails { + page: number + pageSize: number } -type IntlTranslations = { +/* ----------------------------------------------------------------------------- + * Machine context + * -----------------------------------------------------------------------------*/ + +interface IntlTranslations { rootLabel?: string prevPageTriggerLabel?: string nextPageTriggerLabel?: string @@ -25,50 +29,42 @@ type ElementIds = Partial<{ pageTrigger(page: number): string }> -export type ChangeDetails = { - page: number +interface PublicContext extends DirectionProperty, CommonProperties { + /** + * The ids of the elements in the accordion. Useful for composition. + */ + ids?: ElementIds + /** + * Specifies the localized strings that identifies the accessibility elements and their states + */ + translations: IntlTranslations + /** + * Total number of data items + */ + count: number + /** + * Number of data items per page + */ pageSize: number + /** + * Number of pages to show beside active page + */ + siblingCount: number + /** + * The active page + */ + page: number + /** + * Called when the page number is changed, and it takes the resulting page number argument + */ + onPageChange?: (details: PageChangeDetails) => void + /** + * The type of the trigger element + * @default "button" + */ + type: "button" | "link" } -export type PaginationRange = ({ type: "ellipsis" } | { type: "page"; value: number })[] - -type PublicContext = DirectionProperty & - CommonProperties & { - /** - * The ids of the elements in the accordion. Useful for composition. - */ - ids?: ElementIds - /** - * Specifies the localized strings that identifies the accessibility elements and their states - */ - translations: IntlTranslations - /** - * Total number of data items - */ - count: number - /** - * Number of data items per page - */ - pageSize: number - /** - * Number of pages to show beside active page - */ - siblingCount: number - /** - * The active page - */ - page: number - /** - * Called when the page number is changed, and it takes the resulting page number argument - */ - onChange?: (details: ChangeDetails) => void - /** - * The type of the trigger element - * @default "button" - */ - type: "button" | "link" - } - type PrivateContext = Context<{}> type ComputedContext = Readonly<{ @@ -81,7 +77,7 @@ type ComputedContext = Readonly<{ * @computed * Pages to render in pagination */ - items: PaginationRange + items: Pages /** * @computed * Index of first and last data items on current page @@ -106,7 +102,7 @@ type ComputedContext = Readonly<{ export type UserDefinedContext = RequiredBy -export type MachineContext = PublicContext & PrivateContext & ComputedContext +export interface MachineContext extends PublicContext, PrivateContext, ComputedContext {} export type MachineState = { value: "idle" @@ -116,7 +112,27 @@ export type State = S.State export type Send = S.Send -export type MachineApi = { +/* ----------------------------------------------------------------------------- + * Component API + * -----------------------------------------------------------------------------*/ + +export interface PageTriggerProps { + type: "page" + value: number +} + +export interface EllipsisProps { + index: number +} + +export type Pages = Array<{ type: "ellipsis" } | { type: "page"; value: number }> + +interface PageRange { + start: number + end: number +} + +export interface MachineApi { /** * The current page. */ @@ -128,7 +144,7 @@ export type MachineApi = { /** * The page range. Represented as an array of page numbers (including ellipsis) */ - pages: PaginationRange + pages: Pages /** * The previous page. */ @@ -140,14 +156,11 @@ export type MachineApi = { /** * The page range. Represented as an object with `start` and `end` properties. */ - pageRange: { - start: number - end: number - } + pageRange: PageRange /** * Function to slice an array of data based on the current page. */ - slice(data: T_1[]): T_1[] + slice(data: V[]): V[] /** * Whether the current page is the first page. */ diff --git a/packages/machines/pagination/src/pagination.utils.ts b/packages/machines/pagination/src/pagination.utils.ts index d9cae8c3a2..06473128a2 100644 --- a/packages/machines/pagination/src/pagination.utils.ts +++ b/packages/machines/pagination/src/pagination.utils.ts @@ -1,49 +1,49 @@ -import type { MachineContext as Ctx, PaginationRange } from "./pagination.types" - -export const utils = { - range: (start: number, end: number) => { - let length = end - start + 1 - return Array.from({ length }, (_, idx) => idx + start) - }, - transform: (items: (string | number)[]): PaginationRange => { - return items.map((value) => { - if (typeof value === "number") return { type: "page", value } - return { type: "ellipsis" } - }) - }, - getRange: (ctx: Omit) => { - const totalPageNumbers = ctx.siblingCount + 5 - if (totalPageNumbers >= ctx.totalPages) return utils.transform(utils.range(1, ctx.totalPages)) - - const ELLIPSIS = "ellipsis" - - const leftSiblingIndex = Math.max(ctx.page - ctx.siblingCount, 1) - const rightSiblingIndex = Math.min(ctx.page + ctx.siblingCount, ctx.totalPages) - - const showLeftEllipsis = leftSiblingIndex > 2 - const showRightEllipsis = rightSiblingIndex < ctx.totalPages - 2 - - const firstPageIndex = 1 - const lastPageIndex = ctx.totalPages - - if (!showLeftEllipsis && showRightEllipsis) { - let leftItemCount = 3 + 2 * ctx.siblingCount - let leftRange = utils.range(1, leftItemCount) - - return utils.transform([...leftRange, ELLIPSIS, ctx.totalPages]) - } - - if (showLeftEllipsis && !showRightEllipsis) { - let rightItemCount = 3 + 2 * ctx.siblingCount - let rightRange = utils.range(ctx.totalPages - rightItemCount + 1, ctx.totalPages) - return utils.transform([firstPageIndex, ELLIPSIS, ...rightRange]) - } - - if (showLeftEllipsis && showRightEllipsis) { - let middleRange = utils.range(leftSiblingIndex, rightSiblingIndex) - return utils.transform([firstPageIndex, ELLIPSIS, ...middleRange, ELLIPSIS, lastPageIndex]) - } - - return [] - }, +import type { MachineContext as Ctx, Pages } from "./pagination.types" + +export const range = (start: number, end: number) => { + let length = end - start + 1 + return Array.from({ length }, (_, idx) => idx + start) +} + +export const transform = (items: (string | number)[]): Pages => { + return items.map((value) => { + if (typeof value === "number") return { type: "page", value } + return { type: "ellipsis" } + }) +} + +export const getRange = (ctx: Omit) => { + const totalPageNumbers = ctx.siblingCount + 5 + if (totalPageNumbers >= ctx.totalPages) return transform(range(1, ctx.totalPages)) + + const ELLIPSIS = "ellipsis" + + const leftSiblingIndex = Math.max(ctx.page - ctx.siblingCount, 1) + const rightSiblingIndex = Math.min(ctx.page + ctx.siblingCount, ctx.totalPages) + + const showLeftEllipsis = leftSiblingIndex > 2 + const showRightEllipsis = rightSiblingIndex < ctx.totalPages - 2 + + const firstPageIndex = 1 + const lastPageIndex = ctx.totalPages + + if (!showLeftEllipsis && showRightEllipsis) { + let leftItemCount = 3 + 2 * ctx.siblingCount + let leftRange = range(1, leftItemCount) + + return transform([...leftRange, ELLIPSIS, ctx.totalPages]) + } + + if (showLeftEllipsis && !showRightEllipsis) { + let rightItemCount = 3 + 2 * ctx.siblingCount + let rightRange = range(ctx.totalPages - rightItemCount + 1, ctx.totalPages) + return transform([firstPageIndex, ELLIPSIS, ...rightRange]) + } + + if (showLeftEllipsis && showRightEllipsis) { + let middleRange = range(leftSiblingIndex, rightSiblingIndex) + return transform([firstPageIndex, ELLIPSIS, ...middleRange, ELLIPSIS, lastPageIndex]) + } + + return [] } diff --git a/packages/machines/pagination/tests/pagination.utils.test.ts b/packages/machines/pagination/tests/pagination.utils.test.ts index cdb24c7812..811a25217c 100644 --- a/packages/machines/pagination/tests/pagination.utils.test.ts +++ b/packages/machines/pagination/tests/pagination.utils.test.ts @@ -1,5 +1,5 @@ import { expect, describe, test } from "vitest" -import { utils } from "../src/pagination.utils" +import * as utils from "../src/pagination.utils" describe("@zag-js/pagination utils", () => { test("range method", () => { diff --git a/packages/machines/pin-input/src/pin-input.machine.ts b/packages/machines/pin-input/src/pin-input.machine.ts index fe3c41ea0e..2c8d2ee015 100644 --- a/packages/machines/pin-input/src/pin-input.machine.ts +++ b/packages/machines/pin-input/src/pin-input.machine.ts @@ -169,10 +169,16 @@ export function machine(userContext: UserDefinedContext) { }, invokeOnComplete(ctx) { if (!ctx.isValueComplete) return - ctx.onComplete?.({ value: Array.from(ctx.value), valueAsString: ctx.valueAsString }) + ctx.onValueComplete?.({ + value: Array.from(ctx.value), + valueAsString: ctx.valueAsString, + }) }, invokeOnInvalid(ctx, evt) { - ctx.onInvalid?.({ value: evt.value, index: ctx.focusedIndex }) + ctx.onValueInvalid?.({ + value: evt.value, + index: ctx.focusedIndex, + }) }, clearFocusedIndex(ctx) { ctx.focusedIndex = -1 @@ -280,7 +286,10 @@ function getNextValue(current: string, next: string) { const invoke = { change(ctx: MachineContext) { // callback - ctx.onChange?.({ value: Array.from(ctx.value) }) + ctx.onValueChange?.({ + value: Array.from(ctx.value), + valueAsString: ctx.valueAsString, + }) // form event const inputEl = dom.getHiddenInputEl(ctx) diff --git a/packages/machines/pin-input/src/pin-input.types.ts b/packages/machines/pin-input/src/pin-input.types.ts index 066f313043..4fd81c389d 100644 --- a/packages/machines/pin-input/src/pin-input.types.ts +++ b/packages/machines/pin-input/src/pin-input.types.ts @@ -1,6 +1,24 @@ import type { StateMachine as S } from "@zag-js/core" import type { CommonProperties, Context, DirectionProperty, PropTypes, RequiredBy } from "@zag-js/types" +/* ----------------------------------------------------------------------------- + * Callback details + * -----------------------------------------------------------------------------*/ + +export interface ValueChangeDetails { + value: string[] + valueAsString: string +} + +export interface ValueInvalidDetails { + value: string + index: number +} + +/* ----------------------------------------------------------------------------- + * Machine context + * -----------------------------------------------------------------------------*/ + type IntlTranslations = { inputLabel: (index: number, length: number) => string } @@ -13,82 +31,81 @@ type ElementIds = Partial<{ input(id: string): string }> -type PublicContext = DirectionProperty & - CommonProperties & { - /** - * The name of the input element. Useful for form submission. - */ - name?: string - /** - * The associate form of the underlying input element. - */ - form?: string - /** - * The regular expression that the user-entered input value is checked against. - */ - pattern?: string - /** - * The ids of the elements in the pin input. Useful for composition. - */ - ids?: ElementIds - /** - * Whether the inputs are disabled - */ - disabled?: boolean - /** - * The placeholder text for the input - */ - placeholder?: string - /** - * Whether to auto-focus the first input. - */ - autoFocus?: boolean - /** - * Whether the pin input is in the invalid state - */ - invalid?: boolean - /** - * If `true`, the pin input component signals to its fields that they should - * use `autocomplete="one-time-code"`. - */ - otp?: boolean - /** - * The value of the the pin input. - */ - value: string[] - /** - * The type of value the pin-input should allow - */ - type?: "alphanumeric" | "numeric" | "alphabetic" - /** - * Function called when all inputs have valid values - */ - onComplete?: (details: { value: string[]; valueAsString: string }) => void - /** - * Function called on input change - */ - onChange?: (details: { value: string[] }) => void - /** - * Function called when an invalid value is entered - */ - onInvalid?: (details: { value: string; index: number }) => void - /** - * If `true`, the input's value will be masked just like `type=password` - */ - mask?: boolean - /** - * Whether to blur the input when the value is complete - */ - blurOnComplete?: boolean - /** - * Whether to select input value when input is focused - */ - selectOnFocus?: boolean - /** - * Specifies the localized strings that identifies the accessibility elements and their states - */ - translations: IntlTranslations - } +interface PublicContext extends DirectionProperty, CommonProperties { + /** + * The name of the input element. Useful for form submission. + */ + name?: string + /** + * The associate form of the underlying input element. + */ + form?: string + /** + * The regular expression that the user-entered input value is checked against. + */ + pattern?: string + /** + * The ids of the elements in the pin input. Useful for composition. + */ + ids?: ElementIds + /** + * Whether the inputs are disabled + */ + disabled?: boolean + /** + * The placeholder text for the input + */ + placeholder?: string + /** + * Whether to auto-focus the first input. + */ + autoFocus?: boolean + /** + * Whether the pin input is in the invalid state + */ + invalid?: boolean + /** + * If `true`, the pin input component signals to its fields that they should + * use `autocomplete="one-time-code"`. + */ + otp?: boolean + /** + * The value of the the pin input. + */ + value: string[] + /** + * The type of value the pin-input should allow + */ + type?: "alphanumeric" | "numeric" | "alphabetic" + /** + * Function called when all inputs have valid values + */ + onValueComplete?: (details: ValueChangeDetails) => void + /** + * Function called on input change + */ + onValueChange?: (details: ValueChangeDetails) => void + /** + * Function called when an invalid value is entered + */ + onValueInvalid?: (details: ValueInvalidDetails) => void + /** + * If `true`, the input's value will be masked just like `type=password` + */ + mask?: boolean + /** + * Whether to blur the input when the value is complete + */ + blurOnComplete?: boolean + /** + * Whether to select input value when input is focused + */ + selectOnFocus?: boolean + /** + * Specifies the localized strings that identifies the accessibility elements and their states + */ + translations: IntlTranslations +} export type UserDefinedContext = RequiredBy @@ -128,9 +145,9 @@ type PrivateContext = Context<{ focusedIndex: number }> -export type MachineContext = PublicContext & PrivateContext & ComputedContext +export interface MachineContext extends PublicContext, PrivateContext, ComputedContext {} -export type MachineState = { +export interface MachineState { value: "idle" | "focused" } @@ -138,7 +155,11 @@ export type State = S.State export type Send = S.Send -export type MachineApi = { +/* ----------------------------------------------------------------------------- + * Component API + * -----------------------------------------------------------------------------*/ + +export interface MachineApi { /** * The value of the input as an array of strings. */ diff --git a/packages/machines/popover/src/popover.machine.ts b/packages/machines/popover/src/popover.machine.ts index 3e23fd41ff..0c622bc1aa 100644 --- a/packages/machines/popover/src/popover.machine.ts +++ b/packages/machines/popover/src/popover.machine.ts @@ -205,10 +205,10 @@ export function machine(userContext: UserDefinedContext) { }) }, invokeOnOpen(ctx) { - ctx.onOpen?.() + ctx.onOpenChange?.({ open: true }) }, invokeOnClose(ctx) { - ctx.onClose?.() + ctx.onOpenChange?.({ open: false }) }, toggleVisibility(ctx, _evt, { send }) { send({ type: ctx.open ? "OPEN" : "CLOSE", src: "controlled" }) diff --git a/packages/machines/popover/src/popover.types.ts b/packages/machines/popover/src/popover.types.ts index df6a8556ae..f8f01ee633 100644 --- a/packages/machines/popover/src/popover.types.ts +++ b/packages/machines/popover/src/popover.types.ts @@ -3,6 +3,18 @@ import type { DismissableElementHandlers } from "@zag-js/dismissable" import type { Placement, PositioningOptions } from "@zag-js/popper" import type { CommonProperties, Context, MaybeElement, PropTypes, RequiredBy } from "@zag-js/types" +/* ----------------------------------------------------------------------------- + * Callback details + * -----------------------------------------------------------------------------*/ + +export interface OpenChangeDetails { + open: boolean +} + +/* ----------------------------------------------------------------------------- + * Machine context + * -----------------------------------------------------------------------------*/ + type ElementIds = Partial<{ anchor: string trigger: string @@ -14,62 +26,57 @@ type ElementIds = Partial<{ arrow: string }> -type PublicContext = DismissableElementHandlers & - CommonProperties & { - /** - * The ids of the elements in the popover. Useful for composition. - */ - ids?: ElementIds - /** - * Whether the popover should be modal. When set to `true`: - * - interaction with outside elements will be disabled - * - only popover content will be visible to screen readers - * - scrolling is blocked - * - focus is trapped within the popover - * - * @default false - */ - modal?: boolean - /** - * Whether the popover is rendered in a portal - * - * @default true - */ - portalled?: boolean - /** - * Whether to automatically set focus on the first focusable - * content within the popover when opened. - */ - autoFocus?: boolean - /** - * The element to focus on when the popover is opened. - */ - initialFocusEl?: MaybeElement | (() => MaybeElement) - /** - * Whether to close the popover when the user clicks outside of the popover. - */ - closeOnInteractOutside?: boolean - /** - * Whether to close the popover when the escape key is pressed. - */ - closeOnEsc?: boolean - /** - * Function invoked when the popover is closed - */ - onClose?: VoidFunction - /** - * Function invoked when the popover is opened - */ - onOpen?: VoidFunction - /** - * The user provided options used to position the popover content - */ - positioning: PositioningOptions - /** - * Whether the popover is open - */ - open?: boolean - } +interface PublicContext extends DismissableElementHandlers, CommonProperties { + /** + * The ids of the elements in the popover. Useful for composition. + */ + ids?: ElementIds + /** + * Whether the popover should be modal. When set to `true`: + * - interaction with outside elements will be disabled + * - only popover content will be visible to screen readers + * - scrolling is blocked + * - focus is trapped within the popover + * + * @default false + */ + modal?: boolean + /** + * Whether the popover is rendered in a portal + * + * @default true + */ + portalled?: boolean + /** + * Whether to automatically set focus on the first focusable + * content within the popover when opened. + */ + autoFocus?: boolean + /** + * The element to focus on when the popover is opened. + */ + initialFocusEl?: MaybeElement | (() => MaybeElement) + /** + * Whether to close the popover when the user clicks outside of the popover. + */ + closeOnInteractOutside?: boolean + /** + * Whether to close the popover when the escape key is pressed. + */ + closeOnEsc?: boolean + /** + * Function invoked when the popover opens or closes + */ + onOpenChange?: (details: OpenChangeDetails) => void + /** + * The user provided options used to position the popover content + */ + positioning: PositioningOptions + /** + * Whether the popover is open + */ + open?: boolean +} export type UserDefinedContext = RequiredBy @@ -97,9 +104,9 @@ type PrivateContext = Context<{ currentPlacement?: Placement }> -export type MachineContext = PublicContext & ComputedContext & PrivateContext +export interface MachineContext extends PublicContext, ComputedContext, PrivateContext {} -export type MachineState = { +export interface MachineState { value: "open" | "closed" } @@ -107,9 +114,11 @@ export type State = S.State export type Send = S.Send -export type { Placement, PositioningOptions } +/* ----------------------------------------------------------------------------- + * Component API + * -----------------------------------------------------------------------------*/ -export type MachineApi = { +export interface MachineApi { /** * Whether the popover is portalled */ @@ -140,3 +149,9 @@ export type MachineApi = { descriptionProps: T["element"] closeTriggerProps: T["button"] } + +/* ----------------------------------------------------------------------------- + * Re-exported types + * -----------------------------------------------------------------------------*/ + +export type { Placement, PositioningOptions } diff --git a/packages/machines/presence/src/presence.types.ts b/packages/machines/presence/src/presence.types.ts index 09d4a1693e..d67ebd3430 100644 --- a/packages/machines/presence/src/presence.types.ts +++ b/packages/machines/presence/src/presence.types.ts @@ -1,22 +1,15 @@ import type { StateMachine as S } from "@zag-js/core" -type PublicContext = { +/* ----------------------------------------------------------------------------- + * Machine context + * -----------------------------------------------------------------------------*/ + +interface PublicContext { present: boolean onExitComplete?: () => void } -export type MachineApi = { - /** - * Whether the node is present in the DOM. - */ - isPresent: boolean - /** - * Function to set the node (as early as possible) - */ - setNode(node: HTMLElement | null): void -} - -type PrivateContext = { +interface PrivateContext { node: HTMLElement | null styles: CSSStyleDeclaration | null prevPresent?: boolean @@ -25,12 +18,27 @@ type PrivateContext = { export type UserDefinedContext = PublicContext -export type MachineContext = PublicContext & PrivateContext +export interface MachineContext extends PublicContext, PrivateContext {} -export type MachineState = { +export interface MachineState { value: "mounted" | "unmountSuspended" | "unmounted" } export type State = S.State export type Send = S.Send + +/* ----------------------------------------------------------------------------- + * Component API + * -----------------------------------------------------------------------------*/ + +export interface MachineApi { + /** + * Whether the node is present in the DOM. + */ + isPresent: boolean + /** + * Function to set the node (as early as possible) + */ + setNode(node: HTMLElement | null): void +} diff --git a/packages/machines/pressable/src/pressable.types.ts b/packages/machines/pressable/src/pressable.types.ts index e3dbbdff1c..856662f72e 100644 --- a/packages/machines/pressable/src/pressable.types.ts +++ b/packages/machines/pressable/src/pressable.types.ts @@ -1,6 +1,10 @@ import type { StateMachine as S } from "@zag-js/core" import type { CommonProperties, Context, DirectionProperty, PropTypes, RequiredBy } from "@zag-js/types" +/* ----------------------------------------------------------------------------- + * Machine context + * -----------------------------------------------------------------------------*/ + export type PointerType = "mouse" | "pen" | "touch" | "keyboard" | "virtual" export interface Rect { @@ -34,7 +38,7 @@ export interface PressEvent { target: HTMLElement } -export type PressHandlers = { +export interface PressHandlers { /** * Handler that is called when the press is released over the target. */ @@ -59,43 +63,33 @@ export type PressHandlers = { onLongPress?: (event: PressEvent) => void } -type PublicContext = DirectionProperty & - CommonProperties & - PressHandlers & { - /** - * Whether the element is disabled - */ - disabled?: boolean - /** - * Whether the target should not receive focus on press. - */ - preventFocusOnPress?: boolean - /** - * Whether press events should be canceled when the pointer leaves the target while pressed. - * - * By default, this is `false`, which means if the pointer returns back over the target while - * still pressed, onPressStart will be fired again. - * - * If set to `true`, the press is canceled when the pointer leaves the target and - * onPressStart will not be fired if the pointer returns. - */ - cancelOnPointerExit?: boolean - /** - * Whether text selection should be enabled on the pressable element. - */ - allowTextSelectionOnPress?: boolean - /** - * The amount of time (in milliseconds) to wait before firing the `onLongPress` event. - */ - longPressDelay: number - } - -export type MachineApi = { +interface PublicContext extends DirectionProperty, CommonProperties, PressHandlers { /** - * Whether the element is pressed. + * Whether the element is disabled */ - isPressed: boolean - pressableProps: T["element"] + disabled?: boolean + /** + * Whether the target should not receive focus on press. + */ + preventFocusOnPress?: boolean + /** + * Whether press events should be canceled when the pointer leaves the target while pressed. + * + * By default, this is `false`, which means if the pointer returns back over the target while + * still pressed, onPressStart will be fired again. + * + * If set to `true`, the press is canceled when the pointer leaves the target and + * onPressStart will not be fired if the pointer returns. + */ + cancelOnPointerExit?: boolean + /** + * Whether text selection should be enabled on the pressable element. + */ + allowTextSelectionOnPress?: boolean + /** + * The amount of time (in milliseconds) to wait before firing the `onLongPress` event. + */ + longPressDelay: number } interface FocusableElement extends HTMLElement, HTMLOrSVGElement {} @@ -113,9 +107,9 @@ export type UserDefinedContext = RequiredBy type ComputedContext = Readonly<{}> -export type MachineContext = PublicContext & PrivateContext & ComputedContext +export interface MachineContext extends PublicContext, PrivateContext, ComputedContext {} -export type MachineState = { +export interface MachineState { value: "idle" | "pressed:in" | "pressed:out" tags: "pressed" } @@ -123,3 +117,15 @@ export type MachineState = { export type State = S.State export type Send = S.Send + +/* ----------------------------------------------------------------------------- + * Component API + * -----------------------------------------------------------------------------*/ + +export interface MachineApi { + /** + * Whether the element is pressed. + */ + isPressed: boolean + pressableProps: T["element"] +} diff --git a/packages/machines/radio-group/src/radio-group.machine.ts b/packages/machines/radio-group/src/radio-group.machine.ts index 27d0a467e9..0ce5fcfd31 100644 --- a/packages/machines/radio-group/src/radio-group.machine.ts +++ b/packages/machines/radio-group/src/radio-group.machine.ts @@ -151,7 +151,7 @@ export function machine(userContext: UserDefinedContext) { const invoke = { change: (ctx: MachineContext) => { if (ctx.value == null) return - ctx.onChange?.({ value: ctx.value }) + ctx.onValueChange?.({ value: ctx.value }) }, } diff --git a/packages/machines/radio-group/src/radio-group.types.ts b/packages/machines/radio-group/src/radio-group.types.ts index 2103c5aa97..817a898775 100644 --- a/packages/machines/radio-group/src/radio-group.types.ts +++ b/packages/machines/radio-group/src/radio-group.types.ts @@ -1,6 +1,14 @@ import type { StateMachine as S } from "@zag-js/core" import type { CommonProperties, Context, DirectionProperty, PropTypes, RequiredBy } from "@zag-js/types" +export interface ValueChangeDetails { + value: string +} + +/* ----------------------------------------------------------------------------- + * Machine context + * -----------------------------------------------------------------------------*/ + type ElementIds = Partial<{ root: string label: string @@ -11,40 +19,39 @@ type ElementIds = Partial<{ radioHiddenInput(value: string): string }> -type PublicContext = DirectionProperty & - CommonProperties & { - /** - * The ids of the elements in the radio. Useful for composition. - */ - ids?: ElementIds - /** - * The value of the checked radio - */ - value: string | null - - /** - * The name of the input fields in the radio - * (Useful for form submission). - */ - name?: string - /** - * The associate form of the underlying input. - */ - form?: string - /** - * If `true`, the radio group will be disabled - */ - disabled?: boolean - /** - * Function called once a radio is checked - * @param value the value of the checked radio - */ - onChange?(details: { value: string }): void - /** - * Orientation of the radio group - */ - orientation?: "horizontal" | "vertical" - } +interface PublicContext extends DirectionProperty, CommonProperties { + /** + * The ids of the elements in the radio. Useful for composition. + */ + ids?: ElementIds + /** + * The value of the checked radio + */ + value: string | null + + /** + * The name of the input fields in the radio + * (Useful for form submission). + */ + name?: string + /** + * The associate form of the underlying input. + */ + form?: string + /** + * If `true`, the radio group will be disabled + */ + disabled?: boolean + /** + * Function called once a radio is checked + * @param value the value of the checked radio + */ + onValueChange?(details: ValueChangeDetails): void + /** + * Orientation of the radio group + */ + orientation?: "horizontal" | "vertical" +} type PrivateContext = Context<{ /** @@ -88,14 +95,15 @@ export type UserDefinedContext = RequiredBy type ComputedContext = Readonly<{ /** + * @computed * Whether the radio group is disabled */ isDisabled: boolean }> -export type MachineContext = PublicContext & PrivateContext & ComputedContext +export interface MachineContext extends PublicContext, PrivateContext, ComputedContext {} -export type MachineState = { +export interface MachineState { value: "idle" } @@ -103,19 +111,17 @@ export type State = S.State export type Send = S.Send -export type RadioProps = { +/* ----------------------------------------------------------------------------- + * Component API + * -----------------------------------------------------------------------------*/ + +export interface RadioProps { value: string - /** - * If `true`, the radio will be disabled - */ disabled?: boolean - /** - * If `true`, the radio is marked as invalid. - */ invalid?: boolean } -export type RadioState = { +export interface RadioState { isInteractive: boolean isInvalid: boolean isDisabled: boolean @@ -125,7 +131,11 @@ export type RadioState = { isActive: boolean } -export type MachineApi = { +/* ----------------------------------------------------------------------------- + * Component API + * -----------------------------------------------------------------------------*/ + +export interface MachineApi { /** * The current value of the radio group */ diff --git a/packages/machines/range-slider/src/range-slider.machine.ts b/packages/machines/range-slider/src/range-slider.machine.ts index 63ba2f59bf..9d73b4b6b4 100644 --- a/packages/machines/range-slider/src/range-slider.machine.ts +++ b/packages/machines/range-slider/src/range-slider.machine.ts @@ -208,10 +208,10 @@ export function machine(userContext: UserDefinedContext) { }) }, invokeOnChangeStart(ctx) { - ctx.onChangeStart?.({ value: ctx.value }) + ctx.onValueChangeStart?.({ value: ctx.value }) }, invokeOnChangeEnd(ctx) { - ctx.onChangeEnd?.({ value: ctx.value }) + ctx.onValueChangeEnd?.({ value: ctx.value }) }, setClosestThumbIndex(ctx, evt) { const pointValue = dom.getValueFromPoint(ctx, evt.point) @@ -274,11 +274,16 @@ export function machine(userContext: UserDefinedContext) { const invoke = { change: (ctx: MachineContext) => { - ctx.onChange?.({ value: Array.from(ctx.value) }) + ctx.onValueChange?.({ + value: Array.from(ctx.value), + }) dom.dispatchChangeEvent(ctx) }, focusChange: (ctx: MachineContext) => { - ctx.onFocusChange?.({ value: Array.from(ctx.value), index: ctx.focusedIndex }) + ctx.onFocusChange?.({ + value: Array.from(ctx.value), + focusedIndex: ctx.focusedIndex, + }) }, } diff --git a/packages/machines/range-slider/src/range-slider.types.ts b/packages/machines/range-slider/src/range-slider.types.ts index 60298de84d..7b27201ba4 100644 --- a/packages/machines/range-slider/src/range-slider.types.ts +++ b/packages/machines/range-slider/src/range-slider.types.ts @@ -1,6 +1,23 @@ import type { StateMachine as S } from "@zag-js/core" import type { CommonProperties, Context, DirectionProperty, PropTypes, RequiredBy } from "@zag-js/types" +/* ----------------------------------------------------------------------------- + * Callback details + * -----------------------------------------------------------------------------*/ + +export interface ValueChangeDetails { + value: number[] +} + +export interface FocusChangeDetails { + focusedIndex: number + value: number[] +} + +/* ----------------------------------------------------------------------------- + * Machine context + * -----------------------------------------------------------------------------*/ + type ElementIds = Partial<{ root: string thumb(index: number): string @@ -12,95 +29,94 @@ type ElementIds = Partial<{ marker(index: number): string }> -type PublicContext = DirectionProperty & - CommonProperties & { - /** - * The ids of the elements in the range slider. Useful for composition. - */ - ids?: ElementIds - /** - * The aria-label of each slider thumb. Useful for providing an accessible name to the slider - */ - "aria-label"?: string[] - /** - * The `id` of the elements that labels each slider thumb. Useful for providing an accessible name to the slider - */ - "aria-labelledby"?: string[] - /** - * The name associated with each slider thumb (when used in a form) - */ - name?: string - /** - * The associate form of the underlying input element. - */ - form?: string - /** - * The value of the range slider - */ - value: number[] - /** - * Whether the slider is disabled - */ - disabled?: boolean - /** - * Whether the slider is read-only - */ - readOnly?: boolean - /** - * Whether the slider is invalid - */ - invalid?: boolean - /** - * Function invoked when the value of the slider changes - */ - onChange?(details: { value: number[] }): void - /** - * Function invoked when the slider value change is started - */ - onChangeStart?(details: { value: number[] }): void - /** - * Function invoked when the slider value change is done - */ - onChangeEnd?(details: { value: number[] }): void - /** - * Function invoked when the slider's focused index changes - */ - onFocusChange?(details: { index: number; value: number[] }): void - /** - * Function that returns a human readable value for the slider thumb - */ - getAriaValueText?(value: number, index: number): string - /** - * The minimum value of the slider - */ - min: number - /** - * The maximum value of the slider - */ - max: number - /** - * The step value of the slider - */ - step: number - /** - * The minimum permitted steps between multiple thumbs. - */ - minStepsBetweenThumbs: number - /** - * The orientation of the slider - */ - orientation: "vertical" | "horizontal" - /** - * The alignment of the slider thumb relative to the track - * - `center`: the thumb will extend beyond the bounds of the slider track. - * - `contain`: the thumb will be contained within the bounds of the track. - */ - thumbAlignment?: "contain" | "center" - /** - * The slider thumbs dimensions - */ - thumbSize: { width: number; height: number } | null - } +interface PublicContext extends DirectionProperty, CommonProperties { + /** + * The ids of the elements in the range slider. Useful for composition. + */ + ids?: ElementIds + /** + * The aria-label of each slider thumb. Useful for providing an accessible name to the slider + */ + "aria-label"?: string[] + /** + * The `id` of the elements that labels each slider thumb. Useful for providing an accessible name to the slider + */ + "aria-labelledby"?: string[] + /** + * The name associated with each slider thumb (when used in a form) + */ + name?: string + /** + * The associate form of the underlying input element. + */ + form?: string + /** + * The value of the range slider + */ + value: number[] + /** + * Whether the slider is disabled + */ + disabled?: boolean + /** + * Whether the slider is read-only + */ + readOnly?: boolean + /** + * Whether the slider is invalid + */ + invalid?: boolean + /** + * Function invoked when the value of the slider changes + */ + onValueChange?(details: ValueChangeDetails): void + /** + * Function invoked when the slider value change is started + */ + onValueChangeStart?(details: ValueChangeDetails): void + /** + * Function invoked when the slider value change is done + */ + onValueChangeEnd?(details: ValueChangeDetails): void + /** + * Function invoked when the slider's focused index changes + */ + onFocusChange?(details: FocusChangeDetails): void + /** + * Function that returns a human readable value for the slider thumb + */ + getAriaValueText?(value: number, index: number): string + /** + * The minimum value of the slider + */ + min: number + /** + * The maximum value of the slider + */ + max: number + /** + * The step value of the slider + */ + step: number + /** + * The minimum permitted steps between multiple thumbs. + */ + minStepsBetweenThumbs: number + /** + * The orientation of the slider + */ + orientation: "vertical" | "horizontal" + /** + * The alignment of the slider thumb relative to the track + * - `center`: the thumb will extend beyond the bounds of the slider track. + * - `contain`: the thumb will be contained within the bounds of the track. + */ + thumbAlignment?: "contain" | "center" + /** + * The slider thumbs dimensions + */ + thumbSize: { width: number; height: number } | null +} export type UserDefinedContext = RequiredBy @@ -166,9 +182,9 @@ type PrivateContext = Context<{ fieldsetDisabled: boolean }> -export type MachineContext = PublicContext & ComputedContext & PrivateContext +export interface MachineContext extends PublicContext, ComputedContext, PrivateContext {} -export type MachineState = { +export interface MachineState { value: "idle" | "dragging" | "focus" } @@ -176,7 +192,15 @@ export type State = S.State export type Send = S.Send -export type MachineApi = { +/* ----------------------------------------------------------------------------- + * Component API + * -----------------------------------------------------------------------------*/ + +export interface MarkerProps { + value: number +} + +export interface MachineApi { /** * The value of the slider. */ @@ -246,5 +270,5 @@ export type MachineApi = { rangeProps: T["element"] controlProps: T["element"] markerGroupProps: T["element"] - getMarkerProps({ value }: { value: number }): T["element"] + getMarkerProps(props: MarkerProps): T["element"] } diff --git a/packages/machines/rating-group/src/rating-group.connect.ts b/packages/machines/rating-group/src/rating-group.connect.ts index d659b1c576..062e7172f6 100644 --- a/packages/machines/rating-group/src/rating-group.connect.ts +++ b/packages/machines/rating-group/src/rating-group.connect.ts @@ -20,7 +20,23 @@ export function connect(state: State, send: Send, normalize const hoveredValue = state.context.hoveredValue const translations = state.context.translations - const api = { + function getRatingState(props: ItemProps): ItemState { + const value = state.context.isHovering ? state.context.hoveredValue : state.context.value + const isEqual = Math.ceil(value) === props.index + + const isHighlighted = props.index <= value || isEqual + const isHalf = isEqual && Math.abs(value - props.index) === 0.5 + + return { + isEqual, + isValueEmpty: state.context.value === -1, + isHighlighted, + isHalf, + isChecked: isEqual || (state.context.value === -1 && props.index === 1), + } + } + + return { setValue(value: number) { send({ type: "SET_VALUE", value }) }, @@ -34,22 +50,7 @@ export function connect(state: State, send: Send, normalize hoveredValue, size: state.context.max, sizeArray: Array.from({ length: state.context.max }).map((_, index) => index + 1), - - getRatingState(props: ItemProps): ItemState { - const value = state.context.isHovering ? state.context.hoveredValue : state.context.value - const isEqual = Math.ceil(value) === props.index - - const isHighlighted = props.index <= value || isEqual - const isHalf = isEqual && Math.abs(value - props.index) === 0.5 - - return { - isEqual, - isValueEmpty: state.context.value === -1, - isHighlighted, - isHalf, - isChecked: isEqual || (state.context.value === -1 && props.index === 1), - } - }, + getRatingState, rootProps: normalize.element({ dir: state.context.dir, @@ -92,14 +93,14 @@ export function connect(state: State, send: Send, normalize getRatingProps(props: ItemProps) { const { index } = props - const { isHalf, isHighlighted, isChecked } = api.getRatingState(props) + const itemState = getRatingState(props) const valueText = translations.ratingValueText(index) return normalize.element({ ...parts.rating.attrs, id: dom.getRatingId(state.context, index.toString()), role: "radio", - tabIndex: isDisabled ? undefined : isChecked ? 0 : -1, + tabIndex: isDisabled ? undefined : itemState.isChecked ? 0 : -1, "aria-roledescription": "rating", "aria-label": valueText, "aria-disabled": isDisabled, @@ -107,11 +108,11 @@ export function connect(state: State, send: Send, normalize "aria-readonly": ariaAttr(state.context.readOnly), "data-readonly": dataAttr(state.context.readOnly), "aria-setsize": state.context.max, - "aria-checked": isChecked, - "data-checked": dataAttr(isChecked), + "aria-checked": itemState.isChecked, + "data-checked": dataAttr(itemState.isChecked), "aria-posinset": index, - "data-highlighted": dataAttr(isHighlighted), - "data-half": dataAttr(isHalf), + "data-highlighted": dataAttr(itemState.isHighlighted), + "data-half": dataAttr(itemState.isHalf), onPointerDown(event) { if (!isInteractive) return const evt = getNativeEvent(event) @@ -179,6 +180,4 @@ export function connect(state: State, send: Send, normalize }) }, } - - return api } diff --git a/packages/machines/rating-group/src/rating-group.machine.ts b/packages/machines/rating-group/src/rating-group.machine.ts index 60e4edda1e..e201b710be 100644 --- a/packages/machines/rating-group/src/rating-group.machine.ts +++ b/packages/machines/rating-group/src/rating-group.machine.ts @@ -181,11 +181,11 @@ export function machine(userContext: UserDefinedContext) { const invoke = { change: (ctx: MachineContext) => { - ctx.onChange?.({ value: ctx.value }) + ctx.onValueChange?.({ value: ctx.value }) dom.dispatchChangeEvent(ctx) }, hoverChange: (ctx: MachineContext) => { - ctx.onHover?.({ value: ctx.hoveredValue }) + ctx.onHoverChange?.({ hoveredValue: ctx.hoveredValue }) }, } diff --git a/packages/machines/rating-group/src/rating-group.types.ts b/packages/machines/rating-group/src/rating-group.types.ts index 9a61b6a979..bdb0e37960 100644 --- a/packages/machines/rating-group/src/rating-group.types.ts +++ b/packages/machines/rating-group/src/rating-group.types.ts @@ -1,20 +1,24 @@ import type { StateMachine as S } from "@zag-js/core" import type { CommonProperties, Context, DirectionProperty, PropTypes, RequiredBy } from "@zag-js/types" -type IntlTranslations = { - ratingValueText(index: number): string +/* ----------------------------------------------------------------------------- + * Callback details + * -----------------------------------------------------------------------------*/ + +export interface ValueChangeDetails { + value: number } -export type ItemProps = { - index: number +export interface HoverChangeDetails { + hoveredValue: number } -export type ItemState = { - isEqual: boolean - isValueEmpty: boolean - isHighlighted: boolean - isHalf: boolean - isChecked: boolean +/* ----------------------------------------------------------------------------- + * Machine context + * -----------------------------------------------------------------------------*/ + +interface IntlTranslations { + ratingValueText(index: number): string } type ElementIds = Partial<{ @@ -25,57 +29,56 @@ type ElementIds = Partial<{ rating(id: string): string }> -type PublicContext = DirectionProperty & - CommonProperties & { - /** - * The ids of the elements in the rating. Useful for composition. - */ - ids?: ElementIds - /** - * Specifies the localized strings that identifies the accessibility elements and their states - */ - translations: IntlTranslations - /** - * The maximum rating value. - */ - max: number - /** - * The name attribute of the rating element (used in forms). - */ - name?: string - /** - * The associate form of the underlying input element. - */ - form?: string - /** - * The current rating value. - */ - value: number - /** - * Whether the rating is readonly. - */ - readOnly?: boolean - /** - * Whether the rating is disabled. - */ - disabled?: boolean - /** - * Whether to allow half stars. - */ - allowHalf?: boolean - /** - * Whether to autofocus the rating. - */ - autoFocus?: boolean - /** - * Function to be called when the rating value changes. - */ - onChange?: (details: { value: number }) => void - /** - * Function to be called when the rating value is hovered. - */ - onHover?: (details: { value: number }) => void - } +interface PublicContext extends DirectionProperty, CommonProperties { + /** + * The ids of the elements in the rating. Useful for composition. + */ + ids?: ElementIds + /** + * Specifies the localized strings that identifies the accessibility elements and their states + */ + translations: IntlTranslations + /** + * The maximum rating value. + */ + max: number + /** + * The name attribute of the rating element (used in forms). + */ + name?: string + /** + * The associate form of the underlying input element. + */ + form?: string + /** + * The current rating value. + */ + value: number + /** + * Whether the rating is readonly. + */ + readOnly?: boolean + /** + * Whether the rating is disabled. + */ + disabled?: boolean + /** + * Whether to allow half stars. + */ + allowHalf?: boolean + /** + * Whether to autofocus the rating. + */ + autoFocus?: boolean + /** + * Function to be called when the rating value changes. + */ + onValueChange?: (details: ValueChangeDetails) => void + /** + * Function to be called when the rating value is hovered. + */ + onHoverChange?: (details: HoverChangeDetails) => void +} export type UserDefinedContext = RequiredBy @@ -108,9 +111,9 @@ type PrivateContext = Context<{ fieldsetDisabled: boolean }> -export type MachineContext = PublicContext & ComputedContext & PrivateContext +export interface MachineContext extends PublicContext, ComputedContext, PrivateContext {} -export type MachineState = { +export interface MachineState { value: "idle" | "hover" | "focus" } @@ -118,7 +121,23 @@ export type State = S.State export type Send = S.Send -export type MachineApi = { +/* ----------------------------------------------------------------------------- + * Component API + * -----------------------------------------------------------------------------*/ + +export interface ItemProps { + index: number +} + +export interface ItemState { + isEqual: boolean + isValueEmpty: boolean + isHighlighted: boolean + isHalf: boolean + isChecked: boolean +} + +export interface MachineApi { /** * Sets the value of the rating group */ @@ -151,6 +170,7 @@ export type MachineApi = { * Returns the state of a rating item */ getRatingState(props: ItemProps): ItemState + rootProps: T["element"] hiddenInputProps: T["input"] labelProps: T["element"] diff --git a/packages/machines/select/src/select.machine.ts b/packages/machines/select/src/select.machine.ts index 4b8036a877..489f1dc7dc 100644 --- a/packages/machines/select/src/select.machine.ts +++ b/packages/machines/select/src/select.machine.ts @@ -452,10 +452,10 @@ export function machine(userContext: UserDefinedContex dom.getContentEl(ctx)?.scrollTo(0, 0) }, invokeOnOpen(ctx) { - ctx.onOpen?.() + ctx.onOpenChange?.({ open: true }) }, invokeOnClose(ctx) { - ctx.onClose?.() + ctx.onOpenChange?.({ open: false }) }, syncSelectElement(ctx) { const selectEl = dom.getHiddenSelectEl(ctx) @@ -480,11 +480,17 @@ function dispatchChangeEvent(ctx: MachineContext) { const invoke = { change: (ctx: MachineContext) => { - ctx.onChange?.({ value: Array.from(ctx.value), items: ctx.selectedItems }) + ctx.onValueChange?.({ + value: Array.from(ctx.value), + items: ctx.selectedItems, + }) dispatchChangeEvent(ctx) }, highlightChange: (ctx: MachineContext) => { - ctx.onHighlight?.({ value: ctx.highlightedValue, item: ctx.highlightedItem }) + ctx.onHighlightChange?.({ + highlightedValue: ctx.highlightedValue, + highlightedItem: ctx.highlightedItem, + }) }, } diff --git a/packages/machines/select/src/select.types.ts b/packages/machines/select/src/select.types.ts index fb67d8edf1..b7a33198bb 100644 --- a/packages/machines/select/src/select.types.ts +++ b/packages/machines/select/src/select.types.ts @@ -5,7 +5,27 @@ import type { TypeaheadState } from "@zag-js/dom-query" import type { Placement, PositioningOptions } from "@zag-js/popper" import type { CommonProperties, Context, DirectionProperty, PropTypes, RequiredBy } from "@zag-js/types" -export type { CollectionOptions, CollectionItem } +/* ----------------------------------------------------------------------------- + * Callback details + * -----------------------------------------------------------------------------*/ + +export interface ValueChangeDetails { + value: string[] + items: T[] +} + +export interface HighlightChangeDetails { + highlightedValue: string | null + highlightedItem: T | null +} + +export interface OpenChangeDetails { + open: boolean +} + +/* ----------------------------------------------------------------------------- + * Machine context + * -----------------------------------------------------------------------------*/ type ElementIds = Partial<{ root: string @@ -21,98 +41,85 @@ type ElementIds = Partial<{ itemGroupLabel(id: string | number): string }> -export type ValueChangeDetails = { +interface PublicContext + extends DirectionProperty, + CommonProperties, + InteractOutsideHandlers { + /** + * The item collection + */ + collection: Collection + /** + * The ids of the elements in the select. Useful for composition. + */ + ids?: ElementIds + /** + * The `name` attribute of the underlying select. + */ + name?: string + /** + * The associate form of the underlying select. + */ + form?: string + /** + * Whether the select is disabled + */ + disabled?: boolean + /** + * Whether the select is invalid + */ + invalid?: boolean + /** + * Whether the select is read-only + */ + readOnly?: boolean + /** + * Whether the select should close after an item is selected + */ + closeOnSelect?: boolean + /** + * Whether to select the highlighted item when the user presses Tab, + * and the menu is open. + */ + selectOnBlur?: boolean + /** + * The callback fired when the highlighted item changes. + */ + onHighlightChange?: (details: HighlightChangeDetails) => void + /** + * The callback fired when the selected item changes. + */ + onValueChange?: (details: ValueChangeDetails) => void + /** + * Function called when the popup is opened + */ + onOpenChange?: (details: OpenChangeDetails) => void + /** + * The positioning options of the menu. + */ + positioning: PositioningOptions + /** + * The keys of the selected items + */ value: string[] - items: T[] -} - -export type HighlightChangeDetails = { - value: string | null - item: T | null + /** + * The key of the highlighted item + */ + highlightedValue: string | null + /** + * Whether to loop the keyboard navigation through the options + */ + loop?: boolean + /** + * Whether to allow multiple selection + */ + multiple?: boolean + /** + * Whether the select menu is open + */ + open?: boolean } -type PublicContext = DirectionProperty & - CommonProperties & - InteractOutsideHandlers & { - /** - * The item collection - */ - collection: Collection - /** - * The ids of the elements in the select. Useful for composition. - */ - ids?: ElementIds - /** - * The `name` attribute of the underlying select. - */ - name?: string - /** - * The associate form of the underlying select. - */ - form?: string - /** - * Whether the select is disabled - */ - disabled?: boolean - /** - * Whether the select is invalid - */ - invalid?: boolean - /** - * Whether the select is read-only - */ - readOnly?: boolean - /** - * Whether the select should close after an item is selected - */ - closeOnSelect?: boolean - /** - * Whether to select the highlighted item when the user presses Tab, - * and the menu is open. - */ - selectOnBlur?: boolean - /** - * The callback fired when the highlighted item changes. - */ - onHighlight?: (details: HighlightChangeDetails) => void - /** - * The callback fired when the selected item changes. - */ - onChange?: (details: ValueChangeDetails) => void - /** - * Function called when the popup is opened - */ - onOpen?: VoidFunction - /** - * Function called when the popup is closed - */ - onClose?: VoidFunction - /** - * The positioning options of the menu. - */ - positioning: PositioningOptions - /** - * The keys of the selected items - */ - value: string[] - /** - * The key of the highlighted item - */ - highlightedValue: string | null - /** - * Whether to loop the keyboard navigation through the options - */ - loop?: boolean - /** - * Whether to allow multiple selection - */ - multiple?: boolean - /** - * Whether the select menu is open - */ - open?: boolean - } - type PrivateContext = Context<{ /** * @internal @@ -173,36 +180,40 @@ export type UserDefinedContext = Requ "id" | "collection" > -export type MachineContext = PublicContext & PrivateContext & ComputedContext +export interface MachineContext extends PublicContext, PrivateContext, ComputedContext {} -export type ItemProps = { +export interface MachineState { + value: "idle" | "focused" | "open" +} + +export type State = S.State + +export type Send = S.Send + +/* ----------------------------------------------------------------------------- + * Component API + * -----------------------------------------------------------------------------*/ + +export interface ItemProps { item: T } -export type ItemState = { +export interface ItemState { value: string isDisabled: boolean isSelected: boolean isHighlighted: boolean } -export type MachineState = { - value: "idle" | "focused" | "open" -} - -export type State = S.State - -export type Send = S.Send - -export type ItemGroupProps = { +export interface ItemGroupProps { id: string } -export type ItemGroupLabelProps = { +export interface ItemGroupLabelProps { htmlFor: string } -export type MachineApi = { +export interface MachineApi { /** * Whether the select is focused */ @@ -286,3 +297,9 @@ export type MachineApi { - ctx.onChange?.({ value: ctx.value }) + ctx.onValueChange?.({ value: ctx.value }) dom.dispatchChangeEvent(ctx) }, } diff --git a/packages/machines/slider/src/slider.types.ts b/packages/machines/slider/src/slider.types.ts index 226ab4d7a9..6d49bd1e98 100644 --- a/packages/machines/slider/src/slider.types.ts +++ b/packages/machines/slider/src/slider.types.ts @@ -1,6 +1,18 @@ import type { StateMachine as S } from "@zag-js/core" import type { CommonProperties, Context, DirectionProperty, PropTypes, RequiredBy } from "@zag-js/types" +/* ----------------------------------------------------------------------------- + * Callback details + * -----------------------------------------------------------------------------*/ + +export interface ValueChangeDetails { + value: number +} + +/* ----------------------------------------------------------------------------- + * Machine context + * -----------------------------------------------------------------------------*/ + type ElementIds = Partial<{ root: string thumb: string @@ -12,96 +24,95 @@ type ElementIds = Partial<{ hiddenInput: string }> -type PublicContext = DirectionProperty & - CommonProperties & { - /** - * The ids of the elements in the slider. Useful for composition. - */ - ids?: ElementIds - /** - * The value of the slider - */ - value: number - /** - * The name associated with the slider (when used in a form) - */ - name?: string - /** - * The associate form of the underlying input element. - */ - form?: string - /** - * Whether the slider is disabled - */ - disabled?: boolean - /** - * Whether the slider is read-only - */ - readOnly?: boolean - /** - * Whether the slider value is invalid - */ - invalid?: boolean - /** - * The minimum value of the slider - */ - min: number - /** - * The maximum value of the slider - */ - max: number - /** - * The step value of the slider - */ - step: number - /** - * The orientation of the slider - */ - orientation?: "vertical" | "horizontal" - /** - * - "start": Useful when the value represents an absolute value - * - "center": Useful when the value represents an offset (relative) - */ - origin?: "start" | "center" - /** - * The aria-label of the slider. Useful for providing an accessible name to the slider - */ - "aria-label"?: string - /** - * The `id` of the element that labels the slider. Useful for providing an accessible name to the slider - */ - "aria-labelledby"?: string - /** - * Whether to focus the slider thumb after interaction (scrub and keyboard) - */ - focusThumbOnChange?: boolean - /** - * Function that returns a human readable value for the slider - */ - getAriaValueText?(value: number): string - /** - * Function invoked when the value of the slider changes - */ - onChange?(details: { value: number }): void - /** - * Function invoked when the slider value change is done - */ - onChangeEnd?(details: { value: number }): void - /** - * Function invoked when the slider value change is started - */ - onChangeStart?(details: { value: number }): void - /** - * The alignment of the slider thumb relative to the track - * - `center`: the thumb will extend beyond the bounds of the slider track. - * - `contain`: the thumb will be contained within the bounds of the track. - */ - thumbAlignment?: "contain" | "center" - /** - * The slider thumb dimensions.If not provided, the thumb size will be measured automatically. - */ - thumbSize: { width: number; height: number } | null - } +interface PublicContext extends DirectionProperty, CommonProperties { + /** + * The ids of the elements in the slider. Useful for composition. + */ + ids?: ElementIds + /** + * The value of the slider + */ + value: number + /** + * The name associated with the slider (when used in a form) + */ + name?: string + /** + * The associate form of the underlying input element. + */ + form?: string + /** + * Whether the slider is disabled + */ + disabled?: boolean + /** + * Whether the slider is read-only + */ + readOnly?: boolean + /** + * Whether the slider value is invalid + */ + invalid?: boolean + /** + * The minimum value of the slider + */ + min: number + /** + * The maximum value of the slider + */ + max: number + /** + * The step value of the slider + */ + step: number + /** + * The orientation of the slider + */ + orientation?: "vertical" | "horizontal" + /** + * - "start": Useful when the value represents an absolute value + * - "center": Useful when the value represents an offset (relative) + */ + origin?: "start" | "center" + /** + * The aria-label of the slider. Useful for providing an accessible name to the slider + */ + "aria-label"?: string + /** + * The `id` of the element that labels the slider. Useful for providing an accessible name to the slider + */ + "aria-labelledby"?: string + /** + * Whether to focus the slider thumb after interaction (scrub and keyboard) + */ + focusThumbOnChange?: boolean + /** + * Function that returns a human readable value for the slider + */ + getAriaValueText?(value: number): string + /** + * Function invoked when the value of the slider changes + */ + onValueChange?(details: ValueChangeDetails): void + /** + * Function invoked when the slider value change is done + */ + onValueChangeEnd?(details: ValueChangeDetails): void + /** + * Function invoked when the slider value change is started + */ + onValueChangeStart?(details: ValueChangeDetails): void + /** + * The alignment of the slider thumb relative to the track + * - `center`: the thumb will extend beyond the bounds of the slider track. + * - `contain`: the thumb will be contained within the bounds of the track. + */ + thumbAlignment?: "contain" | "center" + /** + * The slider thumb dimensions.If not provided, the thumb size will be measured automatically. + */ + thumbSize: Size | null +} export type UserDefinedContext = RequiredBy @@ -156,9 +167,9 @@ type PrivateContext = Context<{ fieldsetDisabled: boolean }> -export type MachineContext = PublicContext & ComputedContext & PrivateContext +export interface MachineContext extends PublicContext, ComputedContext, PrivateContext {} -export type MachineState = { +export interface MachineState { value: "idle" | "dragging" | "focus" } @@ -166,27 +177,25 @@ export type State = S.State export type Send = S.Send -export type SharedContext = { - min: number - max: number - step: number - dir?: "ltr" | "rtl" - isRtl: boolean - isVertical: boolean - isHorizontal: boolean - value: number - thumbSize: { width: number; height: number } | null - thumbAlignment?: "contain" | "center" - orientation?: "horizontal" | "vertical" - readonly hasMeasuredThumbSize: boolean -} +/* ----------------------------------------------------------------------------- + * Component API + * -----------------------------------------------------------------------------*/ -export type Point = { +export interface Point { x: number y: number } -export type MachineApi = { +interface Size { + width: number + height: number +} + +interface MarkerProps { + value: number +} + +export interface MachineApi { /** * Whether the slider is focused. */ @@ -227,6 +236,7 @@ export type MachineApi = { * Function to decrement the value of the slider by the step. */ decrement(): void + rootProps: T["element"] labelProps: T["label"] thumbProps: T["element"] @@ -236,5 +246,24 @@ export type MachineApi = { rangeProps: T["element"] controlProps: T["element"] markerGroupProps: T["element"] - getMarkerProps({ value }: { value: number }): T["element"] + getMarkerProps(props: MarkerProps): T["element"] +} + +/* ----------------------------------------------------------------------------- + * Re-exported types + * -----------------------------------------------------------------------------*/ + +export interface SharedContext { + min: number + max: number + step: number + dir?: "ltr" | "rtl" + isRtl: boolean + isVertical: boolean + isHorizontal: boolean + value: number + thumbSize: Size | null + thumbAlignment?: "contain" | "center" + orientation?: "horizontal" | "vertical" + readonly hasMeasuredThumbSize: boolean } diff --git a/packages/machines/splitter/src/splitter.connect.ts b/packages/machines/splitter/src/splitter.connect.ts index 76161b2ca4..84a0c06eee 100644 --- a/packages/machines/splitter/src/splitter.connect.ts +++ b/packages/machines/splitter/src/splitter.connect.ts @@ -3,7 +3,7 @@ import { dataAttr } from "@zag-js/dom-query" import type { NormalizeProps, PropTypes } from "@zag-js/types" import { parts } from "./splitter.anatomy" import { dom } from "./splitter.dom" -import type { PanelId, PanelProps, MachineApi, ResizeTriggerProps, Send, State } from "./splitter.types" +import type { MachineApi, ResizeTriggerProps, Send, State } from "./splitter.types" import { getHandleBounds } from "./splitter.utils" export function connect(state: State, send: Send, normalize: NormalizeProps): MachineApi { @@ -12,40 +12,42 @@ export function connect(state: State, send: Send, normalize const isDragging = state.matches("dragging") const panels = state.context.panels - const api = { + function getResizeTriggerState(props: ResizeTriggerProps) { + const { id, disabled } = props + const ids = id.split(":") + const panelIds = ids.map((id) => dom.getPanelId(state.context, id)) + const panels = getHandleBounds(state.context, id) + + return { + isDisabled: !!disabled, + isFocused: state.context.activeResizeId === id && isFocused, + panelIds, + min: panels?.min, + max: panels?.max, + value: 0, + } + } + + return { isFocused, isDragging, bounds: getHandleBounds(state.context), - setToMinSize(id: PanelId) { + setToMinSize(id) { const panel = panels.find((panel) => panel.id === id) send({ type: "SET_PANEL_SIZE", id, size: panel?.minSize, src: "setToMinSize" }) }, - setToMaxSize(id: PanelId) { + setToMaxSize(id) { const panel = panels.find((panel) => panel.id === id) send({ type: "SET_PANEL_SIZE", id, size: panel?.maxSize, src: "setToMaxSize" }) }, - setSize(id: PanelId, size: number) { + setSize(id, size) { send({ type: "SET_PANEL_SIZE", id, size }) }, - getResizeTriggerState(props: ResizeTriggerProps) { - const { id, disabled } = props - const ids = id.split(":") - const panelIds = ids.map((id) => dom.getPanelId(state.context, id)) - const panels = getHandleBounds(state.context, id) - - return { - isDisabled: !!disabled, - isFocused: state.context.activeResizeId === id && isFocused, - panelIds, - min: panels?.min, - max: panels?.max, - value: 0, - } - }, + getResizeTriggerState, rootProps: normalize.element({ ...parts.root.attrs, @@ -61,7 +63,7 @@ export function connect(state: State, send: Send, normalize }, }), - getPanelProps(props: PanelProps) { + getPanelProps(props) { const { id } = props return normalize.element({ ...parts.panel.attrs, @@ -72,9 +74,9 @@ export function connect(state: State, send: Send, normalize }) }, - getResizeTriggerProps(props: ResizeTriggerProps) { + getResizeTriggerProps(props) { const { id, disabled, step = 1 } = props - const triggerState = api.getResizeTriggerState(props) + const triggerState = getResizeTriggerState(props) return normalize.element({ ...parts.resizeTrigger.attrs, @@ -164,6 +166,4 @@ export function connect(state: State, send: Send, normalize }) }, } - - return api } diff --git a/packages/machines/splitter/src/splitter.machine.ts b/packages/machines/splitter/src/splitter.machine.ts index 81d742dc4d..13b3a2fc96 100644 --- a/packages/machines/splitter/src/splitter.machine.ts +++ b/packages/machines/splitter/src/splitter.machine.ts @@ -170,13 +170,13 @@ export function machine(userContext: UserDefinedContext) { dom.removeGlobalCursor(ctx) }, invokeOnResize(ctx) { - ctx.onResize?.({ size: ctx.size, activeHandleId: ctx.activeResizeId }) + ctx.onSizeChange?.({ size: Array.from(ctx.size), activeHandleId: ctx.activeResizeId }) }, invokeOnResizeStart(ctx) { - ctx.onResizeStart?.({ size: ctx.size, activeHandleId: ctx.activeResizeId }) + ctx.onSizeChangeStart?.({ size: Array.from(ctx.size), activeHandleId: ctx.activeResizeId }) }, invokeOnResizeEnd(ctx) { - ctx.onResizeEnd?.({ size: ctx.size, activeHandleId: ctx.activeResizeId }) + ctx.onSizeChangeEnd?.({ size: Array.from(ctx.size), activeHandleId: ctx.activeResizeId }) }, setActiveHandleId(ctx, evt) { ctx.activeResizeId = evt.id diff --git a/packages/machines/splitter/src/splitter.types.ts b/packages/machines/splitter/src/splitter.types.ts index 4125562eb9..586474ca58 100644 --- a/packages/machines/splitter/src/splitter.types.ts +++ b/packages/machines/splitter/src/splitter.types.ts @@ -1,20 +1,28 @@ import type { StateMachine as S } from "@zag-js/core" import type { CommonProperties, Context, DirectionProperty, PropTypes, RequiredBy } from "@zag-js/types" +/* ----------------------------------------------------------------------------- + * Callback details + * -----------------------------------------------------------------------------*/ + export type PanelId = string | number -type PanelSizeData = { +interface PanelSizeData { id: PanelId size?: number minSize?: number maxSize?: number } -type ResizeDetails = { +export interface SizeChangeDetails { size: PanelSizeData[] activeHandleId: string | null } +/* ----------------------------------------------------------------------------- + * Machine context + * -----------------------------------------------------------------------------*/ + type ElementIds = Partial<{ root: string resizeTrigger(id: string): string @@ -22,33 +30,32 @@ type ElementIds = Partial<{ panel(id: string | number): string }> -type PublicContext = DirectionProperty & - CommonProperties & { - /** - * The orientation of the splitter. Can be `horizontal` or `vertical` - */ - orientation: "horizontal" | "vertical" - /** - * The size data of the panels - */ - size: PanelSizeData[] - /** - * Function called when the splitter is resized. - */ - onResize?: (details: ResizeDetails) => void - /** - * Function called when the splitter resize starts. - */ - onResizeStart?: (details: ResizeDetails) => void - /** - * Function called when the splitter resize ends. - */ - onResizeEnd?: (details: ResizeDetails) => void - /** - * The ids of the elements in the splitter. Useful for composition. - */ - ids?: ElementIds - } +interface PublicContext extends DirectionProperty, CommonProperties { + /** + * The orientation of the splitter. Can be `horizontal` or `vertical` + */ + orientation: "horizontal" | "vertical" + /** + * The size data of the panels + */ + size: PanelSizeData[] + /** + * Function called when the splitter is resized. + */ + onSizeChange?: (details: SizeChangeDetails) => void + /** + * Function called when the splitter resize starts. + */ + onSizeChangeStart?: (details: SizeChangeDetails) => void + /** + * Function called when the splitter resize ends. + */ + onSizeChangeEnd?: (details: SizeChangeDetails) => void + /** + * The ids of the elements in the splitter. Useful for composition. + */ + ids?: ElementIds +} export type UserDefinedContext = RequiredBy @@ -76,9 +83,9 @@ type PrivateContext = Context<{ initialSize: Array>> }> -export type MachineContext = PublicContext & ComputedContext & PrivateContext +export interface MachineContext extends PublicContext, ComputedContext, PrivateContext {} -export type MachineState = { +export interface MachineState { value: "idle" | "hover:temp" | "hover" | "dragging" | "focused" tags: "focus" } @@ -87,23 +94,36 @@ export type State = S.State export type Send = S.Send -export type PanelProps = { +/* ----------------------------------------------------------------------------- + * Component API + * -----------------------------------------------------------------------------*/ + +export interface PanelProps { id: PanelId snapSize?: number } -export type ResizeTriggerProps = { +export interface ResizeTriggerProps { id: `${PanelId}:${PanelId}` step?: number disabled?: boolean } -export type PanelBounds = { +export interface ResizeTriggerState { + isDisabled: boolean + isFocused: boolean + panelIds: string[] + min: number | undefined + max: number | undefined + value: number +} + +export interface PanelBounds { min: number max: number } -export type MachineApi = { +export interface MachineApi { /** * Whether the splitter is focused. */ @@ -131,14 +151,7 @@ export type MachineApi = { /** * Returns the state details for a resize trigger. */ - getResizeTriggerState(props: ResizeTriggerProps): { - isDisabled: boolean - isFocused: boolean - panelIds: string[] - min: number | undefined - max: number | undefined - value: number - } + getResizeTriggerState(props: ResizeTriggerProps): ResizeTriggerState rootProps: T["element"] getPanelProps(props: PanelProps): T["element"] getResizeTriggerProps(props: ResizeTriggerProps): T["element"] diff --git a/packages/machines/switch/src/switch.machine.ts b/packages/machines/switch/src/switch.machine.ts index 1bf1ab041d..e4ad39884d 100644 --- a/packages/machines/switch/src/switch.machine.ts +++ b/packages/machines/switch/src/switch.machine.ts @@ -109,7 +109,7 @@ export function machine(userContext: UserDefinedContext) { const invoke = { change: (ctx: MachineContext) => { - ctx.onChange?.({ checked: ctx.checked }) + ctx.onCheckedChange?.({ checked: ctx.checked }) }, } diff --git a/packages/machines/switch/src/switch.types.ts b/packages/machines/switch/src/switch.types.ts index 6e96e61707..ec959d4021 100644 --- a/packages/machines/switch/src/switch.types.ts +++ b/packages/machines/switch/src/switch.types.ts @@ -1,6 +1,18 @@ import type { StateMachine as S } from "@zag-js/core" import type { CommonProperties, Context, DirectionProperty, PropTypes, RequiredBy } from "@zag-js/types" +/* ----------------------------------------------------------------------------- + * Callback details + * -----------------------------------------------------------------------------*/ + +export interface CheckedChangeDetails { + checked: boolean +} + +/* ----------------------------------------------------------------------------- + * Machine context + * -----------------------------------------------------------------------------*/ + type ElementIds = Partial<{ root: string input: string @@ -9,55 +21,54 @@ type ElementIds = Partial<{ thumb: string }> -type PublicContext = DirectionProperty & - CommonProperties & { - /** - * The ids of the elements in the switch. Useful for composition. - */ - ids?: ElementIds - /** - * Specifies the localized strings that identifies the accessibility elements and their states - */ - label: string - /** - * Whether the switch is disabled. - */ - disabled?: boolean - /** - * If `true`, the switch will be readonly - */ - readOnly?: boolean - /** - * If `true`, the switch is marked as invalid. - */ - invalid?: boolean - /** - * If `true`, the switch input is marked as required, - */ - required?: boolean - /** - * Function to call when the switch is clicked. - */ - onChange?: (details: { checked: boolean }) => void - /** - * Whether the switch is checked. - */ - checked: boolean - /** - * The name of the input field in a switch - * (Useful for form submission). - */ - name?: string - /** - * The id of the form that the switch belongs to - */ - form?: string - /** - * The value of switch input. Useful for form submission. - * @default "on" - */ - value?: string | number - } +interface PublicContext extends DirectionProperty, CommonProperties { + /** + * The ids of the elements in the switch. Useful for composition. + */ + ids?: ElementIds + /** + * Specifies the localized strings that identifies the accessibility elements and their states + */ + label: string + /** + * Whether the switch is disabled. + */ + disabled?: boolean + /** + * If `true`, the switch will be readonly + */ + readOnly?: boolean + /** + * If `true`, the switch is marked as invalid. + */ + invalid?: boolean + /** + * If `true`, the switch input is marked as required, + */ + required?: boolean + /** + * Function to call when the switch is clicked. + */ + onCheckedChange?: (details: CheckedChangeDetails) => void + /** + * Whether the switch is checked. + */ + checked: boolean + /** + * The name of the input field in a switch + * (Useful for form submission). + */ + name?: string + /** + * The id of the form that the switch belongs to + */ + form?: string + /** + * The value of switch input. Useful for form submission. + * @default "on" + */ + value?: string | number +} export type UserDefinedContext = RequiredBy @@ -91,9 +102,9 @@ type PrivateContext = Context<{ fieldsetDisabled: boolean }> -export type MachineContext = PublicContext & PrivateContext & ComputedContext +export interface MachineContext extends PublicContext, PrivateContext, ComputedContext {} -export type MachineState = { +export interface MachineState { value: "ready" } @@ -101,7 +112,11 @@ export type State = S.State export type Send = S.Send -export type MachineApi = { +/* ----------------------------------------------------------------------------- + * Component API + * -----------------------------------------------------------------------------*/ + +export interface MachineApi { /** * Whether the checkbox is checked */ diff --git a/packages/machines/tabs/src/tabs.connect.ts b/packages/machines/tabs/src/tabs.connect.ts index a83849640e..d008564da3 100644 --- a/packages/machines/tabs/src/tabs.connect.ts +++ b/packages/machines/tabs/src/tabs.connect.ts @@ -3,18 +3,26 @@ import { dataAttr, isSafari, isSelfEvent } from "@zag-js/dom-query" import type { NormalizeProps, PropTypes } from "@zag-js/types" import { parts } from "./tabs.anatomy" import { dom } from "./tabs.dom" -import type { ContentProps, MachineApi, Send, State, TriggerProps } from "./tabs.types" +import type { MachineApi, Send, State, TriggerProps } from "./tabs.types" export function connect(state: State, send: Send, normalize: NormalizeProps): MachineApi { const translations = state.context.translations const isFocused = state.matches("focused") + function getTriggerState(props: TriggerProps) { + return { + isSelected: state.context.value === props.value, + isFocused: state.context.focusedValue === props.value, + isDisabled: !!props.disabled, + } + } + return { value: state.context.value, focusedValue: state.context.focusedValue, previousValues: Array.from(state.context.previousValues), - setValue(value: string) { + setValue(value) { send({ type: "SET_VALUE", value }) }, @@ -22,10 +30,13 @@ export function connect(state: State, send: Send, normalize send({ type: "CLEAR_VALUE" }) }, - setIndicatorRect(id: string | null | undefined) { + setIndicatorRect(value) { + const id = dom.getTriggerId(state.context, value) send({ type: "SET_INDICATOR_RECT", id }) }, + getTriggerState, + rootProps: normalize.element({ ...parts.root.attrs, id: dom.getRootId(state.context), @@ -38,6 +49,7 @@ export function connect(state: State, send: Send, normalize ...parts.list.attrs, id: dom.getTablistId(state.context), role: "tablist", + dir: state.context.dir, "data-focus": dataAttr(isFocused), "aria-orientation": state.context.orientation, "data-orientation": state.context.orientation, @@ -79,25 +91,27 @@ export function connect(state: State, send: Send, normalize }, }), - getTriggerProps(props: TriggerProps) { + getTriggerProps(props) { const { value, disabled } = props - const selected = state.context.value === value + const triggerState = getTriggerState(props) return normalize.button({ ...parts.trigger.attrs, role: "tab", type: "button", disabled, + dir: state.context.dir, "data-orientation": state.context.orientation, "data-disabled": dataAttr(disabled), "aria-disabled": disabled, "data-value": value, - "aria-selected": selected, - "data-selected": dataAttr(selected), + "aria-selected": triggerState.isSelected, + "data-selected": dataAttr(triggerState.isSelected), + "data-focus": dataAttr(triggerState.isFocused), "aria-controls": dom.getContentId(state.context, value), "data-ownedby": dom.getTablistId(state.context), id: dom.getTriggerId(state.context, value), - tabIndex: selected ? 0 : -1, + tabIndex: triggerState.isSelected ? 0 : -1, onFocus() { send({ type: "TAB_FOCUS", value }) }, @@ -117,16 +131,19 @@ export function connect(state: State, send: Send, normalize }) }, - getContentProps({ value }: ContentProps) { + getContentProps(props) { + const { value } = props const selected = state.context.value === value return normalize.element({ ...parts.content.attrs, + dir: state.context.dir, id: dom.getContentId(state.context, value), tabIndex: 0, "aria-labelledby": dom.getTriggerId(state.context, value), role: "tabpanel", "data-ownedby": dom.getTablistId(state.context), "data-selected": dataAttr(selected), + "data-orientation": state.context.orientation, hidden: !selected, }) }, diff --git a/packages/machines/tabs/src/tabs.machine.ts b/packages/machines/tabs/src/tabs.machine.ts index acc2775369..b2208811a1 100644 --- a/packages/machines/tabs/src/tabs.machine.ts +++ b/packages/machines/tabs/src/tabs.machine.ts @@ -245,11 +245,11 @@ function pushUnique(arr: string[], value: any) { const invoke = { change: (ctx: MachineContext) => { if (ctx.value == null) return - ctx.onChange?.({ value: ctx.value }) + ctx.onValueChange?.({ value: ctx.value }) }, focusChange: (ctx: MachineContext) => { if (ctx.focusedValue == null) return - ctx.onFocus?.({ value: ctx.focusedValue }) + ctx.onFocusChange?.({ focusedValue: ctx.focusedValue }) }, } diff --git a/packages/machines/tabs/src/tabs.types.ts b/packages/machines/tabs/src/tabs.types.ts index 13b63aa744..01ddcfe55c 100644 --- a/packages/machines/tabs/src/tabs.types.ts +++ b/packages/machines/tabs/src/tabs.types.ts @@ -1,7 +1,23 @@ import type { StateMachine as S } from "@zag-js/core" import type { CommonProperties, Context, DirectionProperty, PropTypes, RequiredBy } from "@zag-js/types" -type IntlTranslations = { +/* ----------------------------------------------------------------------------- + * Callback details + * -----------------------------------------------------------------------------*/ + +export interface ValueChangeDetails { + value: string +} + +export interface FocusChangeDetails { + focusedValue: string +} + +/* ----------------------------------------------------------------------------- + * Machine context + * -----------------------------------------------------------------------------*/ + +interface IntlTranslations { tablistLabel?: string } @@ -13,59 +29,49 @@ type ElementIds = Partial<{ indicator: string }> -export type TriggerProps = { - value: string - disabled?: boolean -} - -export type ContentProps = { - value: string +interface PublicContext extends DirectionProperty, CommonProperties { + /** + * The ids of the elements in the tabs. Useful for composition. + */ + ids?: ElementIds + /** + * Specifies the localized strings that identifies the accessibility elements and their states + */ + translations: IntlTranslations + /** + * Whether the keyboard navigation will loop from last tab to first, and vice versa. + * @default true + */ + loop: boolean + /** + * The selected tab id + */ + value: string | null + /** + * The orientation of the tabs. Can be `horizontal` or `vertical` + * - `horizontal`: only left and right arrow key navigation will work. + * - `vertical`: only up and down arrow key navigation will work. + * + * @default "horizontal" + */ + orientation?: "horizontal" | "vertical" + /** + * The activation mode of the tabs. Can be `manual` or `automatic` + * - `manual`: Tabs are activated when clicked or press `enter` key. + * - `automatic`: Tabs are activated when receiving focus + * @default "automatic" + */ + activationMode?: "manual" | "automatic" + /** + * Callback to be called when the selected/active tab changes + */ + onValueChange?: (details: ValueChangeDetails) => void + /** + * Callback to be called when the focused tab changes + */ + onFocusChange?: (details: FocusChangeDetails) => void } -type PublicContext = DirectionProperty & - CommonProperties & { - /** - * The ids of the elements in the tabs. Useful for composition. - */ - ids?: ElementIds - /** - * Specifies the localized strings that identifies the accessibility elements and their states - */ - translations: IntlTranslations - /** - * Whether the keyboard navigation will loop from last tab to first, and vice versa. - * @default true - */ - loop: boolean - /** - * The selected tab id - */ - value: string | null - /** - * The orientation of the tabs. Can be `horizontal` or `vertical` - * - `horizontal`: only left and right arrow key navigation will work. - * - `vertical`: only up and down arrow key navigation will work. - * - * @default "horizontal" - */ - orientation?: "horizontal" | "vertical" - /** - * The activation mode of the tabs. Can be `manual` or `automatic` - * - `manual`: Tabs are activated when clicked or press `enter` key. - * - `automatic`: Tabs are activated when receiving focus - * @default "automatic" - */ - activationMode?: "manual" | "automatic" - /** - * Callback to be called when the selected/active tab changes - */ - onChange?: (details: { value: string }) => void - /** - * Callback to be called when the focused tab changes - */ - onFocus?: (details: { value: string }) => void - } - export type UserDefinedContext = RequiredBy type ComputedContext = Readonly<{ @@ -114,9 +120,9 @@ type PrivateContext = Context<{ indicatorCleanup?: VoidFunction | null }> -export type MachineContext = PublicContext & ComputedContext & PrivateContext +export interface MachineContext extends PublicContext, ComputedContext, PrivateContext {} -export type MachineState = { +export interface MachineState { value: "idle" | "focused" } @@ -124,7 +130,25 @@ export type State = S.State export type Send = S.Send -export type MachineApi = { +/* ----------------------------------------------------------------------------- + * Component API + * -----------------------------------------------------------------------------*/ + +export interface TriggerProps { + value: string + disabled?: boolean +} + +export interface TriggerState { + isSelected: boolean + isFocused: boolean +} + +export interface ContentProps { + value: string +} + +export interface MachineApi { /** * The current value of the tabs. */ @@ -146,12 +170,17 @@ export type MachineApi = { */ clearValue(): void /** - * Sets the indicator rect to the tab with the given id. + * Sets the indicator rect to the tab with the given value */ - setIndicatorRect(id: string | null | undefined): void + setIndicatorRect(value: string): void + /** + * Returns the state of the trigger with the given props + */ + getTriggerState(props: TriggerProps): TriggerState + rootProps: T["element"] tablistProps: T["element"] getTriggerProps(props: TriggerProps): T["button"] - getContentProps({ value }: ContentProps): T["element"] + getContentProps(props: ContentProps): T["element"] indicatorProps: T["element"] } diff --git a/packages/machines/tags-input/src/index.ts b/packages/machines/tags-input/src/index.ts index 220ed72d2a..ec0d279319 100644 --- a/packages/machines/tags-input/src/index.ts +++ b/packages/machines/tags-input/src/index.ts @@ -1,4 +1,4 @@ export { anatomy } from "./tags-input.anatomy" export { connect } from "./tags-input.connect" export { machine } from "./tags-input.machine" -export type { UserDefinedContext as Context, InteractOutsideEvent, MachineApi as Api } from "./tags-input.types" +export type { UserDefinedContext as Context, MachineApi as Api } from "./tags-input.types" diff --git a/packages/machines/tags-input/src/tags-input.connect.ts b/packages/machines/tags-input/src/tags-input.connect.ts index 214d60335f..0d16b752ed 100644 --- a/packages/machines/tags-input/src/tags-input.connect.ts +++ b/packages/machines/tags-input/src/tags-input.connect.ts @@ -17,6 +17,16 @@ export function connect(state: State, send: Send, normalize const isEditingTag = state.matches("editing:tag") const isEmpty = state.context.count === 0 + function getTagState(options: TagProps) { + const id = dom.getTagId(state.context, options) + return { + id, + isEditing: isEditingTag && state.context.editedTagId === id, + isHighlighted: id === state.context.highlightedTagId, + isDisabled: options.disabled || isDisabled, + } + } + return { isEmpty, inputValue: state.context.trimmedInputValue, @@ -57,6 +67,8 @@ export function connect(state: State, send: Send, normalize dom.getInputEl(state.context)?.focus() }, + getTagState, + rootProps: normalize.element({ dir: state.context.dir, ...parts.root.attrs, @@ -140,7 +152,7 @@ export function connect(state: State, send: Send, normalize send("ARROW_LEFT") }, ArrowRight() { - if (state.context.focusedId) { + if (state.context.highlightedTagId) { event.preventDefault() } if (isCombobox && isExpanded) return @@ -181,46 +193,44 @@ export function connect(state: State, send: Send, normalize defaultValue: state.context.valueAsString, }), - getTagProps(options: TagProps) { - const { value } = options - const id = dom.getTagId(state.context, options) + getTagProps(props) { + const tagState = getTagState(props) return normalize.element({ ...parts.tag.attrs, - id, - hidden: isEditingTag ? state.context.editedTagId === id : false, - "data-value": value, + id: tagState.id, + hidden: tagState.isEditing, + "data-value": props.value, "data-disabled": dataAttr(isDisabled), - "data-highlighted": dataAttr(id === state.context.focusedId), + "data-highlighted": dataAttr(tagState.isHighlighted), onPointerDown(event) { - if (!isInteractive) return + if (!isInteractive || tagState.isDisabled) return event.preventDefault() - send({ type: "POINTER_DOWN_TAG", id }) + send({ type: "POINTER_DOWN_TAG", id: tagState.id }) }, onDoubleClick() { - if (!isInteractive) return - send({ type: "DOUBLE_CLICK_TAG", id }) + if (!isInteractive || tagState.isDisabled) return + send({ type: "DOUBLE_CLICK_TAG", id: tagState.id }) }, }) }, - getTagInputProps(options: TagProps) { - const id = dom.getTagId(state.context, options) - const active = state.context.editedTagId === id + getTagInputProps(props) { + const tagState = getTagState(props) + return normalize.input({ ...parts.tagInput.attrs, - "aria-label": translations.tagEdited(options.value), + "aria-label": translations.tagEdited(props.value), "aria-hidden": true, disabled: isDisabled, - id: dom.getTagInputId(state.context, options), - type: "text", + id: dom.getTagInputId(state.context, props), tabIndex: -1, - hidden: isEditingTag ? !active : true, - defaultValue: active ? state.context.editedTagValue : "", + hidden: !tagState.isEditing, + defaultValue: tagState.isEditing ? state.context.editedTagValue : "", onChange(event) { send({ type: "TAG_INPUT_TYPE", value: event.target.value }) }, onBlur(event) { - send({ type: "TAG_INPUT_BLUR", target: event.relatedTarget, id }) + send({ type: "TAG_INPUT_BLUR", target: event.relatedTarget, id: tagState.id }) }, onKeyDown(event) { const keyMap: EventKeyMap = { @@ -242,14 +252,14 @@ export function connect(state: State, send: Send, normalize }) }, - getTagDeleteTriggerProps(options: TagProps) { - const id = dom.getTagId(state.context, options) + getTagDeleteTriggerProps(props) { + const id = dom.getTagId(state.context, props) return normalize.button({ ...parts.tagDeleteTrigger.attrs, - id: dom.getTagDeleteTriggerId(state.context, options), + id: dom.getTagDeleteTriggerId(state.context, props), type: "button", disabled: isDisabled, - "aria-label": translations.deleteTagTriggerLabel(options.value), + "aria-label": translations.deleteTagTriggerLabel(props.value), tabIndex: -1, onPointerDown(event) { if (!isInteractive) { diff --git a/packages/machines/tags-input/src/tags-input.dom.ts b/packages/machines/tags-input/src/tags-input.dom.ts index 50466ca46e..3c8f34d352 100644 --- a/packages/machines/tags-input/src/tags-input.dom.ts +++ b/packages/machines/tags-input/src/tags-input.dom.ts @@ -30,21 +30,20 @@ export const dom = createScope({ getIndexOfId: (ctx: Ctx, id: string) => indexOfId(dom.getTagElements(ctx), id), isInputFocused: (ctx: Ctx) => dom.getDoc(ctx).activeElement === dom.getInputEl(ctx), - getFocusedTagValue: (ctx: Ctx) => { - if (!ctx.focusedId) return null - const idx = dom.getIndexOfId(ctx, ctx.focusedId) - if (idx === -1) return null - return dom.getTagElements(ctx)[idx].dataset.value ?? null + getHighlightedTagValue: (ctx: Ctx) => { + if (!ctx.highlightedTagId) return null + const tagEl = dom.getById(ctx, ctx.highlightedTagId) + return tagEl?.dataset.value ?? null }, setHoverIntent: (el: Element) => { - const tag = el.closest("[data-part=tag]") - if (!tag) return - tag.dataset.deleteIntent = "" + const tagEl = el.closest("[data-part=tag]") + if (!tagEl) return + tagEl.dataset.deleteIntent = "" }, clearHoverIntent: (el: Element) => { - const tag = el.closest("[data-part=tag]") - if (!tag) return - delete tag.dataset.deleteIntent + const tagEl = el.closest("[data-part=tag]") + if (!tagEl) return + delete tagEl.dataset.deleteIntent }, dispatchInputEvent(ctx: Ctx) { const inputEl = dom.getHiddenInputEl(ctx) diff --git a/packages/machines/tags-input/src/tags-input.machine.ts b/packages/machines/tags-input/src/tags-input.machine.ts index 8e25ffc25b..cbbff8f935 100644 --- a/packages/machines/tags-input/src/tags-input.machine.ts +++ b/packages/machines/tags-input/src/tags-input.machine.ts @@ -21,7 +21,7 @@ export function machine(userContext: UserDefinedContext) { inputValue: "", editedTagValue: "", editedTagId: null, - focusedId: null, + highlightedTagId: null, value: [], dir: "ltr", max: Infinity, @@ -56,18 +56,16 @@ export function machine(userContext: UserDefinedContext) { isOverflowing: (ctx) => ctx.count > ctx.max, }, watch: { - focusedId: "logFocusedTag", + highlightedTagId: "logHighlightedTag", isOverflowing: "invokeOnInvalid", log: "announceLog", inputValue: "syncInputValue", editedTagValue: "syncEditedTagInputValue", }, - activities: ["trackFormControlState"], + activities: ["trackLiveRegion", "trackFormControlState"], - entry: ["setupLiveRegion"], - - exit: ["removeLiveRegion", "clearLog"], + exit: ["clearLog"], on: { DOUBLE_CLICK_TAG: { @@ -78,9 +76,9 @@ export function machine(userContext: UserDefinedContext) { }, POINTER_DOWN_TAG: { internal: true, - guard: not("isTagFocused"), + guard: not("isTagHighlighted"), target: "navigating:tag", - actions: ["focusTag", "focusInput"], + actions: ["highlightTag", "focusInput"], }, SET_INPUT_VALUE: { actions: ["setInputValue"], @@ -113,7 +111,7 @@ export function machine(userContext: UserDefinedContext) { on: { FOCUS: "focused:input", POINTER_DOWN: { - guard: not("hasFocusedId"), + guard: not("hasHighlightedTag"), target: "focused:input", }, }, @@ -121,7 +119,7 @@ export function machine(userContext: UserDefinedContext) { "focused:input": { tags: ["focused"], - entry: ["focusInput", "clearFocusedId"], + entry: ["focusInput", "clearHighlightedId"], activities: ["trackInteractOutside"], on: { TYPE: { @@ -149,12 +147,12 @@ export function machine(userContext: UserDefinedContext) { ARROW_LEFT: { guard: and("hasTags", "isInputCaretAtStart"), target: "navigating:tag", - actions: "focusLastTag", + actions: "highlightLastTag", }, BACKSPACE: { target: "navigating:tag", guard: and("hasTags", "isInputCaretAtStart"), - actions: "focusLastTag", + actions: "highlightLastTag", }, PASTE: { guard: "addOnPaste", @@ -169,20 +167,20 @@ export function machine(userContext: UserDefinedContext) { on: { ARROW_RIGHT: [ { - guard: and("hasTags", "isInputCaretAtStart", not("isLastTagFocused")), - actions: "focusNextTag", + guard: and("hasTags", "isInputCaretAtStart", not("isLastTagHighlighted")), + actions: "highlightNextTag", }, { target: "focused:input" }, ], ARROW_LEFT: { - actions: "focusPrevTag", + actions: "highlightPrevTag", }, BLUR: { target: "idle", - actions: "clearFocusedId", + actions: "clearHighlightedId", }, ENTER: { - guard: and("allowEditTag", "hasFocusedId"), + guard: and("allowEditTag", "hasHighlightedTag"), target: "editing:tag", actions: ["setEditedId", "initializeEditedTagValue", "focusEditedTagInput"], }, @@ -194,15 +192,15 @@ export function machine(userContext: UserDefinedContext) { }, BACKSPACE: [ { - guard: "isFirstTagFocused", - actions: ["deleteFocusedTag", "focusFirstTag"], + guard: "isFirstTagHighlighted", + actions: ["deleteHighlightedTag", "highlightFirstTag"], }, { - actions: ["deleteFocusedTag", "focusPrevTag"], + actions: ["deleteHighlightedTag", "highlightPrevTag"], }, ], DELETE: { - actions: ["deleteFocusedTag", "focusTagAtIndex"], + actions: ["deleteHighlightedTag", "highlightTagAtIndex"], }, }, }, @@ -217,22 +215,22 @@ export function machine(userContext: UserDefinedContext) { }, TAG_INPUT_ESCAPE: { target: "navigating:tag", - actions: ["clearEditedTagValue", "focusInput", "clearEditedId", "focusTagAtIndex"], + actions: ["clearEditedTagValue", "focusInput", "clearEditedId", "highlightTagAtIndex"], }, TAG_INPUT_BLUR: [ { guard: "isInputRelatedTarget", target: "navigating:tag", - actions: ["clearEditedTagValue", "clearFocusedId", "clearEditedId"], + actions: ["clearEditedTagValue", "clearHighlightedId", "clearEditedId"], }, { target: "idle", - actions: ["clearEditedTagValue", "clearFocusedId", "clearEditedId", "raiseExternalBlurEvent"], + actions: ["clearEditedTagValue", "clearHighlightedId", "clearEditedId", "raiseExternalBlurEvent"], }, ], TAG_INPUT_ENTER: { target: "navigating:tag", - actions: ["submitEditedTagValue", "focusInput", "clearEditedId", "focusTagAtIndex"], + actions: ["submitEditedTagValue", "focusInput", "clearEditedId", "highlightTagAtIndex"], }, }, }, @@ -242,10 +240,10 @@ export function machine(userContext: UserDefinedContext) { guards: { isInputRelatedTarget: (ctx, evt) => evt.relatedTarget === dom.getInputEl(ctx), isAtMax: (ctx) => ctx.isAtMax, - hasFocusedId: (ctx) => ctx.focusedId !== null, - isTagFocused: (ctx, evt) => ctx.focusedId === evt.id, - isFirstTagFocused: (ctx) => dom.getFirstEl(ctx)?.id === ctx.focusedId, - isLastTagFocused: (ctx) => dom.getLastEl(ctx)?.id === ctx.focusedId, + hasHighlightedTag: (ctx) => ctx.highlightedTagId !== null, + isTagHighlighted: (ctx, evt) => ctx.highlightedTagId === evt.id, + isFirstTagHighlighted: (ctx) => dom.getFirstEl(ctx)?.id === ctx.highlightedTagId, + isLastTagHighlighted: (ctx) => dom.getLastEl(ctx)?.id === ctx.highlightedTagId, isInputValueEmpty: (ctx) => ctx.trimmedInputValue.length === 0, hasTags: (ctx) => ctx.value.length > 0, allowOverflow: (ctx) => !!ctx.allowOverflow, @@ -295,6 +293,13 @@ export function machine(userContext: UserDefinedContext) { const input = dom.getTagInputEl(ctx, { value: ctx.editedTagValue, index: ctx.idx }) return autoResizeInput(input) }, + trackLiveRegion(ctx) { + ctx.liveRegion = createLiveRegion({ + level: "assertive", + document: dom.getDoc(ctx), + }) + return () => ctx.liveRegion?.destroy() + }, }, actions: { @@ -307,46 +312,40 @@ export function machine(userContext: UserDefinedContext) { dispatchChangeEvent(ctx) { dom.dispatchInputEvent(ctx) }, - setupLiveRegion(ctx) { - ctx.liveRegion = createLiveRegion({ - level: "assertive", - document: dom.getDoc(ctx), - }) - }, - focusNextTag(ctx) { - if (ctx.focusedId == null) return - const next = dom.getNextEl(ctx, ctx.focusedId) + highlightNextTag(ctx) { + if (ctx.highlightedTagId == null) return + const next = dom.getNextEl(ctx, ctx.highlightedTagId) if (next == null) return - set.focusedId(ctx, next.id) + set.highlightedId(ctx, next.id) }, - focusFirstTag(ctx) { + highlightFirstTag(ctx) { raf(() => { const first = dom.getFirstEl(ctx) if (first == null) return - set.focusedId(ctx, first.id) + set.highlightedId(ctx, first.id) }) }, - focusLastTag(ctx) { + highlightLastTag(ctx) { const last = dom.getLastEl(ctx) if (last == null) return - set.focusedId(ctx, last.id) + set.highlightedId(ctx, last.id) }, - focusPrevTag(ctx) { - if (ctx.focusedId == null) return - const prev = dom.getPrevEl(ctx, ctx.focusedId) - set.focusedId(ctx, prev?.id || null) + highlightPrevTag(ctx) { + if (ctx.highlightedTagId == null) return + const prev = dom.getPrevEl(ctx, ctx.highlightedTagId) + set.highlightedId(ctx, prev?.id || null) }, - focusTag(ctx, evt) { - set.focusedId(ctx, evt.id) + highlightTag(ctx, evt) { + set.highlightedId(ctx, evt.id) }, - focusTagAtIndex(ctx) { + highlightTagAtIndex(ctx) { raf(() => { if (ctx.idx == null) return const tagEl = dom.getTagElAtIndex(ctx, ctx.idx) if (tagEl == null) return - set.focusedId(ctx, tagEl.id) + set.highlightedId(ctx, tagEl.id) ctx.idx = undefined }) }, @@ -360,9 +359,9 @@ export function machine(userContext: UserDefinedContext) { set.value(ctx, removeAt(ctx.value, index)) }, - deleteFocusedTag(ctx) { - if (ctx.focusedId == null) return - const index = dom.getIndexOfId(ctx, ctx.focusedId) + deleteHighlightedTag(ctx) { + if (ctx.highlightedTagId == null) return + const index = dom.getIndexOfId(ctx, ctx.highlightedTagId) ctx.idx = index const value = ctx.value[index] @@ -373,7 +372,7 @@ export function machine(userContext: UserDefinedContext) { set.value(ctx, removeAt(ctx.value, index)) }, setEditedId(ctx, evt) { - ctx.editedTagId = evt.id ?? ctx.focusedId + ctx.editedTagId = evt.id ?? ctx.highlightedTagId ctx.idx = dom.getIndexOfId(ctx, ctx.editedTagId!) }, clearEditedId(ctx) { @@ -418,8 +417,8 @@ export function machine(userContext: UserDefinedContext) { setInputValue(ctx, evt) { ctx.inputValue = evt.value }, - clearFocusedId(ctx) { - ctx.focusedId = null + clearHighlightedId(ctx) { + ctx.highlightedTagId = null }, focusInput(ctx) { raf(() => { @@ -434,27 +433,27 @@ export function machine(userContext: UserDefinedContext) { dom.setValue(inputEl, ctx.inputValue) }, syncEditedTagInputValue(ctx, evt) { - const id = ctx.editedTagId || ctx.focusedId || evt.id + const id = ctx.editedTagId || ctx.highlightedTagId || evt.id if (id == null) return const editTagInputEl = dom.getById(ctx, `${id}:input`) dom.setValue(editTagInputEl, ctx.editedTagValue) }, addTag(ctx, evt) { const value = evt.value ?? ctx.trimmedInputValue - const guard = ctx.validate?.({ inputValue: value, values: ctx.value }) + const guard = ctx.validate?.({ inputValue: value, value: Array.from(ctx.value) }) if (guard) { set.value(ctx, ctx.value.concat(value)) // log ctx.log.prev = ctx.log.current ctx.log.current = { type: "add", value } } else { - ctx.onInvalid?.({ reason: "invalidTag" }) + ctx.onValueInvalid?.({ reason: "invalidTag" }) } }, addTagFromPaste(ctx) { raf(() => { const value = ctx.trimmedInputValue - const guard = ctx.validate?.({ inputValue: value, values: ctx.value }) + const guard = ctx.validate?.({ inputValue: value, value: Array.from(ctx.value) }) if (guard) { const trimmedValue = ctx.delimiter ? value.split(ctx.delimiter).map((v) => v.trim()) : [value] set.value(ctx, ctx.value.concat(...trimmedValue)) @@ -462,7 +461,7 @@ export function machine(userContext: UserDefinedContext) { ctx.log.prev = ctx.log.current ctx.log.current = { type: "paste", values: trimmedValue } } else { - ctx.onInvalid?.({ reason: "invalidTag" }) + ctx.onValueInvalid?.({ reason: "invalidTag" }) } ctx.inputValue = "" }) @@ -476,20 +475,17 @@ export function machine(userContext: UserDefinedContext) { setValue(ctx, evt) { set.value(ctx, evt.value) }, - removeLiveRegion(ctx) { - ctx.liveRegion?.destroy() - }, invokeOnInvalid(ctx) { if (ctx.isOverflowing) { - ctx.onInvalid?.({ reason: "rangeOverflow" }) + ctx.onValueInvalid?.({ reason: "rangeOverflow" }) } }, clearLog(ctx) { ctx.log = { prev: null, current: null } }, - logFocusedTag(ctx) { - if (ctx.focusedId == null) return - const index = dom.getIndexOfId(ctx, ctx.focusedId) + logHighlightedTag(ctx) { + if (ctx.highlightedTagId == null) return + const index = dom.getIndexOfId(ctx, ctx.highlightedTagId) // log ctx.log.prev = ctx.log.current @@ -537,12 +533,12 @@ export function machine(userContext: UserDefinedContext) { const invoke = { change: (ctx: MachineContext) => { - ctx.onChange?.({ values: Array.from(ctx.value) }) + ctx.onValueChange?.({ value: Array.from(ctx.value) }) dom.dispatchInputEvent(ctx) }, - focusChange: (ctx: MachineContext) => { - const value = dom.getFocusedTagValue(ctx) - ctx.onFocusChange?.({ value }) + highlightChange: (ctx: MachineContext) => { + const highlightedValue = dom.getHighlightedTagValue(ctx) + ctx.onHighlightChange?.({ highlightedValue }) }, } @@ -557,9 +553,9 @@ const set = { ctx.value[index] = value invoke.change(ctx) }, - focusedId: (ctx: MachineContext, id: string | null) => { - if (isEqual(ctx.focusedId, id)) return - ctx.focusedId = id - invoke.focusChange(ctx) + highlightedId: (ctx: MachineContext, id: string | null) => { + if (isEqual(ctx.highlightedTagId, id)) return + ctx.highlightedTagId = id + invoke.highlightChange(ctx) }, } diff --git a/packages/machines/tags-input/src/tags-input.types.ts b/packages/machines/tags-input/src/tags-input.types.ts index e98d1df558..c686164b7d 100644 --- a/packages/machines/tags-input/src/tags-input.types.ts +++ b/packages/machines/tags-input/src/tags-input.types.ts @@ -1,14 +1,41 @@ import type { StateMachine as S } from "@zag-js/core" import type { LiveRegion } from "@zag-js/live-region" import type { CommonProperties, Context, DirectionProperty, PropTypes, RequiredBy } from "@zag-js/types" -import type { InteractOutsideEvent, InteractOutsideHandlers } from "@zag-js/interact-outside" +import type { InteractOutsideHandlers } from "@zag-js/interact-outside" -type IntlTranslations = { +/* ----------------------------------------------------------------------------- + * Callback details + * -----------------------------------------------------------------------------*/ + +export interface ValueChangeDetails { + value: string[] +} + +export interface HighlightChangeDetails { + highlightedValue: string | null +} + +export type ValidityState = "rangeOverflow" | "invalidTag" + +export interface ValidityChangeDetails { + reason: ValidityState +} + +interface ValidateArgs { + inputValue: string + value: string[] +} + +/* ----------------------------------------------------------------------------- + * Machine context + * -----------------------------------------------------------------------------*/ + +interface IntlTranslations { clearTriggerLabel: string deleteTagTriggerLabel(value: string): string tagSelected(value: string): string tagAdded(value: string): string - tagsPasted(values: string[]): string + tagsPasted(value: string[]): string tagEdited(value: string): string tagUpdated(value: string): string tagDeleted(value: string): string @@ -33,106 +60,104 @@ type ElementIds = Partial<{ tagInput(opts: TagProps): string }> -type PublicContext = DirectionProperty & - CommonProperties & - InteractOutsideHandlers & { - /** - * The ids of the elements in the tags input. Useful for composition. - */ - ids?: ElementIds - /** - * Specifies the localized strings that identifies the accessibility elements and their states - */ - translations: IntlTranslations - /** - * The max length of the input. - */ - maxLength?: number - /** - * The character that serves has: - * - event key to trigger the addition of a new tag - * - character used to split tags when pasting into the input - * - * @default "," (aka COMMA) - */ - delimiter: string | null - /** - * Whether the input should be auto-focused - */ - autoFocus?: boolean - /** - * Whether the tags input should be disabled - */ - disabled?: boolean - /** - * Whether the tags input should be read-only - */ - readOnly?: boolean - /** - * Whether the tags input is invalid - */ - invalid?: boolean - /** - * Whether a tag can be edited after creation. - * If `true` and focus is on a tag, pressing `Enter`or double clicking will edit the tag. - */ - allowEditTag?: boolean - /** - * The tag input's value - */ - inputValue: string - /** - * The tag values - */ - value: string[] - /** - * Callback fired when the tag values is updated - */ - onChange?(details: { values: string[] }): void - /** - * Callback fired when a tag is focused by pointer or keyboard navigation - */ - onFocusChange?(details: { value: string | null }): void - /** - * Callback fired when the max tag count is reached or the `validateTag` function returns `false` - */ - onInvalid?(details: { reason: ValidityState }): void - /** - * Returns a boolean that determines whether a tag can be added. - * Useful for preventing duplicates or invalid tag values. - */ - validate?(details: { inputValue: string; values: string[] }): boolean - /** - * The behavior of the tags input when the input is blurred - * - `"add"`: add the input value as a new tag - * - `"none"`: do nothing - * - `"clear"`: clear the input value - * - * @default "none" - */ - blurBehavior?: "clear" | "add" - /** - * Whether to add a tag when you paste values into the tag input - */ - addOnPaste?: boolean - /** - * The max number of tags - */ - max: number - /** - * Whether to allow tags to exceed max. In this case, - * we'll attach `data-invalid` to the root - */ - allowOverflow?: boolean - /** - * The name attribute for the input. Useful for form submissions - */ - name?: string - /** - * The associate form of the underlying input element. - */ - form?: string - } +interface PublicContext extends DirectionProperty, CommonProperties, InteractOutsideHandlers { + /** + * The ids of the elements in the tags input. Useful for composition. + */ + ids?: ElementIds + /** + * Specifies the localized strings that identifies the accessibility elements and their states + */ + translations: IntlTranslations + /** + * The max length of the input. + */ + maxLength?: number + /** + * The character that serves has: + * - event key to trigger the addition of a new tag + * - character used to split tags when pasting into the input + * + * @default "," (aka COMMA) + */ + delimiter: string | null + /** + * Whether the input should be auto-focused + */ + autoFocus?: boolean + /** + * Whether the tags input should be disabled + */ + disabled?: boolean + /** + * Whether the tags input should be read-only + */ + readOnly?: boolean + /** + * Whether the tags input is invalid + */ + invalid?: boolean + /** + * Whether a tag can be edited after creation. + * If `true` and focus is on a tag, pressing `Enter`or double clicking will edit the tag. + */ + allowEditTag?: boolean + /** + * The tag input's value + */ + inputValue: string + /** + * The tag values + */ + value: string[] + /** + * Callback fired when the tag values is updated + */ + onValueChange?(details: ValueChangeDetails): void + /** + * Callback fired when a tag is highlighted by pointer or keyboard navigation + */ + onHighlightChange?(details: HighlightChangeDetails): void + /** + * Callback fired when the max tag count is reached or the `validateTag` function returns `false` + */ + onValueInvalid?(details: ValidityChangeDetails): void + /** + * Returns a boolean that determines whether a tag can be added. + * Useful for preventing duplicates or invalid tag values. + */ + validate?(details: ValidateArgs): boolean + /** + * The behavior of the tags input when the input is blurred + * - `"add"`: add the input value as a new tag + * - `"none"`: do nothing + * - `"clear"`: clear the input value + * + * @default "none" + */ + blurBehavior?: "clear" | "add" + /** + * Whether to add a tag when you paste values into the tag input + */ + addOnPaste?: boolean + /** + * The max number of tags + */ + max: number + /** + * Whether to allow tags to exceed max. In this case, + * we'll attach `data-invalid` to the root + */ + allowOverflow?: boolean + /** + * The name attribute for the input. Useful for form submissions + */ + name?: string + /** + * The associate form of the underlying input element. + */ + form?: string +} export type UserDefinedContext = RequiredBy @@ -187,9 +212,9 @@ type PrivateContext = Context<{ liveRegion: LiveRegion | null /** * @internal - * The `id` of the currently focused tag + * The `id` of the currently highlighted tag */ - focusedId: string | null + highlightedTagId: string | null /** * @internal * The index of the deleted tag. Used to determine the next tag to focus. @@ -212,9 +237,9 @@ type PrivateContext = Context<{ fieldsetDisabled: boolean }> -export type MachineContext = PublicContext & ComputedContext & PrivateContext +export interface MachineContext extends PublicContext, ComputedContext, PrivateContext {} -export type MachineState = { +export interface MachineState { value: "idle" | "navigating:tag" | "focused:input" | "editing:tag" tags: "focused" | "editing" } @@ -223,17 +248,24 @@ export type State = S.State export type Send = S.Send -export type ValidityState = "rangeOverflow" | "invalidTag" +/* ----------------------------------------------------------------------------- + * Component API + * -----------------------------------------------------------------------------*/ -export type TagProps = { +export interface TagProps { index: string | number value: string disabled?: boolean } -export type { InteractOutsideEvent } +export interface TagState { + id: string + isEditing: boolean + isHighlighted: boolean + isDisabled: boolean +} -export type MachineApi = { +export interface MachineApi { /** * Whether the tags are empty */ @@ -286,6 +318,11 @@ export type MachineApi = { * Function to focus the tags entry input. */ focus(): void + /** + * Returns the state of a tag + */ + getTagState(props: TagProps): TagState + rootProps: T["element"] labelProps: T["label"] controlProps: T["element"] diff --git a/packages/machines/toast/src/toast.types.ts b/packages/machines/toast/src/toast.types.ts index 607e757d56..3e1b9f3be8 100644 --- a/packages/machines/toast/src/toast.types.ts +++ b/packages/machines/toast/src/toast.types.ts @@ -13,23 +13,7 @@ export type Type = "success" | "error" | "loading" | "info" | "custom" export type Placement = "top-start" | "top" | "top-end" | "bottom-start" | "bottom" | "bottom-end" -type SharedContext = { - /** - * Whether to pause toast when the user leaves the browser tab - */ - pauseOnPageIdle?: boolean - /** - * Whether to pause the toast when interacted with - */ - pauseOnInteraction?: boolean - - /** - * The default options for the toast - */ - defaultOptions?: Partial> -} - -export type ToastOptions = { +export interface ToastOptions { /** * The unique id of the toast */ @@ -87,30 +71,51 @@ export type RenderOptions = Omit & { dismiss(): void } -export type MachineContext = SharedContext & - RootProperties & - CommonProperties & - Omit & { - /** - * The duration for the toast to kept alive before it is removed. - * Useful for exit transitions. - */ - removeDelay: number - /** - * The document's text/writing direction. - */ - dir?: Direction - /** - * The time the toast was created - */ - createdAt: number - /** - * The time left before the toast is removed - */ - remaining: number - } +/* ----------------------------------------------------------------------------- + * Machine context + * -----------------------------------------------------------------------------*/ + +interface SharedContext { + /** + * Whether to pause toast when the user leaves the browser tab + */ + pauseOnPageIdle?: boolean + /** + * Whether to pause the toast when interacted with + */ + pauseOnInteraction?: boolean + + /** + * The default options for the toast + */ + defaultOptions?: Partial> +} + +export interface MachineContext + extends SharedContext, + RootProperties, + CommonProperties, + Omit { + /** + * The duration for the toast to kept alive before it is removed. + * Useful for exit transitions. + */ + removeDelay: number + /** + * The document's text/writing direction. + */ + dir?: Direction + /** + * The time the toast was created + */ + createdAt: number + /** + * The time left before the toast is removed + */ + remaining: number +} -export type MachineState = { +export interface MachineState { value: "active" | "active:temp" | "dismissing" | "inactive" | "persist" tags: "visible" | "paused" | "updating" } @@ -121,26 +126,24 @@ export type Send = S.Send export type Service = Machine -type GroupPublicContext = SharedContext & - DirectionProperty & - CommonProperties & { - /** - * The gutter or spacing between toasts - */ - gutter: string - /** - * The z-index applied to each toast group - */ - zIndex: number - /** - * The maximum number of toasts that can be shown at once - */ - max: number - /** - * The offset from the safe environment edge of the viewport - */ - offsets: string | Record<"left" | "right" | "bottom" | "top", string> - } +interface GroupPublicContext extends SharedContext, DirectionProperty, CommonProperties { + /** + * The gutter or spacing between toasts + */ + gutter: string + /** + * The z-index applied to each toast group + */ + zIndex: number + /** + * The maximum number of toasts that can be shown at once + */ + max: number + /** + * The offset from the safe environment edge of the viewport + */ + offsets: string | Record<"left" | "right" | "bottom" | "top", string> +} export type UserDefinedGroupContext = RequiredBy @@ -160,26 +163,30 @@ type GroupPrivateContext = Context<{ toasts: Service[] }> -export type GroupMachineContext = GroupPublicContext & GroupComputedContext & GroupPrivateContext +export interface GroupMachineContext extends GroupPublicContext, GroupComputedContext, GroupPrivateContext {} export type GroupState = S.State -export type GroupSend = (event: S.Event) => void +export type GroupSend = S.Send + +/* ----------------------------------------------------------------------------- + * Component API + * -----------------------------------------------------------------------------*/ type MaybeFunction = Value | ((arg: Args) => Value) -export type PromiseOptions = { +export interface PromiseOptions { loading: ToastOptions success: MaybeFunction error: MaybeFunction } -export type GroupProps = { +export interface GroupProps { placement: Placement label?: string } -export type GroupMachineApi = { +export interface GroupMachineApi { /** * The total number of toasts */ @@ -255,7 +262,7 @@ export type GroupMachineApi = { getGroupProps(options: GroupProps): T["element"] } -export type MachineApi = { +export interface MachineApi { /** * The type of the toast. */ diff --git a/packages/machines/toggle-group/src/toggle-group.machine.ts b/packages/machines/toggle-group/src/toggle-group.machine.ts index 40f7e9dcee..bc194de0d4 100644 --- a/packages/machines/toggle-group/src/toggle-group.machine.ts +++ b/packages/machines/toggle-group/src/toggle-group.machine.ts @@ -159,7 +159,7 @@ export function machine(userContext: UserDefinedContext) { const invoke = { change(ctx: MachineContext) { - ctx.onChange?.({ value: Array.from(ctx.value) }) + ctx.onValueChange?.({ value: Array.from(ctx.value) }) }, } diff --git a/packages/machines/toggle-group/src/toggle-group.types.ts b/packages/machines/toggle-group/src/toggle-group.types.ts index 46fe2a4874..a890a94c14 100644 --- a/packages/machines/toggle-group/src/toggle-group.types.ts +++ b/packages/machines/toggle-group/src/toggle-group.types.ts @@ -1,46 +1,57 @@ import type { StateMachine as S } from "@zag-js/core" import type { CommonProperties, Context, DirectionProperty, Orientation, PropTypes, RequiredBy } from "@zag-js/types" +/* ----------------------------------------------------------------------------- + * Callback details + * -----------------------------------------------------------------------------*/ + +export interface ValueChangeDetails { + value: string[] +} + +/* ----------------------------------------------------------------------------- + * Machine context + * -----------------------------------------------------------------------------*/ + type ElementIds = Partial<{ root: string toggle(value: string): string }> -type PublicContext = DirectionProperty & - CommonProperties & { - /** - * The ids of the elements in the toggle. Useful for composition. - */ - ids?: ElementIds - /** - * Whether the toggle is disabled. - */ - disabled?: boolean - /** - * The values of the toggles in the group. - */ - value: string[] - /** - * Function to call when the toggle is clicked. - */ - onChange?: (details: { value: string[] }) => void - /** - * Whether to loop focus inside the toggle group. - */ - loop: boolean - /** - * Whether to use roving tab index to manage focus. - */ - rovingFocus?: boolean - /** - * The orientation of the toggle group. - */ - orientation: Orientation - /** - * Whether to allow multiple toggles to be selected. - */ - multiple?: boolean - } +interface PublicContext extends DirectionProperty, CommonProperties { + /** + * The ids of the elements in the toggle. Useful for composition. + */ + ids?: ElementIds + /** + * Whether the toggle is disabled. + */ + disabled?: boolean + /** + * The values of the toggles in the group. + */ + value: string[] + /** + * Function to call when the toggle is clicked. + */ + onValueChange?: (details: ValueChangeDetails) => void + /** + * Whether to loop focus inside the toggle group. + */ + loop: boolean + /** + * Whether to use roving tab index to manage focus. + */ + rovingFocus?: boolean + /** + * The orientation of the toggle group. + */ + orientation: Orientation + /** + * Whether to allow multiple toggles to be selected. + */ + multiple?: boolean +} export type UserDefinedContext = RequiredBy @@ -50,31 +61,36 @@ type ComputedContext = Readonly<{ type PrivateContext = Context<{ /** + * @internal * Whether the user is tabbing backward. */ isTabbingBackward: boolean /** + * @internal * Whether the toggle group has focusable toggles. */ hasFocusableToggle: boolean /** + * @internal * Whether the toggle was focused by a click. */ isClickFocus: boolean /** + * @internal * The value of the toggle that was focused. */ focusedId: string | null /** + * @internal * Whether the toggle group is within a toolbar. * This is used to determine whether to use roving tab index. */ isWithinToolbar: boolean }> -export type MachineContext = PublicContext & PrivateContext & ComputedContext +export interface MachineContext extends PublicContext, PrivateContext, ComputedContext {} -export type MachineState = { +export interface MachineState { value: "idle" | "focused" } @@ -82,12 +98,16 @@ export type State = S.State export type Send = S.Send -export type ToggleProps = { +/* ----------------------------------------------------------------------------- + * Component API + * -----------------------------------------------------------------------------*/ + +export interface ToggleProps { value: string disabled?: boolean } -export type MachineApi = { +export interface MachineApi { /** * The value of the toggle group. */ diff --git a/packages/machines/tooltip/src/tooltip.machine.ts b/packages/machines/tooltip/src/tooltip.machine.ts index 0984999b25..4863a7a7d1 100644 --- a/packages/machines/tooltip/src/tooltip.machine.ts +++ b/packages/machines/tooltip/src/tooltip.machine.ts @@ -222,11 +222,11 @@ export function machine(userContext: UserDefinedContext) { invokeOnOpen(ctx, evt) { const omit = ["CONTENT.POINTER_MOVE", "POINTER_MOVE"] if (!omit.includes(evt.type)) { - ctx.onOpen?.() + ctx.onOpenChange?.({ open: true }) } }, invokeOnClose(ctx) { - ctx.onClose?.() + ctx.onOpenChange?.({ open: false }) }, closeIfDisabled(ctx, _evt, { send }) { if (!ctx.disabled) return diff --git a/packages/machines/tooltip/src/tooltip.types.ts b/packages/machines/tooltip/src/tooltip.types.ts index 318f2df80e..ecc1ec9ca1 100644 --- a/packages/machines/tooltip/src/tooltip.types.ts +++ b/packages/machines/tooltip/src/tooltip.types.ts @@ -2,6 +2,18 @@ import type { StateMachine as S } from "@zag-js/core" import type { Placement, PositioningOptions } from "@zag-js/popper" import type { CommonProperties, PropTypes, RequiredBy, RootProperties } from "@zag-js/types" +/* ----------------------------------------------------------------------------- + * Callback details + * -----------------------------------------------------------------------------*/ + +export interface OpenChangeDetails { + open: boolean +} + +/* ----------------------------------------------------------------------------- + * Machine context + * -----------------------------------------------------------------------------*/ + type ElementIds = Partial<{ trigger: string content: string @@ -9,7 +21,7 @@ type ElementIds = Partial<{ positioner: string }> -type PublicContext = CommonProperties & { +interface PublicContext extends CommonProperties { /** * The ids of the elements in the tooltip. Useful for composition. */ @@ -43,11 +55,7 @@ type PublicContext = CommonProperties & { /** * Function called when the tooltip is opened. */ - onOpen?: VoidFunction - /** - * Function called when the tooltip is closed. - */ - onClose?: VoidFunction + onOpenChange?(details: OpenChangeDetails): void /** * Custom label for the tooltip. */ @@ -88,9 +96,9 @@ type PrivateContext = RootProperties & { hasPointerMoveOpened?: boolean } -export type MachineContext = PublicContext & ComputedContext & PrivateContext +export interface MachineContext extends PublicContext, ComputedContext, PrivateContext {} -export type MachineState = { +export interface MachineState { value: "opening" | "open" | "closing" | "closed" tags: "open" | "closed" } @@ -99,9 +107,11 @@ export type State = S.State export type Send = S.Send -export type { PositioningOptions, Placement } +/* ----------------------------------------------------------------------------- + * Component API + * -----------------------------------------------------------------------------*/ -export type MachineApi = { +export interface MachineApi { /** * Whether the tooltip is open. */ @@ -118,9 +128,16 @@ export type MachineApi = { * Function to reposition the popover */ setPositioning(options?: Partial): void + triggerProps: T["button"] arrowProps: T["element"] arrowTipProps: T["element"] positionerProps: T["element"] contentProps: T["element"] } + +/* ----------------------------------------------------------------------------- + * Re-exported types + * -----------------------------------------------------------------------------*/ + +export type { Placement, PositioningOptions } diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index e15ef57717..7b55795616 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -8,7 +8,15 @@ export type Orientation = "horizontal" | "vertical" export type MaybeElement = T | null -export type DirectionProperty = { +export interface OrientationProperty { + /** + * The orientation of the element. + * @default "horizontal" + */ + orientation?: Orientation +} + +export interface DirectionProperty { /** * The document's text/writing direction. * @default "ltr" @@ -16,7 +24,7 @@ export type DirectionProperty = { dir?: "ltr" | "rtl" } -export type CommonProperties = { +export interface CommonProperties { /** * The unique identifier of the machine. */ @@ -27,7 +35,7 @@ export type CommonProperties = { getRootNode?: () => ShadowRoot | Document | Node } -export type RootProperties = { +export interface RootProperties { /** * The owner document of the machine. */ diff --git a/packages/utilities/aria-hidden/src/index.ts b/packages/utilities/aria-hidden/src/index.ts index 7cfd915dbc..451a3e9683 100644 --- a/packages/utilities/aria-hidden/src/index.ts +++ b/packages/utilities/aria-hidden/src/index.ts @@ -4,7 +4,7 @@ import { raf } from "@zag-js/dom-query" const refCountMap = new WeakMap() const observerStack: any[] = [] -export type AriaHiddenOptions = { +export interface AriaHiddenOptions { rootEl?: HTMLElement defer?: boolean } diff --git a/packages/utilities/collection/src/types.ts b/packages/utilities/collection/src/types.ts index 878331f91c..eac6193986 100644 --- a/packages/utilities/collection/src/types.ts +++ b/packages/utilities/collection/src/types.ts @@ -1,9 +1,9 @@ -export type CollectionSearchState = { +export interface CollectionSearchState { keysSoFar: string timer: number } -export type CollectionSearchOptions = { +export interface CollectionSearchOptions { state: CollectionSearchState currentValue: string | null timeout?: number @@ -11,7 +11,7 @@ export type CollectionSearchOptions = { export type CollectionItem = any -export type CollectionNode = { +export interface CollectionNode { item: T index: number label: string @@ -20,7 +20,7 @@ export type CollectionNode = { nextValue: string | null } -export type CollectionOptions = { +export interface CollectionOptions { /** * The options of the select */ diff --git a/packages/utilities/dismissable/src/dismissable-layer.ts b/packages/utilities/dismissable/src/dismissable-layer.ts index ed149ab404..5591d98332 100644 --- a/packages/utilities/dismissable/src/dismissable-layer.ts +++ b/packages/utilities/dismissable/src/dismissable-layer.ts @@ -14,11 +14,14 @@ type MaybeElement = HTMLElement | null type Container = MaybeElement | Array type NodeOrFn = MaybeElement | (() => MaybeElement) -export type DismissableElementHandlers = InteractOutsideHandlers & { +export interface DismissableElementHandlers extends InteractOutsideHandlers { + /** + * Function called when the escape key is pressed + */ onEscapeKeyDown?: (event: KeyboardEvent) => void } -export type DismissableElementOptions = DismissableElementHandlers & { +export interface DismissableElementOptions extends DismissableElementHandlers { debug?: boolean pointerBlocking?: boolean onDismiss: () => void diff --git a/packages/utilities/dom-event/src/types.ts b/packages/utilities/dom-event/src/types.ts index 87fae8434c..7454662cd2 100644 --- a/packages/utilities/dom-event/src/types.ts +++ b/packages/utilities/dom-event/src/types.ts @@ -22,7 +22,7 @@ export type EventKeyMap = { [key in EventKey]?: (event: JSX.KeyboardEvent) => void } -export type EventKeyOptions = { +export interface EventKeyOptions { dir?: "ltr" | "rtl" orientation?: "horizontal" | "vertical" } diff --git a/packages/utilities/dom-query/src/get-by-typeahead.ts b/packages/utilities/dom-query/src/get-by-typeahead.ts index 0bdc9e0cb1..984bde8354 100644 --- a/packages/utilities/dom-query/src/get-by-typeahead.ts +++ b/packages/utilities/dom-query/src/get-by-typeahead.ts @@ -1,11 +1,11 @@ import { getByText } from "./get-by-text" -export type TypeaheadState = { +export interface TypeaheadState { keysSoFar: string timer: number } -export type TypeaheadOptions = { +export interface TypeaheadOptions { state: TypeaheadState activeId: string | null key: string diff --git a/packages/utilities/element-rect/src/index.ts b/packages/utilities/element-rect/src/index.ts index 37bc49e464..279c4f50e5 100644 --- a/packages/utilities/element-rect/src/index.ts +++ b/packages/utilities/element-rect/src/index.ts @@ -1,13 +1,13 @@ type Fn = (rect: Rect) => void -type Rect = { +interface Rect { top: number left: number width: number height: number } -type ObservedData = { +interface ObservedData { rect: Rect callbacks: Fn[] } diff --git a/packages/utilities/element-size/src/track-size.ts b/packages/utilities/element-size/src/track-size.ts index 17c9daec76..9b0cba2391 100644 --- a/packages/utilities/element-size/src/track-size.ts +++ b/packages/utilities/element-size/src/track-size.ts @@ -1,4 +1,4 @@ -export type ElementSize = { +export interface ElementSize { width: number height: number } diff --git a/packages/utilities/element-size/src/track-sizes.ts b/packages/utilities/element-size/src/track-sizes.ts index cd68c35c9f..cf0dffaddd 100644 --- a/packages/utilities/element-size/src/track-sizes.ts +++ b/packages/utilities/element-size/src/track-sizes.ts @@ -1,6 +1,6 @@ import { trackElementSize, type ElementSize } from "./track-size" -export type TrackElementsSizeOptions = { +export interface TrackElementsSizeOptions { getNodes: () => T[] observeMutation?: boolean callback: (size: ElementSize | undefined, index: number) => void diff --git a/packages/utilities/file-utils/README.md b/packages/utilities/file-utils/README.md new file mode 100644 index 0000000000..96f4e184e3 --- /dev/null +++ b/packages/utilities/file-utils/README.md @@ -0,0 +1,19 @@ +# @zag-js/file-utils + +JS File API utilities + +## Installation + +```sh +yarn add @zag-js/file-utils +# or +npm i @zag-js/file-utils +``` + +## Contribution + +Yes please! See the [contributing guidelines](https://github.com/chakra-ui/zag/blob/main/CONTRIBUTING.md) for details. + +## Licence + +This project is licensed under the terms of the [MIT license](https://github.com/chakra-ui/zag/blob/main/LICENSE). diff --git a/packages/utilities/file-utils/package.json b/packages/utilities/file-utils/package.json new file mode 100644 index 0000000000..c44feca9e4 --- /dev/null +++ b/packages/utilities/file-utils/package.json @@ -0,0 +1,36 @@ +{ + "name": "@zag-js/file-utils", + "version": "0.0.0", + "description": "JS File API utilities", + "keywords": [ + "js", + "utils", + "file-utils" + ], + "author": "Segun Adebayo ", + "homepage": "https://github.com/chakra-ui/zag#readme", + "license": "MIT", + "main": "src/index.ts", + "repository": "https://github.com/chakra-ui/zag/tree/main/packages/utilities/file-utils", + "sideEffects": false, + "files": [ + "dist/**/*" + ], + "scripts": { + "build": "tsup", + "lint": "eslint src --ext .ts,.tsx", + "typecheck": "tsc --noEmit", + "prepack": "clean-package", + "postpack": "clean-package restore" + }, + "publishConfig": { + "access": "public" + }, + "bugs": { + "url": "https://github.com/chakra-ui/zag/issues" + }, + "clean-package": "../../../clean-package.config.json", + "devDependencies": { + "clean-package": "2.2.0" + } +} diff --git a/packages/utilities/file-utils/src/format-bytes.ts b/packages/utilities/file-utils/src/format-bytes.ts new file mode 100644 index 0000000000..e24fb52ab2 --- /dev/null +++ b/packages/utilities/file-utils/src/format-bytes.ts @@ -0,0 +1,11 @@ +const SIZES = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"] +const KILO = 1024 + +export const formatBytes = (bytes: number, decimals = 2) => { + if (bytes === 0) { + return "0 Bytes" + } + const dm = decimals <= 0 ? 0 : decimals || 2 + const i = Math.floor(Math.log(bytes) / Math.log(KILO)) + return parseFloat((bytes / Math.pow(KILO, i)).toFixed(dm)) + " " + SIZES[i] +} diff --git a/packages/utilities/file-utils/src/get-file-data-url.ts b/packages/utilities/file-utils/src/get-file-data-url.ts new file mode 100644 index 0000000000..3e38d45249 --- /dev/null +++ b/packages/utilities/file-utils/src/get-file-data-url.ts @@ -0,0 +1,20 @@ +export const getFileDataUrl = async (file: File | Blob) => { + const reader = new FileReader() + return new Promise((resolve, reject) => { + reader.onerror = () => { + reader.abort() + reject(new Error("There was an error reading a file")) + } + + reader.onloadend = () => { + const { result } = reader + if (result instanceof ArrayBuffer) { + reject(new Error("Expected DataURL as string from Blob/File, got ArrayBuffer")) + } else { + resolve(result || undefined) + } + } + + reader.readAsDataURL(file) + }) +} diff --git a/packages/utilities/file-utils/src/get-total-file-size.ts b/packages/utilities/file-utils/src/get-total-file-size.ts new file mode 100644 index 0000000000..e683fb0487 --- /dev/null +++ b/packages/utilities/file-utils/src/get-total-file-size.ts @@ -0,0 +1,3 @@ +export const getTotalFileSize = (files: File[]) => { + return files.reduce((acc, file) => acc + file.size, 0) +} diff --git a/packages/utilities/file-utils/src/index.ts b/packages/utilities/file-utils/src/index.ts new file mode 100644 index 0000000000..db04c2b77a --- /dev/null +++ b/packages/utilities/file-utils/src/index.ts @@ -0,0 +1,4 @@ +export * from "./format-bytes" +export * from "./get-file-data-url" +export * from "./get-total-file-size" +export * from "./is-file-equal" diff --git a/packages/utilities/file-utils/src/is-file-equal.ts b/packages/utilities/file-utils/src/is-file-equal.ts new file mode 100644 index 0000000000..d0864ca9aa --- /dev/null +++ b/packages/utilities/file-utils/src/is-file-equal.ts @@ -0,0 +1,3 @@ +export const isFileEqual = (file1: File, file2: File) => { + return file1.name === file2.name && file1.size === file2.size && file1.type === file2.type +} diff --git a/packages/utilities/file-utils/tsconfig.json b/packages/utilities/file-utils/tsconfig.json new file mode 100644 index 0000000000..8e781cd154 --- /dev/null +++ b/packages/utilities/file-utils/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../../tsconfig.json", + "include": ["src"], + "compilerOptions": { + "tsBuildInfoFile": "node_modules/.cache/.tsbuildinfo" + } +} diff --git a/packages/utilities/focus-scope/src/focus-containment.ts b/packages/utilities/focus-scope/src/focus-containment.ts index c4c958ef2a..15191df70e 100644 --- a/packages/utilities/focus-scope/src/focus-containment.ts +++ b/packages/utilities/focus-scope/src/focus-containment.ts @@ -2,7 +2,7 @@ import { addDomEvent } from "@zag-js/dom-event" import { setVisuallyHidden } from "@zag-js/visually-hidden" import type { FocusContext } from "./focus-context" -export type FocusContainmentOptions = { +export interface FocusContainmentOptions { loop?: boolean } diff --git a/packages/utilities/focus-scope/src/focus-move.ts b/packages/utilities/focus-scope/src/focus-move.ts index 04b1f41760..7266c21e0c 100644 --- a/packages/utilities/focus-scope/src/focus-move.ts +++ b/packages/utilities/focus-scope/src/focus-move.ts @@ -2,7 +2,7 @@ import { getActiveElement } from "@zag-js/dom-query" import type { FocusContext } from "./focus-context" import { focusScopeStack } from "./focus-stack" -export type FocusMoveEffectOptions = { +export interface FocusMoveEffectOptions { onMountAutoFocus?: EventListener onUnmountAutoFocus?: EventListener } diff --git a/packages/utilities/focus-scope/src/focus-stack.ts b/packages/utilities/focus-scope/src/focus-stack.ts index 3660e352d9..7ce2655a66 100644 --- a/packages/utilities/focus-scope/src/focus-stack.ts +++ b/packages/utilities/focus-scope/src/focus-stack.ts @@ -1,6 +1,6 @@ import { remove } from "@zag-js/utils" -export type Scope = { +export interface Scope { node: HTMLElement paused: boolean pause(): void diff --git a/packages/utilities/focus-scope/src/index.ts b/packages/utilities/focus-scope/src/index.ts index 581c17afcb..dd8ae09b96 100644 --- a/packages/utilities/focus-scope/src/index.ts +++ b/packages/utilities/focus-scope/src/index.ts @@ -5,11 +5,10 @@ import { focusMoveEffect, type FocusMoveEffectOptions } from "./focus-move" import { focusOnChildUnmountEffect } from "./focus-on-child-unmount" import { focusOutsideEffect } from "./focus-outside" -export type TrapFocusOptions = FocusMoveEffectOptions & - FocusContainmentOptions & { - contain?: boolean - guards?: boolean - } +export interface TrapFocusOptions extends FocusMoveEffectOptions, FocusContainmentOptions { + contain?: boolean + guards?: boolean +} export function trapFocus(node: HTMLElement | null, options: TrapFocusOptions = {}) { if (!node) return diff --git a/packages/utilities/form-utils/src/input-event.ts b/packages/utilities/form-utils/src/input-event.ts index 59ae133dea..1a7606f6f8 100644 --- a/packages/utilities/form-utils/src/input-event.ts +++ b/packages/utilities/form-utils/src/input-event.ts @@ -1,4 +1,4 @@ -type DescriptorOptions = { +interface DescriptorOptions { type?: "HTMLInputElement" | "HTMLTextAreaElement" | "HTMLSelectElement" property?: "value" | "checked" } diff --git a/packages/utilities/interact-outside/src/index.ts b/packages/utilities/interact-outside/src/index.ts index 9acc380194..f989a0f3b0 100644 --- a/packages/utilities/interact-outside/src/index.ts +++ b/packages/utilities/interact-outside/src/index.ts @@ -4,18 +4,27 @@ import { isFocusable } from "@zag-js/tabbable" import { callAll } from "@zag-js/utils" import { getWindowFrames } from "./get-window-frames" -export type InteractOutsideHandlers = { +export interface InteractOutsideHandlers { + /** + * Function called when the pointer is pressed down outside the combobox + */ onPointerDownOutside?: (event: PointerDownOutsideEvent) => void + /** + * Function called when the focus is moved outside the combobox + */ onFocusOutside?: (event: FocusOutsideEvent) => void + /** + * Function called when an interaction happens outside the combobox + */ onInteractOutside?: (event: InteractOutsideEvent) => void } -export type InteractOutsideOptions = InteractOutsideHandlers & { +export interface InteractOutsideOptions extends InteractOutsideHandlers { exclude?: (target: HTMLElement) => boolean defer?: boolean } -export type EventDetails = { +export interface EventDetails { originalEvent: T contextmenu: boolean focusable: boolean diff --git a/packages/utilities/live-region/src/index.ts b/packages/utilities/live-region/src/index.ts index a12fd56df6..a07cc31f85 100644 --- a/packages/utilities/live-region/src/index.ts +++ b/packages/utilities/live-region/src/index.ts @@ -1,6 +1,6 @@ import { setVisuallyHidden } from "@zag-js/visually-hidden" -export type LiveRegionOptions = { +export interface LiveRegionOptions { level: "polite" | "assertive" document?: Document root?: HTMLElement | null diff --git a/packages/utilities/popper/src/auto-update.ts b/packages/utilities/popper/src/auto-update.ts index a13eda1173..8171f994aa 100644 --- a/packages/utilities/popper/src/auto-update.ts +++ b/packages/utilities/popper/src/auto-update.ts @@ -4,7 +4,7 @@ import { trackElementRect } from "@zag-js/element-rect" export type { Placement } -export type AutoUpdateOptions = { +export interface AutoUpdateOptions { ancestorScroll?: boolean ancestorResize?: boolean referenceResize?: boolean diff --git a/packages/utilities/popper/src/get-styles.ts b/packages/utilities/popper/src/get-styles.ts index 5e918630f1..a09a497d29 100644 --- a/packages/utilities/popper/src/get-styles.ts +++ b/packages/utilities/popper/src/get-styles.ts @@ -2,7 +2,7 @@ import type { Placement } from "@floating-ui/dom" import { cssVars } from "./middleware" import type { PositioningOptions } from "./types" -export type GetPlacementStylesOptions = { +export interface GetPlacementStylesOptions { placement?: Placement } diff --git a/packages/utilities/popper/src/types.ts b/packages/utilities/popper/src/types.ts index 6ece017f1c..edd08d8dac 100644 --- a/packages/utilities/popper/src/types.ts +++ b/packages/utilities/popper/src/types.ts @@ -3,7 +3,7 @@ import type { AutoUpdateOptions } from "./auto-update" export type { Placement, Boundary, ComputePositionReturn, AutoUpdateOptions } -export type PositioningOptions = { +export interface PositioningOptions { /** * The strategy to use for positioning */ diff --git a/packages/utilities/tabbable/src/proxy-tab-focus.ts b/packages/utilities/tabbable/src/proxy-tab-focus.ts index d41f1476bd..b0972fabba 100644 --- a/packages/utilities/tabbable/src/proxy-tab-focus.ts +++ b/packages/utilities/tabbable/src/proxy-tab-focus.ts @@ -4,7 +4,7 @@ import { getNextTabbable, getTabbableEdges } from "./tabbable" type MaybeElement = HTMLElement | null type NodeOrFn = MaybeElement | (() => MaybeElement) -type ProxyTabFocusOptions = { +interface ProxyTabFocusOptions { triggerElement?: T onFocus?: (elementToFocus: HTMLElement) => void defer?: boolean diff --git a/packages/utilities/text-selection/src/index.ts b/packages/utilities/text-selection/src/index.ts index b3ee1d0e18..2b90beed3e 100644 --- a/packages/utilities/text-selection/src/index.ts +++ b/packages/utilities/text-selection/src/index.ts @@ -6,7 +6,7 @@ let state: State = "default" let userSelect = "" const elementMap = new WeakMap() -export type DisableTextSelectionOptions = { +export interface DisableTextSelectionOptions { target?: T doc?: Document defer?: boolean diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0f8ca89bcc..26f411c970 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -209,6 +209,9 @@ importers: '@zag-js/file-upload': specifier: workspace:* version: link:../../packages/machines/file-upload + '@zag-js/file-utils': + specifier: workspace:* + version: link:../../packages/utilities/file-utils '@zag-js/focus-scope': specifier: workspace:* version: link:../../packages/utilities/focus-scope @@ -441,6 +444,9 @@ importers: '@zag-js/file-upload': specifier: workspace:* version: link:../../packages/machines/file-upload + '@zag-js/file-utils': + specifier: workspace:* + version: link:../../packages/utilities/file-utils '@zag-js/focus-scope': specifier: workspace:* version: link:../../packages/utilities/focus-scope @@ -649,6 +655,9 @@ importers: '@zag-js/file-upload': specifier: workspace:* version: link:../../packages/machines/file-upload + '@zag-js/file-utils': + specifier: workspace:* + version: link:../../packages/utilities/file-utils '@zag-js/focus-scope': specifier: workspace:* version: link:../../packages/utilities/focus-scope @@ -866,6 +875,9 @@ importers: '@zag-js/file-upload': specifier: workspace:* version: link:../../packages/machines/file-upload + '@zag-js/file-utils': + specifier: workspace:* + version: link:../../packages/utilities/file-utils '@zag-js/focus-scope': specifier: workspace:* version: link:../../packages/utilities/focus-scope @@ -1077,6 +1089,9 @@ importers: '@zag-js/file-upload': specifier: workspace:* version: link:../../packages/machines/file-upload + '@zag-js/file-utils': + specifier: workspace:* + version: link:../../packages/utilities/file-utils '@zag-js/focus-scope': specifier: workspace:* version: link:../../packages/utilities/focus-scope @@ -2416,6 +2431,12 @@ importers: specifier: 2.2.0 version: 2.2.0 + packages/utilities/file-utils: + devDependencies: + clean-package: + specifier: 2.2.0 + version: 2.2.0 + packages/utilities/focus-scope: dependencies: '@zag-js/dom-event': @@ -2801,6 +2822,7 @@ packages: dependencies: '@babel/highlight': 7.22.13 chalk: 2.4.2 + dev: true /@babel/code-frame@7.22.13: resolution: {integrity: sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==} diff --git a/scripts/typedocs.ts b/scripts/typedocs.ts index 08bf0235ff..d20ad67c22 100644 --- a/scripts/typedocs.ts +++ b/scripts/typedocs.ts @@ -19,12 +19,12 @@ function getDescription(property: Symbol, typeChecker?: TypeChecker) { return description?.text } -function getContextReturnType(sourceFile: SourceFile | undefined, typeAlias: string, typeChecker?: TypeChecker) { +function getContextReturnType(sourceFile: SourceFile | undefined, typeName: string, typeChecker?: TypeChecker) { if (!sourceFile) return {} const result: Record = {} - const contextType = sourceFile.getTypeAlias(typeAlias)?.getType() + const contextType = sourceFile.getInterface(typeName)?.getType() contextType?.getProperties().forEach((property) => { const name = property.getName() @@ -44,7 +44,7 @@ function getContextReturnType(sourceFile: SourceFile | undefined, typeAlias: str async function main() { const project = new Project({ compilerOptions: { - moduleResolution: ModuleResolutionKind.NodeJs, + moduleResolution: ModuleResolutionKind.NodeNext, }, }) @@ -64,10 +64,14 @@ async function main() { const contextSrc = project.getSourceFile(typesFilePath) - const publicContext = getContextReturnType(contextSrc, "PublicContext", typeChecker) - const publicApi = getContextReturnType(contextSrc, "MachineApi", typeChecker) + try { + const publicContext = getContextReturnType(contextSrc, "PublicContext", typeChecker) + const publicApi = getContextReturnType(contextSrc, "MachineApi", typeChecker) - result[baseDir] = { api: publicApi, context: publicContext } + result[baseDir] = { api: publicApi, context: publicContext } + } catch (error) { + console.error("failed --------->", dir) + } } const outPath = join(process.cwd(), "packages", "docs", "api.json") diff --git a/website/components/machines/combobox.tsx b/website/components/machines/combobox.tsx index ad2933e194..fac3e25929 100644 --- a/website/components/machines/combobox.tsx +++ b/website/components/machines/combobox.tsx @@ -72,10 +72,11 @@ export function Combobox(props: ComboboxProps) { combobox.machine({ id: useId(), collection, - onOpen() { + onOpenChange(details) { + if (!details.open) return setOptions(comboboxData) }, - onInputChange({ value }) { + onInputValueChange({ value }) { const filtered = comboboxData.filter((item) => item.label.toLowerCase().includes(value.toLowerCase()), ) diff --git a/website/data/components/accordion.mdx b/website/data/components/accordion.mdx index 18557add11..5b37ece7e7 100644 --- a/website/data/components/accordion.mdx +++ b/website/data/components/accordion.mdx @@ -127,6 +127,21 @@ const [state, send] = useMachine( ) ``` +## Listening for changes + +When the accordion value changes, the `onValueChange` callback is invoked. + +```jsx {3-6} +const [state, send] = useMachine( + accordion.machine({ + onValueChange(details) { + // details => { value: string[] } + console.log("selected accordion:", details.value) + }, + }), +) +``` + ## Disabling specific accordion items To disable a specific accordion, pass the `disabled: true` property to the diff --git a/website/data/components/avatar.mdx b/website/data/components/avatar.mdx index 8761a79566..15dcd228a2 100644 --- a/website/data/components/avatar.mdx +++ b/website/data/components/avatar.mdx @@ -64,20 +64,16 @@ avatar machine in your project 🔥 -## Listening to loading status changes +## Listening for loading status changes -The avatar machine provides a `onLoad` and `onError` event that you can listen -to. This is useful when you want to perform an action when the image loads or -fails to load. +When the image has loaded or failed to load, the `onLoadingStatusChange` +callback is invoked. ```jsx {3} const [state, send] = useMachine( avatar.machine({ - onLoad() { - // Do something when the image loads - }, - onError() { - // Do something when the image fails to load + onLoadingStatusChange(details) { + // details => { status: "error" | "loaded" } }, }), ) diff --git a/website/data/components/checkbox.mdx b/website/data/components/checkbox.mdx index 3ba72d04e1..1ac7918965 100644 --- a/website/data/components/checkbox.mdx +++ b/website/data/components/checkbox.mdx @@ -106,14 +106,14 @@ const [state, send] = useMachine( ## Listening for changes -When the checkbox value changes, the `onChange` callback is invoked. +When the checkbox value changes, the `onCheckChange` callback is invoked. ```jsx {3-6} const [state, send] = useMachine( checkbox.machine({ - onChange({ checked }) { - console.log("checkbox is:", checked) - // 'checked' | 'unchecked' | 'indeterminate' + onCheckChange(details) { + // details => { checked: boolean } + console.log("checkbox is:", details.checked) }, }), ) diff --git a/website/data/components/combobox.mdx b/website/data/components/combobox.mdx index bb03575e55..0a1132f74e 100644 --- a/website/data/components/combobox.mdx +++ b/website/data/components/combobox.mdx @@ -202,13 +202,13 @@ const [state, send] = useMachine( ## Listening for highlight changes When an option is highlighted with the pointer or keyboard, use the -`onHighlight` property to listen for this change and do something with it. +`onHighlightChange` property to listen for this change and do something with it. ```jsx {4-7} const [state, send] = useMachine( combobox.machine({ id: useId(), - onHighlight(details) { + onHighlightChange(details) { // details => { value: string | null; item: CollectionItem | null } console.log(details) }, @@ -216,15 +216,15 @@ const [state, send] = useMachine( ) ``` -## Listening for selection changes +## Listening for value changes -When an item is selected, use `onChange` property to listen for this change and -do something with it. +When an item is selected, use `onValueChange` property to listen for this change +and do something with it. ```jsx {3-6} const [state, send] = useMachine( combobox.machine({ - onSelect(details) { + onValueChange(details) { // details => { value: string[]; items: CollectionItem[] } console.log(details) }, diff --git a/website/data/components/dialog.mdx b/website/data/components/dialog.mdx index bb9f05ab0c..eba53b4133 100644 --- a/website/data/components/dialog.mdx +++ b/website/data/components/dialog.mdx @@ -104,6 +104,21 @@ const [state, send] = useMachine( ) ``` +## Listening for open state changes + +When the dialog is opened or closed, the `onOpenChange` callback is invoked. + +```jsx {3-8} +const [state, send] = useMachine( + dialog.machine({ + onOpenChange(details) { + // details => { open: boolean } + console.log("open:", details.open) + }, + }), +) +``` + ## Controlling the scroll behavior When the dialog is open, it prevents scrolling on the `body` element. To disable @@ -117,7 +132,7 @@ const [state, send] = useMachine( ) ``` -## Creating an Alert dialog +## Creating an alert dialog The dialog has support for dialog and alert dialog roles. It's set to `dialog` by default. To change it's role, pass the `role: alertdialog` property to the diff --git a/website/data/components/editable.mdx b/website/data/components/editable.mdx index 344e78e1b4..a54bba9577 100644 --- a/website/data/components/editable.mdx +++ b/website/data/components/editable.mdx @@ -77,6 +77,39 @@ editable machine in your project 🔥 +## Setting the initial value + +To set the initial value of the editable, pass the `value` property to the +machine's context. + +```jsx {3} +const [state, send] = useMachine( + editable.machine({ + value: "Hello World", + }), +) +``` + +## Listening for value changes + +The editable machine supports two ways of listening for value changes: + +- `onValueChange`: called when value changes. +- `onValueCommit`: called when the value is committed. + +```jsx {3-4} +const [state, send] = useMachine( + editable.machine({ + onValueChange(details) { + console.log("Value changed", details.value) + }, + onValueCommit: (details) => { + console.log("Value submitted", details.value) + }, + }), +) +``` + ## Using custom controls In some cases, you might need to use custom controls to toggle the edit and read @@ -137,7 +170,6 @@ const [state, send] = useMachine( ) ``` - ## Styling guide Earlier, we mentioned that each editable part has a `data-part` attribute added @@ -180,4 +212,4 @@ part. The editable's `api` method exposes the following methods: - \ No newline at end of file + diff --git a/website/data/components/file-upload.mdx b/website/data/components/file-upload.mdx index b33a26ae50..69cd1de681 100644 --- a/website/data/components/file-upload.mdx +++ b/website/data/components/file-upload.mdx @@ -111,17 +111,18 @@ const [state, send] = useMachine( ) ``` -## Listening to file upload events +## Listening to file changes -When files are uploaded, the `onChange` callback is invoked with the details of -the accepted and rejected files. +When files are uploaded, the `onFilesChange` callback is invoked with the +details of the accepted and rejected files. ```jsx const [state, send] = useMachine( fileUpload.machine({ - onChange: (details) => { - console.log(details.acceptedFiles) // File[] - console.log(details.rejectedFiles) // { file: File, errors: [] } + onFilesChange: (details) => { + // details => { acceptedFiles: File[], rejectedFiles: { file: File, errors: [] }[] } + console.log(details.acceptedFiles) + console.log(details.rejectedFiles) }, }), ) @@ -148,7 +149,7 @@ read the file and set the `src` attribute of an image element. ```jsx const [state, send] = useMachine( fileUpload.machine({ - onChange: (details) => { + onFilesChange: (details) => { const reader = new FileReader() reader.onload = (event) => { const image = event.target.result diff --git a/website/data/components/hover-card.mdx b/website/data/components/hover-card.mdx index 389b9ac3fc..0350df168d 100644 --- a/website/data/components/hover-card.mdx +++ b/website/data/components/hover-card.mdx @@ -76,7 +76,7 @@ const [state, send] = useMachine( ) ``` -## Listening for changes in `open` state +## Listening for open state changes When the hover card is `opened` or `closed`, the `onOpenChange` callback is invoked. @@ -84,9 +84,9 @@ invoked. ```jsx {3-6} const [state, send] = useMachine( hoverCard.machine({ - onOpenChange(open) { - console.log("hovercard is:", open ? "opened" : "closed") - // open - true | false + onOpenChange(details) { + // details => { open: boolean } + console.log("hovercard is:", details.open ? "opened" : "closed") }, }), ) diff --git a/website/data/components/menu.mdx b/website/data/components/menu.mdx index 521b2d2ddf..3713257090 100644 --- a/website/data/components/menu.mdx +++ b/website/data/components/menu.mdx @@ -74,19 +74,31 @@ menu machine in your project 🔥 -## Listening to item selection +## Listening for item selection -Pass `onSelect` to the machine's context to perform some custom logic when an -item is selected. When an item is selected, the machine invokes the `onSelect` -callback. This callback is invoked with the `value` of the item. +When a menu item is clicked, the `onSelect` callback is invoked. ```jsx {4-7} const [state, send] = useMachine( menu.machine({ - values: { order: "", type: [] }, - onSelect(value) { - // value => "nigeria" - console.log("selected value is ", value) + onSelect(details) { + // details => { value: string } + console.log("selected value is ", details.value) + }, + }), +) +``` + +## Listening for open state changes + +When a menu is opened or closed, the `onOpenChange` callback is invoked. + +```jsx {4-7} +const [state, send] = useMachine( + menu.machine({ + onOpenChange(details) { + // details => { open: boolean } + console.log("open state is ", details.open) }, }), ) diff --git a/website/data/components/number-input.mdx b/website/data/components/number-input.mdx index b1d32ced64..10a13c9cf6 100644 --- a/website/data/components/number-input.mdx +++ b/website/data/components/number-input.mdx @@ -162,6 +162,21 @@ const [state, send] = useMachine( ) ``` +## Listening for value changes + +When the value changes, the `onValueChange` callback is invoked. + +```jsx {3-8} +const [state, send] = useMachine( + numberInput.machine({ + onValueChange(details) { + // details => { value: string, valueAsNumber: number } + console.log("value is:", details.value) + }, + }), +) +``` + ## Usage within forms To use the number input within forms, set the `name` property in the machine's diff --git a/website/data/components/pin-input.mdx b/website/data/components/pin-input.mdx index 3123aa4e3b..97c47dc6c2 100644 --- a/website/data/components/pin-input.mdx +++ b/website/data/components/pin-input.mdx @@ -156,24 +156,24 @@ const [state, send] = useMachine( The pin input machine invokes several callback functions when the user enters: -- `onChange` — Function invoked when the value is changed. -- `onComplete` — Function invoked when all fields have been completed (by typing - or pasting). -- `onInvalid` — Function invoked when an invalid value is entered into the +- `onValueChange` — Function invoked when the value is changed. +- `onValueComplete` — Function invoked when all fields have been completed (by + typing or pasting). +- `onValueInvalid` — Function invoked when an invalid value is entered into the input. An invalid value is any value that doesn't match the specified "type". ```jsx const [state, send] = useMachine( pinInput.machine({ - onChange(value) { + onValueChange(value) { // value => string[] console.log("value changed to:", value) }, - onComplete(details) { + onValueComplete(details) { // details => { value: string[], valueAsString: string } console.log("completed value:", details) }, - onInvalid(details) { + onValueInvalid(details) { // details => { index: number, value: string } console.log("invalid value:", details) }, @@ -196,7 +196,6 @@ const [state, send] = useMachine( ) ``` - ## Styling guide Earlier, we mentioned that each pin input's part has a `data-part` attribute @@ -247,4 +246,4 @@ root and input parts. The pin input's `api` exposes the following methods: - \ No newline at end of file + diff --git a/website/data/components/popover.mdx b/website/data/components/popover.mdx index e29a26d19b..909a7b3ef2 100644 --- a/website/data/components/popover.mdx +++ b/website/data/components/popover.mdx @@ -180,19 +180,16 @@ const [state, send] = useMachine( ) ``` -## Listening for open and close events +## Listening for open state changes -When the popover is opened or closed, we invoke the `onOpen` and `onClose` -callbacks. +When the popover is opened or closed, the `onOpenChange` callback is invoked. ```jsx {3-8} const [state, send] = useMachine( popover.machine({ - onOpen() { - console.log("Popover opened") - }, - onClose() { - console.log("Popover closed") + onOpenChange(details) { + // details => { open: boolean } + console.log("Popover", details.open) }, }), ) diff --git a/website/data/components/radio-group.mdx b/website/data/components/radio-group.mdx index 08c1638ea0..82472bbdf7 100644 --- a/website/data/components/radio-group.mdx +++ b/website/data/components/radio-group.mdx @@ -98,14 +98,14 @@ const [state, send] = useMachine( ## Listening for changes -When the radio group value changes, the `onChange` callback is invoked. +When the radio group value changes, the `onValueChange` callback is invoked. ```jsx {3-8} const [state, send] = useMachine( radio.machine({ - onChange({ value }) { - console.log("radio value is:", value) - // 'apple' | 'orange' | 'grape' + onValueChange(details) { + // details => { value: string } + console.log("radio value is:", details.value) }, }), ) diff --git a/website/data/components/range-slider.mdx b/website/data/components/range-slider.mdx index ded54cdbd8..d924952dd4 100644 --- a/website/data/components/range-slider.mdx +++ b/website/data/components/range-slider.mdx @@ -139,17 +139,19 @@ const [state, send] = useMachine( ## Listening for changes -When the slider value changes, the `onChange` and `onChangeEnd` callbacks are -invoked. You can use this to setup custom behaviors in your app. +When the slider value changes, the `onValueChange` and `onValueChangeEnd` +callbacks are invoked. You can use this to setup custom behaviors in your app. ```jsx {3-8} const [state, send] = useMachine( rangeSlider.machine({ - onChange(values) { - console.log("value changing to:", values) + onValueChange(details) { + // details => { values: number[] } + console.log("value changing to:", details) }, - onChangeEnd(values) { - console.log("value has changed to:", values) + onValueChangeEnd(details) { + // details => { values: number[] } + console.log("value has changed to:", details) }, }), ) @@ -203,7 +205,6 @@ const [state, send] = useMachine( > While we take care of the interactions in RTL mode, you'll have to ensure you > apply the correct CSS styles to flip the layout. - ## Styling guide Earlier, we mentioned that each slider part has a `data-part` attribute added to @@ -288,4 +289,4 @@ label, control and thumb. The range slider's `api` provides properties and methods you can use to programmatically read and set the slider's value. - \ No newline at end of file + diff --git a/website/data/components/rating-group.mdx b/website/data/components/rating-group.mdx index e36ad2d346..98d1cad690 100644 --- a/website/data/components/rating-group.mdx +++ b/website/data/components/rating-group.mdx @@ -104,12 +104,12 @@ const [state, send] = useMachine( ## Listening for changes -When the rating value changes, the `onChange` callback is invoked. +When the rating value changes, the `onValueChange` callback is invoked. ```jsx {3-8} const [state, send] = useMachine( rating.machine({ - onChange({ value }) { + onValueChange({ value }) { console.log("rating value is:", value) // '1' | '2.5' | '4' }, diff --git a/website/data/components/segmented-control.mdx b/website/data/components/segmented-control.mdx index 69fe9ebb8b..139e4fa780 100644 --- a/website/data/components/segmented-control.mdx +++ b/website/data/components/segmented-control.mdx @@ -112,14 +112,15 @@ const [state, send] = useMachine( ## Listening for changes -When the segmented control value changes, the `onChange` callback is invoked. +When the segmented control value changes, the `onValueChange` callback is +invoked. ```jsx {3-8} const [state, send] = useMachine( radio.machine({ - onChange({ value }) { - console.log("segmented control value is:", value) - // 'apple' | 'orange' | 'grape' + onValueChange(details) { + // details => { value: string } + console.log("segmented control value is:", details.value) }, }), ) diff --git a/website/data/components/select.mdx b/website/data/components/select.mdx index 15b8ace485..3bcd59616a 100644 --- a/website/data/components/select.mdx +++ b/website/data/components/select.mdx @@ -214,15 +214,15 @@ const [state, send] = useMachine( ## Listening for highlight changes -When an item is highlighted with the pointer or keyboard, use the `onHighlight` -to listen for the change and do something with it. +When an item is highlighted with the pointer or keyboard, use the +`onHighlightChange` to listen for the change and do something with it. ```jsx {4-7} const [state, send] = useMachine( select.machine({ id: useId(), - onHighlight(details) { - // details => { value: string | null, item: Item | null } + onHighlightChange(details) { + // details => { highlightedValue: string | null, highlightedItem: CollectionItem | null } console.log(details) }, }), @@ -231,15 +231,15 @@ const [state, send] = useMachine( ## Listening for selection changes -When an item is selected, use the `onChange` property to listen for the change -and do something with it. +When an item is selected, use the `onValueChange` property to listen for the +change and do something with it. ```jsx {5-7} const [state, send] = useMachine( select.machine({ id: useId(), collection, - onChange(details) { + onValueChange(details) { // details => { value: string[], items: Item[] } console.log(details) }, @@ -249,21 +249,18 @@ const [state, send] = useMachine( ## Listening for open and close events -When the select is opened or closed, the `onOpen` and `onClose` properties in -the machine's context are called. You can listen for these events and do -something with it. +When the select is opened or closed, the `onOpenChange` callback is called. You +can listen for these events and do something with it. -```jsx {5-9} +```jsx {5-8} const [state, send] = useMachine( select.machine({ id: useId(), collection, - onOpen() { + onOpenChange(details) { + // details => { open: boolean } console.log("Select opened") }, - onClose() { - console.log("Select closed") - }, }), ) ``` diff --git a/website/data/components/slider.mdx b/website/data/components/slider.mdx index 5cd6efb603..3f24112ae8 100644 --- a/website/data/components/slider.mdx +++ b/website/data/components/slider.mdx @@ -142,23 +142,22 @@ const [state, send] = useMachine( ## Listening for changes -When the slider value changes, the `onChange` and `onChangeEnd` callbacks are -invoked. You can use this to setup custom behaviors in your app. +When the slider value changes, the `onValueChange` and `onValueChangeEnd` +callbacks are invoked. You can use this to setup custom behaviors in your app. ```jsx {3-8} const [state, send] = useMachine( slider.machine({ - onChange(value) { - console.log("value is changing to:", value) + onValueChange(details) { + console.log("value is changing to:", details) }, - onChangeEnd(value) { - console.log("value has changed to:", value) + onValueChangeEnd(details) { + console.log("value has changed to:", details) }, }), ) ``` - ## Changing the start position By default, the slider's "zero position" is usually at the start position (left @@ -341,4 +340,4 @@ track, range, label, and thumb parts. The slider's `api` provides properties and methods you can use to programmatically read and set the slider's value. - \ No newline at end of file + diff --git a/website/data/components/switch.mdx b/website/data/components/switch.mdx index 44425bf6df..edb559d470 100644 --- a/website/data/components/switch.mdx +++ b/website/data/components/switch.mdx @@ -103,13 +103,14 @@ const [state, send] = useMachine( ## Listening for changes -When the switch value changes, the `onChange` callback is invoked. +When the switch value changes, the `onCheckChange` callback is invoked. ```jsx {3-5} const [state, send] = useMachine( zagSwitch.machine({ - onChange({ checked }) { - console.log("switch is:", checked ? "On" : "Off") + onCheckChange(details) { + // details => { checked: boolean } + console.log("switch is:", details.checked ? "On" : "Off") }, }), ) diff --git a/website/data/components/tabs.mdx b/website/data/components/tabs.mdx index c010534ab9..9b7a9713fb 100644 --- a/website/data/components/tabs.mdx +++ b/website/data/components/tabs.mdx @@ -124,17 +124,19 @@ clickable. ## Listening for events -- `onChange` — Callback invoked when the selected tab changes. -- `onFocus` — Callback invoked when the focused tab changes. +- `onValueChange` — Callback invoked when the selected tab changes. +- `onFocusChange` — Callback invoked when the focused tab changes. ```jsx {3-8} const [state, send] = useMachine( tabs.machine({ - onFocus(value) { - console.log("focused tab:", value) + onFocusChange(details) { + // details => { value: string | null } + console.log("focused tab:", details.value) }, - onChange(value) { - console.log("selected tab:", value) + onValueChange(details) { + // details => { value: string } + console.log("selected tab:", details.value) }, }), ) diff --git a/website/data/components/tags-input.mdx b/website/data/components/tags-input.mdx index 03475ebf63..ee321bdced 100644 --- a/website/data/components/tags-input.mdx +++ b/website/data/components/tags-input.mdx @@ -224,27 +224,29 @@ const [state, send] = useMachine( ) ``` -## Events +## Listening for events During the lifetime of the tags input interaction, here's a list of events we emit: -- `onChange` — invoked when the tag value changes. -- `onHighlight` — invoked when a tag has visual focus. -- `onInvalid` — invoked when the max tag count is reached or the `validate` +- `onValueChange` — invoked when the tag value changes. +- `onHighlightChange` — invoked when a tag has visual focus. +- `onValueInvalid` — invoked when the max tag count is reached or the `validate` function returns `false`. ```jsx const [state, send] = useMachine( tagsInput.machine({ - onChange(tags) { - console.log("tags changed to:", tags) + onValueChange(details) { + // details => { value: string[] } + console.log("tags changed to:", details.value) }, - onHighlight(tag) { - console.log("highlighted tag:", tag) + onHighlightChange(details) { + // details => { value: string } + console.log("highlighted tag:", details.value) }, - onInvalid(err) { - console.log("Invalid!", err) + onValueInvalid(details) { + console.log("Invalid!", details.reason) }, }), ) diff --git a/website/data/components/toggle-group.mdx b/website/data/components/toggle-group.mdx index 0e559430f0..503933ae94 100644 --- a/website/data/components/toggle-group.mdx +++ b/website/data/components/toggle-group.mdx @@ -79,15 +79,17 @@ const [state, send] = useMachine( ) ``` -## Listening for changes +## Listening for value changes -When the pressed toggle in the group changes, `onChange` callback is invoked. +When the pressed toggle in the group changes, `onValueChange` callback is +invoked. ```jsx {3-5} const [state, send] = useMachine( toggle.machine({ - onChange(details) { - console.log(details.value) // => ["option-1"] + onValueChange(details) { + // details => { value: string[] } + console.log(details.value) }, }), ) diff --git a/website/data/components/tooltip.mdx b/website/data/components/tooltip.mdx index a3f7fa861c..210d34864f 100644 --- a/website/data/components/tooltip.mdx +++ b/website/data/components/tooltip.mdx @@ -214,18 +214,16 @@ const [state, send] = useMachine( ) ``` -## Events +## Listening for open state changes -When the tooltip open or closes, the `onOpen` and `onClose` callbacks are fired. +When the tooltip is opened or closed, the `onOpenChange` callback is invoked. ```jsx {3-8} const [state, send] = useMachine( tooltip.machine({ - onOpen() { - console.log("Tooltip opened") - }, - onClose() { - console.log("Tooltip closed") + onOpenChange(details) { + // details => { open: boolean } + console.log("Tooltip", details.open) }, }), ) diff --git a/website/data/overview/programmatic-control.mdx b/website/data/overview/programmatic-control.mdx index 1756075272..deafd2951f 100644 --- a/website/data/overview/programmatic-control.mdx +++ b/website/data/overview/programmatic-control.mdx @@ -122,7 +122,7 @@ const NumberInput = () => { const [state, send] = useMachine(machine({ id: "1" }), { context: { value, - onChange(details) { + onValueChange(details) { flushSync(() => { setValue(details.value) }) diff --git a/website/data/snippets/react/combobox/usage.mdx b/website/data/snippets/react/combobox/usage.mdx index aa9681fdc8..1b96b03400 100644 --- a/website/data/snippets/react/combobox/usage.mdx +++ b/website/data/snippets/react/combobox/usage.mdx @@ -21,10 +21,11 @@ export function Combobox() { combobox.machine({ id: useId(), collection, - onOpen() { + onOpenChange(details) { + if (!details.open) return setOptions(comboboxData) }, - onInputChange({ value }) { + onInputValueChange({ value }) { const filtered = comboboxData.filter((item) => item.label.toLowerCase().includes(value.toLowerCase()), ) diff --git a/website/data/snippets/solid/combobox/usage.mdx b/website/data/snippets/solid/combobox/usage.mdx index dcfb3adc1d..72056e06ed 100644 --- a/website/data/snippets/solid/combobox/usage.mdx +++ b/website/data/snippets/solid/combobox/usage.mdx @@ -24,10 +24,11 @@ export function Combobox() { combobox.machine({ id: createUniqueId(), collection: collection(), - onOpen() { + onOpenChange(details) { + if (!details.open) return setOptions(comboboxData) }, - onInputChange({ value }) { + onInputValueChange({ value }) { const filtered = comboboxData.filter((item) => item.label.toLowerCase().includes(value.toLowerCase()), ) diff --git a/website/data/snippets/vue-jsx/combobox/usage.mdx b/website/data/snippets/vue-jsx/combobox/usage.mdx index 6cc51495d7..8db83feda9 100644 --- a/website/data/snippets/vue-jsx/combobox/usage.mdx +++ b/website/data/snippets/vue-jsx/combobox/usage.mdx @@ -26,10 +26,11 @@ export default defineComponent({ combobox.machine({ id: "v1", collection: collectionRef.value, - onOpen() { + onOpenChange(details) { + if (!details.open) return options.value = comboboxData }, - onInputChange({ value }) { + onInputValueChange({ value }) { const filtered = comboboxData.filter((item) => item.label.toLowerCase().includes(value.toLowerCase()), ) diff --git a/website/data/snippets/vue-sfc/combobox/usage.mdx b/website/data/snippets/vue-sfc/combobox/usage.mdx index ed294e336a..bce022176c 100644 --- a/website/data/snippets/vue-sfc/combobox/usage.mdx +++ b/website/data/snippets/vue-sfc/combobox/usage.mdx @@ -24,10 +24,11 @@ combobox.machine({ id: "1", collection: collectionRef.value, - onOpen() { + onOpenChange(details) { + if (!details.open) return options.value = comboboxData }, - onInputChange({ value }) { + onInputValueChange({ value }) { const filtered = comboboxData.filter((item) => item.label.toLowerCase().includes(value.toLowerCase()), ) diff --git a/website/lib/use-search.ts b/website/lib/use-search.ts index 2ef8fb5914..a4945b478d 100644 --- a/website/lib/use-search.ts +++ b/website/lib/use-search.ts @@ -52,7 +52,7 @@ export function useSearch(): UseSearchReturn { inputBehavior: "autohighlight", selectionBehavior: "clear", collection, - onChange({ items }) { + onValueChange({ items }) { const [item] = items as SearchMetaItem[] if (!item) return try { @@ -63,7 +63,7 @@ export function useSearch(): UseSearchReturn { } dialog_api.close() }, - onInputChange({ value }) { + onInputValueChange({ value }) { if (value.length < 3) return const results = matchSorter(searchData, value, { keys: [