Skip to content

Commit 24a372e

Browse files
authored
fix: allow form submission with single Enter press in ComboBox (#884)
1 parent f1c32c6 commit 24a372e

File tree

7 files changed

+72
-8
lines changed

7 files changed

+72
-8
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@cube-dev/ui-kit": patch
3+
---
4+
5+
Fix ComboBox with `allowsCustomValue` to allow form submission with single Enter press when typing custom values that don't match any items.

.cursor/rules/changeset.mdc

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
description: When user asked to add a changeset
3+
alwaysApply: false
4+
---
5+
6+
Place the changeset to `/.changeset` folder.
7+
Use `patch` version for small fixed (even with breaking changes if they are minor). Use `minor` version for bigger features and noticeable breaking changes.
8+
Use diff of the working tree to analyze changes. If it's empty then use the diff with the `main` branch.
9+
Try to make the changeset concise.

.cursor/rules/commit-changes.mdc

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
description: When commiting the changes
3+
alwaysApply: false
4+
---
5+
6+
Use Conventional Commits convention.
7+
If changes described in changesets are isolated (For example in a single component) then use syntax with `()` like `fix(ComboBox): what exactly is fixed` and so on.
8+
The name of the commit should be as short as possible.

src/components/Root.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
import { TOKENS } from '../tokens';
1717
import { useViewportSize } from '../utils/react';
1818
import { EventBusProvider } from '../utils/react/useEventBus';
19+
import { VERSION } from '../version';
1920

2021
import { GlobalStyles } from './GlobalStyles';
2122
import { AlertDialogApiProvider } from './overlays/AlertDialog';
@@ -132,6 +133,7 @@ export function Root(allProps: CubeRootProps) {
132133
<StyleSheetManager>
133134
<RootElement
134135
ref={ref}
136+
data-tasty={VERSION}
135137
data-font-display={fontDisplay}
136138
{...filterBaseProps(props, { eventProps: true })}
137139
styles={styles}

src/components/fields/ComboBox/ComboBox.test.tsx

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,36 @@ describe('<ComboBox />', () => {
463463
});
464464
});
465465

466+
it('should allow form submission with single Enter press when allowsCustomValue and no matches', async () => {
467+
const onSubmit = jest.fn();
468+
const { getByRole, formInstance } = renderWithForm(
469+
<ComboBox allowsCustomValue name="tag" label="Tag">
470+
{items.map((item) => (
471+
<ComboBox.Item key={item.key}>{item.children}</ComboBox.Item>
472+
))}
473+
</ComboBox>,
474+
{ formProps: { onSubmit } },
475+
);
476+
477+
const combobox = getByRole('combobox');
478+
479+
// Type custom value that doesn't match any items (no popover)
480+
await userEvent.type(combobox, 'CustomValue');
481+
482+
// Wait for popover to close (if it opened during typing)
483+
await waitFor(() => {
484+
expect(combobox).toHaveAttribute('aria-expanded', 'false');
485+
});
486+
487+
// Press Enter once - should commit value AND submit form
488+
await userEvent.keyboard('{Enter}');
489+
490+
await waitFor(() => {
491+
expect(formInstance.getFieldValue('tag')).toBe('CustomValue');
492+
expect(onSubmit).toHaveBeenCalledWith({ tag: 'CustomValue' });
493+
});
494+
});
495+
466496
it('should clear invalid input on blur when clearOnBlur is true', async () => {
467497
const onSelectionChange = jest.fn();
468498

src/components/fields/ComboBox/ComboBox.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -605,15 +605,21 @@ function useComboBoxKeyboard({
605605

606606
// If no results, handle empty input or custom values
607607
if (!hasResults) {
608-
e.preventDefault();
609-
610608
if (allowsCustomValue) {
611-
const value = effectiveInputValue;
609+
const value = effectiveInputValue.trim();
612610

611+
// Commit the custom value
613612
onSelectionChange(
614613
(value as unknown as Key) ?? (null as unknown as Key),
615614
);
615+
616+
// If popover is closed and we have a value, allow Enter to propagate for form submission
617+
// If popover is open OR value is empty, prevent default to avoid premature submission
618+
if (isPopoverOpen || !value) {
619+
e.preventDefault();
620+
}
616621
} else {
622+
e.preventDefault();
617623
onSelectionChange(null);
618624
}
619625

src/version.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1-
interface Window {
2-
CubeUIKit: {
3-
version: string;
4-
};
1+
declare global {
2+
interface Window {
3+
CubeUIKit: {
4+
version: string;
5+
};
6+
}
57
}
68

9+
export const VERSION = '__UIKIT_VERSION__';
10+
711
if (typeof window !== 'undefined') {
8-
const version = '__UIKIT_VERSION__';
12+
const version = VERSION;
913

1014
// Ensure CubeUIKit is defined on window in a way bundlers recognize
1115
window.CubeUIKit = window.CubeUIKit || { version };

0 commit comments

Comments
 (0)