From 7484549fc28f52b8e01128cab5f791b4676c630b Mon Sep 17 00:00:00 2001 From: roman Date: Mon, 10 Aug 2026 14:34:23 +0200 Subject: [PATCH] refactor(text-input): migrate TextInput from Flow to TypeScript --- .../{TextInput.js => TextInput.js.flow} | 0 ...Input.stories.js => TextInput.stories.tsx} | 1 - src/components/text-input/TextInput.tsx | 112 ++++++++++++++++++ ...xtInputField.js => TextInputField.js.flow} | 0 src/components/text-input/TextInputField.tsx | 29 +++++ .../{TextInput.test.js => TextInput.test.tsx} | 11 +- ...tField.test.js => TextInputField.test.tsx} | 7 +- ...t.test.js.snap => TextInput.test.tsx.snap} | 1 + ...t.js.snap => TextInputField.test.tsx.snap} | 0 .../text-input/{index.js => index.js.flow} | 0 src/components/text-input/index.ts | 4 + src/components/time-input/TimeInput.tsx | 2 +- 12 files changed, 158 insertions(+), 9 deletions(-) rename src/components/text-input/{TextInput.js => TextInput.js.flow} (100%) rename src/components/text-input/{TextInput.stories.js => TextInput.stories.tsx} (99%) create mode 100644 src/components/text-input/TextInput.tsx rename src/components/text-input/{TextInputField.js => TextInputField.js.flow} (100%) create mode 100644 src/components/text-input/TextInputField.tsx rename src/components/text-input/__tests__/{TextInput.test.js => TextInput.test.tsx} (93%) rename src/components/text-input/__tests__/{TextInputField.test.js => TextInputField.test.tsx} (92%) rename src/components/text-input/__tests__/__snapshots__/{TextInput.test.js.snap => TextInput.test.tsx.snap} (97%) rename src/components/text-input/__tests__/__snapshots__/{TextInputField.test.js.snap => TextInputField.test.tsx.snap} (100%) rename src/components/text-input/{index.js => index.js.flow} (100%) create mode 100644 src/components/text-input/index.ts diff --git a/src/components/text-input/TextInput.js b/src/components/text-input/TextInput.js.flow similarity index 100% rename from src/components/text-input/TextInput.js rename to src/components/text-input/TextInput.js.flow diff --git a/src/components/text-input/TextInput.stories.js b/src/components/text-input/TextInput.stories.tsx similarity index 99% rename from src/components/text-input/TextInput.stories.js rename to src/components/text-input/TextInput.stories.tsx index 388171295a..f9d4224f5d 100644 --- a/src/components/text-input/TextInput.stories.js +++ b/src/components/text-input/TextInput.stories.tsx @@ -1,4 +1,3 @@ -// @flow /* eslint-disable react-hooks/rules-of-hooks */ import * as React from 'react'; diff --git a/src/components/text-input/TextInput.tsx b/src/components/text-input/TextInput.tsx new file mode 100644 index 0000000000..ac0f2dac38 --- /dev/null +++ b/src/components/text-input/TextInput.tsx @@ -0,0 +1,112 @@ +import * as React from 'react'; +import classNames from 'classnames'; +import uniqueId from 'lodash/uniqueId'; + +import IconVerified from '../../icons/general/IconVerified'; + +import Label from '../label'; +import LoadingIndicator from '../loading-indicator'; +import Tooltip, { TooltipPosition, TooltipTheme, type TooltipProps } from '../tooltip'; + +import './TextInput.scss'; + +export interface TextInputProps extends React.InputHTMLAttributes { + /** Add a class to the component */ + className?: string; + /** Description shown below the label */ + description?: React.ReactNode; + /** Error message shown in the error tooltip */ + error?: React.ReactNode; + /** Renders error tooltip at the specified position (positions are those from Tooltip) */ + errorPosition?: NonNullable; + /** Hides the label */ + hideLabel?: boolean; + /** Hides (optional) text from the label */ + hideOptionalLabel?: boolean; + /** Icon to display in the input field */ + icon?: React.ReactNode; + /** Ref to the underlying input element. @TODO: eventually rename to innerRef for consistancy across all form elements */ + inputRef?: React.Ref; + /** Renders a loading indicator within the component when true */ + isLoading?: boolean; + /** Makes the input value required */ + isRequired?: boolean; + /** Renders a green verified checkmark within the component when true */ + isValid?: boolean; + /** Label displayed for the text input */ + label: React.ReactNode; + /** Tooltip shown on the label */ + labelTooltip?: React.ReactNode; + /** A CSS class for the tooltip's tether element component */ + tooltipTetherClassName?: string; + /** A CSS class for the tooltip's target wrapper element */ + tooltipWrapperClassName?: string; +} + +const TextInput = ({ + className = '', + description, + error, + errorPosition, + hideLabel, + hideOptionalLabel, + icon, + inputRef, + isLoading, + isRequired, + isValid, + label, + labelTooltip, + tooltipTetherClassName: tetherElementClassName, + tooltipWrapperClassName, + ...rest +}: TextInputProps) => { + const hasError = !!error; + const classes = classNames(className, 'text-input-container', { + 'show-error': hasError, + }); + + const descriptionID = React.useRef(uniqueId('description')).current; + + const ariaAttrs = { + 'aria-invalid': hasError, + 'aria-required': isRequired, + 'aria-describedby': description ? descriptionID : undefined, + }; + + return ( +
+ +
+ ); +}; + +TextInput.displayName = 'TextInput'; + +export default TextInput; diff --git a/src/components/text-input/TextInputField.js b/src/components/text-input/TextInputField.js.flow similarity index 100% rename from src/components/text-input/TextInputField.js rename to src/components/text-input/TextInputField.js.flow diff --git a/src/components/text-input/TextInputField.tsx b/src/components/text-input/TextInputField.tsx new file mode 100644 index 0000000000..b8f90510b8 --- /dev/null +++ b/src/components/text-input/TextInputField.tsx @@ -0,0 +1,29 @@ +import * as React from 'react'; +import getProp from 'lodash/get'; +import type { FieldProps } from 'formik'; + +import TextInputPrimitive from './TextInput'; +import type { TextInputProps } from './TextInput'; + +export interface TextInputFieldProps extends Omit, FieldProps { + /** Ref forwarded to the underlying input element as inputRef */ + innerRef?: (instance: HTMLInputElement | null) => void; +} + +const TextInputField = ({ field, form, innerRef, isRequired, ...rest }: TextInputFieldProps) => { + const { name } = field; + const { errors, touched } = form; + const isTouched = getProp(touched, name); + const error = isTouched ? getProp(errors, name) : null; + return ( + + ); +}; + +export default TextInputField; diff --git a/src/components/text-input/__tests__/TextInput.test.js b/src/components/text-input/__tests__/TextInput.test.tsx similarity index 93% rename from src/components/text-input/__tests__/TextInput.test.js rename to src/components/text-input/__tests__/TextInput.test.tsx index 8fe088d04f..ab137cf120 100644 --- a/src/components/text-input/__tests__/TextInput.test.js +++ b/src/components/text-input/__tests__/TextInput.test.tsx @@ -6,6 +6,7 @@ import TetherComponent from 'react-tether'; import ClockBadge16 from '../../../icon/line/ClockBadge16'; import IconVerified from '../../../icons/general/IconVerified'; import LoadingIndicator from '../../loading-indicator'; +import { TooltipPosition } from '../../tooltip'; import TextInput from '..'; jest.mock('lodash/uniqueId', () => () => 'description20'); @@ -68,10 +69,12 @@ describe('components/text-input/TextInput', () => { }); test('should show Tooltip for an error at a custom position', () => { - const wrapper = shallow(); + const wrapper = shallow( + , + ); const tooltip = wrapper.find('Tooltip'); - expect(tooltip.prop('position')).toBe('bottom-center'); + expect(tooltip.prop('position')).toBe(TooltipPosition.BOTTOM_CENTER); }); test('should not show Tooltip when no error exists', () => { @@ -90,7 +93,7 @@ describe('components/text-input/TextInput', () => { }); test('should render text input with description', () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); }); @@ -107,7 +110,7 @@ describe('components/text-input/TextInput', () => { `( 'should render $description', ({ isLoading, isValid, icon, loadingIndicatorExists, validIconExists, customIconExists }) => { - const wrapper = shallow(); + const wrapper = shallow(); if (icon) { expect(wrapper.exists(ClockBadge16)).toBe(customIconExists); } diff --git a/src/components/text-input/__tests__/TextInputField.test.js b/src/components/text-input/__tests__/TextInputField.test.tsx similarity index 92% rename from src/components/text-input/__tests__/TextInputField.test.js rename to src/components/text-input/__tests__/TextInputField.test.tsx index 03bfbd67ab..7414c96659 100644 --- a/src/components/text-input/__tests__/TextInputField.test.js +++ b/src/components/text-input/__tests__/TextInputField.test.tsx @@ -1,10 +1,11 @@ -// @flow - import * as React from 'react'; +import { shallow } from 'enzyme'; + import TextInputField from '../TextInputField'; describe('components/text-input/TextInputField', () => { - const getWrapper = (props = {}) => shallow(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const getWrapper = (props: any = {}) => shallow(); test('should render properly', () => { const wrapper = getWrapper({ diff --git a/src/components/text-input/__tests__/__snapshots__/TextInput.test.js.snap b/src/components/text-input/__tests__/__snapshots__/TextInput.test.tsx.snap similarity index 97% rename from src/components/text-input/__tests__/__snapshots__/TextInput.test.js.snap rename to src/components/text-input/__tests__/__snapshots__/TextInput.test.tsx.snap index 3aed83615e..f571b65a2b 100644 --- a/src/components/text-input/__tests__/__snapshots__/TextInput.test.js.snap +++ b/src/components/text-input/__tests__/__snapshots__/TextInput.test.tsx.snap @@ -6,6 +6,7 @@ exports[`components/text-input/TextInput should render text input with descripti >