Skip to content

Commit 23cc995

Browse files
feat: Add copy-to-clipboard button for table cells
- Add CopyButton component with PatternFly icons (CopyIcon/CheckIcon) - Automatically detects copyable columns (id, name, url, email, cluster) - Shows on row hover with smooth animations - Visual feedback: icon changes and animates on successful copy - Includes CSS for hover states, transitions, and success animation - Uses modern Clipboard API with error handling - Non-intrusive UX: hidden until hover, doesn't interfere with row clicks Tested in OCM Genie POC with cluster IDs, names, and URLs.
1 parent 38b3969 commit 23cc995

File tree

2 files changed

+111
-3
lines changed

2 files changed

+111
-3
lines changed

src/components/TableWrapper.tsx

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import {
88
Td,
99
Caption,
1010
} from "@patternfly/react-table";
11+
import { CopyIcon, CheckIcon } from "@patternfly/react-icons";
12+
import React, { useState } from "react";
1113

1214
import ErrorPlaceholder from "./ErrorPlaceholder";
1315

@@ -26,6 +28,33 @@ interface TableWrapperProps {
2628
onRowClick?: (rowData: Record<string, string | number | null>) => void;
2729
}
2830

31+
// Copy button component with visual feedback
32+
const CopyButton: React.FC<{ text: string }> = ({ text }) => {
33+
const [copied, setCopied] = useState(false);
34+
35+
const handleCopy = async (e: React.MouseEvent) => {
36+
e.stopPropagation(); // Prevent row click
37+
try {
38+
await navigator.clipboard.writeText(text);
39+
setCopied(true);
40+
setTimeout(() => setCopied(false), 2000);
41+
} catch (err) {
42+
console.error('Failed to copy:', err);
43+
}
44+
};
45+
46+
return (
47+
<button
48+
onClick={handleCopy}
49+
className="copy-button"
50+
title={copied ? 'Copied!' : 'Copy to clipboard'}
51+
aria-label={copied ? 'Copied' : 'Copy to clipboard'}
52+
>
53+
{copied ? <CheckIcon /> : <CopyIcon />}
54+
</button>
55+
);
56+
};
57+
2958
const TableWrapper = (props: TableWrapperProps) => {
3059
const { title, id, fields, className, onRowClick } = props;
3160

@@ -110,9 +139,27 @@ const TableWrapper = (props: TableWrapperProps) => {
110139
style={onRowClick ? { cursor: 'pointer' } : undefined}
111140
isHoverable={!!onRowClick}
112141
>
113-
{columns.map((col, colIndex) => (
114-
<Td key={colIndex}>{row[col.key]}</Td>
115-
))}
142+
{columns.map((col, colIndex) => {
143+
const cellValue = row[col.key];
144+
145+
// Add copy button for ID, Name, URL, Email columns
146+
const isCopyableColumn = ['id', 'name', 'url', 'email', 'cluster'].some(
147+
keyword => col.key.toLowerCase().includes(keyword)
148+
);
149+
150+
return (
151+
<Td key={colIndex}>
152+
{isCopyableColumn ? (
153+
<span className="cell-with-copy">
154+
<span className="cell-value">{cellValue}</span>
155+
<CopyButton text={String(cellValue)} />
156+
</span>
157+
) : (
158+
cellValue
159+
)}
160+
</Td>
161+
);
162+
})}
116163
</Tr>
117164
))}
118165
</Tbody>

src/global.css

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,3 +163,64 @@
163163
color: var(--pf-global--Color--danger-200);
164164
border: 1px solid var(--pf-global--Color--danger-200);
165165
}
166+
167+
/* Copy Button Styles */
168+
.cell-with-copy {
169+
display: inline-flex;
170+
align-items: center;
171+
gap: 0.5rem;
172+
width: 100%;
173+
}
174+
175+
.cell-value {
176+
flex: 1;
177+
}
178+
179+
/* Copy button - hidden by default */
180+
.copy-button {
181+
opacity: 0;
182+
background: transparent;
183+
border: none;
184+
padding: 0.25rem 0.5rem;
185+
cursor: pointer;
186+
font-size: 1rem;
187+
line-height: 1;
188+
transition: all 0.2s ease;
189+
border-radius: 4px;
190+
flex-shrink: 0;
191+
color: #6b7280; /* PatternFly neutral gray */
192+
}
193+
194+
.copy-button:hover {
195+
background: #f3f4f6;
196+
transform: scale(1.1);
197+
color: #374151;
198+
}
199+
200+
.copy-button:active {
201+
transform: scale(0.95);
202+
}
203+
204+
/* Show copy button on row hover */
205+
.pf-v6-c-table tbody tr:hover .copy-button {
206+
opacity: 1;
207+
}
208+
209+
/* When copied, always show and make it green */
210+
.copy-button[title="Copied!"] {
211+
opacity: 1 !important;
212+
color: #10b981;
213+
animation: copySuccess 0.3s ease;
214+
}
215+
216+
@keyframes copySuccess {
217+
0% {
218+
transform: scale(1);
219+
}
220+
50% {
221+
transform: scale(1.3);
222+
}
223+
100% {
224+
transform: scale(1);
225+
}
226+
}

0 commit comments

Comments
 (0)