diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/user-input.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/user-input.tsx index 981dc9d0e58..b50580143e6 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/user-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/user-input.tsx @@ -361,6 +361,7 @@ export function UserInput({ } catch { // Invalid JSON — ignore } + textareaRef.current?.focus() return } const resourceJson = e.dataTransfer.getData(SIM_RESOURCE_DRAG_TYPE) @@ -374,11 +375,13 @@ export function UserInput({ } catch { // Invalid JSON — ignore } + textareaRef.current?.focus() return } filesRef.current.handleDrop(e) + requestAnimationFrame(() => textareaRef.current?.focus()) }, - [handleResourceSelect] + [handleResourceSelect, textareaRef] ) const handleDragEnter = useCallback((e: React.DragEvent) => { @@ -407,10 +410,11 @@ export function UserInput({ }, [isSending, textareaRef]) useEffect(() => { - if (isInitialView) { + const raf = window.requestAnimationFrame(() => { textareaRef.current?.focus() - } - }, [isInitialView, textareaRef]) + }) + return () => window.cancelAnimationFrame(raf) + }, [textareaRef]) const handleContainerClick = useCallback( (e: React.MouseEvent) => { 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 33144a19d23..5cd47d45af4 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 @@ -94,7 +94,6 @@ export function RenameDocumentModal({ placeholder='Enter document name' className={cn(error && 'border-[var(--text-error)]')} disabled={isSubmitting} - autoFocus maxLength={255} autoComplete='off' autoCorrect='off' diff --git a/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/create-schedule-modal/schedule-modal.tsx b/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/create-schedule-modal/schedule-modal.tsx index 50806f02a0a..9a3f5d27674 100644 --- a/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/create-schedule-modal/schedule-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/create-schedule-modal/schedule-modal.tsx @@ -324,7 +324,6 @@ export function ScheduleModal({ open, onOpenChange, workspaceId, schedule }: Sch }} placeholder='e.g., Daily report generation' className='h-9' - autoFocus autoComplete='off' /> diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/api-keys/components/create-api-key-modal/create-api-key-modal.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/api-keys/components/create-api-key-modal/create-api-key-modal.tsx index 5b6e32aa1c3..269c883ebb0 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/api-keys/components/create-api-key-modal/create-api-key-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/api-keys/components/create-api-key-modal/create-api-key-modal.tsx @@ -168,7 +168,6 @@ export function CreateApiKeyModal({ }} placeholder='e.g., Development, Production' className='h-9' - autoFocus name='api_key_label' autoComplete='off' autoCorrect='off' diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/byok/byok.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/byok/byok.tsx index 4ae4547a869..b674f385747 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/byok/byok.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/byok/byok.tsx @@ -354,7 +354,6 @@ export function BYOK() { }} placeholder={PROVIDERS.find((p) => p.id === editingProvider)?.placeholder} className='h-9 pr-9' - autoFocus name='byok_api_key' autoComplete='off' autoCorrect='off' diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/copilot/copilot.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/copilot/copilot.tsx index 1d8527f7313..cb14534f5e7 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/copilot/copilot.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/copilot/copilot.tsx @@ -279,7 +279,6 @@ export function Copilot() { }} placeholder='e.g., Development, Production' className='h-9' - autoFocus /> {createError && (

{createError}

diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/inbox/inbox-enable-toggle.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/inbox/inbox-enable-toggle.tsx index dbc4627e5d6..6b63f36ca41 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/inbox/inbox-enable-toggle.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/inbox/inbox-enable-toggle.tsx @@ -95,7 +95,6 @@ export function InboxEnableToggle() { onChange={(e) => setEnableUsername(e.target.value)} placeholder='e.g., acme' className='h-9' - autoFocus />

Leave blank for an auto-generated address. diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/inbox/inbox-settings-tab.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/inbox/inbox-settings-tab.tsx index 93bcf2f98a1..6ba91d7aa3b 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/inbox/inbox-settings-tab.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/inbox/inbox-settings-tab.tsx @@ -250,7 +250,6 @@ export function InboxSettingsTab() { }} placeholder='user@example.com' className='h-9' - autoFocus />

@@ -304,7 +303,6 @@ export function InboxSettingsTab() { }} placeholder='e.g., new-acme' className='h-9' - autoFocus /> {editAddressError && (

diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/integrations/integrations-manager.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/integrations/integrations-manager.tsx index b62fa6a093b..5fbe4188648 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/integrations/integrations-manager.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/integrations/integrations-manager.tsx @@ -10,6 +10,7 @@ import { Badge, Button, Combobox, + focusFirstTextInputIn, Input, Label, Modal, @@ -84,6 +85,15 @@ export function IntegrationsManager() { const [selectedDescriptionDraft, setSelectedDescriptionDraft] = useState('') const [selectedDisplayNameDraft, setSelectedDisplayNameDraft] = useState('') const [createStep, setCreateStep] = useState<1 | 2>(1) + const createModalContentRef = useRef(null) + + useEffect(() => { + if (!showCreateModal || createStep !== 2) return + const id = window.setTimeout(() => { + focusFirstTextInputIn(createModalContentRef.current) + }, 0) + return () => window.clearTimeout(id) + }, [showCreateModal, createStep]) const [serviceSearch, setServiceSearch] = useState('') const [copyIdSuccess, setCopyIdSuccess] = useState(false) const [credentialToDelete, setCredentialToDelete] = useState(null) @@ -769,7 +779,7 @@ export function IntegrationsManager() { if (!open) resetCreateForm() }} > - + {createStep === 1 ? ( <> Connect Integration @@ -785,7 +795,6 @@ export function IntegrationsManager() { value={serviceSearch} onChange={(e) => setServiceSearch(e.target.value)} className='h-auto flex-1 border-0 bg-transparent p-0 font-base leading-none placeholder:text-[var(--text-tertiary)] focus-visible:ring-0 focus-visible:ring-offset-0' - autoFocus />

@@ -916,7 +925,6 @@ export function IntegrationsManager() { autoComplete='off' data-lpignore='true' className='mt-1.5' - autoFocus />
@@ -1053,7 +1061,6 @@ export function IntegrationsManager() { 'min-h-[120px] resize-none border-0 font-mono text-[12px]', saDragActive && 'opacity-30' )} - autoFocus />
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx index da51910789b..d7232aa8cd8 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx @@ -439,6 +439,17 @@ export const Panel = memo(function Panel({ workspaceId: propWorkspaceId }: Panel return () => window.removeEventListener('mothership-send-message', handler) }, [setActiveTab, copilotSendMessage]) + useEffect(() => { + if (activeTab !== 'copilot') return + const id = window.setTimeout(() => { + const textarea = document.querySelector( + "[data-tab-content='copilot'] textarea" + ) + textarea?.focus() + }, 0) + return () => window.clearTimeout(id) + }, [activeTab]) + /** * Handles tab click events */ diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/invite-modal/invite-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/invite-modal/invite-modal.tsx index ef7297e96be..19fe82bb06d 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/invite-modal/invite-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/invite-modal/invite-modal.tsx @@ -529,7 +529,6 @@ export function InviteModal({ open, onOpenChange, workspaceName }: InviteModalPr : 'Enter emails' } placeholderWithTags='Add email' - autoFocus={userPerms.canAdmin} disabled={isSubmitting || !userPerms.canAdmin} fileInputOptions={fileInputOptions} /> diff --git a/apps/sim/components/emcn/components/index.ts b/apps/sim/components/emcn/components/index.ts index f7a6fe5c82d..c9148935ac0 100644 --- a/apps/sim/components/emcn/components/index.ts +++ b/apps/sim/components/emcn/components/index.ts @@ -61,6 +61,7 @@ export { FormField, type FormFieldProps } from './form-field/form-field' export { Input, type InputProps, inputVariants } from './input/input' export { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot } from './input-otp/input-otp' export { Label } from './label/label' +export { focusFirstTextInput, focusFirstTextInputIn } from './modal/auto-focus' export { MODAL_SIZES, Modal, diff --git a/apps/sim/components/emcn/components/modal/auto-focus.ts b/apps/sim/components/emcn/components/modal/auto-focus.ts new file mode 100644 index 00000000000..8cd4db32b71 --- /dev/null +++ b/apps/sim/components/emcn/components/modal/auto-focus.ts @@ -0,0 +1,58 @@ +/** + * Default `onOpenAutoFocus` handler for emcn modals. + * + * Radix's native behavior focuses the first focusable descendant — usually the close + * button in `ModalHeader`. We instead focus the first visible text-entry control + * (input/textarea/contenteditable) inside the dialog, with the caret at the end. + * + * If no such control exists, we let Radix's default behavior run by not calling + * `preventDefault()`. + */ + +const TEXT_INPUT_SELECTOR = [ + 'input:not([type="hidden"]):not([type="checkbox"]):not([type="radio"])' + + ':not([type="button"]):not([type="submit"]):not([type="reset"])' + + ':not([disabled]):not([readonly]):not([tabindex="-1"])', + 'textarea:not([disabled]):not([readonly]):not([tabindex="-1"])', + '[contenteditable="true"]:not([tabindex="-1"])', + '[contenteditable=""]:not([tabindex="-1"])', +].join(',') + +function isVisible(el: HTMLElement): boolean { + return el.offsetParent !== null || el.getClientRects().length > 0 +} + +/** + * Focus the first visible text-entry input within `root`, placing caret at end. + * Returns true if an element was focused. + */ +export function focusFirstTextInputIn(root: HTMLElement | null): boolean { + if (!root) return false + const target = Array.from(root.querySelectorAll(TEXT_INPUT_SELECTOR)).find(isVisible) + if (!target) return false + + target.focus({ preventScroll: false }) + + if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement) { + const end = target.value.length + try { + target.setSelectionRange(end, end) + } catch { + // Some input types (number, email, etc.) reject setSelectionRange — ignore. + } + } else if (target.isContentEditable) { + const range = document.createRange() + range.selectNodeContents(target) + range.collapse(false) + const sel = window.getSelection() + sel?.removeAllRanges() + sel?.addRange(range) + } + return true +} + +export function focusFirstTextInput(event: Event): void { + if (focusFirstTextInputIn(event.currentTarget as HTMLElement | null)) { + event.preventDefault() + } +} diff --git a/apps/sim/components/emcn/components/modal/modal.tsx b/apps/sim/components/emcn/components/modal/modal.tsx index 91fc76ebd93..dcca3f3cb69 100644 --- a/apps/sim/components/emcn/components/modal/modal.tsx +++ b/apps/sim/components/emcn/components/modal/modal.tsx @@ -43,6 +43,7 @@ import { X } from 'lucide-react' import { usePathname } from 'next/navigation' import { cn } from '@/lib/core/utils/cn' import { Button } from '../button/button' +import { focusFirstTextInput, focusFirstTextInputIn } from './auto-focus' /** * Shared animation classes for modal transitions. @@ -137,58 +138,64 @@ export interface ModalContentProps const ModalContent = React.forwardRef< React.ElementRef, ModalContentProps ->(({ className, children, showClose = true, size = 'md', style, ...props }, ref) => { - const [isInteractionReady, setIsInteractionReady] = React.useState(false) - const pathname = usePathname() - const isWorkflowPage = pathname?.includes('/w/') ?? false - - React.useEffect(() => { - const timer = setTimeout(() => setIsInteractionReady(true), 100) - return () => clearTimeout(timer) - }, []) - - return ( - - -
- { - if (!isInteractionReady) { - e.preventDefault() - return - } - e.stopPropagation() +>( + ( + { className, children, showClose = true, size = 'md', style, onOpenAutoFocus, ...props }, + ref + ) => { + const [isInteractionReady, setIsInteractionReady] = React.useState(false) + const pathname = usePathname() + const isWorkflowPage = pathname?.includes('/w/') ?? false + + React.useEffect(() => { + const timer = setTimeout(() => setIsInteractionReady(true), 100) + return () => clearTimeout(timer) + }, []) + + return ( + + +
{ - e.stopPropagation() - }} - onPointerUp={(e) => { - e.stopPropagation() - }} - {...props} > - {children} - -
-
- ) -}) + { + if (!isInteractionReady) { + e.preventDefault() + return + } + e.stopPropagation() + }} + onPointerDown={(e) => { + e.stopPropagation() + }} + onPointerUp={(e) => { + e.stopPropagation() + }} + onOpenAutoFocus={onOpenAutoFocus ?? focusFirstTextInput} + {...props} + > + {children} + +
+
+ ) + } +) ModalContent.displayName = 'ModalContent' @@ -247,7 +254,27 @@ ModalDescription.displayName = 'ModalDescription' /** * Modal tabs root component. Wraps tab list and content panels. */ -const ModalTabs = TabsPrimitive.Root +const ModalTabs = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ onValueChange, ...props }, ref) => { + const rootRef = React.useRef(null) + React.useImperativeHandle(ref, () => rootRef.current as HTMLDivElement, []) + + const handleValueChange = (value: string) => { + onValueChange?.(value) + window.requestAnimationFrame(() => { + const root = rootRef.current + if (!root) return + const panel = root.querySelector('[role="tabpanel"][data-state="active"]') + focusFirstTextInputIn(panel) + }) + } + + return +}) + +ModalTabs.displayName = 'ModalTabs' interface ModalTabsListProps extends React.ComponentPropsWithoutRef { /** Currently active tab value for indicator positioning */ @@ -346,6 +373,10 @@ ModalTabsTrigger.displayName = 'ModalTabsTrigger' /** * Modal tab content component. Content panel for each tab. * Includes bottom padding for consistent spacing across all tabbed modals. + * + * When this panel mounts (i.e. its tab becomes active), focus moves to the first + * visible text-entry input inside it so typing works immediately. Tabs with no + * text input are untouched. */ const ModalTabsContent = React.forwardRef< React.ElementRef, diff --git a/apps/sim/components/emcn/components/s-modal/s-modal.tsx b/apps/sim/components/emcn/components/s-modal/s-modal.tsx index 9ed6172c27d..face5ea02e3 100644 --- a/apps/sim/components/emcn/components/s-modal/s-modal.tsx +++ b/apps/sim/components/emcn/components/s-modal/s-modal.tsx @@ -30,6 +30,7 @@ import * as TabsPrimitive from '@radix-ui/react-tabs' import { X } from 'lucide-react' import { cn } from '@/lib/core/utils/cn' import { Button } from '../button/button' +import { focusFirstTextInput } from '../modal/auto-focus' import { Modal, type ModalContentProps, ModalOverlay, ModalPortal } from '../modal/modal' const ANIMATION_CLASSES = @@ -59,7 +60,7 @@ const SModalClose = DialogPrimitive.Close const SModalContent = React.forwardRef< React.ElementRef, ModalContentProps ->(({ className, children, style, ...props }, ref) => { +>(({ className, children, style, onOpenAutoFocus, ...props }, ref) => { const [isInteractionReady, setIsInteractionReady] = React.useState(false) React.useEffect(() => { @@ -95,6 +96,7 @@ const SModalContent = React.forwardRef< onPointerUp={(e) => { e.stopPropagation() }} + onOpenAutoFocus={onOpenAutoFocus ?? focusFirstTextInput} {...props} > {children}