Skip to content

Commit f1111ec

Browse files
authored
fix(modals): fix z-index for various modals and output selector and variables (#2005)
1 parent d076750 commit f1111ec

File tree

6 files changed

+446
-307
lines changed

6 files changed

+446
-307
lines changed

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -600,6 +600,7 @@ export function Chat() {
600600
onOutputSelect={handleOutputSelection}
601601
disabled={!activeWorkflowId}
602602
placeholder='Select outputs'
603+
align='end'
603604
/>
604605
</div>
605606

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/output-select/output-select.tsx

Lines changed: 128 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import {
77
Popover,
88
PopoverContent,
99
PopoverItem,
10-
PopoverScrollArea,
1110
PopoverSection,
1211
PopoverTrigger,
1312
} from '@/components/emcn'
@@ -24,6 +23,7 @@ interface OutputSelectProps {
2423
disabled?: boolean
2524
placeholder?: string
2625
valueMode?: 'id' | 'label'
26+
align?: 'start' | 'end' | 'center'
2727
}
2828

2929
export function OutputSelect({
@@ -33,10 +33,13 @@ export function OutputSelect({
3333
disabled = false,
3434
placeholder = 'Select outputs',
3535
valueMode = 'id',
36+
align = 'start',
3637
}: OutputSelectProps) {
3738
const [open, setOpen] = useState(false)
39+
const [highlightedIndex, setHighlightedIndex] = useState(-1)
3840
const triggerRef = useRef<HTMLDivElement>(null)
3941
const popoverRef = useRef<HTMLDivElement>(null)
42+
const contentRef = useRef<HTMLDivElement>(null)
4043
const blocks = useWorkflowStore((state) => state.blocks)
4144
const { isShowingDiff, isDiffReady, diffWorkflow } = useWorkflowDiffStore()
4245
const subBlockValues = useSubBlockStore((state) =>
@@ -230,6 +233,13 @@ export function OutputSelect({
230233
return blockConfig?.bgColor || '#2F55FF'
231234
}
232235

236+
/**
237+
* Flattened outputs for keyboard navigation
238+
*/
239+
const flattenedOutputs = useMemo(() => {
240+
return Object.values(groupedOutputs).flat()
241+
}, [groupedOutputs])
242+
233243
/**
234244
* Handles output selection - toggle selection
235245
*/
@@ -246,6 +256,75 @@ export function OutputSelect({
246256
onOutputSelect(newSelectedOutputs)
247257
}
248258

259+
/**
260+
* Keyboard navigation handler
261+
*/
262+
const handleKeyDown = (e: React.KeyboardEvent) => {
263+
if (flattenedOutputs.length === 0) return
264+
265+
switch (e.key) {
266+
case 'ArrowDown':
267+
e.preventDefault()
268+
setHighlightedIndex((prev) => {
269+
const next = prev < flattenedOutputs.length - 1 ? prev + 1 : 0
270+
return next
271+
})
272+
break
273+
274+
case 'ArrowUp':
275+
e.preventDefault()
276+
setHighlightedIndex((prev) => {
277+
const next = prev > 0 ? prev - 1 : flattenedOutputs.length - 1
278+
return next
279+
})
280+
break
281+
282+
case 'Enter':
283+
e.preventDefault()
284+
if (highlightedIndex >= 0 && highlightedIndex < flattenedOutputs.length) {
285+
handleOutputSelection(flattenedOutputs[highlightedIndex].label)
286+
}
287+
break
288+
289+
case 'Escape':
290+
e.preventDefault()
291+
setOpen(false)
292+
break
293+
}
294+
}
295+
296+
/**
297+
* Reset highlighted index when popover opens/closes
298+
*/
299+
useEffect(() => {
300+
if (open) {
301+
// Find first selected item, or start at -1
302+
const firstSelectedIndex = flattenedOutputs.findIndex((output) => isSelectedValue(output))
303+
setHighlightedIndex(firstSelectedIndex >= 0 ? firstSelectedIndex : -1)
304+
305+
// Focus the content for keyboard navigation
306+
setTimeout(() => {
307+
contentRef.current?.focus()
308+
}, 0)
309+
} else {
310+
setHighlightedIndex(-1)
311+
}
312+
}, [open, flattenedOutputs])
313+
314+
/**
315+
* Scroll highlighted item into view
316+
*/
317+
useEffect(() => {
318+
if (highlightedIndex >= 0 && contentRef.current) {
319+
const highlightedElement = contentRef.current.querySelector(
320+
`[data-option-index="${highlightedIndex}"]`
321+
)
322+
if (highlightedElement) {
323+
highlightedElement.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
324+
}
325+
}
326+
}, [highlightedIndex])
327+
249328
/**
250329
* Closes popover when clicking outside
251330
*/
@@ -288,44 +367,57 @@ export function OutputSelect({
288367
<PopoverContent
289368
ref={popoverRef}
290369
side='bottom'
291-
align='start'
370+
align={align}
292371
sideOffset={4}
293-
maxHeight={140}
294-
maxWidth={140}
295-
minWidth={140}
296-
onOpenAutoFocus={(e) => e.preventDefault()}
297-
onCloseAutoFocus={(e) => e.preventDefault()}
372+
maxHeight={300}
373+
maxWidth={300}
374+
minWidth={200}
375+
onKeyDown={handleKeyDown}
376+
tabIndex={0}
377+
style={{ outline: 'none' }}
298378
>
299-
<PopoverScrollArea className='space-y-[2px]'>
300-
{Object.entries(groupedOutputs).map(([blockName, outputs]) => (
301-
<div key={blockName}>
302-
<PopoverSection>{blockName}</PopoverSection>
303-
304-
<div className='flex flex-col gap-[2px]'>
305-
{outputs.map((output) => (
306-
<PopoverItem
307-
key={output.id}
308-
active={isSelectedValue(output)}
309-
onClick={() => handleOutputSelection(output.label)}
310-
>
311-
<div
312-
className='flex h-[14px] w-[14px] flex-shrink-0 items-center justify-center rounded'
313-
style={{
314-
backgroundColor: getOutputColor(output.blockId, output.blockType),
315-
}}
316-
>
317-
<span className='font-bold text-[10px] text-white'>
318-
{blockName.charAt(0).toUpperCase()}
319-
</span>
320-
</div>
321-
<span className='min-w-0 flex-1 truncate'>{output.path}</span>
322-
{isSelectedValue(output) && <Check className='h-3 w-3 flex-shrink-0' />}
323-
</PopoverItem>
324-
))}
379+
<div ref={contentRef} className='space-y-[2px]'>
380+
{Object.entries(groupedOutputs).map(([blockName, outputs]) => {
381+
// Calculate the starting index for this group
382+
const startIndex = flattenedOutputs.findIndex((o) => o.blockName === blockName)
383+
384+
return (
385+
<div key={blockName}>
386+
<PopoverSection>{blockName}</PopoverSection>
387+
388+
<div className='flex flex-col gap-[2px]'>
389+
{outputs.map((output, localIndex) => {
390+
const globalIndex = startIndex + localIndex
391+
const isHighlighted = globalIndex === highlightedIndex
392+
393+
return (
394+
<PopoverItem
395+
key={output.id}
396+
active={isSelectedValue(output) || isHighlighted}
397+
data-option-index={globalIndex}
398+
onClick={() => handleOutputSelection(output.label)}
399+
onMouseEnter={() => setHighlightedIndex(globalIndex)}
400+
>
401+
<div
402+
className='flex h-[14px] w-[14px] flex-shrink-0 items-center justify-center rounded'
403+
style={{
404+
backgroundColor: getOutputColor(output.blockId, output.blockType),
405+
}}
406+
>
407+
<span className='font-bold text-[10px] text-white'>
408+
{blockName.charAt(0).toUpperCase()}
409+
</span>
410+
</div>
411+
<span className='min-w-0 flex-1 truncate'>{output.path}</span>
412+
{isSelectedValue(output) && <Check className='h-3 w-3 flex-shrink-0' />}
413+
</PopoverItem>
414+
)
415+
})}
416+
</div>
325417
</div>
326-
</div>
327-
))}
328-
</PopoverScrollArea>
418+
)
419+
})}
420+
</div>
329421
</PopoverContent>
330422
</Popover>
331423
)

0 commit comments

Comments
 (0)