Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
32d08d2
fix(input): pass domRef?.current?.value to controlled state
wingkwong Mar 28, 2024
f9b7911
fix(input): pass domRef?.current?.value to useTextField instead
wingkwong Mar 28, 2024
d9dbf2c
fix(checkbox): handle RHF case
wingkwong Mar 30, 2024
3862689
fix(checkbox): add missing isSelected case
wingkwong Mar 30, 2024
89c9a10
chore(checkbox): update ref type
wingkwong Mar 30, 2024
1989d8b
chore(deps): add @nextui-org/use-safe-layout-effect
wingkwong Mar 30, 2024
acdeccd
chore(deps): update pnpm-lock.yaml
wingkwong Mar 30, 2024
0b5e526
chore(deps): update pnpm-lock.yaml
wingkwong Mar 30, 2024
090106a
fix(select): handle RHF case
wingkwong Mar 30, 2024
f671842
chore(deps): add @nextui-org/use-safe-layout-effect to select
wingkwong Mar 30, 2024
c081fc8
fix(autocomplete): handle RHF case
wingkwong Mar 30, 2024
7a28829
chore(deps): add @nextui-org/use-safe-layout-effect to autocomplete
wingkwong Mar 30, 2024
045f47b
refactor(components): revise comments
wingkwong Mar 31, 2024
f26fe94
Merge branch 'v.2.3.0' into fix/react-hook-form
wingkwong Apr 2, 2024
ad00047
Merge branch 'v.2.3.0' into fix/react-hook-form
wingkwong Apr 5, 2024
f8c21b0
feat(changeset): react-hook-form uncontrolled components
wingkwong Apr 5, 2024
c36d759
Merge branch 'v.2.3.0' into pr/2603
wingkwong Apr 10, 2024
4e5b15e
Merge branch 'v.2.3.0' into pr/2603
wingkwong Apr 15, 2024
5511e4d
chore(deps): pnpm-lock.yaml
wingkwong Apr 15, 2024
b6def5c
fix(input): domRef.current.value has higher precedence
wingkwong Apr 15, 2024
158cd3a
fix(checkbox): set isChecked based on input ref checked
wingkwong Apr 15, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/many-ways-laugh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@nextui-org/autocomplete": patch
"@nextui-org/checkbox": patch
"@nextui-org/input": patch
"@nextui-org/select": patch
---

