Skip to content

Commit bc4f994

Browse files
committed
improvement(ui): standardize modal default actions
1 parent 1aa714c commit bc4f994

37 files changed

Lines changed: 1173 additions & 501 deletions

File tree

.claude/rules/emcn-components.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,16 @@ The menu surface intentionally diverges from the pill: `dropdown-menu.tsx` items
3333
- **`ChipTimePicker`** — minute-granular time sibling of `ChipDatePicker`, a `ChipInput` that leniently parses typed input (`9:47`, `947`, `2:05pm`, `14:30`), commits on Enter/blur, and re-renders the canonical `9:47 AM` label.
3434
- **`DropdownMenu`** — the canonical context/action menu (Radix-backed). Not a chip, but the standard menu for command/action lists; reach for it instead of a hand-rolled popover. Its surface intentionally diverges from the chip pill (`text-small`, `gap-2`) — keep them distinct. For a pill that opens a value picker, use `ChipDropdown`/`ChipSelect` instead.
3535

36+
## Modal keyboard defaults
37+
38+
Declare keyboard intent on the action-owning primitive; never add document-level or per-callsite Enter listeners.
39+
40+
- `ChipModalFooter` defaults to `defaultAction='primary'`. A plain Enter in a canonical single-line field or a custom plain input invokes the enabled primary action. Use `'none'` when submission must require an explicit click, such as an irreversible destructive action or an editor whose nested control owns Enter. Use `'dismiss'` only when dismissal is genuinely the modal's default decision.
41+
- `ChipConfirmModal` fails safe with `defaultAction='dismiss'`. Opt into `'confirm'` only for an audited, reversible or non-destructive decision. Use `'none'` for typed confirmations and severe account, ownership, or access changes. Button color never determines keyboard behavior.
42+
- Textareas, native forms, buttons, links, comboboxes, menus, listboxes, tag/email inputs, IME composition, modified Enter, and disabled or pending actions retain their native behavior. A native form remains the sole submission path so browser validation is not bypassed.
43+
- A custom field containing a search, token editor, or another input that owns Enter must set `submitOnEnter={false}` on `ChipModalField`. Do not attach a duplicate `onKeyDown` handler merely to call the footer action.
44+
- Initial focus goes to the first visible editable text control. With no text control, the declared real button receives focus; `'none'` focuses the dialog surface. A safe dismiss default never turns Enter in a text field into data loss—the field simply does not publish a submit action.
45+
3646
## Authoring principles
3747

3848
- **One source of truth for shared chrome.** Compose from `chip-chrome.ts` / `chipVariants`; never duplicate the chrome string.

