🤖 Switch InputField + InputFieldWithIcon JSX components to TS, add more test coverage, fix Storybook build (#43307)
Zed + Opus 4.6; prompt: Convert the InputField JSX component to TypeScript and remove the ts-ignore directives that we no longer need after doing so. - [x] Changes file added - [x] Automated tests updated
This commit is contained in:
@@ -1,291 +0,0 @@
|
||||
import React, { Component } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import classnames from "classnames";
|
||||
import { noop, pick } from "lodash";
|
||||
|
||||
import { stringToClipboard } from "utilities/copy_text";
|
||||
|
||||
import FormField from "components/forms/FormField";
|
||||
import Button from "components/buttons/Button";
|
||||
import Icon from "components/Icon";
|
||||
|
||||
const baseClass = "input-field";
|
||||
|
||||
class InputField extends Component {
|
||||
static propTypes = {
|
||||
autofocus: PropTypes.bool,
|
||||
/** readOnly displays a non-editable field */
|
||||
readOnly: PropTypes.bool,
|
||||
/** disabled displays a greyed out non-editable field */
|
||||
disabled: PropTypes.bool,
|
||||
error: PropTypes.string,
|
||||
inputClassName: PropTypes.string, // eslint-disable-line react/forbid-prop-types
|
||||
inputWrapperClass: PropTypes.string,
|
||||
inputOptions: PropTypes.object, // eslint-disable-line react/forbid-prop-types
|
||||
name: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
onBlur: PropTypes.func,
|
||||
onFocus: PropTypes.func,
|
||||
placeholder: PropTypes.string,
|
||||
type: PropTypes.string,
|
||||
blockAutoComplete: PropTypes.bool,
|
||||
value: PropTypes.oneOfType([
|
||||
PropTypes.bool,
|
||||
PropTypes.string,
|
||||
PropTypes.number,
|
||||
]).isRequired,
|
||||
/** Returns both name and value */
|
||||
parseTarget: PropTypes.bool,
|
||||
tooltip: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
|
||||
labelTooltipPosition: PropTypes.string,
|
||||
helpText: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.arrayOf(PropTypes.string),
|
||||
PropTypes.object,
|
||||
]),
|
||||
/** Use in conjunction with type "password" and enableCopy to see eye icon to view */
|
||||
enableShowSecret: PropTypes.bool,
|
||||
enableCopy: PropTypes.bool,
|
||||
ignore1password: PropTypes.bool,
|
||||
// Accepts string or number for HTML compatibility, (e.g., step="0.1", step={0.1})
|
||||
/** Only effective on input type number */
|
||||
step: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
/** Only effective on input type number */
|
||||
min: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
/** Only effective on input type number */
|
||||
max: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
autofocus: false,
|
||||
inputWrapperClass: "",
|
||||
inputOptions: {},
|
||||
label: null,
|
||||
labelClassName: "",
|
||||
onFocus: noop,
|
||||
onBlur: noop,
|
||||
type: "text",
|
||||
blockAutoComplete: false,
|
||||
value: "",
|
||||
parseTarget: false,
|
||||
tooltip: "",
|
||||
labelTooltipPosition: undefined,
|
||||
helpText: "",
|
||||
enableCopy: false,
|
||||
enableShowSecret: false,
|
||||
ignore1password: false,
|
||||
step: undefined,
|
||||
min: undefined,
|
||||
max: undefined,
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
copied: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { autofocus } = this.props;
|
||||
const { input } = this;
|
||||
|
||||
if (autofocus) {
|
||||
input.focus();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
onInputChange = (evt) => {
|
||||
evt.preventDefault();
|
||||
|
||||
const { value, name } = evt.target;
|
||||
const { onChange, parseTarget } = this.props;
|
||||
|
||||
if (parseTarget) {
|
||||
// Returns both name and value
|
||||
return onChange({ value, name });
|
||||
}
|
||||
|
||||
return onChange(value);
|
||||
};
|
||||
|
||||
onToggleSecret = (evt) => {
|
||||
evt.preventDefault();
|
||||
|
||||
this.setState({ showSecret: !this.state.showSecret });
|
||||
return false;
|
||||
};
|
||||
|
||||
onClickCopy = (e) => {
|
||||
e.preventDefault();
|
||||
stringToClipboard(this.props.value).then(() => {
|
||||
this.setState({ copied: true });
|
||||
setTimeout(() => {
|
||||
this.setState({ copied: false });
|
||||
}, 2000);
|
||||
});
|
||||
};
|
||||
|
||||
renderShowSecretButton = () => {
|
||||
const { onToggleSecret } = this;
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="icon"
|
||||
className={`${baseClass}__show-secret-icon`}
|
||||
onClick={onToggleSecret}
|
||||
size="small"
|
||||
>
|
||||
<Icon name="eye" />
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
renderCopyButton = () => {
|
||||
const { onClickCopy } = this;
|
||||
|
||||
const copyButtonValue = <Icon name="copy" />;
|
||||
const wrapperClasses = classnames(`${baseClass}__copy-wrapper`, {
|
||||
[`${baseClass}__copy-wrapper__text-area`]: this.props.type === "textarea",
|
||||
});
|
||||
|
||||
const copiedConfirmationClasses = classnames(
|
||||
`${baseClass}__copied-confirmation`
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={wrapperClasses}>
|
||||
{this.state.copied && (
|
||||
<span className={copiedConfirmationClasses}>Copied!</span>
|
||||
)}
|
||||
<Button variant="icon" onClick={onClickCopy} size="small" iconStroke>
|
||||
{copyButtonValue}
|
||||
</Button>
|
||||
{this.props.enableShowSecret && this.renderShowSecretButton()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
readOnly,
|
||||
disabled,
|
||||
error,
|
||||
inputClassName,
|
||||
inputOptions,
|
||||
inputWrapperClass,
|
||||
name,
|
||||
onFocus,
|
||||
onBlur,
|
||||
placeholder,
|
||||
type,
|
||||
blockAutoComplete,
|
||||
value,
|
||||
ignore1password,
|
||||
enableCopy,
|
||||
enableShowSecret,
|
||||
step,
|
||||
min,
|
||||
max,
|
||||
} = this.props;
|
||||
|
||||
const { onInputChange } = this;
|
||||
const shouldShowPasswordClass =
|
||||
type === "password" && !this.state.showSecret;
|
||||
const inputClasses = classnames(baseClass, inputClassName, {
|
||||
[`${baseClass}--password`]: shouldShowPasswordClass,
|
||||
[`${baseClass}--read-only`]: readOnly || disabled,
|
||||
[`${baseClass}--disabled`]: disabled,
|
||||
[`${baseClass}--error`]: error,
|
||||
[`${baseClass}__textarea`]: type === "textarea",
|
||||
});
|
||||
|
||||
const inputWrapperClasses = classnames(inputWrapperClass, {
|
||||
[`input-field--read-only`]: readOnly || disabled,
|
||||
[`input-field--disabled`]: disabled,
|
||||
});
|
||||
|
||||
const formFieldProps = pick(this.props, [
|
||||
"helpText",
|
||||
"label",
|
||||
"error",
|
||||
"name",
|
||||
"tooltip",
|
||||
"labelTooltipPosition",
|
||||
]);
|
||||
|
||||
const inputContainerClasses = classnames(`${baseClass}__input-container`, {
|
||||
"copy-enabled": enableCopy,
|
||||
});
|
||||
|
||||
if (type === "textarea") {
|
||||
return (
|
||||
<FormField
|
||||
{...formFieldProps}
|
||||
type="textarea"
|
||||
className={inputWrapperClasses}
|
||||
>
|
||||
<div className={inputContainerClasses}>
|
||||
<textarea
|
||||
name={name}
|
||||
id={name}
|
||||
onChange={onInputChange}
|
||||
onBlur={onBlur}
|
||||
onFocus={onFocus}
|
||||
className={inputClasses}
|
||||
disabled={readOnly || disabled}
|
||||
placeholder={placeholder}
|
||||
ref={(r) => {
|
||||
this.input = r;
|
||||
}}
|
||||
type={type}
|
||||
{...inputOptions}
|
||||
value={value}
|
||||
/>
|
||||
{enableCopy && this.renderCopyButton()}
|
||||
</div>
|
||||
</FormField>
|
||||
);
|
||||
}
|
||||
|
||||
const inputType = this.state.showSecret ? "text" : type;
|
||||
|
||||
return (
|
||||
<FormField
|
||||
{...formFieldProps}
|
||||
type="input"
|
||||
className={inputWrapperClasses}
|
||||
>
|
||||
<div className={inputContainerClasses}>
|
||||
<input
|
||||
disabled={readOnly || disabled}
|
||||
name={name}
|
||||
id={name}
|
||||
onChange={onInputChange}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
className={inputClasses}
|
||||
placeholder={placeholder}
|
||||
ref={(r) => {
|
||||
this.input = r;
|
||||
}}
|
||||
type={inputType}
|
||||
{...inputOptions}
|
||||
value={value}
|
||||
autoComplete={blockAutoComplete ? "new-password" : ""}
|
||||
data-1p-ignore={ignore1password}
|
||||
step={step}
|
||||
min={min}
|
||||
max={max}
|
||||
/>
|
||||
|
||||
{enableCopy && this.renderCopyButton()}
|
||||
</div>
|
||||
</FormField>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default InputField;
|
||||
@@ -1,124 +0,0 @@
|
||||
import React from "react";
|
||||
import InputField from ".";
|
||||
|
||||
export default {
|
||||
component: InputField,
|
||||
title: "Components/FormFields/InputField",
|
||||
argTypes: {
|
||||
type: {
|
||||
control: "select",
|
||||
options: ["text", "password", "email", "number", "textarea"],
|
||||
},
|
||||
value: {
|
||||
control: "text",
|
||||
},
|
||||
placeholder: {
|
||||
control: "text",
|
||||
},
|
||||
label: {
|
||||
control: "text",
|
||||
},
|
||||
error: {
|
||||
control: "text",
|
||||
},
|
||||
helpText: {
|
||||
control: "text",
|
||||
},
|
||||
disabled: {
|
||||
control: "boolean",
|
||||
},
|
||||
readOnly: {
|
||||
control: "boolean",
|
||||
},
|
||||
autofocus: {
|
||||
control: "boolean",
|
||||
},
|
||||
enableCopy: {
|
||||
control: "boolean",
|
||||
},
|
||||
enableShowSecret: {
|
||||
control: "boolean",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const Template = (args) => <InputField {...args} />;
|
||||
|
||||
export const Basic = Template.bind({});
|
||||
Basic.args = {
|
||||
name: "basic-input",
|
||||
label: "Basic Input",
|
||||
value: "",
|
||||
placeholder: "Enter text here",
|
||||
};
|
||||
|
||||
export const WithValue = Template.bind({});
|
||||
WithValue.args = {
|
||||
...Basic.args,
|
||||
value: "Sample text",
|
||||
};
|
||||
|
||||
export const WithError = Template.bind({});
|
||||
WithError.args = {
|
||||
...Basic.args,
|
||||
error: "This field is required",
|
||||
};
|
||||
|
||||
export const Disabled = Template.bind({});
|
||||
Disabled.args = {
|
||||
...Basic.args,
|
||||
disabled: true,
|
||||
};
|
||||
|
||||
export const ReadOnly = Template.bind({});
|
||||
ReadOnly.args = {
|
||||
...Basic.args,
|
||||
readOnly: true,
|
||||
value: "Read-only content",
|
||||
};
|
||||
|
||||
export const WithHelpText = Template.bind({});
|
||||
WithHelpText.args = {
|
||||
...Basic.args,
|
||||
helpText: "This is some helpful information about the input field.",
|
||||
};
|
||||
|
||||
export const Password = Template.bind({});
|
||||
Password.args = {
|
||||
...Basic.args,
|
||||
type: "password",
|
||||
label: "Password",
|
||||
placeholder: "Enter your password",
|
||||
};
|
||||
|
||||
export const Textarea = Template.bind({});
|
||||
Textarea.args = {
|
||||
...Basic.args,
|
||||
type: "textarea",
|
||||
label: "Text area",
|
||||
placeholder: "Enter multiple lines of text",
|
||||
};
|
||||
|
||||
export const WithCopyEnabled = Template.bind({});
|
||||
WithCopyEnabled.args = {
|
||||
...Basic.args,
|
||||
enableCopy: true,
|
||||
value: "This text can be copied",
|
||||
};
|
||||
|
||||
export const WithCopyEnabledInput = Template.bind({});
|
||||
WithCopyEnabledInput.args = {
|
||||
...WithCopyEnabled.args,
|
||||
};
|
||||
|
||||
export const WithTooltip = Template.bind({});
|
||||
WithTooltip.args = {
|
||||
...Basic.args,
|
||||
tooltip: "This is a tooltip for the input field",
|
||||
};
|
||||
|
||||
export const AutoFocus = Template.bind({});
|
||||
AutoFocus.args = {
|
||||
...Basic.args,
|
||||
autofocus: true,
|
||||
};
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Meta, StoryObj } from "@storybook/react";
|
||||
import { action } from "@storybook/addon-actions";
|
||||
|
||||
// @ts-ignore
|
||||
import InputField from ".";
|
||||
|
||||
import "../../../../index.scss";
|
||||
@@ -20,10 +19,6 @@ const meta: Meta<typeof InputField> = {
|
||||
blockAutoComplete: { control: "boolean" },
|
||||
enableCopy: { control: "boolean" },
|
||||
enableShowSecret: { control: "boolean" },
|
||||
copyButtonPosition: {
|
||||
control: "radio",
|
||||
options: ["inside", "outside"],
|
||||
},
|
||||
labelTooltipPosition: {
|
||||
control: "select",
|
||||
options: ["top", "right", "bottom", "left"],
|
||||
@@ -47,6 +42,15 @@ export const Default: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
export const WithValue: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
name: "with-value-input",
|
||||
label: "Input with Value",
|
||||
value: "Sample text",
|
||||
},
|
||||
};
|
||||
|
||||
export const WithError: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
@@ -116,6 +120,15 @@ export const WithCopyButton: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
export const AutoFocus: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
name: "autofocus-input",
|
||||
label: "Autofocus Input",
|
||||
autofocus: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const Textarea: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React from "react";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { renderWithSetup } from "test/test-utils";
|
||||
|
||||
// @ts-ignore
|
||||
import InputField from "./InputField";
|
||||
|
||||
describe("InputField Component", () => {
|
||||
@@ -161,4 +161,208 @@ describe("InputField Component", () => {
|
||||
|
||||
expect(screen.getByPlaceholderText(/enter text/i)).toBeDisabled();
|
||||
});
|
||||
|
||||
test("calls onChange with { name, value } when parseTarget is true", async () => {
|
||||
render(
|
||||
<InputField
|
||||
value=""
|
||||
onChange={mockOnChange}
|
||||
label="Test Input"
|
||||
placeholder="Enter text"
|
||||
name="my-field"
|
||||
parseTarget
|
||||
/>
|
||||
);
|
||||
|
||||
await userEvent.type(screen.getByPlaceholderText(/enter text/i), "A");
|
||||
|
||||
expect(mockOnChange).toHaveBeenCalledTimes(1);
|
||||
expect(mockOnChange).toHaveBeenCalledWith({
|
||||
name: "my-field",
|
||||
value: "A",
|
||||
});
|
||||
});
|
||||
|
||||
test("renders as read-only when readOnly prop is true", () => {
|
||||
render(
|
||||
<InputField
|
||||
value="read only value"
|
||||
onChange={mockOnChange}
|
||||
label="Test Input"
|
||||
placeholder="Enter text"
|
||||
name="test-input"
|
||||
readOnly
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByPlaceholderText(/enter text/i)).toBeDisabled();
|
||||
});
|
||||
|
||||
test("auto-focuses the input when autofocus is true", () => {
|
||||
render(
|
||||
<InputField
|
||||
value=""
|
||||
onChange={mockOnChange}
|
||||
label="Test Input"
|
||||
placeholder="Enter text"
|
||||
name="test-input"
|
||||
autofocus
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByPlaceholderText(/enter text/i)).toHaveFocus();
|
||||
});
|
||||
|
||||
test("sets autocomplete to 'new-password' when blockAutoComplete is true", () => {
|
||||
render(
|
||||
<InputField
|
||||
value=""
|
||||
onChange={mockOnChange}
|
||||
label="Test Input"
|
||||
placeholder="Enter text"
|
||||
name="test-input"
|
||||
blockAutoComplete
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByPlaceholderText(/enter text/i)).toHaveAttribute(
|
||||
"autocomplete",
|
||||
"new-password"
|
||||
);
|
||||
});
|
||||
|
||||
test("sets data-1p-ignore when ignore1password is true", () => {
|
||||
render(
|
||||
<InputField
|
||||
value=""
|
||||
onChange={mockOnChange}
|
||||
label="Test Input"
|
||||
placeholder="Enter text"
|
||||
name="test-input"
|
||||
ignore1password
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByPlaceholderText(/enter text/i)).toHaveAttribute(
|
||||
"data-1p-ignore",
|
||||
"true"
|
||||
);
|
||||
});
|
||||
|
||||
test("renders tooltip on label hover", async () => {
|
||||
const { user } = renderWithSetup(
|
||||
<InputField
|
||||
value=""
|
||||
onChange={mockOnChange}
|
||||
label="Test Input"
|
||||
placeholder="Enter text"
|
||||
name="test-input"
|
||||
tooltip="Helpful tooltip text"
|
||||
/>
|
||||
);
|
||||
|
||||
await user.hover(screen.getByText(/test input/i));
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Helpful tooltip text")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test("sets step, min, and max attributes on number input", () => {
|
||||
render(
|
||||
<InputField
|
||||
value={5}
|
||||
onChange={mockOnChange}
|
||||
label="Number Input"
|
||||
placeholder="Enter number"
|
||||
name="test-number"
|
||||
type="number"
|
||||
step={0.5}
|
||||
min={0}
|
||||
max={100}
|
||||
/>
|
||||
);
|
||||
|
||||
const input = screen.getByPlaceholderText(/enter number/i);
|
||||
expect(input).toHaveAttribute("step", "0.5");
|
||||
expect(input).toHaveAttribute("min", "0");
|
||||
expect(input).toHaveAttribute("max", "100");
|
||||
});
|
||||
|
||||
test("copies value to clipboard when copy button is clicked", async () => {
|
||||
const writeTextMock = jest.fn().mockResolvedValue(undefined);
|
||||
Object.defineProperty(navigator, "clipboard", {
|
||||
value: { writeText: writeTextMock },
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
render(
|
||||
<InputField
|
||||
value="Copy me"
|
||||
onChange={mockOnChange}
|
||||
label="Test Input"
|
||||
placeholder="Enter text"
|
||||
name="test-input"
|
||||
enableCopy
|
||||
/>
|
||||
);
|
||||
|
||||
const copyButton = screen.getByTestId("copy-icon").closest("button")!;
|
||||
await userEvent.click(copyButton);
|
||||
|
||||
expect(writeTextMock).toHaveBeenCalledWith("Copy me");
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Copied!")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test("renders show-secret eye toggle with enableCopy and enableShowSecret on password field", async () => {
|
||||
render(
|
||||
<InputField
|
||||
value="s3cret"
|
||||
onChange={mockOnChange}
|
||||
label="Password"
|
||||
placeholder="Enter password"
|
||||
name="test-password"
|
||||
type="password"
|
||||
enableCopy
|
||||
enableShowSecret
|
||||
/>
|
||||
);
|
||||
|
||||
// The eye icon should be present
|
||||
const eyeIcon = screen.getByTestId("eye-icon");
|
||||
expect(eyeIcon).toBeInTheDocument();
|
||||
|
||||
// Initially the input type should be password
|
||||
const input = screen.getByPlaceholderText(/enter password/i);
|
||||
expect(input).toHaveAttribute("type", "password");
|
||||
|
||||
// Click the eye toggle to reveal the secret
|
||||
const eyeButton = eyeIcon.closest("button")!;
|
||||
await userEvent.click(eyeButton);
|
||||
|
||||
// After toggling, the input type should be text
|
||||
expect(input).toHaveAttribute("type", "text");
|
||||
|
||||
// Click again to hide
|
||||
await userEvent.click(eyeButton);
|
||||
expect(input).toHaveAttribute("type", "password");
|
||||
});
|
||||
|
||||
test("renders copy button in textarea mode when enableCopy is true", () => {
|
||||
render(
|
||||
<InputField
|
||||
value="Textarea content"
|
||||
onChange={mockOnChange}
|
||||
label="Test Textarea"
|
||||
placeholder="Enter text"
|
||||
name="test-textarea"
|
||||
type="textarea"
|
||||
enableCopy
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("copy-icon")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,257 @@
|
||||
import React, { useState, useRef, useEffect, useCallback } from "react";
|
||||
import classnames from "classnames";
|
||||
|
||||
import { PlacesType } from "react-tooltip-5";
|
||||
|
||||
import { stringToClipboard } from "utilities/copy_text";
|
||||
|
||||
import FormField from "components/forms/FormField";
|
||||
import Button from "components/buttons/Button";
|
||||
import Icon from "components/Icon";
|
||||
|
||||
const baseClass = "input-field";
|
||||
|
||||
export interface IInputFieldProps {
|
||||
autofocus?: boolean;
|
||||
/** readOnly displays a non-editable field */
|
||||
readOnly?: boolean;
|
||||
/** disabled displays a greyed out non-editable field */
|
||||
disabled?: boolean;
|
||||
error?: string | null;
|
||||
inputClassName?: string;
|
||||
inputWrapperClass?: string;
|
||||
inputOptions?: React.InputHTMLAttributes<HTMLInputElement>;
|
||||
name?: string;
|
||||
/**
|
||||
* Receives the field value (string) by default, or { name, value } when
|
||||
* parseTarget is true. See IInputFieldParseTarget and InputFieldOnChange
|
||||
* in interfaces/form_field.ts for caller-side typing helpers.
|
||||
*/
|
||||
onChange?: (value: any) => void;
|
||||
onBlur?: (
|
||||
evt: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement>
|
||||
) => void;
|
||||
onFocus?: (
|
||||
evt: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement>
|
||||
) => void;
|
||||
placeholder?: string;
|
||||
type?: string;
|
||||
blockAutoComplete?: boolean;
|
||||
value?: boolean | string | number | null;
|
||||
/** Returns both name and value */
|
||||
parseTarget?: boolean;
|
||||
tooltip?: React.ReactNode;
|
||||
labelTooltipPosition?: PlacesType;
|
||||
label?: React.ReactNode;
|
||||
labelClassName?: string;
|
||||
helpText?: React.ReactNode;
|
||||
/** Use in conjunction with type "password" and enableCopy to see eye icon to view */
|
||||
enableShowSecret?: boolean;
|
||||
enableCopy?: boolean;
|
||||
ignore1password?: boolean;
|
||||
/** Only effective on input type number */
|
||||
step?: string | number;
|
||||
/** Only effective on input type number */
|
||||
min?: string | number;
|
||||
/** Only effective on input type number */
|
||||
max?: string | number;
|
||||
}
|
||||
|
||||
const InputField = ({
|
||||
autofocus = false,
|
||||
readOnly,
|
||||
disabled,
|
||||
error,
|
||||
inputClassName,
|
||||
inputWrapperClass = "",
|
||||
inputOptions = {},
|
||||
name,
|
||||
onChange,
|
||||
onBlur,
|
||||
onFocus,
|
||||
placeholder,
|
||||
type = "text",
|
||||
blockAutoComplete = false,
|
||||
value = "",
|
||||
parseTarget = false,
|
||||
tooltip = "",
|
||||
labelTooltipPosition,
|
||||
label = null,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
labelClassName: _labelClassName = "",
|
||||
helpText = "",
|
||||
enableShowSecret = false,
|
||||
enableCopy = false,
|
||||
ignore1password = false,
|
||||
step,
|
||||
min,
|
||||
max,
|
||||
}: IInputFieldProps): JSX.Element => {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [showSecret, setShowSecret] = useState(false);
|
||||
const inputRef = useRef<HTMLInputElement | HTMLTextAreaElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (autofocus && inputRef.current) {
|
||||
(inputRef.current as HTMLElement).focus();
|
||||
}
|
||||
}, [autofocus]);
|
||||
|
||||
const onInputChange = useCallback(
|
||||
(evt: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
evt.preventDefault();
|
||||
|
||||
const target = evt.target as HTMLInputElement;
|
||||
const { value: inputValue, name: inputName } = target;
|
||||
|
||||
if (parseTarget) {
|
||||
// Returns both name and value
|
||||
return onChange?.({ value: inputValue, name: inputName });
|
||||
}
|
||||
|
||||
return onChange?.(inputValue);
|
||||
},
|
||||
[onChange, parseTarget]
|
||||
);
|
||||
|
||||
const onToggleSecret = useCallback((evt: React.MouseEvent) => {
|
||||
evt.preventDefault();
|
||||
setShowSecret((prev) => !prev);
|
||||
}, []);
|
||||
|
||||
const onClickCopy = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
stringToClipboard(value).then(() => {
|
||||
setCopied(true);
|
||||
setTimeout(() => {
|
||||
setCopied(false);
|
||||
}, 2000);
|
||||
});
|
||||
},
|
||||
[value]
|
||||
);
|
||||
|
||||
const renderShowSecretButton = () => {
|
||||
return (
|
||||
<Button
|
||||
variant="icon"
|
||||
className={`${baseClass}__show-secret-icon`}
|
||||
onClick={onToggleSecret}
|
||||
size="small"
|
||||
>
|
||||
<Icon name="eye" />
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
const renderCopyButton = () => {
|
||||
const copyButtonValue = <Icon name="copy" />;
|
||||
const wrapperClasses = classnames(`${baseClass}__copy-wrapper`, {
|
||||
[`${baseClass}__copy-wrapper__text-area`]: type === "textarea",
|
||||
});
|
||||
|
||||
const copiedConfirmationClasses = classnames(
|
||||
`${baseClass}__copied-confirmation`
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={wrapperClasses}>
|
||||
{copied && <span className={copiedConfirmationClasses}>Copied!</span>}
|
||||
<Button variant="icon" onClick={onClickCopy} size="small" iconStroke>
|
||||
{copyButtonValue}
|
||||
</Button>
|
||||
{enableShowSecret && renderShowSecretButton()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const shouldShowPasswordClass = type === "password" && !showSecret;
|
||||
const inputClasses = classnames(baseClass, inputClassName, {
|
||||
[`${baseClass}--password`]: shouldShowPasswordClass,
|
||||
[`${baseClass}--read-only`]: readOnly || disabled,
|
||||
[`${baseClass}--disabled`]: disabled,
|
||||
[`${baseClass}--error`]: !!error,
|
||||
[`${baseClass}__textarea`]: type === "textarea",
|
||||
});
|
||||
|
||||
const inputWrapperClasses = classnames(inputWrapperClass, {
|
||||
[`input-field--read-only`]: readOnly || disabled,
|
||||
[`input-field--disabled`]: disabled,
|
||||
});
|
||||
|
||||
const formFieldProps = {
|
||||
helpText,
|
||||
label,
|
||||
error,
|
||||
name: name ?? "",
|
||||
tooltip,
|
||||
labelTooltipPosition,
|
||||
};
|
||||
|
||||
const inputContainerClasses = classnames(`${baseClass}__input-container`, {
|
||||
"copy-enabled": enableCopy,
|
||||
});
|
||||
|
||||
if (type === "textarea") {
|
||||
return (
|
||||
<FormField
|
||||
{...formFieldProps}
|
||||
type="textarea"
|
||||
className={inputWrapperClasses}
|
||||
>
|
||||
<div className={inputContainerClasses}>
|
||||
<textarea
|
||||
name={name}
|
||||
id={name}
|
||||
onChange={onInputChange}
|
||||
onBlur={onBlur}
|
||||
onFocus={onFocus}
|
||||
className={inputClasses}
|
||||
disabled={readOnly || disabled}
|
||||
placeholder={placeholder}
|
||||
ref={(r) => {
|
||||
inputRef.current = r;
|
||||
}}
|
||||
{...(inputOptions as React.TextareaHTMLAttributes<HTMLTextAreaElement>)}
|
||||
value={value as string | number}
|
||||
/>
|
||||
{enableCopy && renderCopyButton()}
|
||||
</div>
|
||||
</FormField>
|
||||
);
|
||||
}
|
||||
|
||||
const inputType = showSecret ? "text" : type;
|
||||
|
||||
return (
|
||||
<FormField {...formFieldProps} type="input" className={inputWrapperClasses}>
|
||||
<div className={inputContainerClasses}>
|
||||
<input
|
||||
disabled={readOnly || disabled}
|
||||
name={name}
|
||||
id={name}
|
||||
onChange={onInputChange}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
className={inputClasses}
|
||||
placeholder={placeholder}
|
||||
ref={(r) => {
|
||||
inputRef.current = r;
|
||||
}}
|
||||
type={inputType}
|
||||
{...inputOptions}
|
||||
value={value as string | number}
|
||||
autoComplete={blockAutoComplete ? "new-password" : ""}
|
||||
data-1p-ignore={ignore1password}
|
||||
step={step}
|
||||
min={min}
|
||||
max={max}
|
||||
/>
|
||||
{enableCopy && renderCopyButton()}
|
||||
</div>
|
||||
</FormField>
|
||||
);
|
||||
};
|
||||
|
||||
export default InputField;
|
||||
Reference in New Issue
Block a user