Skip to content
Merged
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/shiny-ghosts-argue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"bako-ui": patch
---

rhf-combobox can work only with lowercase value
83 changes: 83 additions & 0 deletions packages/ui/src/components/RhfCombobox/rhf-combobox.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -253,4 +253,87 @@ describe('RhfCombobox', () => {
) as HTMLInputElement;
expect(hiddenInput?.value).toBe('banana');
});

it('converts input to lowercase when onlyLowercase is true', async () => {
const onSubmit = vi.fn();
const { user } = renderSingleCombobox(
{ onlyLowercase: true },
{ onSubmit }
);

const input = screen.getByRole('combobox');
await user.click(input);
await user.type(input, 'BaNaNa');

// Wait for the value to be normalized to lowercase letters
await waitFor(() => {
const hiddenInput = document.querySelector(
'input[type="hidden"]'
) as HTMLInputElement;
expect(hiddenInput?.value).toBe('banana');
});

await user.click(screen.getByText('Submit'));

await waitFor(() => {
expect(onSubmit).toHaveBeenCalledWith(
expect.objectContaining({ single: 'banana' }),
expect.anything()
);
});
});

it('converts selected option to lowercase when onlyLowercase is true', async () => {
const onSubmit = vi.fn();
const { user } = renderSingleCombobox(
{
onlyLowercase: true,
options: [
{ label: 'Apple', value: 'APPLE' },
{ label: 'Banana', value: 'BANANA' },
{ label: 'Cherry', value: 'CHERRY' },
],
},
{ onSubmit }
);

await user.click(screen.getByRole('combobox'));
await user.click(await screen.findByText('Banana'));

await waitFor(() => {
const hiddenInput = document.querySelector(
'input[type="hidden"]'
) as HTMLInputElement;
expect(hiddenInput?.value).toBe('banana');
});

await user.click(screen.getByText('Submit'));

await waitFor(() => {
expect(onSubmit).toHaveBeenCalledWith(
expect.objectContaining({ single: 'banana' }),
expect.anything()
);
});
});

it('converts defaultValue to lowercase when onlyLowercase is true', async () => {
renderSingleCombobox({
defaultValue: 'APPLE',
onlyLowercase: true,
});

const input = screen.getByRole('combobox');

// The input should display “apple” (converted to lowercase)
await waitFor(() => {
expect(input).toHaveValue('apple');
});

// The hidden input must also have a lowercase value
const hiddenInput = document.querySelector(
'input[type="hidden"]'
) as HTMLInputElement;
expect(hiddenInput?.value).toBe('apple');
});
});
47 changes: 31 additions & 16 deletions packages/ui/src/components/RhfCombobox/rhf-combobox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export function RhfCombobox<
clearTriggerIcon,
showTrigger = false,
allowCustomValue = true,
onlyLowercase = false,
onInputValueChange,
}: RhfComboboxProps<TFieldValues, TName>) {
const {
Expand All @@ -80,46 +81,57 @@ export function RhfCombobox<
filter: contains,
});

const [inputValue, setInputValue] = useState(value || '');
const normalizeValue = useCallback(
(val: string) => {
if (!val) return val;
return onlyLowercase ? val.toLowerCase() : val;
},
[onlyLowercase]
);

const [inputValue, setInputValue] = useState(normalizeValue(value || ''));
const [isTyping, setIsTyping] = useState(false);

useEffect(() => {
if (isTyping) return;

if (value !== inputValue) {
setInputValue(value || '');
const normalizedValue = normalizeValue(value || '');
if (normalizedValue !== inputValue) {
setInputValue(normalizedValue || '');
}
}, [value, inputValue, isTyping]);
}, [value, inputValue, isTyping, normalizeValue]);

const handleValueChange = useCallback(
(details: ComboboxValueChangeDetails<RhfComboboxOptions>) => {
const newValue = details.value[0] || '';
const normalizedValue = normalizeValue(newValue);
setIsTyping(false);
onChange(newValue);
setInputValue(newValue);
onInputValueChange?.(newValue);
onChange(normalizedValue);
setInputValue(normalizedValue);
onInputValueChange?.(normalizedValue);
},
[onChange, onInputValueChange]
[onChange, onInputValueChange, normalizeValue]
);

const handleInputValueChange = useCallback(
(details: ComboboxInputValueChangeDetails) => {
setIsTyping(true);

setInputValue(details.inputValue);
const normalizedInputValue = normalizeValue(details.inputValue);
setInputValue(normalizedInputValue);

flushSync(() => {
filter(details.inputValue);
filter(normalizedInputValue);
});

onInputValueChange?.(details.inputValue);
onInputValueChange?.(normalizedInputValue);
if (allowCustomValue) {
onChange(details.inputValue);
onChange(normalizedInputValue);
}

setTimeout(() => setIsTyping(false), 100);
},
[filter, allowCustomValue, onChange, onInputValueChange]
[filter, allowCustomValue, onChange, onInputValueChange, normalizeValue]
);

const handleOpenChange = useCallback(
Expand Down Expand Up @@ -176,7 +188,7 @@ export function RhfCombobox<
width="full"
variant={variant}
inputValue={inputValue}
value={value ? [value] : []}
value={value ? [normalizeValue(value)] : []}
borderRadius="lg"
onValueChange={handleValueChange}
onOpenChange={handleOpenChange}
Expand All @@ -187,7 +199,7 @@ export function RhfCombobox<
invalid={!!error}
allowCustomValue={allowCustomValue}
selectionBehavior="preserve"
defaultValue={[defaultValue || '']}
defaultValue={[normalizeValue(defaultValue || '')]}
placeholder={placeholder}
{...slotProps?.root}
>
Expand All @@ -210,7 +222,10 @@ export function RhfCombobox<
</Combobox.IndicatorGroup>
</Combobox.Control>

<ComboboxHiddenInput name={rest.name} value={value ?? ''} />
<ComboboxHiddenInput
name={rest.name}
value={normalizeValue(value ?? '')}
/>

{helperText && <Field.HelperText>{helperText}</Field.HelperText>}
{error?.message && <Field.ErrorText>{error.message}</Field.ErrorText>}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,5 @@ export type RhfComboboxProps<
label?: FieldLabelProps;
input?: Omit<InputProps, 'value' | 'onChange' | 'disabled' | 'type'>;
};
onlyLowercase?: boolean;
};