apps/sim/app/(auth)/login/login-form.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -464,9 +464,6 @@ export default function LoginPage({
464464
title='Email'
465465
value={forgotPasswordEmail}
466466
onChange={(value) => setForgotPasswordEmail(value)}
467-
onSubmit={() => {
468-
if (!isSubmittingReset) void handleForgotPassword()
469-
}}
470467
required
471468
placeholder='you@example.com'
472469
/>

apps/sim/app/(landing)/components/auth-modal/auth-modal.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,13 @@ const FALLBACK_STATUS: ProviderStatus = {
4848
const SOCIAL_BTN =
4949
'relative flex h-[32px] w-full items-center justify-center rounded-[5px] border border-[var(--border-1)] text-[13.5px] text-[var(--text-primary)] transition-colors hover:bg-[var(--surface-hover)] disabled:cursor-not-allowed disabled:opacity-50'
5050

51+
/** Auth providers are peer choices, so opening the dialog must not arm one or dismissal. */
52+
function focusAuthDialog(event: Event): void {
53+
event.preventDefault()
54+
const content = event.currentTarget as HTMLElement | null
55+
content?.focus()
56+
}
57+
5158
function fetchProviderStatus(): Promise<ProviderStatus> {
5259
if (fetchPromise) return fetchPromise
5360
fetchPromise = requestJson(getAuthProvidersContract, {})
@@ -155,7 +162,11 @@ export function AuthModal({ children, defaultView = 'login', source }: AuthModal
155162
return (
156163
<Modal open={open} onOpenChange={handleOpenChange}>
157164
<ModalTrigger asChild>{children}</ModalTrigger>
158-
<ModalContent size='sm' className='dark bg-[var(--bg)] text-[var(--text-primary)]'>
165+
<ModalContent
166+
size='sm'
167+
className='dark bg-[var(--bg)] text-[var(--text-primary)]'
168+
onOpenAutoFocus={focusAuthDialog}
169+
>
159170
<ModalTitle className='sr-only'>
160171
{effectiveView === 'login' ? 'Log in' : 'Create account'}
161172
</ModalTitle>

apps/sim/app/workspace/[workspaceId]/files/components/delete-confirm-modal/delete-confirm-modal.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export const DeleteConfirmModal = memo(function DeleteConfirmModal({
3939
onOpenChange={onOpenChange}
4040
srTitle={title}
4141
title={title}
42+
defaultAction={totalCount === 1 && !hasFolders ? 'confirm' : 'dismiss'}
4243
text={[
4344
'Are you sure you want to delete ',
4445
fileName

apps/sim/app/workspace/[workspaceId]/files/components/share-modal/share-modal.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,7 @@ export function ShareModal({
252252
</ChipModalBody>
253253
<ChipModalFooter
254254
onCancel={handleClose}
255+
defaultAction={isUnshareAction ? 'none' : 'primary'}
255256
secondaryActions={
256257
saved?.isActive && saved.url
257258
? [

apps/sim/app/workspace/[workspaceId]/files/files.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2297,6 +2297,7 @@ export function Files() {
22972297
open={Boolean(extractTarget)}
22982298
onOpenChange={(open) => !open && setExtractTargetId(null)}
22992299
title='Unzip archive?'
2300+
defaultAction='confirm'
23002301
text={[
23012302
'This will unzip ',
23022303
{ text: extractTarget?.name ?? 'this archive', bold: true },

apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/document-tags-modal/document-tags-modal.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,7 @@ export function DocumentTagsModal({
381381
<ChipModalHeader onClose={() => handleClose(false)}>Document Tags</ChipModalHeader>
382382

383383
<ChipModalBody>
384-
<ChipModalField type='custom' title='Tags'>
384+
<ChipModalField type='custom' title='Tags' submitOnEnter={false}>
385385
<div className='space-y-2'>
386386
{documentTags.map((tag, index) => (
387387
<div key={tag.displayName} className='space-y-2'>
@@ -737,6 +737,7 @@ export function DocumentTagsModal({
737737

738738
<ChipModalFooter
739739
onCancel={() => handleClose(false)}
740+
defaultAction='none'
740741
primaryAction={{ label: 'Close', onClick: () => handleClose(false) }}
741742
/>
742743
</ChipModal>

apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1372,6 +1372,7 @@ export function KnowledgeBase({
13721372
onOpenChange={setShowDeleteDialog}
13731373
srTitle='Delete Knowledge Base'
13741374
title='Delete Knowledge Base'
1375+
defaultAction='confirm'
13751376
text={[
13761377
'Are you sure you want to delete ',
13771378
{ text: knowledgeBaseName, bold: true },

apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/base-tags-modal/base-tags-modal.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,7 @@ export function BaseTagsModal({ open, onOpenChange, knowledgeBaseId }: BaseTagsM
248248
<ChipModalBody>
249249
<ChipModalField
250250
type='custom'
251+
submitOnEnter={false}
251252
title={
252253
<>
253254
Tags:{' '}
@@ -389,6 +390,7 @@ export function BaseTagsModal({ open, onOpenChange, knowledgeBaseId }: BaseTagsM
389390

390391
<ChipModalFooter
391392
onCancel={() => handleClose(false)}
393+
defaultAction='none'
392394
primaryAction={{ label: 'Close', onClick: () => handleClose(false) }}
393395
/>
394396
</ChipModal>

apps/sim/app/workspace/[workspaceId]/knowledge/components/delete-knowledge-base-modal/delete-knowledge-base-modal.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export const DeleteKnowledgeBaseModal = memo(function DeleteKnowledgeBaseModal({
4343
onOpenChange={onClose}
4444
srTitle='Delete Knowledge Base'
4545
title='Delete Knowledge Base'
46+
defaultAction='confirm'
4647
text={
4748
knowledgeBaseName
4849
? [

0 commit comments

Comments
 (0)