Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(autocomplete): return value instead of label in form submission #3375

Draft
wants to merge 16 commits into
base: canary
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions .changeset/purple-pillows-beg.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@nextui-org/autocomplete": minor
---

return autocomplete value instead of label in form submission (#3353, #3343)
1 change: 1 addition & 0 deletions packages/components/autocomplete/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"@nextui-org/use-safe-layout-effect": "workspace:*",
"@react-aria/combobox": "3.9.1",
"@react-aria/focus": "3.17.1",
"@react-aria/form": "3.0.5",
"@react-aria/i18n": "3.11.1",
"@react-aria/interactions": "3.21.3",
"@react-aria/utils": "3.24.1",
Expand Down
3 changes: 3 additions & 0 deletions packages/components/autocomplete/src/autocomplete.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {ForwardedRef, ReactElement, Ref} from "react";
import {AnimatePresence} from "framer-motion";

import {UseAutocompleteProps, useAutocomplete} from "./use-autocomplete";
import {HiddenInput} from "./hidden-input";

interface Props<T> extends UseAutocompleteProps<T> {}

Expand All @@ -29,6 +30,7 @@ function Autocomplete<T extends object>(props: Props<T>, ref: ForwardedRef<HTMLI
getClearButtonProps,
getListBoxWrapperProps,
getEndContentWrapperProps,
getHiddenInputProps,
} = useAutocomplete<T>({...props, ref});

const popoverContent = isOpen ? (
Expand All @@ -43,6 +45,7 @@ function Autocomplete<T extends object>(props: Props<T>, ref: ForwardedRef<HTMLI

return (
<Component {...getBaseProps()}>
<HiddenInput {...getHiddenInputProps()} />
<Input
{...getInputProps()}
endContent={
Expand Down
82 changes: 82 additions & 0 deletions packages/components/autocomplete/src/hidden-input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import type {ComboBoxState} from "@react-stately/combobox";

import React, {ReactNode, RefObject} from "react";
import {useFormReset} from "@react-aria/utils";
import {useFormValidation} from "@react-aria/form";

import {inputData} from "./use-autocomplete";

export interface AriaHiddenInputProps {
/**
* Describes the type of autocomplete functionality the input should provide if any.
* See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#htmlattrdefautocomplete).
*/
autoComplete?: string;
/** The text label for the input. */
label?: ReactNode;
/** HTML form input name. */
name?: string;
/** Sets the disabled state of the input. */
isDisabled?: boolean;
/** Whether the input is required. */
isRequired?: boolean;
}

type NativeHTMLInputProps = Omit<
React.InputHTMLAttributes<HTMLInputElement>,
keyof AriaHiddenInputProps
>;

type CombinedAriaInputProps = NativeHTMLInputProps & AriaHiddenInputProps;

export interface HiddenInputProps<T> extends CombinedAriaInputProps {
/** State for the input. */
state: ComboBoxState<T>;
/** A ref to the hidden `<input>` element. */
inputRef?: RefObject<HTMLInputElement>;
}

export function useHiddenInput<T>(props: HiddenInputProps<T>) {
const data = inputData.get(props.state) || {};

const {
state,
autoComplete,
name = data.name,
isDisabled = data.isDisabled,
inputRef,
onChange,
} = props;

const {validationBehavior, isRequired} = data;

useFormReset(props.inputRef!, state.selectedKey, state.setSelectedKey);
useFormValidation(
{
validationBehavior,
focus: () => inputRef?.current?.focus(),
},
state,
inputRef,
);

return {
name,
ref: inputRef,
type: "hidden",
disabled: isDisabled,
required: isRequired,
autoComplete,
value: state.selectedKey ?? "",
onChange: (e: React.ChangeEvent<HTMLInputElement>) => {
state.setSelectedKey(e.target.value);
onChange?.(e);
},
};
}

export function HiddenInput<T>(props: HiddenInputProps<T>) {
const inputProps = useHiddenInput(props);

return <input {...inputProps} />;
}
61 changes: 52 additions & 9 deletions packages/components/autocomplete/src/use-autocomplete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import {mapPropsVariants, useProviderContext} 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";
import {ComboBoxState, FilterFn, useComboBoxState} from "@react-stately/combobox";
import {ReactRef, useDOMRef} from "@nextui-org/react-utils";
import {ReactNode, useEffect, useMemo, useRef} from "react";
import {ReactNode, useCallback, useEffect, useMemo, useRef} from "react";
import {ComboBoxProps} from "@react-types/combobox";
import {PopoverProps} from "@nextui-org/popover";
import {ListboxProps} from "@nextui-org/listbox";
Expand Down Expand Up @@ -111,6 +111,14 @@ interface Props<T> extends Omit<HTMLNextUIProps<"input">, keyof ComboBoxProps<T>
*/
onClose?: () => void;
}
interface InputData {
isDisabled?: boolean;
isRequired?: boolean;
name?: string;
validationBehavior?: "aria" | "native";
}

export const inputData = new WeakMap<ComboBoxState<any>, InputData>();

export type UseAutocompleteProps<T> = Props<T> &
Omit<InputProps, "children" | "value" | "isClearable" | "defaultValue" | "classNames"> &
Expand Down Expand Up @@ -234,7 +242,7 @@ export function useAutocomplete<T extends object>(originalProps: UseAutocomplete
inputProps: mergeProps(
{
label,
ref: inputRef,
// ref: inputRef,
wrapperRef: inputWrapperRef,
onClick: () => {
if (!state.isOpen && !!state.selectedItem) {
Expand Down Expand Up @@ -402,19 +410,24 @@ export function useAutocomplete<T extends object>(originalProps: UseAutocomplete
}),
} as ButtonProps);

const getInputProps = () =>
({
...otherProps,
...inputProps,
...slotsProps.inputProps,
const getInputProps = () => {
const props = mergeProps(otherProps, inputProps, slotsProps.inputProps);

// `name` will be in the hidden input
// so that users can get the value of the input instead of label in form
delete props["name"];

return {
...props,
isInvalid,
validationBehavior,
errorMessage:
typeof errorMessage === "function"
? errorMessage({isInvalid, validationErrors, validationDetails})
: errorMessage || validationErrors?.join(" "),
onClick: chain(slotsProps.inputProps.onClick, otherProps.onClick),
} as unknown as InputProps);
} as InputProps;
};

const getListBoxProps = () =>
({
Expand Down Expand Up @@ -491,6 +504,35 @@ export function useAutocomplete<T extends object>(originalProps: UseAutocomplete
}),
});

const getHiddenInputProps = useCallback(
(props = {}) => ({
state,
inputRef,
name: originalProps?.name,
isRequired: originalProps?.isRequired,
autoComplete: originalProps?.autoComplete,
isDisabled: originalProps?.isDisabled,
...props,
}),
[
state,
originalProps?.name,
originalProps?.autoComplete,
originalProps?.autoComplete,
originalProps?.isDisabled,
inputRef,
],
);

// store the data to be used in useHiddenInput
inputData.set(state, {
isDisabled: originalProps?.isDisabled,
isRequired: originalProps?.isRequired,
name: originalProps?.name,
// TODO: Future enhancement to support "aria" validation behavior.
validationBehavior: "native",
});

return {
Component,
inputRef,
Expand All @@ -515,6 +557,7 @@ export function useAutocomplete<T extends object>(originalProps: UseAutocomplete
getSelectorButtonProps,
getListBoxWrapperProps,
getEndContentWrapperProps,
getHiddenInputProps,
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -764,6 +764,28 @@ const CustomStylesWithCustomItemsTemplate = ({color, ...args}: AutocompleteProps
);
};

const WithFormTemplate = (args: AutocompleteProps) => {
const handleSubmit = (e) => {
e.preventDefault();
const name = e.target.animal.value;

// eslint-disable-next-line no-console
console.log(name);
alert("Submitted value: " + name);
};

return (
<form className="flex w-full max-w-xs flex-col gap-2" onSubmit={handleSubmit}>
<Autocomplete defaultSelectedKey="cat" label="Favorite Animal" name="animal" {...args}>
<AutocompleteItem key="cat">Big Cat</AutocompleteItem>
<AutocompleteItem key="dog">Big Dog</AutocompleteItem>
</Autocomplete>

<Button type="submit">Submit</Button>
</form>
);
};

const WithReactHookFormTemplate = (args: AutocompleteProps) => {
const {
register,
Expand Down Expand Up @@ -1004,6 +1026,14 @@ export const WithAriaLabel = {
},
};

export const WithForm = {
render: WithFormTemplate,

args: {
...defaultProps,
},
};

export const WithReactHookForm = {
render: WithReactHookFormTemplate,

Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

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

Loading