Repository: mapbox/mr-ui
Pull request #227 (2024-11-21 15:45:23)

Add onCopy property to Copiable component
Adds possibility to pass onCopy to Copiable component


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9e89ca8..4c0971e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,8 @@
 ## Changelog
 
+## 2.12.0
+- [feature] add possibility to pass onCopy to Copiable component
+
 ## 2.11.0
 - [feature] support onOpenChange handler for contextMenu
 
diff --git a/package-lock.json b/package-lock.json
index 0ea242f..44ea86a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
 {
   "name": "@mapbox/mr-ui",
-  "version": "2.11.0",
+  "version": "2.12.0",
   "lockfileVersion": 2,
   "requires": true,
   "packages": {
@@ -49294,4 +49294,4 @@
       "dev": true
     }
   }
-}
+}
\ No newline at end of file
diff --git a/package.json b/package.json
index a86fb2e..b5e7e4f 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@mapbox/mr-ui",
-  "version": "2.11.0",
+  "version": "2.12.0",
   "description": "UI components for Mapbox projects",
   "main": "index.js",
   "homepage": "./",
diff --git a/src/components/copiable/copiable.tsx b/src/components/copiable/copiable.tsx
index d64987e..56863c0 100644
--- a/src/components/copiable/copiable.tsx
+++ b/src/components/copiable/copiable.tsx
@@ -1,4 +1,10 @@
-import React, { ReactElement, useState, useEffect, useRef, CSSProperties } from 'react';
+import React, {
+  ReactElement,
+  useState,
+  useEffect,
+  useRef,
+  CSSProperties
+} from 'react';
 import PropTypes from 'prop-types';
 import classnames from 'classnames';
 import CopyButton from '../copy-button';
@@ -25,6 +31,7 @@ interface Props {
   value: string;
   focusTrapPaused?: boolean;
   truncated?: boolean;
+  onCopy?: (text: string) => void;
 }
 
 /**
@@ -39,6 +46,7 @@ interface Props {
 export default function Copiable({
   value,
   focusTrapPaused,
+  onCopy,
   truncated = false
 }: Props): ReactElement {
   const textEl = useRef(null);
@@ -58,7 +66,7 @@ export default function Copiable({
   }, [copyTooltipActive]);
 
   const handleTextFocus = () => {
-    if (typeof window === 'undefined') return
+    if (typeof window === 'undefined') return;
     if (window.innerWidth < DISABLE_CLICK_TO_SELECT_THRESHOLD) return;
     select(() => textEl.current);
     setCopyTooltipActive(true);
@@ -76,12 +84,13 @@ export default function Copiable({
         text={value}
         block={true}
         focusTrapPaused={focusTrapPaused}
+        onCopy={onCopy}
       />
     </div>
   );
 
   const renderCopyHintText = () => {
-    if (typeof window === 'undefined') return
+    if (typeof window === 'undefined') return;
 
     return (
       <span>
@@ -90,8 +99,8 @@ export default function Copiable({
         </span>{' '}
         to copy
       </span>
-    )
-  }
+    );
+  };
 
   const textClasses = classnames('my3 txt-mono txt-s mr24', {
     'txt-truncate': truncated
@@ -106,7 +115,11 @@ export default function Copiable({
     <div className="relative clearfix bg-darken5 round">
       {showCopyButton.current && renderCopyButton}
       <Popover
-        content={<div className="txt-s">{showCopyButton.current && renderCopyHintText}</div>}
+        content={
+          <div className="txt-s">
+            {showCopyButton.current && renderCopyHintText}
+          </div>
+        }
         active={copyTooltipActive}
         placement="top"
         alignment="center"
@@ -158,5 +171,10 @@ Copiable.propTypes = {
    * Horizontal scrolling is not an option because of things end up getting
    * pretty gross across browsers.
    */
-  truncated: PropTypes.bool
+  truncated: PropTypes.bool,
+  /**
+   * Invoked when the button is clicked.
+   * Passed one argument: the `text` prop.
+   */
+  onCopy: PropTypes.func
 };


Test patch:
diff --git a/src/components/copiable/copiable.test.tsx b/src/components/copiable/copiable.test.tsx
index 94399f6..6ed4fec 100644
--- a/src/components/copiable/copiable.test.tsx
+++ b/src/components/copiable/copiable.test.tsx
@@ -4,6 +4,7 @@ import { act, render, screen, waitFor } from '@testing-library/react';
 import CopyButton from '../copy-button';
 import Copiable from './copiable';
 import select from 'select';
+import userEvent from '@testing-library/user-event';
 
 const FEEDBACK_TIME = 2000;
 
@@ -13,20 +14,20 @@ jest.mock('os-key', () =>
 );
 
 describe('Copiable', () => {
-
   describe('basic', () => {
     const props = {
-      value: 'thetextyoucopythetextyoucopythetextyoucopythetext you copy the text you copy '
+      value:
+        'thetextyoucopythetextyoucopythetextyoucopythetext you copy the text you copy '
     };
 
     test('renders as expected', () => {
-      const { baseElement } = render(<Copiable {...props} />)
+      const { baseElement } = render(<Copiable {...props} />);
       expect(baseElement).toMatchSnapshot();
     });
 
     test('calls select library with element holding text to copy', async () => {
       jest.spyOn(CopyButton, 'isCopySupported').mockImplementation(() => true);
-      render(<Copiable {...props} />)
+      render(<Copiable {...props} />);
 
       act(() => {
         screen.getByTestId('copiable-text-el').focus();
@@ -39,7 +40,7 @@ describe('Copiable', () => {
 
     test('shows copy hint when focused', async () => {
       jest.spyOn(CopyButton, 'isCopySupported').mockImplementation(() => true);
-      render(<Copiable {...props} />)
+      render(<Copiable {...props} />);
 
       act(() => {
         screen.getByTestId('copiable-text-el').focus();
@@ -58,12 +59,45 @@ describe('Copiable', () => {
   describe('truncated', () => {
     const props = {
       truncated: true,
-      value: 'the text you copy the text you copy the text you copy the text you copy the text you copy '
+      value:
+        'the text you copy the text you copy the text you copy the text you copy the text you copy '
     };
 
     test('renders as expected', () => {
-      const { baseElement } = render(<Copiable {...props} />)
+      const { baseElement } = render(<Copiable {...props} />);
       expect(baseElement).toMatchSnapshot();
     });
   });
+
+  describe('onCopy', () => {
+    const mockOnCopy = jest.fn();
+    const text =
+      'the text you copy the text you copy the text you copy the text you copy the text you copy';
+
+    const props = {
+      onCopy: mockOnCopy,
+      value: text
+    };
+
+    test('calls onCopy', async () => {
+      jest.spyOn(CopyButton, 'isCopySupported').mockImplementation(() => true);
+      render(<Copiable {...props} />);
+
+      await userEvent.click(screen.getByTestId('copy-button'));
+
+      expect(mockOnCopy).toHaveBeenCalledTimes(1);
+      expect(mockOnCopy).toHaveBeenCalledWith(text);
+    });
+
+    test('calls onCopy when text is truncated', async () => {
+      jest.spyOn(CopyButton, 'isCopySupported').mockImplementation(() => true);
+      const propsWithTruncated = { ...props, truncated: true };
+      render(<Copiable {...propsWithTruncated} />);
+
+      await userEvent.click(screen.getByTestId('copy-button'));
+
+      expect(mockOnCopy).toHaveBeenCalledTimes(1);
+      expect(mockOnCopy).toHaveBeenCalledWith(text);
+    });
+  });
 });