Fixed react-hook-form uncontrolled components (#1969)
1 change: 1 addition & 0 deletions packages/components/autocomplete/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"@nextui-org/button": "workspace:*",
"@nextui-org/use-aria-button": "workspace:*",
"@nextui-org/shared-icons": "workspace:*",
"@nextui-org/use-safe-layout-effect": "workspace:*",
"@react-aria/combobox": "^3.8.4",
"@react-aria/focus": "^3.16.2",
"@react-aria/i18n": "^3.10.2",
Expand Down
16 changes: 16 additions & 0 deletions packages/components/autocomplete/src/use-autocomplete.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type {AutocompleteVariantProps, SlotsToClasses, AutocompleteSlots} from "@nextui-org/theme";

import {DOMAttributes, HTMLNextUIProps, mapPropsVariants, PropGetter} from "@nextui-org/system";
import {useSafeLayoutEffect} from "@nextui-org/use-safe-layout-effect";
import {autocomplete} from "@nextui-org/theme";
import {useFilter} from "@react-aria/i18n";
import {FilterFn, useComboBoxState} from "@react-stately/combobox";
Expand Down Expand Up @@ -298,6 +299,21 @@ export function useAutocomplete<T extends object>(originalProps: UseAutocomplete
? state.isOpen && !!state.collection.size
: state.isOpen;

// if we use `react-hook-form`, it will set the native input value using the ref in register
// i.e. setting ref.current.value to something which is uncontrolled
// hence, sync the state with `ref.current.value`
useSafeLayoutEffect(() => {
if (!inputRef.current) return;

const key = inputRef.current.value;
const item = state.collection.getItem(key);

if (item) {
state.setSelectedKey(key);
state.setInputValue(item.textValue);
}
}, [inputRef.current, state]);

// apply the same with to the popover as the select
useEffect(() => {
if (isOpen && popoverRef.current && inputWrapperRef.current) {
Expand Down
2 changes: 1 addition & 1 deletion packages/components/checkbox/__tests__/checkbox.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ describe("Checkbox", () => {
});

it("ref should be forwarded", () => {
const ref = React.createRef<HTMLLabelElement>();
const ref = React.createRef<HTMLInputElement>();

render(<Checkbox ref={ref}>Option</Checkbox>);
expect(ref.current).not.toBeNull();
Expand Down
1 change: 1 addition & 0 deletions packages/components/checkbox/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"dependencies": {
"@nextui-org/shared-utils": "workspace:*",
"@nextui-org/react-utils": "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",
Expand Down
50 changes: 38 additions & 12 deletions packages/components/checkbox/src/use-checkbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@ import {useToggleState} from "@react-stately/toggle";
import {checkbox} from "@nextui-org/theme";
import {useHover, usePress} from "@react-aria/interactions";
import {useFocusRing} from "@react-aria/focus";
import {chain, mergeProps} from "@react-aria/utils";
import {useFocusableRef} from "@nextui-org/react-utils";
import {mergeProps, chain} from "@react-aria/utils";
import {__DEV__, warn, clsx, dataAttr, safeAriaLabel} from "@nextui-org/shared-utils";
import {
useCheckbox as useReactAriaCheckbox,
useCheckboxGroupItem as useReactAriaCheckboxGroupItem,
} from "@react-aria/checkbox";
import {FocusableRef} from "@react-types/shared";
import {useSafeLayoutEffect} from "@nextui-org/use-safe-layout-effect";
import {mergeRefs} from "@nextui-org/react-utils";

import {useCheckboxGroupContext} from "./checkbox-group-context";

Expand All @@ -31,7 +31,7 @@ interface Props extends Omit<HTMLNextUIProps<"input">, keyof CheckboxVariantProp
/**
* Ref to the DOM node.
*/
ref?: Ref<HTMLLabelElement>;
ref?: Ref<HTMLInputElement>;
/**
* The label of the checkbox.
*/
Expand Down Expand Up @@ -118,8 +118,9 @@ export function useCheckbox(props: UseCheckboxProps = {}) {

const Component = as || "label";

const domRef = useRef<HTMLLabelElement>(null);

const inputRef = useRef<HTMLInputElement>(null);
const domRef = useFocusableRef(ref as FocusableRef<HTMLLabelElement>, inputRef);

// This workaround might become unnecessary once the following issue is resolved
// https://github.com/adobe/react-spectrum/issues/5693
Expand Down Expand Up @@ -235,14 +236,39 @@ 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`
useSafeLayoutEffect(() => {
if (!inputRef.current) return;
const isInputRefChecked = !!inputRef.current.checked;

setIsChecked(isInputRefChecked);
}, [inputRef.current]);

const handleCheckboxChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
if (isReadOnly || isDisabled) {
event.preventDefault();

return;
}

setIsChecked(!isChecked);
},
[isReadOnly, isDisabled, isChecked],
);

const baseStyles = clsx(classNames?.base, className);

const getBaseProps: PropGetter = useCallback(() => {
return {
ref: domRef,
className: slots.base({class: baseStyles}),
"data-disabled": dataAttr(isDisabled),
"data-selected": dataAttr(isSelected || isIndeterminate),
"data-selected": dataAttr(isSelected || isIndeterminate || isChecked),
"data-invalid": dataAttr(isInvalid),
"data-hover": dataAttr(isHovered),
"data-focus": dataAttr(isFocused),
Expand Down Expand Up @@ -282,11 +308,11 @@ export function useCheckbox(props: UseCheckboxProps = {}) {

const getInputProps: PropGetter = useCallback(() => {
return {
ref: inputRef,
...mergeProps(inputProps, focusProps),
onChange: chain(inputProps.onChange, onChange),
ref: mergeRefs(inputRef, ref),
...mergeProps(inputProps, focusProps, {checked: isChecked}),
onChange: chain(inputProps.onChange, onChange, handleCheckboxChange),
};
}, [inputProps, focusProps, onChange]);
}, [inputProps, focusProps, onChange, handleCheckboxChange]);

const getLabelProps: PropGetter = useCallback(
() => ({
Expand All @@ -299,12 +325,12 @@ export function useCheckbox(props: UseCheckboxProps = {}) {
const getIconProps = useCallback(
() =>
({
isSelected: isSelected,
isSelected: isSelected || isChecked,
isIndeterminate: !!isIndeterminate,
disableAnimation: !!disableAnimation,
className: slots.icon({class: classNames?.icon}),
} as CheckboxIconProps),
[slots, classNames?.icon, isSelected, isIndeterminate, disableAnimation],
[slots, classNames?.icon, isSelected, isIndeterminate, disableAnimation, isChecked],
);

return {
Expand Down
22 changes: 11 additions & 11 deletions packages/components/input/src/use-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,27 +115,27 @@ export function useInput<T extends HTMLInputElement | HTMLTextAreaElement = HTML
[onValueChange],
);

const [inputValue, setInputValue] = useControlledState<string>(
const [isFocusWithin, setFocusWithin] = useState(false);

const Component = as || "div";

const domRef = useDOMRef<T>(ref);
const baseDomRef = useDOMRef<HTMLDivElement>(baseRef);
const inputWrapperRef = useDOMRef<HTMLDivElement>(wrapperRef);
const innerWrapperRef = useDOMRef<HTMLDivElement>(innerWrapperRefProp);

const [inputValue, setInputValue] = useControlledState<string | undefined>(
props.value,
props.defaultValue ?? "",
handleValueChange,
);

const [isFocusWithin, setFocusWithin] = useState(false);

const Component = as || "div";

const isFilledByDefault = ["date", "time", "month", "week", "range"].includes(type!);
const isFilled = !isEmpty(inputValue) || isFilledByDefault;
const isFilledWithin = isFilled || isFocusWithin;
const baseStyles = clsx(classNames?.base, className, isFilled ? "is-filled" : "");
const isMultiline = originalProps.isMultiline;

const domRef = useDOMRef<T>(ref);
const baseDomRef = useDOMRef<HTMLDivElement>(baseRef);
const inputWrapperRef = useDOMRef<HTMLDivElement>(wrapperRef);
const innerWrapperRef = useDOMRef<HTMLDivElement>(innerWrapperRefProp);

const handleClear = useCallback(() => {
setInputValue("");

Expand All @@ -156,7 +156,7 @@ export function useInput<T extends HTMLInputElement | HTMLTextAreaElement = HTML
...originalProps,
validationBehavior: "native",
autoCapitalize: originalProps.autoCapitalize as AutoCapitalize,
value: inputValue,
value: domRef?.current?.value ?? inputValue,
"aria-label": safeAriaLabel(
originalProps?.["aria-label"],
originalProps?.label,
Expand Down
1 change: 1 addition & 0 deletions packages/components/select/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"@nextui-org/shared-utils": "workspace:*",
"@nextui-org/use-aria-button": "workspace:*",
"@nextui-org/use-aria-multiselect": "workspace:*",
"@nextui-org/use-safe-layout-effect": "workspace:*",
"@react-aria/focus": "^3.16.2",
"@react-aria/form": "^3.0.3",
"@react-aria/interactions": "^3.21.1",
Expand Down
10 changes: 10 additions & 0 deletions packages/components/select/src/use-select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
useMultiSelectState,
} from "@nextui-org/use-aria-multiselect";
import {SpinnerProps} from "@nextui-org/spinner";
import {useSafeLayoutEffect} from "@nextui-org/use-safe-layout-effect";
import {CollectionChildren} from "@react-types/shared";

export type SelectedItemProps<T = object> = {
Expand Down Expand Up @@ -251,6 +252,15 @@ export function useSelect<T extends object>(originalProps: UseSelectProps<T>) {
}),
};

// if we use `react-hook-form`, it will set the native select value using the ref in register
// i.e. setting ref.current.value to something which is uncontrolled
// hence, sync the state with `ref.current.value`
useSafeLayoutEffect(() => {
if (!domRef.current) return;

state.setSelectedKeys(new Set([...state.selectedKeys, domRef.current.value]));
}, [domRef.current]);

const {
labelProps,
triggerProps,
Expand Down
22 changes: 12 additions & 10 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.