diff --git a/packages/components/src/components/Action/Action.browser.test.tsx b/packages/components/src/components/Action/Action.browser.test.tsx
index 0392093e80..333b961919 100644
--- a/packages/components/src/components/Action/Action.browser.test.tsx
+++ b/packages/components/src/components/Action/Action.browser.test.tsx
@@ -1,13 +1,14 @@
import { render } from "vitest-browser-react";
import { page, userEvent } from "vitest/browser";
import { type FC } from "react";
-import Action, { type ActionProps } from "@/components/Action";
+import Action, { ActionBatch, type ActionProps } from "@/components/Action";
import { Button, type ButtonProps } from "@/components/Button";
import type { Mock } from "vitest";
import Content from "@/components/Content/Content";
import ActionGroup from "@/components/ActionGroup/ActionGroup";
import Heading from "@/components/Heading/Heading";
import Modal from "@/components/Modal";
+import { duration } from "@/components/Action/models/ActionState";
const asyncActionDuration = 700;
const sleep = () =>
@@ -413,6 +414,59 @@ describe("Feedback", () => {
await rerender();
expectNoIconInDom();
});
+
+ test("can be splitted by batches", async () => {
+ asyncAction1.mockImplementation(async () => {
+ await sleep();
+ await sleep();
+ });
+
+ asyncAction2.mockImplementation(async () => {
+ await sleep();
+ await sleep();
+ });
+
+ const ui = () => (
+
+
+
+
+
+
+
+ );
+
+ const { rerender } = await render(ui());
+ expectNoIconInDom();
+
+ await clickTrigger();
+
+ // First batch
+ await vitest.advanceTimersByTimeAsync(duration.pending);
+ await rerender(ui());
+ expectIconInDom("loader-2");
+
+ // First batch done
+ await vitest.advanceTimersByTimeAsync(
+ asyncActionDuration * 2 - duration.pending,
+ );
+ await rerender(ui());
+ expectIconInDom("check");
+
+ // Second batch
+ await vitest.advanceTimersByTimeAsync(
+ duration.succeeded + duration.pending,
+ );
+ await rerender(ui());
+ expectIconInDom("loader-2");
+
+ // Second batch done
+ await vitest.advanceTimersByTimeAsync(
+ asyncActionDuration * 2 - duration.pending,
+ );
+ await rerender(ui());
+ expectIconInDom("check");
+ });
});
describe("Pending state", () => {
diff --git a/packages/components/src/components/Action/ActionBatch.tsx b/packages/components/src/components/Action/ActionBatch.tsx
new file mode 100644
index 0000000000..bfd16d8c12
--- /dev/null
+++ b/packages/components/src/components/Action/ActionBatch.tsx
@@ -0,0 +1,18 @@
+import Action from "@/components/Action/Action";
+import type { FC, PropsWithChildren } from "react";
+
+export type ActionBatchProps = PropsWithChildren;
+
+/**
+ * Batches multiple actions together and shows feedback when all actions have
+ * completed.
+ *
+ * By default async actions are automatically batched.
+ */
+export const ActionBatch: FC = (props) => {
+ const { children } = props;
+
+ return {children};
+};
+
+export default ActionBatch;
diff --git a/packages/components/src/components/Action/index.ts b/packages/components/src/components/Action/index.ts
index 4a19b25285..150444a3f9 100644
--- a/packages/components/src/components/Action/index.ts
+++ b/packages/components/src/components/Action/index.ts
@@ -1,4 +1,5 @@
export { Action } from "./Action";
+export { ActionBatch } from "./ActionBatch";
export * from "./types";
export { default } from "./Action";
export { useAriaAnnounceSuspense } from "./lib/ariaLive";
diff --git a/packages/components/src/components/Button/Button.tsx b/packages/components/src/components/Button/Button.tsx
index 4734a8f15c..ba61441e30 100644
--- a/packages/components/src/components/Button/Button.tsx
+++ b/packages/components/src/components/Button/Button.tsx
@@ -57,7 +57,6 @@ const disablePendingProps = (props: ButtonProps) => {
props.onPressUp = undefined;
props.onKeyDown = undefined;
props.onKeyUp = undefined;
- props.type = "button";
}
return props;
diff --git a/packages/components/src/index/flr-universal.ts b/packages/components/src/index/flr-universal.ts
index 50740516f9..37c9d625a6 100644
--- a/packages/components/src/index/flr-universal.ts
+++ b/packages/components/src/index/flr-universal.ts
@@ -2,6 +2,7 @@ export * from "@/components/Icon/components/icons";
export {
Action,
+ ActionBatch,
type ActionFn,
type ActionProps,
BrowserOnly,
diff --git a/packages/components/src/integrations/react-hook-form/components/Form/Form.browser.test.tsx b/packages/components/src/integrations/react-hook-form/components/Form/Form.browser.test.tsx
index ae3da234ae..8a6ac91a89 100644
--- a/packages/components/src/integrations/react-hook-form/components/Form/Form.browser.test.tsx
+++ b/packages/components/src/integrations/react-hook-form/components/Form/Form.browser.test.tsx
@@ -1,3 +1,4 @@
+import Action from "@/components/Action";
import Button from "@/components/Button";
import TextField, { type TextFieldProps } from "@/components/TextField";
import {
@@ -113,7 +114,8 @@ describe("resetting", () => {
describe("submission", () => {
const onAfterSubmit = vitest.fn();
- const onSubmit = vitest.fn(() => onAfterSubmit);
+ const onAfterSubmitAction = vitest.fn();
+ const onSubmit = vitest.fn(async () => onAfterSubmit);
const TestForm: FC = () => {
const form = useForm