mirror of
https://github.com/nextui-org/nextui.git
synced 2025-12-08 19:26:11 +00:00
fix(checkbox): checkbox controlled state (#2754)
* fix(checkbox): checkbox controlled state * feat(checkbox): add @nextui-org/use-callback-ref * chore(deps): pnpm-lock.yaml * fix(checkbox): handle checkbox group * fix(checkbox): rely on react aria logic (#2760) * fix(checkbox): add missing dependency in useCheckbox hook --------- Co-authored-by: Junior Garcia <jrgarciadev@gmail.com>
This commit is contained in:
parent
f728c0542c
commit
cadbb30cfb
5
.changeset/wicked-bananas-shave.md
Normal file
5
.changeset/wicked-bananas-shave.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"@nextui-org/checkbox": patch
|
||||
---
|
||||
|
||||
Fixes checkbox controlled state (#2752)
|
||||
@ -34,35 +34,37 @@
|
||||
"postpack": "clean-package restore"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18",
|
||||
"react-dom": ">=18",
|
||||
"@nextui-org/system": ">=2.0.0",
|
||||
"@nextui-org/theme": ">=2.1.0",
|
||||
"@nextui-org/system": ">=2.0.0"
|
||||
"react": ">=18",
|
||||
"react-dom": ">=18"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nextui-org/shared-utils": "workspace:*",
|
||||
"@nextui-org/react-utils": "workspace:*",
|
||||
"@nextui-org/shared-utils": "workspace:*",
|
||||
"@nextui-org/use-callback-ref": "workspace:*",
|
||||
"@nextui-org/use-safe-layout-effect": "workspace:*",
|
||||
"@react-aria/checkbox": "^3.14.1",
|
||||
"@react-aria/focus": "^3.16.2",
|
||||
"@react-aria/interactions": "^3.21.1",
|
||||
"@react-aria/utils": "^3.23.2",
|
||||
"@react-aria/visually-hidden": "^3.8.10",
|
||||
"@react-stately/checkbox": "^3.6.3",
|
||||
"@react-stately/toggle": "^3.7.2",
|
||||
"@react-aria/utils": "^3.23.2",
|
||||
"@react-types/checkbox": "^3.7.1",
|
||||
"@react-types/shared": "^3.22.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nextui-org/theme": "workspace:*",
|
||||
"@nextui-org/system": "workspace:*",
|
||||
"@nextui-org/shared-icons": "workspace:*",
|
||||
"@nextui-org/chip": "workspace:*",
|
||||
"@nextui-org/user": "workspace:*",
|
||||
"@nextui-org/link": "workspace:*",
|
||||
"@nextui-org/shared-icons": "workspace:*",
|
||||
"@nextui-org/system": "workspace:*",
|
||||
"@nextui-org/theme": "workspace:*",
|
||||
"@nextui-org/user": "workspace:*",
|
||||
"clean-package": "2.2.0",
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0"
|
||||
"react-dom": "^18.0.0",
|
||||
"react-hook-form": "^7.51.3"
|
||||
},
|
||||
"clean-package": "../../../clean-package.config.json"
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ import {ReactNode, Ref, useCallback, useId, useState} from "react";
|
||||
import {useMemo, useRef} from "react";
|
||||
import {useToggleState} from "@react-stately/toggle";
|
||||
import {checkbox} from "@nextui-org/theme";
|
||||
import {useCallbackRef} from "@nextui-org/use-callback-ref";
|
||||
import {useHover, usePress} from "@react-aria/interactions";
|
||||
import {useFocusRing} from "@react-aria/focus";
|
||||
import {mergeProps, chain} from "@react-aria/utils";
|
||||
@ -170,6 +171,8 @@ export function useCheckbox(props: UseCheckboxProps = {}) {
|
||||
onValueChange,
|
||||
]);
|
||||
|
||||
const toggleState = useToggleState(ariaCheckboxProps);
|
||||
|
||||
const {
|
||||
inputProps,
|
||||
isSelected,
|
||||
@ -191,7 +194,7 @@ export function useCheckbox(props: UseCheckboxProps = {}) {
|
||||
useReactAriaCheckbox(
|
||||
{...ariaCheckboxProps, validationBehavior: "native"},
|
||||
// eslint-disable-next-line
|
||||
useToggleState(ariaCheckboxProps),
|
||||
toggleState,
|
||||
inputRef,
|
||||
);
|
||||
|
||||
@ -242,8 +245,6 @@ export function useCheckbox(props: UseCheckboxProps = {}) {
|
||||
[color, size, radius, isInvalid, lineThrough, isDisabled, disableAnimation],
|
||||
);
|
||||
|
||||
const [isChecked, setIsChecked] = useState(!!defaultSelected || !!isSelected);
|
||||
|
||||
// if we use `react-hook-form`, it will set the checkbox value using the ref in register
|
||||
// i.e. setting ref.current.checked to true or false which is uncontrolled
|
||||
// hence, sync the state with `ref.current.checked`
|
||||
@ -251,9 +252,11 @@ export function useCheckbox(props: UseCheckboxProps = {}) {
|
||||
if (!inputRef.current) return;
|
||||
const isInputRefChecked = !!inputRef.current.checked;
|
||||
|
||||
setIsChecked(isInputRefChecked);
|
||||
toggleState.setSelected(isInputRefChecked);
|
||||
}, [inputRef.current]);
|
||||
|
||||
const onChangeProp = useCallbackRef(onChange);
|
||||
|
||||
const handleCheckboxChange = useCallback(
|
||||
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (isReadOnly || isDisabled) {
|
||||
@ -262,9 +265,9 @@ export function useCheckbox(props: UseCheckboxProps = {}) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsChecked(!isChecked);
|
||||
onChangeProp?.(event);
|
||||
},
|
||||
[isReadOnly, isDisabled, isChecked],
|
||||
[isReadOnly, isDisabled, onChangeProp],
|
||||
);
|
||||
|
||||
const baseStyles = clsx(classNames?.base, className);
|
||||
@ -274,7 +277,7 @@ export function useCheckbox(props: UseCheckboxProps = {}) {
|
||||
ref: domRef,
|
||||
className: slots.base({class: baseStyles}),
|
||||
"data-disabled": dataAttr(isDisabled),
|
||||
"data-selected": dataAttr(isSelected || isIndeterminate || isChecked),
|
||||
"data-selected": dataAttr(isSelected || isIndeterminate),
|
||||
"data-invalid": dataAttr(isInvalid),
|
||||
"data-hover": dataAttr(isHovered),
|
||||
"data-focus": dataAttr(isFocused),
|
||||
@ -315,10 +318,10 @@ export function useCheckbox(props: UseCheckboxProps = {}) {
|
||||
const getInputProps: PropGetter = useCallback(() => {
|
||||
return {
|
||||
ref: mergeRefs(inputRef, ref),
|
||||
...mergeProps(inputProps, focusProps, {checked: isChecked}),
|
||||
onChange: chain(inputProps.onChange, onChange, handleCheckboxChange),
|
||||
...mergeProps(inputProps, focusProps),
|
||||
onChange: chain(inputProps.onChange, handleCheckboxChange),
|
||||
};
|
||||
}, [inputProps, focusProps, onChange, handleCheckboxChange]);
|
||||
}, [inputProps, focusProps, handleCheckboxChange]);
|
||||
|
||||
const getLabelProps: PropGetter = useCallback(
|
||||
() => ({
|
||||
@ -331,12 +334,12 @@ export function useCheckbox(props: UseCheckboxProps = {}) {
|
||||
const getIconProps = useCallback(
|
||||
() =>
|
||||
({
|
||||
isSelected: isSelected || isChecked,
|
||||
isSelected: isSelected,
|
||||
isIndeterminate: !!isIndeterminate,
|
||||
disableAnimation: !!disableAnimation,
|
||||
className: slots.icon({class: classNames?.icon}),
|
||||
} as CheckboxIconProps),
|
||||
[slots, classNames?.icon, isSelected, isIndeterminate, disableAnimation, isChecked],
|
||||
[slots, classNames?.icon, isSelected, isIndeterminate, disableAnimation],
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@ -107,6 +107,28 @@ const FormTemplate = (args: CheckboxGroupProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
const ControlledTemplate = (args: CheckboxGroupProps) => {
|
||||
const [selected, setSelected] = React.useState<string[]>(["buenos-aires"]);
|
||||
|
||||
React.useEffect(() => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log("Checkbox ", selected);
|
||||
}, [selected]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<CheckboxGroup {...args} label="Select cities" value={selected} onValueChange={setSelected}>
|
||||
<Checkbox value="buenos-aires">Buenos Aires</Checkbox>
|
||||
<Checkbox value="sydney">Sydney</Checkbox>
|
||||
<Checkbox value="san-francisco">San Francisco</Checkbox>
|
||||
<Checkbox value="london">London</Checkbox>
|
||||
<Checkbox value="tokyo">Tokyo</Checkbox>
|
||||
</CheckboxGroup>
|
||||
<p className="text-default-500">Selected: {selected.join(", ")}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Default = {
|
||||
render: Template,
|
||||
|
||||
@ -133,6 +155,14 @@ export const DefaultValue = {
|
||||
},
|
||||
};
|
||||
|
||||
export const Controlled = {
|
||||
render: ControlledTemplate,
|
||||
|
||||
args: {
|
||||
...defaultProps,
|
||||
},
|
||||
};
|
||||
|
||||
export const Horizontal = {
|
||||
render: Template,
|
||||
|
||||
|
||||
@ -3,6 +3,7 @@ import {Meta} from "@storybook/react";
|
||||
import {checkbox} from "@nextui-org/theme";
|
||||
import {CloseIcon} from "@nextui-org/shared-icons";
|
||||
import {button} from "@nextui-org/theme";
|
||||
import {useForm} from "react-hook-form";
|
||||
|
||||
import {Checkbox, CheckboxIconProps, CheckboxProps} from "../src";
|
||||
|
||||
@ -60,6 +61,7 @@ const ControlledTemplate = (args: CheckboxProps) => {
|
||||
Subscribe (controlled)
|
||||
</Checkbox>
|
||||
<p className="text-default-500">Selected: {selected ? "true" : "false"}</p>
|
||||
<button onClick={() => setSelected(!selected)}>Toggle</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -83,6 +85,63 @@ const FormTemplate = (args: CheckboxProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
const GroupTemplate = (args: CheckboxProps) => {
|
||||
const items = ["Apple", "Banana", "Orange", "Mango"];
|
||||
|
||||
const [selectedItems, setSelectedItems] = React.useState<string[]>([]);
|
||||
|
||||
const isSelected = (value: string) => {
|
||||
return selectedItems.some((selected) => selected === value);
|
||||
};
|
||||
|
||||
const handleValueChange = (value: string) => {
|
||||
setSelectedItems([value]);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="text-white flex flex-col gap-2">
|
||||
<h2>List of Fruits</h2>
|
||||
|
||||
{items.map((item, index) => (
|
||||
<Checkbox
|
||||
{...args}
|
||||
key={index}
|
||||
className="text-white"
|
||||
color="primary"
|
||||
isSelected={isSelected(item)}
|
||||
onValueChange={() => handleValueChange(item)}
|
||||
>
|
||||
{item} {isSelected(item) ? "/ state: true" : "/ state: false"}
|
||||
</Checkbox>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const WithReactHookFormTemplate = (args: CheckboxProps) => {
|
||||
const {
|
||||
register,
|
||||
formState: {errors},
|
||||
handleSubmit,
|
||||
} = useForm();
|
||||
|
||||
const onSubmit = (data: any) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(data);
|
||||
alert("Submitted value: " + data.example);
|
||||
};
|
||||
|
||||
return (
|
||||
<form className="flex flex-col gap-4" onSubmit={handleSubmit(onSubmit)}>
|
||||
<Checkbox {...args} {...register("example", {required: true})} />
|
||||
{errors.example && <span className="text-danger">This field is required</span>}
|
||||
<button className={button({class: "w-fit"})} type="submit">
|
||||
Submit
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export const Default = {
|
||||
args: {
|
||||
...defaultProps,
|
||||
@ -110,6 +169,22 @@ export const CustomIconNode = {
|
||||
},
|
||||
};
|
||||
|
||||
export const Group = {
|
||||
render: GroupTemplate,
|
||||
|
||||
args: {
|
||||
...defaultProps,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithReactHookForm = {
|
||||
render: WithReactHookFormTemplate,
|
||||
|
||||
args: {
|
||||
...defaultProps,
|
||||
},
|
||||
};
|
||||
|
||||
export const CustomIconFunction = {
|
||||
args: {
|
||||
...defaultProps,
|
||||
|
||||
31
pnpm-lock.yaml
generated
31
pnpm-lock.yaml
generated
@ -146,7 +146,7 @@ importers:
|
||||
version: 4.0.2(eslint@7.32.0)(webpack@5.91.0)
|
||||
eslint-plugin-import:
|
||||
specifier: ^2.26.0
|
||||
version: 2.29.1(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-typescript@2.7.1)(eslint@7.32.0)
|
||||
version: 2.29.1(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-typescript@3.6.1)(eslint@7.32.0)
|
||||
eslint-plugin-jest:
|
||||
specifier: ^24.3.6
|
||||
version: 24.7.0(@typescript-eslint/eslint-plugin@5.62.0)(eslint@7.32.0)(typescript@4.9.5)
|
||||
@ -1134,6 +1134,9 @@ importers:
|
||||
'@nextui-org/shared-utils':
|
||||
specifier: workspace:*
|
||||
version: link:../../utilities/shared-utils
|
||||
'@nextui-org/use-callback-ref':
|
||||
specifier: workspace:*
|
||||
version: link:../../hooks/use-callback-ref
|
||||
'@nextui-org/use-safe-layout-effect':
|
||||
specifier: workspace:*
|
||||
version: link:../../hooks/use-safe-layout-effect
|
||||
@ -1192,6 +1195,9 @@ importers:
|
||||
react-dom:
|
||||
specifier: ^18.2.0
|
||||
version: 18.2.0(react@18.2.0)
|
||||
react-hook-form:
|
||||
specifier: ^7.51.3
|
||||
version: 7.51.3(react@18.2.0)
|
||||
|
||||
packages/components/chip:
|
||||
dependencies:
|
||||
@ -15962,7 +15968,7 @@ packages:
|
||||
dependencies:
|
||||
confusing-browser-globals: 1.0.11
|
||||
eslint: 7.32.0
|
||||
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-typescript@2.7.1)(eslint@7.32.0)
|
||||
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-typescript@3.6.1)(eslint@7.32.0)
|
||||
object.assign: 4.1.5
|
||||
object.entries: 1.1.8
|
||||
dev: true
|
||||
@ -15995,7 +16001,7 @@ packages:
|
||||
dependencies:
|
||||
eslint: 7.32.0
|
||||
eslint-config-airbnb-base: 14.2.1(eslint-plugin-import@2.29.1)(eslint@7.32.0)
|
||||
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-typescript@2.7.1)(eslint@7.32.0)
|
||||
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-typescript@3.6.1)(eslint@7.32.0)
|
||||
eslint-plugin-jsx-a11y: 6.8.0(eslint@7.32.0)
|
||||
eslint-plugin-react: 7.34.1(eslint@7.32.0)
|
||||
eslint-plugin-react-hooks: 4.6.0(eslint@7.32.0)
|
||||
@ -16018,7 +16024,7 @@ packages:
|
||||
eslint: 7.32.0
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@7.32.0)
|
||||
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-typescript@2.7.1)(eslint@7.32.0)
|
||||
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-typescript@3.6.1)(eslint@7.32.0)
|
||||
eslint-plugin-jsx-a11y: 6.8.0(eslint@7.32.0)
|
||||
eslint-plugin-react: 7.34.1(eslint@7.32.0)
|
||||
eslint-plugin-react-hooks: 4.6.0(eslint@7.32.0)
|
||||
@ -16067,7 +16073,7 @@ packages:
|
||||
confusing-browser-globals: 1.0.11
|
||||
eslint: 7.32.0
|
||||
eslint-plugin-flowtype: 5.10.0(eslint@7.32.0)
|
||||
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-typescript@2.7.1)(eslint@7.32.0)
|
||||
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-typescript@3.6.1)(eslint@7.32.0)
|
||||
eslint-plugin-jest: 24.7.0(@typescript-eslint/eslint-plugin@5.62.0)(eslint@7.32.0)(typescript@4.9.5)
|
||||
eslint-plugin-jsx-a11y: 6.8.0(eslint@7.32.0)
|
||||
eslint-plugin-react: 7.34.1(eslint@7.32.0)
|
||||
@ -16108,7 +16114,7 @@ packages:
|
||||
dependencies:
|
||||
debug: 4.3.4
|
||||
eslint: 7.32.0
|
||||
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-typescript@2.7.1)(eslint@7.32.0)
|
||||
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-typescript@3.6.1)(eslint@7.32.0)
|
||||
glob: 7.2.3
|
||||
is-glob: 4.0.3
|
||||
resolve: 1.22.8
|
||||
@ -16128,7 +16134,7 @@ packages:
|
||||
enhanced-resolve: 5.16.0
|
||||
eslint: 7.32.0
|
||||
eslint-module-utils: 2.8.1(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@7.32.0)
|
||||
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-typescript@2.7.1)(eslint@7.32.0)
|
||||
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-typescript@3.6.1)(eslint@7.32.0)
|
||||
fast-glob: 3.3.2
|
||||
get-tsconfig: 4.7.3
|
||||
is-core-module: 2.13.1
|
||||
@ -16239,7 +16245,7 @@ packages:
|
||||
string-natural-compare: 3.0.1
|
||||
dev: true
|
||||
|
||||
/eslint-plugin-import@2.29.1(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-typescript@2.7.1)(eslint@7.32.0):
|
||||
/eslint-plugin-import@2.29.1(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-typescript@3.6.1)(eslint@7.32.0):
|
||||
resolution: {integrity: sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==}
|
||||
engines: {node: '>=4'}
|
||||
peerDependencies:
|
||||
@ -23349,6 +23355,15 @@ packages:
|
||||
resolution: {integrity: sha512-nQTTcUu+ATDbrSD1BZHr5kgSD4oF8OFjxun8uAaL8RwPBacGBNPf/yAuVVdx17N8XNzRDMrZ9XcKZHCjPW+9ew==}
|
||||
dev: true
|
||||
|
||||
/react-hook-form@7.51.3(react@18.2.0):
|
||||
resolution: {integrity: sha512-cvJ/wbHdhYx8aviSWh28w9ImjmVsb5Y05n1+FW786vEZQJV5STNM0pW6ujS+oiBecb0ARBxJFyAnXj9+GHXACQ==}
|
||||
engines: {node: '>=12.22.0'}
|
||||
peerDependencies:
|
||||
react: ^18.2.0
|
||||
dependencies:
|
||||
react: 18.2.0
|
||||
dev: true
|
||||
|
||||
/react-icons@4.12.0(react@18.2.0):
|
||||
resolution: {integrity: sha512-IBaDuHiShdZqmfc/TwHu6+d6k2ltNCf3AszxNmjJc1KUfXdEeRJOKyNvLmAHaarhzGmTSVygNdyu8/opXv2gaw==}
|
||||
peerDependencies:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user