Skip to content

Commit 3549a33

Browse files
mofojedmattrunyon
andauthored
fix: Copy did not work from embedded iframes (#1528)
- embedded iframes require the 'clipboard-write' permission to copy in Chrome. Added note to the README - Display an error correctly if the copy doesn't succeed, and added unit test - Fixed context menu as well - default menu handler did not return an array as it should've - Tested in Firefox and Chrome on Linux - Fixes #1527 --------- Co-authored-by: Matthew Runyon <mattrunyonstuff@gmail.com>
1 parent 1442ace commit 3549a33

4 files changed

Lines changed: 96 additions & 35 deletions

File tree

packages/embed-grid/README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,24 @@ This project uses [Vite](https://vitejs.dev/). It is to provide an example React
1212

1313
- `name`: Required. The name of the table to load
1414

15+
## Usage
16+
17+
You simply need to provide the URL to embed the iframe. Also add the `clipboard-write` permission to allow copying when embedded, e.g.:
18+
19+
```
20+
<html>
21+
<body>
22+
<h1>Dev</h1>
23+
<iframe
24+
src="http://localhost:4010/?name=t"
25+
width="800"
26+
height="500"
27+
allow="clipboard-write"
28+
></iframe>
29+
</body>
30+
</html>
31+
```
32+
1533
## API
1634

1735
The iframe provides an API to perform some basic actions with the table loaded. Use by posting the command/value as a [postMessage](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage) to the `contentWindow` of the iframe element, e.g. `document.getElementById('my-iframe').contentWindow.postMessage({ command, value }, 'http://localhost:4010')`

packages/iris-grid/src/IrisGrid.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -319,7 +319,9 @@ export interface IrisGridProps {
319319
invertSearchColumns: boolean;
320320

321321
// eslint-disable-next-line react/no-unused-prop-types
322-
onContextMenu: (data: IrisGridContextMenuData) => ResolvableContextAction[];
322+
onContextMenu: (
323+
data: IrisGridContextMenuData
324+
) => readonly ResolvableContextAction[];
323325

324326
pendingDataMap?: PendingDataMap;
325327
getDownloadWorker: () => Promise<ServiceWorker>;
@@ -486,7 +488,7 @@ export class IrisGrid extends Component<IrisGridProps, IrisGridState> {
486488
searchValue: '',
487489
selectedSearchColumns: null,
488490
invertSearchColumns: true,
489-
onContextMenu: (): void => undefined,
491+
onContextMenu: (): readonly ResolvableContextAction[] => EMPTY_ARRAY,
490492
pendingDataMap: EMPTY_MAP,
491493
getDownloadWorker: DownloadServiceWorkerUtils.getServiceWorker,
492494
settings: {

packages/iris-grid/src/IrisGridCopyHandler.test.tsx

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,3 +160,35 @@ it('retry option available if fetching fails', async () => {
160160
expect(copyToClipboard).toHaveBeenCalled();
161161
expect(screen.getByText('Copied to Clipboard!')).toBeTruthy();
162162
});
163+
164+
it('shows an error if the copy fails permissions', async () => {
165+
const user = userEvent.setup({ delay: null });
166+
const error = new Error('Test copy error');
167+
mockedCopyToClipboard.mockReturnValueOnce(Promise.reject(error));
168+
169+
const ranges = GridTestUtils.makeRanges();
170+
const copyOperation = makeCopyOperation(ranges);
171+
mountCopySelection({ copyOperation });
172+
173+
await waitFor(() =>
174+
expect(copyToClipboard).toHaveBeenCalledWith(DEFAULT_EXPECTED_TEXT)
175+
);
176+
177+
expect(screen.getByText('Fetched 50 rows!')).toBeTruthy();
178+
179+
mockedCopyToClipboard.mockClear();
180+
mockedCopyToClipboard.mockReturnValueOnce(Promise.reject(error));
181+
182+
const btn = screen.getByText('Click to Copy');
183+
expect(btn).toBeInTheDocument();
184+
185+
await user.click(btn);
186+
187+
await waitFor(() =>
188+
expect(copyToClipboard).toHaveBeenCalledWith(DEFAULT_EXPECTED_TEXT)
189+
);
190+
191+
expect(
192+
screen.getByText('Unable to copy. Verify your browser permissions.')
193+
).toBeInTheDocument();
194+
});

packages/iris-grid/src/IrisGridCopyHandler.tsx

Lines changed: 42 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -234,11 +234,19 @@ class IrisGridCopyHandler extends Component<
234234
this.setState({ isShown: false });
235235
}
236236

237-
handleCopyClick(): void {
237+
async handleCopyClick(): Promise<void> {
238238
log.debug2('handleCopyClick');
239239

240240
if (this.textData != null) {
241-
this.copyText(this.textData);
241+
try {
242+
await this.copyText(this.textData);
243+
this.showCopyDone();
244+
} catch (e) {
245+
log.error('Error copying text', e);
246+
this.setState({
247+
error: 'Unable to copy. Verify your browser permissions.',
248+
});
249+
}
242250
} else {
243251
this.startFetch();
244252
}
@@ -252,27 +260,20 @@ class IrisGridCopyHandler extends Component<
252260
this.setState({ isShown: false });
253261
}
254262

255-
copyText(text: string): void {
263+
async copyText(text: string): Promise<void> {
256264
log.debug2('copyText', text);
257265

258266
this.textData = text;
259267

260-
copyToClipboard(text).then(
261-
() => {
262-
this.setState({ copyState: IrisGridCopyHandler.COPY_STATES.DONE });
263-
this.startHideTimer();
264-
},
265-
error => {
266-
log.error('copyText error', error);
267-
this.setState({
268-
buttonState: IrisGridCopyHandler.BUTTON_STATES.CLICK_TO_COPY,
269-
copyState: IrisGridCopyHandler.COPY_STATES.CLICK_REQUIRED,
270-
});
271-
}
272-
);
268+
await copyToClipboard(text);
269+
}
270+
271+
showCopyDone(): void {
272+
this.setState({ copyState: IrisGridCopyHandler.COPY_STATES.DONE });
273+
this.startHideTimer();
273274
}
274275

275-
startFetch(): void {
276+
async startFetch(): Promise<void> {
276277
this.stopFetch();
277278

278279
this.setState({
@@ -310,23 +311,31 @@ class IrisGridCopyHandler extends Component<
310311
this.fetchPromise = PromiseUtils.makeCancelable(
311312
model.textSnapshot(modelRanges, includeHeaders, formatValue)
312313
);
313-
this.fetchPromise
314-
.then((text: string) => {
314+
try {
315+
const text = await this.fetchPromise;
316+
this.fetchPromise = undefined;
317+
try {
318+
await this.copyText(text);
319+
this.showCopyDone();
320+
} catch (e) {
321+
log.error('Error copying text', e);
322+
this.setState({
323+
buttonState: IrisGridCopyHandler.BUTTON_STATES.CLICK_TO_COPY,
324+
copyState: IrisGridCopyHandler.COPY_STATES.CLICK_REQUIRED,
325+
});
326+
}
327+
} catch (e) {
328+
if (e instanceof CanceledPromiseError) {
329+
log.debug('User cancelled copy.');
330+
} else {
331+
log.error('Error fetching contents', e);
315332
this.fetchPromise = undefined;
316-
this.copyText(text);
317-
})
318-
.catch((error: unknown) => {
319-
if (error instanceof CanceledPromiseError) {
320-
log.debug('User cancelled copy.');
321-
} else {
322-
log.error('Error fetching contents', error);
323-
this.fetchPromise = undefined;
324-
this.setState({
325-
buttonState: IrisGridCopyHandler.BUTTON_STATES.RETRY,
326-
copyState: IrisGridCopyHandler.COPY_STATES.FETCH_ERROR,
327-
});
328-
}
329-
});
333+
this.setState({
334+
buttonState: IrisGridCopyHandler.BUTTON_STATES.RETRY,
335+
copyState: IrisGridCopyHandler.COPY_STATES.FETCH_ERROR,
336+
});
337+
}
338+
}
330339
}
331340

332341
stopFetch(): void {

0 commit comments

Comments
 (0)