diff --git a/packages/form/src/__tests__/useIndeterminateChecked.tsx b/packages/form/src/__tests__/useIndeterminateChecked.tsx
new file mode 100644
index 0000000000..da9e8bd610
--- /dev/null
+++ b/packages/form/src/__tests__/useIndeterminateChecked.tsx
@@ -0,0 +1,288 @@
+import React from "react";
+import { fireEvent, render } from "@testing-library/react";
+
+import { useIndeterminateChecked } from "../useIndeterminateChecked";
+import { Checkbox } from "../toggle";
+import { MenuItemCheckbox } from "../menu/MenuItemCheckbox";
+
+const values = ["a", "b", "c", "d"] as const;
+const LABELS = {
+ a: "Label 1",
+ b: "Label 2",
+ c: "Label 3",
+ d: "Label 4",
+} as const;
+
+describe("useIndeterminateChecked", () => {
+ it("should work with normal checkboxes", () => {
+ function Test() {
+ const { rootProps, getProps } = useIndeterminateChecked(values);
+ return (
+ <>
+
+ {values.map((value, i) => (
+
+ ))}
+ >
+ );
+ }
+ const { getByRole } = render();
+
+ const root = getByRole("checkbox", {
+ name: "Toggle All",
+ }) as HTMLInputElement;
+ const checkbox1 = getByRole("checkbox", {
+ name: "Label 1",
+ }) as HTMLInputElement;
+ const checkbox2 = getByRole("checkbox", {
+ name: "Label 2",
+ }) as HTMLInputElement;
+ const checkbox3 = getByRole("checkbox", {
+ name: "Label 3",
+ }) as HTMLInputElement;
+ const checkbox4 = getByRole("checkbox", {
+ name: "Label 4",
+ }) as HTMLInputElement;
+
+ expect(root.checked).toBe(false);
+ expect(checkbox1.checked).toBe(false);
+ expect(checkbox2.checked).toBe(false);
+ expect(checkbox3.checked).toBe(false);
+ expect(checkbox4.checked).toBe(false);
+
+ fireEvent.click(root);
+ expect(root.checked).toBe(true);
+ expect(checkbox1.checked).toBe(true);
+ expect(checkbox2.checked).toBe(true);
+ expect(checkbox3.checked).toBe(true);
+ expect(checkbox4.checked).toBe(true);
+
+ fireEvent.click(checkbox1);
+ expect(root.checked).toBe(true);
+ expect(checkbox1.checked).toBe(false);
+ expect(checkbox2.checked).toBe(true);
+ expect(checkbox3.checked).toBe(true);
+ expect(checkbox4.checked).toBe(true);
+
+ fireEvent.click(root);
+ expect(root.checked).toBe(true);
+ expect(checkbox1.checked).toBe(true);
+ expect(checkbox2.checked).toBe(true);
+ expect(checkbox3.checked).toBe(true);
+ expect(checkbox4.checked).toBe(true);
+
+ fireEvent.click(root);
+ expect(root.checked).toBe(false);
+ expect(checkbox1.checked).toBe(false);
+ expect(checkbox2.checked).toBe(false);
+ expect(checkbox3.checked).toBe(false);
+ expect(checkbox4.checked).toBe(false);
+
+ fireEvent.click(checkbox2);
+ expect(root.checked).toBe(true);
+ expect(checkbox1.checked).toBe(false);
+ expect(checkbox2.checked).toBe(true);
+ expect(checkbox3.checked).toBe(false);
+ expect(checkbox4.checked).toBe(false);
+ });
+
+ it("should work for MenuItemCheckbox", () => {
+ function Test() {
+ const { rootProps, getProps } = useIndeterminateChecked(values, {
+ menu: true,
+ });
+ return (
+ <>
+
+ Toggle All
+
+ {values.map((value, i) => (
+
+ {LABELS[value]}
+
+ ))}
+ >
+ );
+ }
+
+ const { getByRole } = render();
+
+ const root = getByRole("menuitemcheckbox", {
+ name: "Toggle All",
+ }) as HTMLInputElement;
+ const checkbox1 = getByRole("menuitemcheckbox", {
+ name: "Label 1",
+ }) as HTMLInputElement;
+ const checkbox2 = getByRole("menuitemcheckbox", {
+ name: "Label 2",
+ }) as HTMLInputElement;
+ const checkbox3 = getByRole("menuitemcheckbox", {
+ name: "Label 3",
+ }) as HTMLInputElement;
+ const checkbox4 = getByRole("menuitemcheckbox", {
+ name: "Label 4",
+ }) as HTMLInputElement;
+
+ expect(root).toHaveAttribute("aria-checked", "false");
+ expect(checkbox1).toHaveAttribute("aria-checked", "false");
+ expect(checkbox2).toHaveAttribute("aria-checked", "false");
+ expect(checkbox3).toHaveAttribute("aria-checked", "false");
+ expect(checkbox4).toHaveAttribute("aria-checked", "false");
+
+ fireEvent.click(root);
+ expect(root).toHaveAttribute("aria-checked", "true");
+ expect(checkbox1).toHaveAttribute("aria-checked", "true");
+ expect(checkbox2).toHaveAttribute("aria-checked", "true");
+ expect(checkbox3).toHaveAttribute("aria-checked", "true");
+ expect(checkbox4).toHaveAttribute("aria-checked", "true");
+
+ fireEvent.click(checkbox1);
+ expect(root).toHaveAttribute("aria-checked", "true");
+ expect(checkbox1).toHaveAttribute("aria-checked", "false");
+ expect(checkbox2).toHaveAttribute("aria-checked", "true");
+ expect(checkbox3).toHaveAttribute("aria-checked", "true");
+ expect(checkbox4).toHaveAttribute("aria-checked", "true");
+
+ fireEvent.click(root);
+ expect(root).toHaveAttribute("aria-checked", "true");
+ expect(checkbox1).toHaveAttribute("aria-checked", "true");
+ expect(checkbox2).toHaveAttribute("aria-checked", "true");
+ expect(checkbox3).toHaveAttribute("aria-checked", "true");
+ expect(checkbox4).toHaveAttribute("aria-checked", "true");
+
+ fireEvent.click(root);
+ expect(root).toHaveAttribute("aria-checked", "false");
+ expect(checkbox1).toHaveAttribute("aria-checked", "false");
+ expect(checkbox2).toHaveAttribute("aria-checked", "false");
+ expect(checkbox3).toHaveAttribute("aria-checked", "false");
+ expect(checkbox4).toHaveAttribute("aria-checked", "false");
+
+ fireEvent.click(checkbox2);
+ expect(root).toHaveAttribute("aria-checked", "true");
+ expect(checkbox1).toHaveAttribute("aria-checked", "false");
+ expect(checkbox2).toHaveAttribute("aria-checked", "true");
+ expect(checkbox3).toHaveAttribute("aria-checked", "false");
+ expect(checkbox4).toHaveAttribute("aria-checked", "false");
+ });
+
+ it("should allow for default values or another onChange handler", () => {
+ const onChange = jest.fn();
+ const defaultCheckedValues = ["b"] as const;
+ function Test() {
+ const { rootProps, getProps } = useIndeterminateChecked(
+ values,
+ defaultCheckedValues,
+ onChange
+ );
+ return (
+ <>
+
+ {values.map((value, i) => (
+
+ ))}
+ >
+ );
+ }
+ const { getByRole } = render();
+ const root = getByRole("checkbox", {
+ name: "Toggle All",
+ }) as HTMLInputElement;
+ const checkbox1 = getByRole("checkbox", {
+ name: "Label 1",
+ }) as HTMLInputElement;
+ const checkbox2 = getByRole("checkbox", {
+ name: "Label 2",
+ }) as HTMLInputElement;
+ const checkbox3 = getByRole("checkbox", {
+ name: "Label 3",
+ }) as HTMLInputElement;
+ const checkbox4 = getByRole("checkbox", {
+ name: "Label 4",
+ }) as HTMLInputElement;
+
+ expect(root.checked).toBe(true);
+ expect(checkbox1.checked).toBe(false);
+ expect(checkbox2.checked).toBe(true);
+ expect(checkbox3.checked).toBe(false);
+ expect(checkbox4.checked).toBe(false);
+
+ fireEvent.click(checkbox1);
+ expect(onChange).toBeCalledWith(["b", "a"]);
+
+ expect(root.checked).toBe(true);
+ expect(checkbox1.checked).toBe(true);
+ expect(checkbox2.checked).toBe(true);
+ expect(checkbox3.checked).toBe(false);
+ expect(checkbox4.checked).toBe(false);
+ });
+
+ it("should allow for default values or another onChange handler with an options object", () => {
+ const onChange = jest.fn();
+ const defaultCheckedValues = ["b"] as const;
+ function Test() {
+ const { rootProps, getProps } = useIndeterminateChecked(values, {
+ onChange,
+ defaultCheckedValues,
+ });
+
+ return (
+ <>
+
+ {values.map((value, i) => (
+
+ ))}
+ >
+ );
+ }
+ const { getByRole } = render();
+ const root = getByRole("checkbox", {
+ name: "Toggle All",
+ }) as HTMLInputElement;
+ const checkbox1 = getByRole("checkbox", {
+ name: "Label 1",
+ }) as HTMLInputElement;
+ const checkbox2 = getByRole("checkbox", {
+ name: "Label 2",
+ }) as HTMLInputElement;
+ const checkbox3 = getByRole("checkbox", {
+ name: "Label 3",
+ }) as HTMLInputElement;
+ const checkbox4 = getByRole("checkbox", {
+ name: "Label 4",
+ }) as HTMLInputElement;
+
+ expect(root.checked).toBe(true);
+ expect(checkbox1.checked).toBe(false);
+ expect(checkbox2.checked).toBe(true);
+ expect(checkbox3.checked).toBe(false);
+ expect(checkbox4.checked).toBe(false);
+
+ fireEvent.click(checkbox1);
+ expect(onChange).toBeCalledWith(["b", "a"]);
+
+ expect(root.checked).toBe(true);
+ expect(checkbox1.checked).toBe(true);
+ expect(checkbox2.checked).toBe(true);
+ expect(checkbox3.checked).toBe(false);
+ expect(checkbox4.checked).toBe(false);
+ });
+});
diff --git a/packages/form/src/useIndeterminateChecked.ts b/packages/form/src/useIndeterminateChecked.ts
index a23c59e580..877bb6c8b0 100644
--- a/packages/form/src/useIndeterminateChecked.ts
+++ b/packages/form/src/useIndeterminateChecked.ts
@@ -59,6 +59,34 @@ export interface BaseProvidedIndeterminateCheckboxProps {
indeterminate: boolean;
}
+/**
+ * @remarks \@since 2.8.5
+ * @internal
+ */
+export interface ProvidedIndeterminateCheckboxProps
+ extends BaseProvidedIndeterminateCheckboxProps {
+ onChange(): void;
+}
+
+/**
+ * @remarks \@since 2.8.5
+ * @internal
+ */
+export interface ProvidedIndeterminateMenuItemCheckboxProps
+ extends BaseProvidedIndeterminateCheckboxProps {
+ onCheckedChange(): void;
+}
+
+/**
+ * @remarks \@since 2.8.5
+ * @internal
+ */
+interface ProvidedCombinedIndeterminateProps
+ extends BaseProvidedIndeterminateCheckboxProps {
+ onChange?(): void;
+ onCheckedChange?(): void;
+}
+
/**
* @remarks \@since 2.8.5
* @typeParam V - The values allowed for the list of checkboxes.
@@ -77,6 +105,38 @@ export interface BaseProvidedIndeterminateControlledCheckboxProps<
checked: boolean;
}
+/**
+ * @remarks \@since 2.8.5
+ * @typeParam V - The values allowed for the list of checkboxes.
+ * @internal
+ */
+export interface ProvidedIndeterminateControlledCheckboxProps
+ extends BaseProvidedIndeterminateControlledCheckboxProps {
+ onChange(): void;
+}
+
+/**
+ * @remarks \@since 2.8.5
+ * @typeParam V - The values allowed for the list of checkboxes.
+ * @internal
+ */
+export interface ProvidedIndeterminateControlledMenuItemCheckboxProps<
+ V extends string
+> extends BaseProvidedIndeterminateControlledCheckboxProps {
+ onCheckedChange(): void;
+}
+
+/**
+ * @remarks \@since 2.8.5
+ * @typeParam V - The values allowed for the list of checkboxes.
+ * @internal
+ */
+interface ProvidedCombinedIndeterminateControlledProps
+ extends BaseProvidedIndeterminateControlledCheckboxProps {
+ onChange?(): void;
+ onCheckedChange?(): void;
+}
+
/**
* @remarks \@since 2.8.5
* @typeParam V - The values allowed for the list of checkboxes.
@@ -101,10 +161,8 @@ export interface BaseIndeterminateCheckedHookReturnValue {
*/
interface OnChangeReturnValue
extends BaseIndeterminateCheckedHookReturnValue {
- rootProps: BaseProvidedIndeterminateCheckboxProps & { onChange(): void };
- getProps(
- value: V
- ): BaseProvidedIndeterminateControlledCheckboxProps & { onChange(): void };
+ rootProps: ProvidedIndeterminateCheckboxProps;
+ getProps(value: V): ProvidedIndeterminateControlledCheckboxProps;
}
/**
@@ -114,12 +172,8 @@ interface OnChangeReturnValue
*/
interface OnCheckedChangeReturnValue
extends BaseIndeterminateCheckedHookReturnValue {
- rootProps: BaseProvidedIndeterminateCheckboxProps & {
- onCheckedChange(): void;
- };
- getProps(value: V): BaseProvidedIndeterminateControlledCheckboxProps & {
- onCheckedChange(): void;
- };
+ rootProps: ProvidedIndeterminateMenuItemCheckboxProps;
+ getProps(value: V): ProvidedIndeterminateControlledMenuItemCheckboxProps;
}
/**
@@ -129,14 +183,8 @@ interface OnCheckedChangeReturnValue
*/
interface CombinedReturnValue
extends BaseIndeterminateCheckedHookReturnValue {
- rootProps: BaseProvidedIndeterminateCheckboxProps & {
- onChange?(): void;
- onCheckedChange?(): void;
- };
- getProps(value: V): BaseProvidedIndeterminateControlledCheckboxProps & {
- onChange?(): void;
- onCheckedChange?(): void;
- };
+ rootProps: ProvidedCombinedIndeterminateProps;
+ getProps(value: V): ProvidedCombinedIndeterminateControlledProps;
}
/**
@@ -297,22 +345,24 @@ export function useIndeterminateChecked(
setCheckedValues(values);
};
- const onChange = (): void => {
- updateCheckedValues(
- checkedValues.length === 0 || indeterminate ? values : []
- );
+ const rootProps: ProvidedCombinedIndeterminateProps = {
+ "aria-checked": indeterminate ? "mixed" : undefined,
+ checked,
+ indeterminate,
+ [menu ? "onCheckedChange" : "onChange"]: () => {
+ updateCheckedValues(
+ checkedValues.length === 0 || indeterminate ? values : []
+ );
+ },
};
- return {
- rootProps: {
- "aria-checked": indeterminate ? "mixed" : undefined,
- checked,
- indeterminate,
- onChange: menu ? undefined : onChange,
- onCheckedChange: menu ? onChange : undefined,
- },
- getProps: (value) => {
- const onChange = (): void => {
+ const getProps = (
+ value: V
+ ): ProvidedCombinedIndeterminateControlledProps => {
+ return {
+ value,
+ checked: checkedValues.includes(value),
+ [menu ? "onCheckedChange" : "onChange"]: () => {
const i = checkedValues.indexOf(value);
const nextChecked = checkedValues.slice();
if (i === -1) {
@@ -322,15 +372,13 @@ export function useIndeterminateChecked(
}
updateCheckedValues(nextChecked);
- };
+ },
+ };
+ };
- return {
- value,
- checked: checkedValues.includes(value),
- onChange: menu ? undefined : onChange,
- onCheckedChange: menu ? onChange : undefined,
- };
- },
+ return {
+ rootProps,
+ getProps,
checkedValues,
setCheckedValues,
};