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/fix-combobox-enter-form-submit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cube-dev/ui-kit": patch
---

Fix ComboBox with `allowsCustomValue` to allow form submission with single Enter press when typing custom values that don't match any items.
9 changes: 9 additions & 0 deletions .cursor/rules/changeset.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
description: When user asked to add a changeset
alwaysApply: false
---

Place the changeset to `/.changeset` folder.
Use `patch` version for small fixed (even with breaking changes if they are minor). Use `minor` version for bigger features and noticeable breaking changes.
Use diff of the working tree to analyze changes. If it's empty then use the diff with the `main` branch.
Try to make the changeset concise.
8 changes: 8 additions & 0 deletions .cursor/rules/commit-changes.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
description: When commiting the changes
alwaysApply: false
---

Use Conventional Commits convention.
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.
The name of the commit should be as short as possible.
2 changes: 2 additions & 0 deletions src/components/Root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
import { TOKENS } from '../tokens';
import { useViewportSize } from '../utils/react';
import { EventBusProvider } from '../utils/react/useEventBus';
import { VERSION } from '../version';

import { GlobalStyles } from './GlobalStyles';
import { AlertDialogApiProvider } from './overlays/AlertDialog';
Expand Down Expand Up @@ -132,6 +133,7 @@ export function Root(allProps: CubeRootProps) {
<StyleSheetManager>
<RootElement
ref={ref}
data-tasty={VERSION}
data-font-display={fontDisplay}
{...filterBaseProps(props, { eventProps: true })}
styles={styles}
Expand Down
30 changes: 30 additions & 0 deletions src/components/fields/ComboBox/ComboBox.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,36 @@ describe('<ComboBox />', () => {
});
});

it('should allow form submission with single Enter press when allowsCustomValue and no matches', async () => {
const onSubmit = jest.fn();
const { getByRole, formInstance } = renderWithForm(
<ComboBox allowsCustomValue name="tag" label="Tag">
{items.map((item) => (
<ComboBox.Item key={item.key}>{item.children}</ComboBox.Item>
))}
</ComboBox>,
{ formProps: { onSubmit } },
);

const combobox = getByRole('combobox');

// Type custom value that doesn't match any items (no popover)
await userEvent.type(combobox, 'CustomValue');

// Wait for popover to close (if it opened during typing)
await waitFor(() => {
expect(combobox).toHaveAttribute('aria-expanded', 'false');
});

// Press Enter once - should commit value AND submit form
await userEvent.keyboard('{Enter}');

await waitFor(() => {
expect(formInstance.getFieldValue('tag')).toBe('CustomValue');
expect(onSubmit).toHaveBeenCalledWith({ tag: 'CustomValue' });
});
});

it('should clear invalid input on blur when clearOnBlur is true', async () => {
const onSelectionChange = jest.fn();

Expand Down
12 changes: 9 additions & 3 deletions src/components/fields/ComboBox/ComboBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -605,15 +605,21 @@ function useComboBoxKeyboard({

// If no results, handle empty input or custom values
if (!hasResults) {
e.preventDefault();

if (allowsCustomValue) {
const value = effectiveInputValue;
const value = effectiveInputValue.trim();

// Commit the custom value
onSelectionChange(
(value as unknown as Key) ?? (null as unknown as Key),
);

// If popover is closed and we have a value, allow Enter to propagate for form submission
// If popover is open OR value is empty, prevent default to avoid premature submission
if (isPopoverOpen || !value) {
e.preventDefault();
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Normalize empty custom input to null.

When allowsCustomValue is true and the user enters only whitespace then presses Enter, onSelectionChange is called with an empty string instead of null. The ?? operator on line 613 doesn't convert empty strings to null (only null/undefined), so onSelectionChange("") is called. This is inconsistent with how empty values are handled elsewhere in the component (e.g., lines 631-634), where null is used to represent "no selection". The code should check if the trimmed value is empty and call onSelectionChange(null) instead.

Fix in Cursor Fix in Web

} else {
e.preventDefault();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Popover: Enter Submits Form Without Focus.

When the popover is open with matching results but no item is focused, pressing Enter doesn't prevent default, causing premature form submission. The Enter handler falls through all conditions without calling e.preventDefault() when hasResults is true, the input is non-empty, and no item is focused in the list.

Fix in Cursor Fix in Web

onSelectionChange(null);
}

Expand Down
14 changes: 9 additions & 5 deletions src/version.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
interface Window {
CubeUIKit: {
version: string;
};
declare global {
interface Window {
CubeUIKit: {
version: string;
};
}
}

export const VERSION = '__UIKIT_VERSION__';

if (typeof window !== 'undefined') {
const version = '__UIKIT_VERSION__';
const version = VERSION;

// Ensure CubeUIKit is defined on window in a way bundlers recognize
window.CubeUIKit = window.CubeUIKit || { version };
Expand Down
Loading