Skip to content

Commit 0e1c3c2

Browse files
committed
fix(workflow): address code preview review feedback
1 parent 4a3ffea commit 0e1c3c2

6 files changed

Lines changed: 153 additions & 6 deletions

File tree

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,9 @@ const areSubBlockRowPropsEqual = (
289289
const prevValue = subBlockId ? prevProps.allSubBlockValues?.[subBlockId]?.value : undefined
290290
const nextValue = subBlockId ? nextProps.allSubBlockValues?.[subBlockId]?.value : undefined
291291
const valueEqual = prevValue === nextValue || isEqual(prevValue, nextValue)
292+
const codeLanguageEqual =
293+
prevProps.subBlock?.type !== 'code' ||
294+
prevProps.allSubBlockValues?.language?.value === nextProps.allSubBlockValues?.language?.value
292295

293296
return (
294297
prevProps.title === nextProps.title &&
@@ -299,6 +302,7 @@ const areSubBlockRowPropsEqual = (
299302
prevProps.workflowId === nextProps.workflowId &&
300303
prevProps.blockId === nextProps.blockId &&
301304
valueEqual &&
305+
codeLanguageEqual &&
302306
prevProps.displayAdvancedOptions === nextProps.displayAdvancedOptions &&
303307
prevProps.canonicalIndex === nextProps.canonicalIndex &&
304308
prevProps.canonicalModeOverrides === nextProps.canonicalModeOverrides &&

apps/sim/blocks/blocks/api.test.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { describe, expect, it } from 'vitest'
22
import { ApiBlock } from '@/blocks/blocks/api'
33

4-
describe('API block redirect policy', () => {
4+
describe('API block', () => {
55
it('uses a versioned safe default without changing legacy blocks', () => {
66
const version = ApiBlock.subBlocks.find((subBlock) => subBlock.id === 'redirectPolicyVersion')
77
const sendCredentials = ApiBlock.subBlocks.find(
@@ -14,4 +14,11 @@ describe('API block redirect policy', () => {
1414
expect(sendCredentials?.mode).toBe('advanced')
1515
expect(sendCredentials?.defaultValue).toBe(true)
1616
})
17+
18+
it('marks the request body as JSON for code previews', () => {
19+
const body = ApiBlock.subBlocks.find((subBlock) => subBlock.id === 'body')
20+
21+
expect(body?.type).toBe('code')
22+
expect(body?.language).toBe('json')
23+
})
1724
})

apps/sim/blocks/blocks/api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ export const ApiBlock: BlockConfig<RequestResponse> = {
6666
id: 'body',
6767
title: 'Body',
6868
type: 'code',
69+
language: 'json',
6970
placeholder: 'Enter JSON...',
7071
wandConfig: {
7172
enabled: true,

packages/emcn/src/components/popover/popover.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -536,7 +536,11 @@ const PopoverContent = React.forwardRef<
536536
// and the component's custom navigation state.
537537

538538
const effectiveMaxWidth =
539-
maxWidth ?? (appearance === 'tooltip' ? TOOLTIP_MAX_WIDTH_PX : undefined)
539+
maxWidth !== undefined
540+
? `${maxWidth}px`
541+
: appearance === 'tooltip'
542+
? `min(${TOOLTIP_MAX_WIDTH_PX}px, calc(100vw - 2rem))`
543+
: undefined
540544
const hasUserWidthConstraint =
541545
effectiveMaxWidth !== undefined ||
542546
minWidth !== undefined ||
@@ -607,8 +611,7 @@ const PopoverContent = React.forwardRef<
607611
)}
608612
style={{
609613
maxHeight: `${maxHeight || 400}px`,
610-
maxWidth:
611-
effectiveMaxWidth !== undefined ? `${effectiveMaxWidth}px` : 'calc(100vw - 16px)',
614+
maxWidth: effectiveMaxWidth ?? 'calc(100vw - 16px)',
612615
minWidth:
613616
minWidth !== undefined
614617
? `${minWidth}px`

packages/workflow-renderer/src/lib/code-hover-card.tsx

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
'use client'
22

33
import {
4+
type KeyboardEvent as ReactKeyboardEvent,
45
type ReactNode,
56
type PointerEvent as ReactPointerEvent,
67
useCallback,
78
useEffect,
9+
useId,
810
useRef,
911
useState,
1012
} from 'react'
@@ -25,6 +27,8 @@ interface CodeHoverCardProps {
2527
/** Interactive, two-axis-scrollable source preview anchored to a canvas code chip. */
2628
export function CodeHoverCard({ preview, className, children }: CodeHoverCardProps) {
2729
const [open, setOpen] = useState(false)
30+
const contentId = useId()
31+
const triggerRef = useRef<HTMLSpanElement>(null)
2832
const openTimerRef = useRef<number | null>(null)
2933
const closeTimerRef = useRef<number | null>(null)
3034

@@ -59,6 +63,13 @@ export function CodeHoverCard({ preview, className, children }: CodeHoverCardPro
5963
}, delay)
6064
}
6165

66+
const openImmediatelyIfClipped = (trigger: HTMLSpanElement) => {
67+
if (!isTextClipped(trigger)) return
68+
clearOpenTimer()
69+
clearCloseTimer()
70+
setOpen(true)
71+
}
72+
6273
const handleOpenChange = useCallback(
6374
(nextOpen: boolean) => {
6475
if (!nextOpen) {
@@ -78,19 +89,73 @@ export function CodeHoverCard({ preview, className, children }: CodeHoverCardPro
7889
[clearCloseTimer, clearOpenTimer]
7990
)
8091

92+
const handleTriggerPointerDown = (event: ReactPointerEvent<HTMLSpanElement>) => {
93+
event.preventDefault()
94+
if (event.pointerType === 'touch' || event.pointerType === 'pen') {
95+
event.stopPropagation()
96+
if (open) {
97+
handleOpenChange(false)
98+
} else {
99+
openImmediatelyIfClipped(event.currentTarget)
100+
}
101+
return
102+
}
103+
handleOpenChange(false)
104+
}
105+
106+
const handleTriggerKeyDown = (event: ReactKeyboardEvent<HTMLSpanElement>) => {
107+
if (event.key === 'Escape') {
108+
event.stopPropagation()
109+
handleOpenChange(false)
110+
return
111+
}
112+
if (event.key !== 'Enter' && event.key !== ' ') return
113+
114+
event.preventDefault()
115+
event.stopPropagation()
116+
if (open) {
117+
handleOpenChange(false)
118+
} else {
119+
openImmediatelyIfClipped(event.currentTarget)
120+
}
121+
}
122+
123+
const handleContentKeyDown = (event: ReactKeyboardEvent<HTMLDivElement>) => {
124+
if (event.key !== 'Escape') return
125+
event.stopPropagation()
126+
handleOpenChange(false)
127+
triggerRef.current?.focus()
128+
}
129+
81130
return (
82131
<Popover open={open} onOpenChange={handleOpenChange}>
83132
<PopoverAnchor asChild>
84133
<span
134+
ref={triggerRef}
135+
role='button'
136+
tabIndex={0}
137+
aria-haspopup='dialog'
138+
aria-expanded={open}
139+
aria-controls={open ? contentId : undefined}
85140
className={className}
86141
onPointerEnter={handleTriggerPointerEnter}
87-
onPointerLeave={() => scheduleClose(TRIGGER_EXIT_GRACE_MS)}
88-
onPointerDown={() => handleOpenChange(false)}
142+
onPointerLeave={() => {
143+
if (document.activeElement !== triggerRef.current) {
144+
scheduleClose(TRIGGER_EXIT_GRACE_MS)
145+
}
146+
}}
147+
onPointerDown={handleTriggerPointerDown}
148+
onFocus={(event) => openImmediatelyIfClipped(event.currentTarget)}
149+
onBlur={() => scheduleClose(TRIGGER_EXIT_GRACE_MS)}
150+
onKeyDown={handleTriggerKeyDown}
89151
>
90152
{children}
91153
</span>
92154
</PopoverAnchor>
93155
<PopoverContent
156+
id={contentId}
157+
role='dialog'
158+
tabIndex={0}
94159
aria-label='Code preview'
95160
data-code-hover-card=''
96161
align='start'
@@ -101,6 +166,9 @@ export function CodeHoverCard({ preview, className, children }: CodeHoverCardPro
101166
maxHeight={CODE_TOOLTIP_MAX_HEIGHT_PX}
102167
onPointerEnter={clearCloseTimer}
103168
onPointerLeave={() => scheduleClose(CONTENT_EXIT_GRACE_MS)}
169+
onFocus={clearCloseTimer}
170+
onBlur={() => scheduleClose(CONTENT_EXIT_GRACE_MS)}
171+
onKeyDown={handleContentKeyDown}
104172
className='nodrag nowheel overflow-hidden overscroll-contain p-0'
105173
>
106174
<Code.Viewer

packages/workflow-renderer/src/lib/overflow-span-mount.test.tsx

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ describe('OverflowSpan code preview', () => {
6868
const preview = document.querySelector('[data-code-hover-card]')
6969
expect(preview).toHaveTextContent('line 20')
7070
expect(preview).toHaveClass('w-fit', 'max-w-[min(16rem,calc(100vw-2rem))]', 'shadow-sm')
71+
expect(preview).toHaveStyle({ maxWidth: 'min(256px, calc(100vw - 2rem))' })
7172
expect(preview?.querySelector('.overflow-x-auto')).toHaveClass('overflow-y-auto')
7273
expect(preview?.querySelector('.tabular-nums')).toBeNull()
7374
expect(preview?.querySelector('pre')).toHaveClass('px-2', 'py-1.5', 'text-caption', 'leading-5')
@@ -119,4 +120,67 @@ describe('OverflowSpan code preview', () => {
119120
})
120121
expect(document.querySelector('[data-code-hover-card]')).toBeNull()
121122
})
123+
124+
it('opens from the keyboard and keeps the preview available while it is focused', () => {
125+
act(() => {
126+
root?.render(
127+
<OverflowSpan
128+
value='const value = 1'
129+
className='truncate'
130+
codePreview={{ code: 'const value = 1', language: 'javascript' }}
131+
/>
132+
)
133+
})
134+
135+
const trigger = host?.querySelector<HTMLElement>('span[role="button"]')
136+
if (!trigger) throw new Error('Overflow trigger did not render')
137+
Object.defineProperties(trigger, {
138+
clientWidth: { configurable: true, value: 50 },
139+
scrollWidth: { configurable: true, value: 200 },
140+
})
141+
142+
act(() => trigger.focus())
143+
const preview = document.querySelector<HTMLElement>('[data-code-hover-card]')
144+
expect(preview).not.toBeNull()
145+
expect(trigger).toHaveAttribute('aria-expanded', 'true')
146+
147+
act(() => preview?.focus())
148+
act(() => vi.advanceTimersByTime(600))
149+
expect(document.querySelector('[data-code-hover-card]')).not.toBeNull()
150+
151+
act(() =>
152+
preview?.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, key: 'Escape' }))
153+
)
154+
expect(document.querySelector('[data-code-hover-card]')).toBeNull()
155+
expect(trigger).toHaveFocus()
156+
})
157+
158+
it('toggles the clipped preview on touch', () => {
159+
act(() => {
160+
root?.render(
161+
<OverflowSpan
162+
value='const value = 1'
163+
className='truncate'
164+
codePreview={{ code: 'const value = 1', language: 'javascript' }}
165+
/>
166+
)
167+
})
168+
169+
const trigger = host?.querySelector<HTMLElement>('span[role="button"]')
170+
if (!trigger) throw new Error('Overflow trigger did not render')
171+
Object.defineProperties(trigger, {
172+
clientWidth: { configurable: true, value: 50 },
173+
scrollWidth: { configurable: true, value: 200 },
174+
})
175+
const pointerDown = () => {
176+
const event = new MouseEvent('pointerdown', { bubbles: true, cancelable: true })
177+
Object.defineProperty(event, 'pointerType', { value: 'touch' })
178+
trigger.dispatchEvent(event)
179+
}
180+
181+
act(pointerDown)
182+
expect(document.querySelector('[data-code-hover-card]')).not.toBeNull()
183+
act(pointerDown)
184+
expect(document.querySelector('[data-code-hover-card]')).toBeNull()
185+
})
122186
})

0 commit comments

Comments
 (0)