From 95204290e410b0cc0f608291fff0e4490f2eae7d Mon Sep 17 00:00:00 2001 From: Bill Leoutsakos Date: Thu, 24 Sep 2026 12:09:49 -0700 Subject: [PATCH 1/3] Share fork picker disclosure trigger chrome --- .../fork-disclosure-trigger.test.tsx | 64 +++++++++++++++++++ .../components/fork-disclosure-trigger.tsx | 45 +++++++++++++ .../fork-excluded-workflows.tsx | 32 ++++------ .../fork-file-tree/fork-file-tree.tsx | 26 +++----- .../fork-resource-picker.tsx | 49 +++++--------- .../components/fork-sync/fork-sync-view.tsx | 31 ++++----- 6 files changed, 159 insertions(+), 88 deletions(-) create mode 100644 apps/sim/ee/workspace-forking/components/fork-disclosure-trigger.test.tsx create mode 100644 apps/sim/ee/workspace-forking/components/fork-disclosure-trigger.tsx diff --git a/apps/sim/ee/workspace-forking/components/fork-disclosure-trigger.test.tsx b/apps/sim/ee/workspace-forking/components/fork-disclosure-trigger.test.tsx new file mode 100644 index 00000000000..671f0aab144 --- /dev/null +++ b/apps/sim/ee/workspace-forking/components/fork-disclosure-trigger.test.tsx @@ -0,0 +1,64 @@ +/** + * @vitest-environment jsdom + */ +import { act, useState } from 'react' +import { createRoot, type Root } from 'react-dom/client' +import { afterEach, beforeEach, describe, expect, it } from 'vitest' +import { ForkDisclosureTrigger } from '@/ee/workspace-forking/components/fork-disclosure-trigger' + +let container: HTMLDivElement +let root: Root + +function Disclosures() { + const [resourcesOpen, setResourcesOpen] = useState(false) + const [mappingOpen, setMappingOpen] = useState(true) + + return ( + <> + setResourcesOpen((value) => !value)} + className='min-w-0 flex-1 gap-1' + labelClassName='flex-1' + /> + setMappingOpen((value) => !value)} + className='w-full gap-2' + trailing={Needs review} + /> + + ) +} + +describe('ForkDisclosureTrigger', () => { + beforeEach(() => { + container = document.createElement('div') + document.body.appendChild(container) + root = createRoot(container) + act(() => root.render()) + }) + + afterEach(() => { + act(() => root.unmount()) + container.remove() + }) + + it('keeps each native disclosure button independent and announces its current state', () => { + const [resources, mapping] = Array.from(container.querySelectorAll('button')) + expect(resources?.getAttribute('aria-expanded')).toBe('false') + expect(mapping?.getAttribute('aria-expanded')).toBe('true') + expect(resources?.textContent).toContain('Workflows (2)') + expect(mapping?.textContent).toContain('CredentialsNeeds review') + + act(() => resources?.click()) + expect(resources?.getAttribute('aria-expanded')).toBe('true') + expect(mapping?.getAttribute('aria-expanded')).toBe('true') + + act(() => mapping?.click()) + expect(resources?.getAttribute('aria-expanded')).toBe('true') + expect(mapping?.getAttribute('aria-expanded')).toBe('false') + }) +}) diff --git a/apps/sim/ee/workspace-forking/components/fork-disclosure-trigger.tsx b/apps/sim/ee/workspace-forking/components/fork-disclosure-trigger.tsx new file mode 100644 index 00000000000..a46721a39d4 --- /dev/null +++ b/apps/sim/ee/workspace-forking/components/fork-disclosure-trigger.tsx @@ -0,0 +1,45 @@ +'use client' + +import type { ReactNode } from 'react' +import { ChevronDown, cn, OverflowText } from '@sim/emcn' + +interface ForkDisclosureTriggerProps { + label: string + expanded: boolean + onToggle: () => void + className: string + labelClassName?: string + children?: ReactNode + trailing?: ReactNode +} + +/** The common label and chevron trigger used by fork picker disclosures. */ +export function ForkDisclosureTrigger({ + label, + expanded, + onToggle, + className, + labelClassName, + children, + trailing, +}: ForkDisclosureTriggerProps) { + return ( + + ) +} diff --git a/apps/sim/ee/workspace-forking/components/fork-excluded-workflows/fork-excluded-workflows.tsx b/apps/sim/ee/workspace-forking/components/fork-excluded-workflows/fork-excluded-workflows.tsx index 80d50291557..f3776f137d7 100644 --- a/apps/sim/ee/workspace-forking/components/fork-excluded-workflows/fork-excluded-workflows.tsx +++ b/apps/sim/ee/workspace-forking/components/fork-excluded-workflows/fork-excluded-workflows.tsx @@ -1,9 +1,10 @@ 'use client' import { useId, useMemo, useState } from 'react' -import { Checkbox, ChevronDown, cn, OverflowText, toast } from '@sim/emcn' +import { Checkbox, cn, OverflowText, toast } from '@sim/emcn' import { getErrorMessage } from '@sim/utils/errors' import { SettingsEmptyState } from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state' +import { ForkDisclosureTrigger } from '@/ee/workspace-forking/components/fork-disclosure-trigger' import { useUpdateForkExcludedWorkflows } from '@/ee/workspace-forking/hooks/workspace-fork' import { useFolders } from '@/hooks/queries/folders' import { useWorkflows } from '@/hooks/queries/workflows' @@ -200,26 +201,17 @@ function ExcludedFolderRow({ onCheckedChange={() => onToggle(folder.descendantWorkflowIds, headerState !== true)} disabled={disabled} /> - + {folder.name}{' '} + + ({selectedCount > 0 ? `${selectedCount}/${total}` : total}) + + {expanded ? (
diff --git a/apps/sim/ee/workspace-forking/components/fork-file-tree/fork-file-tree.tsx b/apps/sim/ee/workspace-forking/components/fork-file-tree/fork-file-tree.tsx index 88a216c5956..bb392f80e1a 100644 --- a/apps/sim/ee/workspace-forking/components/fork-file-tree/fork-file-tree.tsx +++ b/apps/sim/ee/workspace-forking/components/fork-file-tree/fork-file-tree.tsx @@ -1,7 +1,8 @@ 'use client' import { useId, useState } from 'react' -import { Checkbox, ChevronDown, cn, OverflowText } from '@sim/emcn' +import { Checkbox, cn, OverflowText } from '@sim/emcn' +import { ForkDisclosureTrigger } from '@/ee/workspace-forking/components/fork-disclosure-trigger' export interface ForkFileTreeItem { id: string @@ -132,22 +133,13 @@ function ForkFileFolderRow({ onCheckedChange={() => onToggleMany(fileIds, headerState !== true)} disabled={disabled} /> - + 0 ? `${selectedCount}/${total}` : total})`} + expanded={expanded} + onToggle={() => setExpanded((value) => !value)} + className='min-w-0 flex-1 gap-1' + labelClassName='flex-1' + />
{expanded ? (
diff --git a/apps/sim/ee/workspace-forking/components/fork-resource-picker/fork-resource-picker.tsx b/apps/sim/ee/workspace-forking/components/fork-resource-picker/fork-resource-picker.tsx index 03be8855258..466e76e5e99 100644 --- a/apps/sim/ee/workspace-forking/components/fork-resource-picker/fork-resource-picker.tsx +++ b/apps/sim/ee/workspace-forking/components/fork-resource-picker/fork-resource-picker.tsx @@ -1,7 +1,8 @@ 'use client' import { useId, useMemo, useState } from 'react' -import { Checkbox, ChevronDown, cn, OverflowText } from '@sim/emcn' +import { Checkbox, cn, OverflowText } from '@sim/emcn' +import { ForkDisclosureTrigger } from '@/ee/workspace-forking/components/fork-disclosure-trigger' import { ForkFileTree, type ForkFlatFile, @@ -64,22 +65,13 @@ export function ResourceKindRow({ } disabled={disabled} /> - + 0 ? `${selectedCount}/${total}` : total})`} + expanded={expanded} + onToggle={() => setExpanded((value) => !value)} + className='min-w-0 flex-1 gap-1' + labelClassName='flex-1' + />
{expanded ? ( @@ -160,22 +152,13 @@ export function FileKindRow({ onCheckedChange={() => onToggleAll(headerState !== true)} disabled={disabled} /> - + 0 ? `${selectedCount}/${total}` : total})`} + expanded={expanded} + onToggle={() => setExpanded((value) => !value)} + className='min-w-0 flex-1 gap-1' + labelClassName='flex-1' + /> {expanded ? ( diff --git a/apps/sim/ee/workspace-forking/components/fork-sync/fork-sync-view.tsx b/apps/sim/ee/workspace-forking/components/fork-sync/fork-sync-view.tsx index d4309868a56..09650c9944f 100644 --- a/apps/sim/ee/workspace-forking/components/fork-sync/fork-sync-view.tsx +++ b/apps/sim/ee/workspace-forking/components/fork-sync/fork-sync-view.tsx @@ -3,13 +3,11 @@ import { type Dispatch, Fragment, type SetStateAction, useMemo, useState } from 'react' import { Badge, - ChevronDown, Chip, ChipCombobox, ChipInput, ChipSwitch, CollapsibleCard, - cn, FieldDivider, Label, OverflowText, @@ -26,6 +24,7 @@ import type { import type { SelectorKey } from '@/lib/selectors/manifest' import { SettingsEmptyState } from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state' import { SettingsSection } from '@/app/workspace/[workspaceId]/settings/components/settings-section/settings-section' +import { ForkDisclosureTrigger } from '@/ee/workspace-forking/components/fork-disclosure-trigger' import { FileKindRow, ResourceKindRow, @@ -610,22 +609,18 @@ function MappingKindRow({ controller, group, summary }: MappingKindRowProps) { const badge = kindStatusBadge(summary) return (
- + setOpen((value) => !value)} + className='w-full gap-2 text-[var(--text-body)] text-sm transition-colors' + labelClassName='flex-1' + trailing={ + + {badge.label} + + } + /> {open ? (
{group.items.map((entry, index) => ( From 454d9d79d14851050c3801c98e66d811f14ba5d8 Mon Sep 17 00:00:00 2001 From: Bill Leoutsakos Date: Thu, 24 Sep 2026 12:16:30 -0700 Subject: [PATCH 2/3] Add visible keyboard focus to fork disclosures --- .../components/fork-disclosure-trigger.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/sim/ee/workspace-forking/components/fork-disclosure-trigger.tsx b/apps/sim/ee/workspace-forking/components/fork-disclosure-trigger.tsx index a46721a39d4..ddd93e4ebd8 100644 --- a/apps/sim/ee/workspace-forking/components/fork-disclosure-trigger.tsx +++ b/apps/sim/ee/workspace-forking/components/fork-disclosure-trigger.tsx @@ -1,7 +1,7 @@ 'use client' import type { ReactNode } from 'react' -import { ChevronDown, cn, OverflowText } from '@sim/emcn' +import { ChevronDown, cn, disclosureChevronClass, OverflowText } from '@sim/emcn' interface ForkDisclosureTriggerProps { label: string @@ -27,7 +27,10 @@ export function ForkDisclosureTrigger({ ) From bec69dfa739fb22b26cf62041aa66001f8baae33 Mon Sep 17 00:00:00 2001 From: Bill Leoutsakos Date: Thu, 24 Sep 2026 12:52:58 -0700 Subject: [PATCH 3/3] Test fork disclosure keyboard focus --- .../components/fork-disclosure-trigger.test.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/apps/sim/ee/workspace-forking/components/fork-disclosure-trigger.test.tsx b/apps/sim/ee/workspace-forking/components/fork-disclosure-trigger.test.tsx index 671f0aab144..b28081f0284 100644 --- a/apps/sim/ee/workspace-forking/components/fork-disclosure-trigger.test.tsx +++ b/apps/sim/ee/workspace-forking/components/fork-disclosure-trigger.test.tsx @@ -61,4 +61,13 @@ describe('ForkDisclosureTrigger', () => { expect(resources?.getAttribute('aria-expanded')).toBe('true') expect(mapping?.getAttribute('aria-expanded')).toBe('false') }) + + it('keeps the disclosure trigger keyboard focusable with its visible focus treatment', () => { + const trigger = container.querySelector('button') + act(() => trigger?.focus()) + + expect(document.activeElement).toBe(trigger) + expect(trigger?.classList.contains('focus-visible:outline-2')).toBe(true) + expect(trigger?.classList.contains('focus-visible:outline-[var(--selection)]')).toBe(true) + }) })