Skip to content

Commit 8647378

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 44fabf8 commit 8647378

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

@@ -25,6 +27,33 @@ interface TableWrapperProps {
2527
className?: string;
2628
}
2729

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

@@ -103,9 +132,27 @@ const TableWrapper = (props: TableWrapperProps) => {
103132
<Tbody>
104133
{rows.map((row, rowIndex) => (
105134
<Tr key={rowIndex} data-testid={`row-${rowIndex}`}>
106-
{columns.map((col, colIndex) => (
107-
<Td key={colIndex}>{row[col.key]}</Td>
108-
))}
135+
{columns.map((col, colIndex) => {
136+
const cellValue = row[col.key];
137+
138+
// Add copy button for ID, Name, URL, Email columns
139+
const isCopyableColumn = ['id', 'name', 'url', 'email', 'cluster'].some(
140+
keyword => col.key.toLowerCase().includes(keyword)
141+
);
142+
143+
return (
144+
<Td key={colIndex}>
145+
{isCopyableColumn ? (
146+
<span className="cell-with-copy">
147+
<span className="cell-value">{cellValue}</span>
148+
<CopyButton text={String(cellValue)} />
149+
</span>
150+
) : (
151+
cellValue
152+
)}
153+
</Td>
154+
);
155+
})}
109156
</Tr>
110157
))}
111158
</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)