diff --git a/apps/sim/app/workspace/[workspaceId]/components/connect-oauth-modal/connect-oauth-modal.tsx b/apps/sim/app/workspace/[workspaceId]/components/connect-oauth-modal/connect-oauth-modal.tsx index bb9f2618984..337bb878c29 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/connect-oauth-modal/connect-oauth-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/connect-oauth-modal/connect-oauth-modal.tsx @@ -1,6 +1,6 @@ 'use client' -import { type ComponentType, type KeyboardEvent, useEffect, useMemo, useRef, useState } from 'react' +import { type ComponentType, useEffect, useMemo, useRef, useState } from 'react' import { Badge, ChipModal, @@ -309,18 +309,6 @@ export function ConnectOAuthModal(props: ConnectOAuthModalProps) { ? !displayName.trim() || isPending || Boolean(existingCredential) : isPending - /** - * Submits the connect form on Enter, mirroring the Connect button's enabled - * state and excluding the multi-line description. Restores the keyboard - * affordance the pre-consolidation workflow modal provided. - */ - const handleBodyKeyDown = (event: KeyboardEvent) => { - if (event.key !== 'Enter' || !isConnect || isDisabled) return - if (event.target instanceof HTMLTextAreaElement) return - event.preventDefault() - void handleConnect() - } - const displayNameError = validationError ?? (existingCredential @@ -334,7 +322,7 @@ export function ConnectOAuthModal(props: ConnectOAuthModalProps) { {title} - + {!isConnect && (

The "{props.toolName}" tool requires access to your account. diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/rename-document-modal/rename-document-modal.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/rename-document-modal/rename-document-modal.tsx index 5825d796b87..2018525e3c4 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/rename-document-modal/rename-document-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/rename-document-modal/rename-document-modal.tsx @@ -74,17 +74,10 @@ export function RenameDocumentModal({ } } - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Enter') { - e.preventDefault() - void handleSubmit() - } - } - return ( onOpenChange(false)}>Rename Document - + )} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/table/table.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/table/table.tsx index 80996a1adae..339c718591a 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/table/table.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/table/table.tsx @@ -97,6 +97,41 @@ function TableCell({ } } + /** + * Enter commits the current cell (values already persist on change) and + * advances to the same column in the next row, spreadsheet-style. Skipped + * while a tag/env-var dropdown is open (Enter selects an option there) or + * during IME composition. The next row already exists — it auto-appends the + * moment the last row is typed into — so focus lands on a real input. + * + * Focusing an empty cell auto-opens the tag dropdown, so the destination's + * dropdown is closed right after focusing: otherwise a follow-up Enter would + * land on that dropdown and insert a tag instead of continuing down the + * column. Clicking or typing `<` in the cell still opens it deliberately. + */ + const handleKeyDown = (e: React.KeyboardEvent) => { + handlers.onKeyDown(e) + if ( + e.key !== 'Enter' || + e.nativeEvent.isComposing || + e.defaultPrevented || + fieldState.showEnvVars || + fieldState.showTags + ) { + return + } + const nextCellKey = `${rowIndex + 1}-${column}` + const nextInput = inputRefs.current.get(nextCellKey) + // `isConnected` guards against a stale ref: position-keyed entries can + // outlive a deleted row, and focusing a detached node would steal focus + // from the current cell. A real next row's input is always connected. + if (nextInput?.isConnected) { + e.preventDefault() + nextInput.focus() + inputController.fieldHelpers.hideFieldDropdowns(nextCellKey) + } + } + const syncScrollAfterUpdate = () => { requestAnimationFrame(() => { const input = inputRefs.current.get(cellKey) @@ -138,12 +173,13 @@ function TableCell({ { if (el) inputRefs.current.set(cellKey, el) + else inputRefs.current.delete(cellKey) }} type='text' value={cellValue} placeholder={column} onChange={handlers.onChange} - onKeyDown={handlers.onKeyDown} + onKeyDown={handleKeyDown} onScroll={handleScroll} onDrop={handlers.onDrop} onDragOver={handlers.onDragOver} @@ -158,6 +194,7 @@ function TableCell({

{ if (el) overlayRefs.current.set(cellKey, el) + else overlayRefs.current.delete(cellKey) }} data-overlay={cellKey} className='scrollbar-hide pointer-events-none absolute top-0 right-[10px] bottom-0 left-[10px] overflow-x-auto overflow-y-hidden bg-transparent' diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/create-workspace-modal/create-workspace-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/create-workspace-modal/create-workspace-modal.tsx index 9aa5222c8d2..2b6fcb5dda3 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/create-workspace-modal/create-workspace-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/create-workspace-modal/create-workspace-modal.tsx @@ -69,13 +69,6 @@ export function CreateWorkspaceModal({ } } - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Enter') { - e.preventDefault() - void handleSubmit() - } - } - const handleNameChange = (value: string) => { setName(value) setError(null) @@ -86,7 +79,7 @@ export function CreateWorkspaceModal({ return ( onOpenChange(false)}>{copy.title} - +

{copy.description}

void + /** Mirrors the primary action's disabled state so Enter never submits an invalid form. */ + disabled?: boolean +} + +/** + * Carries a mutable handle to the modal's primary action down to its fields. + * A ref (not state) so the footer can keep it current without re-rendering the + * body, and fields read it at Enter-time rather than at render-time. + */ +const ChipModalSubmitContext = + React.createContext | null>(null) + export interface ChipModalProps { /** Controlled open state. */ open: boolean @@ -120,21 +140,24 @@ function ChipModal({ className, children, }: ChipModalProps) { + const submitRef = React.useRef(null) return ( - - -
-
- {children} + + + +
+
+ {children} +
-
- - + + + ) } @@ -364,7 +387,32 @@ interface ChipModalFieldBaseProps { className?: string } -interface ChipModalInputFieldProps extends ChipModalFieldBaseProps { +/** + * Enter-submit behavior shared by the single-line field types (`input`, + * `email`). Both fire the modal's {@link ChipModalFooter} primary action on + * Enter by default; these props override or opt out of that. + */ +interface ChipModalSingleLineEnterProps { + /** + * Overrides the default Enter behavior. By default, pressing Enter in a + * single-line field fires the {@link ChipModalFooter} primary action (unless + * it's disabled), so a plain modal submits on Enter with no wiring. Pass + * `onSubmit` only when Enter should do something OTHER than the primary action + * (e.g. advance a multi-step flow). + */ + onSubmit?: () => void + /** + * Opts this field out of the automatic Enter-submits-the-primary-action + * behavior. Set `false` for a config knob that lives inside a larger form + * (e.g. a "number of runs" input in a scheduling modal) where Enter firing + * the modal's primary action would submit prematurely. Ignored when an + * explicit `onSubmit` is provided. + * @default true + */ + submitOnEnter?: boolean +} + +interface ChipModalInputFieldProps extends ChipModalFieldBaseProps, ChipModalSingleLineEnterProps { type: 'input' value: string onChange: (value: string) => void @@ -380,24 +428,14 @@ interface ChipModalInputFieldProps extends ChipModalFieldBaseProps { * @default false */ mono?: boolean - /** - * Called when the user presses Enter in the field. Wire this to the - * modal's primary action so the field behaves like a form submit. - */ - onSubmit?: () => void } -interface ChipModalEmailFieldProps extends ChipModalFieldBaseProps { +interface ChipModalEmailFieldProps extends ChipModalFieldBaseProps, ChipModalSingleLineEnterProps { type: 'email' value: string onChange: (value: string) => void placeholder?: string autoComplete?: string - /** - * Called when the user presses Enter in the field. Wire this to the - * modal's primary action so the field behaves like a form submit. - */ - onSubmit?: () => void } interface ChipModalTextareaFieldBaseProps extends ChipModalFieldBaseProps { @@ -536,6 +574,7 @@ export type ChipModalFieldProps = */ function ChipModalField(props: ChipModalFieldProps) { const id = React.useId() + const submitRef = React.useContext(ChipModalSubmitContext) const errorId = `${id}-error` const hintId = `${id}-hint` const { title, required, error, hint, flush = false, className } = props @@ -559,7 +598,7 @@ function ChipModalField(props: ChipModalFieldProps) { )} - {renderChipModalControl(props, id, errorId, hintId)} + {renderChipModalControl(props, id, errorId, hintId, submitRef)} {error && props.type !== 'emails' ? (