diff --git a/.changeset/fix-combobox-enter-form-submit.md b/.changeset/fix-combobox-enter-form-submit.md new file mode 100644 index 000000000..e403a5421 --- /dev/null +++ b/.changeset/fix-combobox-enter-form-submit.md @@ -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. diff --git a/.cursor/rules/changeset.mdc b/.cursor/rules/changeset.mdc new file mode 100644 index 000000000..61a154d4b --- /dev/null +++ b/.cursor/rules/changeset.mdc @@ -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. diff --git a/.cursor/rules/commit-changes.mdc b/.cursor/rules/commit-changes.mdc new file mode 100644 index 000000000..e970a8f85 --- /dev/null +++ b/.cursor/rules/commit-changes.mdc @@ -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. diff --git a/src/components/Root.tsx b/src/components/Root.tsx index 856693f4a..df63f17cb 100644 --- a/src/components/Root.tsx +++ b/src/components/Root.tsx @@ -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'; @@ -132,6 +133,7 @@ export function Root(allProps: CubeRootProps) { ', () => { }); }); + it('should allow form submission with single Enter press when allowsCustomValue and no matches', async () => { + const onSubmit = jest.fn(); + const { getByRole, formInstance } = renderWithForm( + + {items.map((item) => ( + {item.children} + ))} + , + { 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(); diff --git a/src/components/fields/ComboBox/ComboBox.tsx b/src/components/fields/ComboBox/ComboBox.tsx index eed6cd9ef..57265d244 100644 --- a/src/components/fields/ComboBox/ComboBox.tsx +++ b/src/components/fields/ComboBox/ComboBox.tsx @@ -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(); + } } else { + e.preventDefault(); onSelectionChange(null); } diff --git a/src/version.ts b/src/version.ts index 250a20c5e..4a9e9d194 100644 --- a/src/version.ts +++ b/src/version.ts @@ -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 };