Skip to content

Commit e5c205a

Browse files
Bill LeoutsakosBill Leoutsakos
authored andcommitted
refactor(ui): centralize overlay action buttons in EMCN
1 parent f472cbb commit e5c205a

7 files changed

Lines changed: 195 additions & 37 deletions

File tree

‎apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-view/trace-view.tsx‎

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
DropdownMenuSeparator,
1616
DropdownMenuTrigger,
1717
Duplicate,
18+
OverlayActionButton,
1819
Search as SearchIcon,
1920
Tooltip,
2021
useCopyToClipboard,
@@ -504,39 +505,35 @@ function DetailCodeSection({
504505
<div className='absolute top-[7px] right-[6px] z-10 flex gap-1'>
505506
<Tooltip.Root>
506507
<Tooltip.Trigger asChild>
507-
<Button
508+
<OverlayActionButton
508509
aria-label={copied ? 'Copied' : 'Copy'}
509510
type='button'
510-
variant='default'
511511
onClick={(e) => {
512512
e.stopPropagation()
513513
handleCopy()
514514
}}
515-
className='size-[20px] cursor-pointer border-[var(--border-1)] bg-transparent p-0 backdrop-blur-xs hover-hover:bg-[var(--surface-3)]'
516515
>
517516
{copied ? (
518517
<Check className='size-[10px] text-[var(--text-success)]' />
519518
) : (
520519
<Clipboard className='size-[10px]' />
521520
)}
522-
</Button>
521+
</OverlayActionButton>
523522
</Tooltip.Trigger>
524523
<Tooltip.Content side='top'>{copied ? 'Copied' : 'Copy'}</Tooltip.Content>
525524
</Tooltip.Root>
526525
<Tooltip.Root>
527526
<Tooltip.Trigger asChild>
528-
<Button
527+
<OverlayActionButton
529528
aria-label='Search'
530529
type='button'
531-
variant='default'
532530
onClick={(e) => {
533531
e.stopPropagation()
534532
activateSearch()
535533
}}
536-
className='size-[20px] cursor-pointer border-[var(--border-1)] bg-transparent p-0 backdrop-blur-xs hover-hover:bg-[var(--surface-3)]'
537534
>
538535
<Search className='size-[10px]' />
539-
</Button>
536+
</OverlayActionButton>
540537
</Tooltip.Trigger>
541538
<Tooltip.Content side='top'>Search</Tooltip.Content>
542539
</Tooltip.Root>

‎apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx‎

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
Duplicate,
2929
Eye,
3030
handleKeyboardActivation,
31+
OverlayActionButton,
3132
Redo,
3233
Search as SearchIcon,
3334
Tooltip,
@@ -169,39 +170,35 @@ export const WorkflowOutputSection = memo(
169170
<div className='absolute top-[7px] right-[6px] z-10 flex gap-1'>
170171
<Tooltip.Root>
171172
<Tooltip.Trigger asChild>
172-
<Button
173+
<OverlayActionButton
173174
aria-label={copied ? 'Copied' : 'Copy'}
174175
type='button'
175-
variant='default'
176176
onClick={(e) => {
177177
e.stopPropagation()
178178
handleCopy()
179179
}}
180-
className='size-[20px] cursor-pointer border-[var(--border-1)] bg-transparent p-0 backdrop-blur-xs hover-hover:bg-[var(--surface-3)]'
181180
>
182181
{copied ? (
183182
<Check className='size-[10px] text-[var(--text-success)]' />
184183
) : (
185184
<Clipboard className='size-[10px]' />
186185
)}
187-
</Button>
186+
</OverlayActionButton>
188187
</Tooltip.Trigger>
189188
<Tooltip.Content side='top'>{copied ? 'Copied' : 'Copy'}</Tooltip.Content>
190189
</Tooltip.Root>
191190
<Tooltip.Root>
192191
<Tooltip.Trigger asChild>
193-
<Button
192+
<OverlayActionButton
194193
aria-label='Search'
195194
type='button'
196-
variant='default'
197195
onClick={(e) => {
198196
e.stopPropagation()
199197
activateSearch()
200198
}}
201-
className='size-[20px] cursor-pointer border-[var(--border-1)] bg-transparent p-0 backdrop-blur-xs hover-hover:bg-[var(--surface-3)]'
202199
>
203200
<Search className='size-[10px]' />
204-
</Button>
201+
</OverlayActionButton>
205202
</Tooltip.Trigger>
206203
<Tooltip.Content side='top'>Search</Tooltip.Content>
207204
</Tooltip.Root>

‎apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/general.tsx‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import { useId, useState } from 'react'
44
import {
5-
Button,
65
ChipButtonGroup,
76
ChipButtonGroupItem,
87
ChipConfirmModal,
@@ -12,6 +11,7 @@ import {
1211
cn,
1312
Expand,
1413
Label,
14+
OverlayActionButton,
1515
Skeleton,
1616
Tooltip,
1717
} from '@sim/emcn'
@@ -241,15 +241,15 @@ export function GeneralDeploy({
241241
</div>
242242
<Tooltip.Root>
243243
<Tooltip.Trigger asChild>
244-
<Button
244+
<OverlayActionButton
245245
aria-label='See preview'
246246
type='button'
247-
variant='default'
247+
size='md'
248248
onClick={() => setShowExpandedPreview(true)}
249-
className='absolute right-[8px] bottom-2 z-10 size-[28px] cursor-pointer bg-transparent p-0 backdrop-blur-xs hover-hover:bg-[var(--surface-3)]'
249+
className='absolute right-[8px] bottom-2 z-10'
250250
>
251251
<Expand className='size-[14px]' />
252-
</Button>
252+
</OverlayActionButton>
253253
</Tooltip.Trigger>
254254
<Tooltip.Content side='top'>See preview</Tooltip.Content>
255255
</Tooltip.Root>

‎apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-editor/preview-editor.tsx‎

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
Input,
1313
Label,
1414
OverflowText,
15+
OverlayActionButton,
1516
Tooltip,
1617
} from '@sim/emcn'
1718
import {
@@ -1235,41 +1236,37 @@ function PreviewEditorContent({
12351236
<div className='absolute top-[7px] right-[6px] z-10 flex gap-1'>
12361237
<Tooltip.Root>
12371238
<Tooltip.Trigger asChild>
1238-
<Button
1239+
<OverlayActionButton
12391240
aria-label={copiedSection === 'input' ? 'Copied' : 'Copy'}
12401241
type='button'
1241-
variant='ghost'
12421242
onClick={(e) => {
12431243
e.stopPropagation()
12441244
handleCopySection(formatValueAsJson(executionData.input), 'input')
12451245
}}
1246-
className='size-[20px] cursor-pointer border border-[var(--border-1)] bg-transparent p-0 backdrop-blur-xs hover-hover:bg-[var(--surface-4)]'
12471246
>
12481247
{copiedSection === 'input' ? (
12491248
<Check className='size-[10px] text-[var(--text-success)]' />
12501249
) : (
12511250
<Clipboard className='size-[10px]' />
12521251
)}
1253-
</Button>
1252+
</OverlayActionButton>
12541253
</Tooltip.Trigger>
12551254
<Tooltip.Content side='top'>
12561255
{copiedSection === 'input' ? 'Copied' : 'Copy'}
12571256
</Tooltip.Content>
12581257
</Tooltip.Root>
12591258
<Tooltip.Root>
12601259
<Tooltip.Trigger asChild>
1261-
<Button
1260+
<OverlayActionButton
12621261
aria-label='Search'
12631262
type='button'
1264-
variant='ghost'
12651263
onClick={(e) => {
12661264
e.stopPropagation()
12671265
activateSearch()
12681266
}}
1269-
className='size-[20px] cursor-pointer border border-[var(--border-1)] bg-transparent p-0 backdrop-blur-xs hover-hover:bg-[var(--surface-4)]'
12701267
>
12711268
<Search className='size-[10px]' />
1272-
</Button>
1269+
</OverlayActionButton>
12731270
</Tooltip.Trigger>
12741271
<Tooltip.Content side='top'>Search</Tooltip.Content>
12751272
</Tooltip.Root>
@@ -1309,41 +1306,37 @@ function PreviewEditorContent({
13091306
<div className='absolute top-[7px] right-[6px] z-10 flex gap-1'>
13101307
<Tooltip.Root>
13111308
<Tooltip.Trigger asChild>
1312-
<Button
1309+
<OverlayActionButton
13131310
aria-label={copiedSection === 'output' ? 'Copied' : 'Copy'}
13141311
type='button'
1315-
variant='ghost'
13161312
onClick={(e) => {
13171313
e.stopPropagation()
13181314
handleCopySection(formatValueAsJson(executionData.output), 'output')
13191315
}}
1320-
className='size-[20px] cursor-pointer border border-[var(--border-1)] bg-transparent p-0 backdrop-blur-xs hover-hover:bg-[var(--surface-4)]'
13211316
>
13221317
{copiedSection === 'output' ? (
13231318
<Check className='size-[10px] text-[var(--text-success)]' />
13241319
) : (
13251320
<Clipboard className='size-[10px]' />
13261321
)}
1327-
</Button>
1322+
</OverlayActionButton>
13281323
</Tooltip.Trigger>
13291324
<Tooltip.Content side='top'>
13301325
{copiedSection === 'output' ? 'Copied' : 'Copy'}
13311326
</Tooltip.Content>
13321327
</Tooltip.Root>
13331328
<Tooltip.Root>
13341329
<Tooltip.Trigger asChild>
1335-
<Button
1330+
<OverlayActionButton
13361331
aria-label='Search'
13371332
type='button'
1338-
variant='ghost'
13391333
onClick={(e) => {
13401334
e.stopPropagation()
13411335
activateSearch()
13421336
}}
1343-
className='size-[20px] cursor-pointer border border-[var(--border-1)] bg-transparent p-0 backdrop-blur-xs hover-hover:bg-[var(--surface-4)]'
13441337
>
13451338
<Search className='size-[10px]' />
1346-
</Button>
1339+
</OverlayActionButton>
13471340
</Tooltip.Trigger>
13481341
<Tooltip.Content side='top'>Search</Tooltip.Content>
13491342
</Tooltip.Root>

‎packages/emcn/src/components/index.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,11 @@ export {
202202
overflowTextClipClass,
203203
overflowTextFadeClass,
204204
} from './overflow-text/overflow-text'
205+
export {
206+
OverlayActionButton,
207+
type OverlayActionButtonProps,
208+
overlayActionButtonVariants,
209+
} from './overlay-action-button/overlay-action-button'
205210
export {
206211
Popover,
207212
PopoverAnchor,
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/** @vitest-environment jsdom */
2+
import { act, createRef, type ReactNode } from 'react'
3+
import { Button, OverlayActionButton, Tooltip } from '@sim/emcn'
4+
import { createRoot, type Root } from 'react-dom/client'
5+
import { afterEach, describe, expect, it, vi } from 'vitest'
6+
7+
let root: Root | null = null
8+
let container: HTMLDivElement | null = null
9+
10+
function mount(children: ReactNode) {
11+
;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
12+
container = document.createElement('div')
13+
document.body.appendChild(container)
14+
root = createRoot(container)
15+
act(() => root?.render(children))
16+
return container
17+
}
18+
19+
afterEach(() => {
20+
if (root) act(() => root?.unmount())
21+
container?.remove()
22+
root = null
23+
container = null
24+
vi.useRealTimers()
25+
})
26+
27+
/** Pre-migration recipes from log details and the deployment preview. */
28+
const PREVIOUS = [
29+
{
30+
name: 'default 20px adaptive action',
31+
props: {},
32+
variant: 'default',
33+
className:
34+
'size-[20px] cursor-pointer border-[var(--border-1)] bg-transparent p-0 backdrop-blur-xs hover-hover:bg-[var(--surface-3)]',
35+
},
36+
{
37+
name: '28px adaptive action',
38+
props: { size: 'md' },
39+
variant: 'default',
40+
className:
41+
'size-[28px] cursor-pointer bg-transparent p-0 backdrop-blur-xs hover-hover:bg-[var(--surface-3)]',
42+
},
43+
] as const
44+
45+
describe('OverlayActionButton', () => {
46+
it.each(PREVIOUS)('preserves the previous $name markup', ({ props, variant, className }) => {
47+
const view = mount(
48+
<>
49+
<Button variant={variant} aria-label='Copy' className={`${className} shrink-0`}>
50+
<svg className='size-[10px]' aria-hidden='true' />
51+
</Button>
52+
<OverlayActionButton {...props} aria-label='Copy' className='shrink-0'>
53+
<svg className='size-[10px]' aria-hidden='true' />
54+
</OverlayActionButton>
55+
</>
56+
)
57+
const [previous, current] = view.querySelectorAll('button')
58+
/** The old border-1 token aliases border; class order changes when recipes are composed. */
59+
for (const button of [previous, current]) {
60+
button.className = button.className
61+
.replaceAll('--border-1', '--border')
62+
.split(/\s+/)
63+
.sort()
64+
.join(' ')
65+
}
66+
expect(current.outerHTML).toBe(previous.outerHTML)
67+
})
68+
69+
it('forwards refs and native props through a tooltip and suppresses disabled clicks', () => {
70+
vi.useFakeTimers()
71+
const ref = createRef<HTMLButtonElement>()
72+
const onClick = vi.fn()
73+
const onKeyDown = vi.fn()
74+
const action = (disabled: boolean) => (
75+
<Tooltip.Root>
76+
<Tooltip.Trigger asChild>
77+
<OverlayActionButton
78+
ref={ref}
79+
type='button'
80+
aria-label='Copy'
81+
data-action='copy'
82+
disabled={disabled}
83+
onClick={onClick}
84+
onKeyDown={onKeyDown}
85+
/>
86+
</Tooltip.Trigger>
87+
<Tooltip.Content>Copy output</Tooltip.Content>
88+
</Tooltip.Root>
89+
)
90+
const view = mount(action(false))
91+
const button = view.querySelector('button')
92+
if (!button) throw new Error('Button did not render')
93+
expect(view.querySelectorAll('button')).toHaveLength(1)
94+
expect(ref.current).toBe(button)
95+
expect(button.type).toBe('button')
96+
expect(button.dataset.action).toBe('copy')
97+
expect(button.getAttribute('aria-label')).toBe('Copy')
98+
act(() =>
99+
button.dispatchEvent(
100+
new MouseEvent('pointerover', { bubbles: true, clientX: 200, clientY: 200 })
101+
)
102+
)
103+
expect(document.querySelector('[role="tooltip"]')?.textContent).toBe('Copy output')
104+
act(() => button.focus())
105+
expect(document.activeElement).toBe(button)
106+
const keyEvent = new KeyboardEvent('keydown', { key: 'Enter', bubbles: true })
107+
act(() => button.dispatchEvent(keyEvent))
108+
expect(onKeyDown).toHaveBeenCalledTimes(1)
109+
expect(onKeyDown.mock.calls[0][0].nativeEvent).toBe(keyEvent)
110+
act(() => button.click())
111+
expect(onClick).toHaveBeenCalledTimes(1)
112+
act(() => root?.render(action(true)))
113+
expect(button.disabled).toBe(true)
114+
act(() => button.click())
115+
expect(onClick).toHaveBeenCalledTimes(1)
116+
})
117+
})

0 commit comments

Comments
 (0)