{
+ static defaultProps = {
+ buttonDefaultText: defaultCopyText,
+ buttonProps: {},
+ buttonSuccessText: defaultCopiedText,
+ className: '',
+ hideOptionalLabel: true,
+ readOnly: true,
+ successStateDuration: DEFAULT_SUCCESS_STATE_DURATION,
+ type: 'text',
+ };
+
+ constructor(props: TextInputWithCopyButtonProps) {
+ super(props);
+
+ this.isCopyCommandSupported = document.queryCommandSupported('copy');
+
+ this.state = {
+ copySuccess: false,
+ buttonText: props.buttonDefaultText,
+ hasFocused: false,
+ };
+ }
+
+ componentDidMount() {
+ const { autofocus, value } = this.props;
+
+ if (autofocus && value) {
+ this.performAutofocus();
+ }
+ }
+
+ componentDidUpdate() {
+ const { autofocus, value, triggerCopyOnLoad } = this.props;
+ const { copySuccess, hasFocused } = this.state;
+
+ // if we've set focus before, and should auto focus on update, make sure to
+ // focus after component update
+ if (autofocus && value) {
+ this.performAutofocus();
+ }
+
+ if (triggerCopyOnLoad && !copySuccess && !hasFocused) {
+ this.animateCopyButton();
+ }
+ }
+
+ componentWillUnmount() {
+ this.clearCopySuccessTimeout();
+ }
+
+ copyInputRef: HTMLInputElement | null = null;
+
+ copySuccessTimeout: number | null = null;
+
+ isCopyCommandSupported: boolean;
+
+ animateCopyButton() {
+ const { successStateDuration, buttonSuccessText } = this.props;
+ this.clearCopySuccessTimeout();
+
+ this.setState(
+ {
+ copySuccess: true,
+ buttonText: buttonSuccessText,
+ hasFocused: true,
+ },
+ () => {
+ this.copySuccessTimeout = window.setTimeout(() => {
+ this.restoreCopyButton();
+ }, successStateDuration);
+ },
+ );
+ }
+
+ clearCopySuccessTimeout() {
+ if (!this.copySuccessTimeout) {
+ return;
+ }
+
+ window.clearTimeout(this.copySuccessTimeout);
+ this.copySuccessTimeout = null;
+ }
+
+ copySelectedText = () => document.execCommand('copy');
+
+ restoreCopyButton = () => {
+ this.setState({
+ copySuccess: false,
+ buttonText: this.props.buttonDefaultText,
+ });
+ };
+
+ handleCopyButtonClick = () => {
+ this.performAutofocus();
+ this.copySelectedText();
+ this.animateCopyButton();
+ };
+
+ handleFocus = (event: React.SyntheticEvent) => {
+ if (this.copyInputRef) {
+ this.performAutofocus();
+ }
+
+ if (this.props.onFocus) {
+ this.props.onFocus(event);
+ }
+ };
+
+ handleCopyEvent = (event: React.SyntheticEvent) => {
+ const { disabled, onCopySuccess } = this.props;
+
+ if (disabled) {
+ event.preventDefault();
+ } else {
+ this.animateCopyButton();
+
+ if (onCopySuccess) {
+ onCopySuccess(event);
+ }
+ }
+ };
+
+ performAutofocus = () => {
+ const { copyInputRef } = this;
+ if (copyInputRef) {
+ copyInputRef.select();
+ copyInputRef.scrollLeft = 0;
+ }
+ };
+
+ renderCopyButton = () =>
+ this.isCopyCommandSupported ? (
+
+ ) : null;
+
+ render() {
+ const { additionalButtons, className, ...rest } = this.props;
+ const { copySuccess } = this.state;
+ const { isCopyCommandSupported } = this;
+
+ const inputProps = omit(rest, [
+ 'autofocus',
+ 'buttonDefaultText',
+ 'buttonSuccessText',
+ 'buttonProps',
+ 'onCopySuccess',
+ 'successStateDuration',
+ 'triggerCopyOnLoad',
+ ]) as TextInputProps;
+
+ if (isCopyCommandSupported) {
+ inputProps.inputRef = ref => {
+ this.copyInputRef = ref;
+ };
+ }
+
+ const wrapperClasses = classNames(className, {
+ 'copy-success': copySuccess,
+ 'text-input-with-copy-button-container': isCopyCommandSupported,
+ });
+
+ const copyEvent = isCopyCommandSupported ? { onCopy: this.handleCopyEvent } : {};
+
+ return (
+
+
+ {additionalButtons}
+ {this.renderCopyButton()}
+
+ );
+ }
+}
+
+export default TextInputWithCopyButton;
diff --git a/src/components/text-input-with-copy-button/__tests__/TextInputWithCopyButton.test.js b/src/components/text-input-with-copy-button/__tests__/TextInputWithCopyButton.test.tsx
similarity index 80%
rename from src/components/text-input-with-copy-button/__tests__/TextInputWithCopyButton.test.js
rename to src/components/text-input-with-copy-button/__tests__/TextInputWithCopyButton.test.tsx
index 5df58417c5..1e0370bacf 100644
--- a/src/components/text-input-with-copy-button/__tests__/TextInputWithCopyButton.test.js
+++ b/src/components/text-input-with-copy-button/__tests__/TextInputWithCopyButton.test.tsx
@@ -2,11 +2,14 @@ import * as React from 'react';
import { shallow, mount } from 'enzyme';
import sinon from 'sinon';
-import TextInputWithCopyButton from '..';
+import TextInputWithCopyButton, { type TextInputWithCopyButtonProps } from '..';
+import Button from '../../button';
const sandbox = sinon.sandbox.create();
+type TextInputWithCopyButtonInstance = InstanceType;
+const createInputRef = (select: () => void) => ({ select, scrollLeft: 0 }) as HTMLInputElement;
-document.execCommand = () => {};
+document.execCommand = jest.fn(() => true);
document.queryCommandSupported = () => false;
describe('components/text-input-with-copy-button/TextInputWithCopyButton', () => {
@@ -17,8 +20,8 @@ describe('components/text-input-with-copy-button/TextInputWithCopyButton', () =>
const buttonDefaultText = 'copy';
const buttonSuccessText = 'copied';
- const renderComponent = props =>
- shallow(
+ const renderComponent = (props: Partial = {}) =>
+ shallow(
expect(wrapper.hasClass('text-input-with-copy-button-container')).toBe(true);
expect(wrapper.hasClass('copy-success')).toBe(true);
- expect(textInputComponent.length).toBe(1);
+ expect(textInputComponent).toHaveLength(1);
expect(textInputComponent.prop('readOnly')).toBe(true);
expect(typeof textInputComponent.prop('inputRef')).toBe('function');
expect(textInputComponent.prop('hideOptionalLabel')).toBe(true);
@@ -61,7 +64,7 @@ describe('components/text-input-with-copy-button/TextInputWithCopyButton', () =>
const textInputComponent = wrapper.find('TextInput');
expect(wrapper.hasClass('text-input-with-copy-button-container')).toBe(false);
- expect(textInputComponent.length).toBe(1);
+ expect(textInputComponent).toHaveLength(1);
expect(textInputComponent.prop('readOnly')).toBe(true);
expect(textInputComponent.prop('inputRef')).not.toBeDefined();
});
@@ -72,9 +75,7 @@ describe('components/text-input-with-copy-button/TextInputWithCopyButton', () =>
autofocus: true,
});
const instance = wrapper.instance();
- instance.copyInputRef = {
- select: selectMock,
- };
+ instance.copyInputRef = createInputRef(selectMock);
instance.componentDidMount();
@@ -89,7 +90,7 @@ describe('components/text-input-with-copy-button/TextInputWithCopyButton', () =>
autofocus: true,
});
const instance = wrapper.instance();
- instance.copyInputRef = { select: selectMock };
+ instance.copyInputRef = createInputRef(selectMock);
instance.componentDidMount();
@@ -97,7 +98,7 @@ describe('components/text-input-with-copy-button/TextInputWithCopyButton', () =>
value: 'http://example.com/',
});
- expect(selectMock.mock.calls.length).toBe(1);
+ expect(selectMock.mock.calls).toHaveLength(1);
});
test('should not autofocus if autofocus is enabled, but there is no value', () => {
@@ -109,9 +110,7 @@ describe('components/text-input-with-copy-button/TextInputWithCopyButton', () =>
});
const instance = wrapper.instance();
- instance.copyInputRef = {
- select: selectMock,
- };
+ instance.copyInputRef = createInputRef(selectMock);
instance.componentDidMount();
@@ -129,7 +128,7 @@ describe('components/text-input-with-copy-button/TextInputWithCopyButton', () =>
value={1}
/>,
);
- const instance = wrapper.instance();
+ const instance = wrapper.instance() as TextInputWithCopyButtonInstance;
instance.clearCopySuccessTimeout = sandbox.mock();
wrapper.unmount();
@@ -157,13 +156,12 @@ describe('components/text-input-with-copy-button/TextInputWithCopyButton', () =>
const instance = wrapper.instance();
instance.isCopyCommandSupported = true;
- const copyButton = wrapper.wrap(instance.renderCopyButton());
- const button = copyButton.find('Button');
+ const copyButton = instance.renderCopyButton() as React.ReactElement>;
- expect(button.length).toBe(1);
- expect(typeof button.prop('onClick')).toBe('function');
- expect(button.prop('type')).toEqual('button');
- expect(button.prop('children')).toEqual(buttonDefaultText);
+ expect(copyButton.type).toBe(Button);
+ expect(typeof copyButton.props.onClick).toBe('function');
+ expect(copyButton.props.type).toEqual('button');
+ expect(copyButton.props.children).toEqual(buttonDefaultText);
});
test('should not render a copy button when copy command is not supported', () => {
@@ -181,9 +179,8 @@ describe('components/text-input-with-copy-button/TextInputWithCopyButton', () =>
const instance = wrapper.instance();
instance.isCopyCommandSupported = true;
- const copyButton = wrapper.wrap(instance.renderCopyButton());
- const button = copyButton.find('Button');
- expect(button.prop('isDisabled')).toBe(true);
+ const copyButton = instance.renderCopyButton() as React.ReactElement>;
+ expect(copyButton.props.isDisabled).toBe(true);
});
});
@@ -192,9 +189,7 @@ describe('components/text-input-with-copy-button/TextInputWithCopyButton', () =>
const wrapper = renderComponent();
const instance = wrapper.instance();
- instance.copyInputRef = {
- select: sandbox.mock(),
- };
+ instance.copyInputRef = createInputRef(sandbox.mock());
instance.copySelectedText = sandbox.mock();
wrapper.setProps({});
@@ -205,9 +200,7 @@ describe('components/text-input-with-copy-button/TextInputWithCopyButton', () =>
const wrapper = renderComponent();
const instance = wrapper.instance();
- instance.copyInputRef = {
- select: sandbox.stub(),
- };
+ instance.copyInputRef = createInputRef(sandbox.stub());
instance.clearCopySuccessTimeout = sandbox.mock();
instance.handleCopyButtonClick();
@@ -219,9 +212,7 @@ describe('components/text-input-with-copy-button/TextInputWithCopyButton', () =>
const instance = wrapper.instance();
let callback;
let data;
- instance.copyInputRef = {
- select: sandbox.stub(),
- };
+ instance.copyInputRef = createInputRef(sandbox.stub());
instance.setState = (obj, cb) => {
data = obj;
callback = cb;
@@ -245,9 +236,9 @@ describe('components/text-input-with-copy-button/TextInputWithCopyButton', () =>
const wrapper = renderComponent();
const instance = wrapper.instance();
- sinon.mock(instance, 'animateCopyButton');
+ sandbox.mock(instance).expects('animateCopyButton');
- instance.handleCopyEvent();
+ instance.handleCopyEvent({} as React.SyntheticEvent);
});
test('should call correct callback', () => {
@@ -256,7 +247,7 @@ describe('components/text-input-with-copy-button/TextInputWithCopyButton', () =>
});
const instance = wrapper.instance();
- instance.handleCopyEvent();
+ instance.handleCopyEvent({} as React.SyntheticEvent);
});
});
@@ -280,11 +271,9 @@ describe('components/text-input-with-copy-button/TextInputWithCopyButton', () =>
});
const instance = wrapper.instance();
- instance.copyInputRef = {
- select: sandbox.mock(),
- };
+ instance.copyInputRef = createInputRef(sandbox.mock());
- instance.handleFocus({});
+ instance.handleFocus({} as React.SyntheticEvent);
});
});
});
diff --git a/src/components/text-input-with-copy-button/index.js b/src/components/text-input-with-copy-button/index.js.flow
similarity index 100%
rename from src/components/text-input-with-copy-button/index.js
rename to src/components/text-input-with-copy-button/index.js.flow
diff --git a/src/components/text-input-with-copy-button/index.ts b/src/components/text-input-with-copy-button/index.ts
new file mode 100644
index 0000000000..20293e9257
--- /dev/null
+++ b/src/components/text-input-with-copy-button/index.ts
@@ -0,0 +1,2 @@
+export { default } from './TextInputWithCopyButton';
+export type { TextInputWithCopyButtonProps } from './TextInputWithCopyButton';