From 54a470fc13bde71841d5cba08db6417c18654d24 Mon Sep 17 00:00:00 2001 From: nathanyoung <1447339+nathanyoung@users.noreply.github.com> Date: Wed, 6 Mar 2024 15:17:38 -0800 Subject: [PATCH 1/3] cleanup --- ...xtareaInputFloating.Playground.stories.tsx | 21 +++---------------- src/components/index.ts | 2 ++ 2 files changed, 5 insertions(+), 18 deletions(-) diff --git a/src/components/TextareaInputFloating/TextareaInputFloating.Playground.stories.tsx b/src/components/TextareaInputFloating/TextareaInputFloating.Playground.stories.tsx index 4f7efe218..ebefd9b4b 100644 --- a/src/components/TextareaInputFloating/TextareaInputFloating.Playground.stories.tsx +++ b/src/components/TextareaInputFloating/TextareaInputFloating.Playground.stories.tsx @@ -33,12 +33,6 @@ export default { helpText: { control: 'text', }, - hideLabel: { - control: 'boolean', - }, - isClearable: { - control: 'boolean', - }, isDisabled: { control: 'boolean', }, @@ -51,15 +45,12 @@ export default { placeholder: { control: 'text', }, - prefix: { - control: 'text', - }, - suffix: { - control: 'text', - }, maxLength: { control: 'number', }, + rows: { + control: 'number', + }, size: { control: { type: 'radio', @@ -69,12 +60,6 @@ export default { requiredIndicator: { control: 'text', }, - type: { - control: { - type: 'select', - options: ['text', 'password', 'email', 'tel', 'url', 'search'], - }, - }, }, } as Meta; diff --git a/src/components/index.ts b/src/components/index.ts index 730a73949..8d0d39367 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -41,6 +41,7 @@ export * from './RadioGroup/RadioGroup'; export * from './ResponsiveProvider/ResponsiveProvider'; export * from './SelectInput/SelectInput'; export * from './SelectInputNative/SelectInputNative'; +export * from './SelectInputNativeFloating/SelectInputNativeFloating'; export * from './Spinner/Spinner'; export * from './TabPanels/TabPanels'; export * from './Tabs/Tabs'; @@ -49,6 +50,7 @@ export * from './Table/Table'; export * from './TextInput/TextInput'; export * from './TextInputFloating/TextInputFloating'; export * from './TextareaInput/TextareaInput'; +export * from './TextareaInputFloating/TextareaInputFloating'; export * from './TextLink/TextLink'; export * from './ThemeProvider/ThemeProvider'; export * from './TimePicker/TimePicker'; From 6a2deac8ef072c5faeb8d0f256afd3755a0ddc8a Mon Sep 17 00:00:00 2001 From: nathanyoung <1447339+nathanyoung@users.noreply.github.com> Date: Wed, 6 Mar 2024 15:27:58 -0800 Subject: [PATCH 2/3] fix: rename floating inputs to inset --- .../SelectInputNativeFloating.tsx | 239 ---------------- ...lectInputNativeInset.Overview.stories.mdx} | 32 +-- ...ctInputNativeInset.Playground.stories.tsx} | 20 +- .../SelectInputNativeInset.module.scss} | 0 .../SelectInputNativeInset.test.tsx} | 32 +-- .../SelectInputNativeInset.tsx | 240 ++++++++++++++++ .../TextInputFloating/TextInputFloating.tsx | 261 ----------------- .../TextInputInset.Overview.stories.mdx} | 46 +-- .../TextInputInset.Playground.stories.tsx} | 16 +- .../TextInputInset.VisualTest.stories.tsx} | 58 ++-- .../TextInputInset.module.scss} | 0 .../TextInputInset.test.tsx} | 56 ++-- .../TextInputInset/TextInputInset.tsx | 262 ++++++++++++++++++ .../TextareaInputInset.Overview.stories.mdx} | 36 +-- ...TextareaInputInset.Playground.stories.tsx} | 20 +- ...TextareaInputInset.VisualTest.stories.tsx} | 14 +- .../TextareaInputInset.module.scss} | 0 .../TextareaInputInset.test.tsx} | 41 ++- .../TextareaInputInset.tsx} | 179 ++++++------ src/components/index.ts | 6 +- src/docs/FormTheming.stories.mdx | 29 +- .../FormControls.VisualTests.stories.tsx | 10 +- src/stories/FormControls.stories.mdx | 20 +- 23 files changed, 807 insertions(+), 810 deletions(-) delete mode 100644 src/components/SelectInputNativeFloating/SelectInputNativeFloating.tsx rename src/components/{SelectInputNativeFloating/SelectInputNativeFloating.Overview.stories.mdx => SelectInputNativeInset/SelectInputNativeInset.Overview.stories.mdx} (88%) rename src/components/{SelectInputNativeFloating/SelectInputNativeFloating.Playground.stories.tsx => SelectInputNativeInset/SelectInputNativeInset.Playground.stories.tsx} (73%) rename src/components/{SelectInputNativeFloating/SelectInputNativeFloating.module.scss => SelectInputNativeInset/SelectInputNativeInset.module.scss} (100%) rename src/components/{SelectInputNativeFloating/SelectInputNativeFloating.test.tsx => SelectInputNativeInset/SelectInputNativeInset.test.tsx} (93%) create mode 100644 src/components/SelectInputNativeInset/SelectInputNativeInset.tsx delete mode 100644 src/components/TextInputFloating/TextInputFloating.tsx rename src/components/{TextInputFloating/TextInputFloating.Overview.stories.mdx => TextInputInset/TextInputInset.Overview.stories.mdx} (89%) rename src/components/{TextInputFloating/TextInputFloating.Playground.stories.tsx => TextInputInset/TextInputInset.Playground.stories.tsx} (77%) rename src/components/{TextInputFloating/TextInputFloating.VisualTest.stories.tsx => TextInputInset/TextInputInset.VisualTest.stories.tsx} (79%) rename src/components/{TextInputFloating/TextInputFloating.module.scss => TextInputInset/TextInputInset.module.scss} (100%) rename src/components/{TextInputFloating/TextInputFloating.test.tsx => TextInputInset/TextInputInset.test.tsx} (80%) create mode 100644 src/components/TextInputInset/TextInputInset.tsx rename src/components/{TextareaInputFloating/TextareaInputFloating.Overview.stories.mdx => TextareaInputInset/TextareaInputInset.Overview.stories.mdx} (87%) rename src/components/{TextareaInputFloating/TextareaInputFloating.Playground.stories.tsx => TextareaInputInset/TextareaInputInset.Playground.stories.tsx} (71%) rename src/components/{TextareaInputFloating/TextareaInputFloating.VisualTest.stories.tsx => TextareaInputInset/TextareaInputInset.VisualTest.stories.tsx} (79%) rename src/components/{TextareaInputFloating/TextareaInputFloating.module.scss => TextareaInputInset/TextareaInputInset.module.scss} (100%) rename src/components/{TextareaInputFloating/TextareaInputFloating.test.tsx => TextareaInputInset/TextareaInputInset.test.tsx} (83%) rename src/components/{TextareaInputFloating/TextareaInputFloating.tsx => TextareaInputInset/TextareaInputInset.tsx} (54%) diff --git a/src/components/SelectInputNativeFloating/SelectInputNativeFloating.tsx b/src/components/SelectInputNativeFloating/SelectInputNativeFloating.tsx deleted file mode 100644 index 9537febd0..000000000 --- a/src/components/SelectInputNativeFloating/SelectInputNativeFloating.tsx +++ /dev/null @@ -1,239 +0,0 @@ -import React, { - ChangeEvent, - forwardRef, - MouseEvent, - KeyboardEvent, - FocusEvent, - ForwardRefExoticComponent, - ReactNode, - HTMLProps, -} from 'react'; -import classNames from 'classnames'; -import { ChangeEvent as CleaveChangeEvent } from 'cleave.js/react/props'; -import { ResponsiveProp } from '../../types'; -import { generateResponsiveClasses } from '../../lib/generateResponsiveClasses'; - -import { Box, BoxProps } from '../Box/Box'; -import { HelpText } from '../HelpText/HelpText'; -import { Icon } from '../Icon/Icon'; -import { getAutoCompleteValue } from '../../lib/getAutoCompleteValue'; -import styles from './SelectInputNativeFloating.module.scss'; -import { InputValidationMessage } from '../InputValidationMessage/InputValidationMessage'; - -export type SelectInputNativeFloatingSize = 'md' | 'lg'; -export interface SelectInputNativeFloatingProps { - /** - * The input's id attribute. Used to programmatically tie the input with its label. - */ - id: string; - /** - * Custom content to be displayed above the input. If the label is hidden, will be used to set aria-label attribute. - */ - label: string; - /** - * List of options for the select input. - */ - options: { value: string | number; label: string | number; }[]; - /** - * Callback function to call on change event. - */ - onChange: ( - event: ChangeEvent | CleaveChangeEvent, - ) => void; - /** - * Value of selected option. Should match the value key in the option object. - */ - value: string | number | null; - /** - * Automatically focus the input when the page is loaded. - */ - autoFocus?: boolean; - /** - * Custom class to be added to standard input classes. - */ - className?: string; - /** - * Mark the input field as invalid and display a validation message. - * Pass a string or node to render a validation message below the input. - */ - error?: ReactNode; - /** - * Additional clarifying text to help describe the input - */ - helpText?: ReactNode; - /** - * Props passed directly to the input element of the component - */ - inputProps?: BoxProps & HTMLProps; - /** - * The input's disabled attribute - */ - isDisabled?: boolean; - /** - * The required and aria-required attributes on the input - */ - isRequired?: boolean; - /** - * The input's 'name' attribute. - */ - name?: string; - /** - * Callback function to call on blur event. - */ - onBlur?: (event: FocusEvent) => void; - /** - * Callback function to call when input us cleared. When this is passed, - * the input will display an icon on the right side, for triggering this callback. - */ - onClear?: ( - event: MouseEvent | KeyboardEvent, - ) => void; - /** - * Callback function to call on focus event. - */ - onFocus?: (event: FocusEvent) => void; - /** - * The input placeholder attribute. - */ - placeholder?: string; - /** - * Visual indicator that the field is required, that gets appended to the label - */ - requiredIndicator?: ReactNode; - /** - * The size of the text input. - */ - size?: - | SelectInputNativeFloatingSize - | ResponsiveProp; - /** - * Additional props to be spread to rendered element - */ - [x: string]: any; // eslint-disable-line -} - -export const SelectInputNativeFloating: ForwardRefExoticComponent = forwardRef( - ( - { - id, - label, - onChange, - value, - autoComplete = false, - autoFocus = false, - error = false, - helpText, - inputProps = {}, - isDisabled = false, - isRequired = false, - name = '', - onBlur = undefined, - onClear = undefined, - onFocus = undefined, - options, - placeholder = 'Select...', - requiredIndicator = ' *', - size = 'md', - }, - ref, - ) => { - const placeholderOption = { value: '', label: placeholder }; - const optionsWithPlaceholder = [{ ...placeholderOption }, ...options]; - - const responsiveClasses = generateResponsiveClasses('size', size); - - const inputWrapperClasses = classNames( - 'palmetto-components__variables__form-control', - styles['text-input-wrapper'], - ...responsiveClasses.map(c => styles[c]), - { - [styles.error]: error, - [styles.disabled]: isDisabled, - [styles['is-clearable']]: onClear, - }, - ); - - const clearBtnClasses = classNames(styles['clear-button'], styles.md); - - const renderClearIcon = (): ReactNode => { - const handleKeyPress = ( - event: KeyboardEvent, - ): void => { - if (event.keyCode === 13 && onClear) onClear(event); - }; - - return ( - - ); - }; - - const computedInputProps: SelectInputNativeFloatingProps['inputProps'] = { - ...inputProps, // These are spread first so that we don't have top level props overwritten by the user. - 'aria-required': isRequired, - 'aria-invalid': !!error, - 'aria-label': label, - 'aria-labelledby': label ? `${id}Label` : undefined, - autoComplete: getAutoCompleteValue(autoComplete), - autoFocus, - disabled: isDisabled, - id, - name, - onBlur, - onChange, - onFocus, - required: isRequired, - value: value ?? '', - className: classNames(inputProps.className), - }; - - return ( -
- - - {optionsWithPlaceholder.map(option => ( - - ))} - - {!!onClear && !!value && renderClearIcon()} - - - {helpText && {helpText}} - {error && error !== true && ( - {error} - )} -
- ); - }, -); diff --git a/src/components/SelectInputNativeFloating/SelectInputNativeFloating.Overview.stories.mdx b/src/components/SelectInputNativeInset/SelectInputNativeInset.Overview.stories.mdx similarity index 88% rename from src/components/SelectInputNativeFloating/SelectInputNativeFloating.Overview.stories.mdx rename to src/components/SelectInputNativeInset/SelectInputNativeInset.Overview.stories.mdx index 89d5aad44..689cba3b2 100644 --- a/src/components/SelectInputNativeFloating/SelectInputNativeFloating.Overview.stories.mdx +++ b/src/components/SelectInputNativeInset/SelectInputNativeInset.Overview.stories.mdx @@ -1,22 +1,22 @@ import { useState } from 'react'; import { action } from '@storybook/addon-actions'; import { Meta, Story, Canvas, ArgsTable } from '@storybook/addon-docs'; -import { SelectInputNativeFloating } from './SelectInputNativeFloating'; +import { SelectInputNativeInset } from './SelectInputNativeInset'; import { RESPONSIVE_STORY } from '../../docs/constants'; import { Box } from '../Box/Box'; import { Icon } from '../Icon/Icon'; -# SelectInputNativeFloating +# SelectInputNativeInset -Use `SelectInputNativeFloating` to create a floating label input field, but unlike `TextInputFloating` they always show the `label` in the floated state. It is a controlled component, which means that you need to provide a value and an `onChange` handler to update the value. +Use `SelectInputNativeInset` to create a floating label input field, but unlike `TextInputInset` they always show the `label` in the floated state. It is a controlled component, which means that you need to provide a value and an `onChange` handler to update the value. { @@ -52,7 +52,7 @@ Use `SelectInputNativeFloating` to create a floating label input field, but unli ## Props - + ### Required @@ -69,7 +69,7 @@ Use the `isRequired` prop to display an `*` after the label and set the `require ]; return ( - { @@ -79,7 +79,7 @@ Use the `isRequired` prop to display an `*` after the label and set the `require value={value} isRequired /> - { @@ -110,7 +110,7 @@ Use `helpText` to provide additional information about the input field. { value: 'Tesla', label: 'Tesla' }, ]; return ( - { @@ -199,7 +199,7 @@ a callback function when the clear icon is clicked, which can then be handled to { value: 'option3', label: 'Option 3' }, ]; return ( - { @@ -229,14 +229,14 @@ Two sizes, `md` and `lg` are available, with `md` being the default. ]; return ( - setValue(event.target.value)} options={options} /> - setValue(event.target.value)} options={options} /> - = ({ ...args }) => ( - +const Template: Story = ({ ...args }) => ( + ); export const Playground = Template.bind({}); Playground.args = { - id: 'playgroundSelectInputNativeFloating', - label: 'Playground SelectInputNativeFloating', - name: 'playgroundSelectInputNativeFloating', + id: 'playgroundSelectInputNativeInset', + label: 'Playground SelectInputNativeInset', + name: 'playgroundSelectInputNativeInset', options: [ { value: 'chocolate', label: 'Chocolate' }, { value: 'strawberry', label: 'Strawberry' }, diff --git a/src/components/SelectInputNativeFloating/SelectInputNativeFloating.module.scss b/src/components/SelectInputNativeInset/SelectInputNativeInset.module.scss similarity index 100% rename from src/components/SelectInputNativeFloating/SelectInputNativeFloating.module.scss rename to src/components/SelectInputNativeInset/SelectInputNativeInset.module.scss diff --git a/src/components/SelectInputNativeFloating/SelectInputNativeFloating.test.tsx b/src/components/SelectInputNativeInset/SelectInputNativeInset.test.tsx similarity index 93% rename from src/components/SelectInputNativeFloating/SelectInputNativeFloating.test.tsx rename to src/components/SelectInputNativeInset/SelectInputNativeInset.test.tsx index e47ae9a9e..9d062e426 100644 --- a/src/components/SelectInputNativeFloating/SelectInputNativeFloating.test.tsx +++ b/src/components/SelectInputNativeInset/SelectInputNativeInset.test.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { render, fireEvent, screen } from '@testing-library/react'; import { BreakpointSize } from '../../types'; -import { SelectInputNativeFloating } from './SelectInputNativeFloating'; +import { SelectInputNativeInset } from './SelectInputNativeInset'; const selectOptions = [ { value: 'chocolate', label: 'Chocolate' }, @@ -9,13 +9,13 @@ const selectOptions = [ { value: 'vanilla', label: 'Vanilla' }, ]; -describe('SelectInputNativeFloating', () => { +describe('SelectInputNativeInset', () => { describe('Callback Handling', () => { test('it fires onChange callback on change', async () => { const mockedHandleChange = jest.fn(); const { getByLabelText } = render( - { const mockedHandleFocus = jest.fn(); render( - { const mockedHandleBlur = jest.fn(); render( - { test('onClear prop renders clear icon when input has value', () => { const mockedHandleChange = jest.fn(); render( - { const mockedHandleClear = jest.fn(() => null); render( - { const mockedHandleChange = jest.fn(); render( - { test('assigns the "aria-labelledby" attribute and renders label correct id, when a label is provided', () => { render( - { const mockedHandleChange = jest.fn(); render( - { const mockedHandleChange = jest.fn(); render( - { const mockedHandleChange = jest.fn(); render( - { const mockedHandleChange = jest.fn(); render( - { sizes.forEach(size => { test(`it has a ${size} class applied to it`, () => { render( - { breakpoints.forEach(breakpoint => { test(`it applies responsive classes for breakpoint: ${breakpoint} and size: ${size}`, () => { render( - { test('It applies responsive classes when multiple are applied', () => { render( - | CleaveChangeEvent, + ) => void; + /** + * Value of selected option. Should match the value key in the option object. + */ + value: string | number | null; + /** + * Automatically focus the input when the page is loaded. + */ + autoFocus?: boolean; + /** + * Custom class to be added to standard input classes. + */ + className?: string; + /** + * Mark the input field as invalid and display a validation message. + * Pass a string or node to render a validation message below the input. + */ + error?: ReactNode; + /** + * Additional clarifying text to help describe the input + */ + helpText?: ReactNode; + /** + * Props passed directly to the input element of the component + */ + inputProps?: BoxProps & HTMLProps; + /** + * The input's disabled attribute + */ + isDisabled?: boolean; + /** + * The required and aria-required attributes on the input + */ + isRequired?: boolean; + /** + * The input's 'name' attribute. + */ + name?: string; + /** + * Callback function to call on blur event. + */ + onBlur?: (event: FocusEvent) => void; + /** + * Callback function to call when input us cleared. When this is passed, + * the input will display an icon on the right side, for triggering this callback. + */ + onClear?: ( + event: MouseEvent | KeyboardEvent, + ) => void; + /** + * Callback function to call on focus event. + */ + onFocus?: (event: FocusEvent) => void; + /** + * The input placeholder attribute. + */ + placeholder?: string; + /** + * Visual indicator that the field is required, that gets appended to the label + */ + requiredIndicator?: ReactNode; + /** + * The size of the text input. + */ + size?: + | SelectInputNativeInsetSize + | ResponsiveProp; + /** + * Additional props to be spread to rendered element + */ + [x: string]: any; // eslint-disable-line +} + +export const SelectInputNativeInset: ForwardRefExoticComponent = + forwardRef( + ( + { + id, + label, + onChange, + value, + autoComplete = false, + autoFocus = false, + error = false, + helpText, + inputProps = {}, + isDisabled = false, + isRequired = false, + name = '', + onBlur = undefined, + onClear = undefined, + onFocus = undefined, + options, + placeholder = 'Select...', + requiredIndicator = ' *', + size = 'md', + }, + ref, + ) => { + const placeholderOption = { value: '', label: placeholder }; + const optionsWithPlaceholder = [{ ...placeholderOption }, ...options]; + + const responsiveClasses = generateResponsiveClasses('size', size); + + const inputWrapperClasses = classNames( + 'palmetto-components__variables__form-control', + styles['text-input-wrapper'], + ...responsiveClasses.map(c => styles[c]), + { + [styles.error]: error, + [styles.disabled]: isDisabled, + [styles['is-clearable']]: onClear, + }, + ); + + const clearBtnClasses = classNames(styles['clear-button'], styles.md); + + const renderClearIcon = (): ReactNode => { + const handleKeyPress = ( + event: KeyboardEvent, + ): void => { + if (event.keyCode === 13 && onClear) onClear(event); + }; + + return ( + + ); + }; + + const computedInputProps: SelectInputNativeInsetProps['inputProps'] = { + ...inputProps, // These are spread first so that we don't have top level props overwritten by the user. + 'aria-required': isRequired, + 'aria-invalid': !!error, + 'aria-label': label, + 'aria-labelledby': label ? `${id}Label` : undefined, + autoComplete: getAutoCompleteValue(autoComplete), + autoFocus, + disabled: isDisabled, + id, + name, + onBlur, + onChange, + onFocus, + required: isRequired, + value: value ?? '', + className: classNames(inputProps.className), + }; + + return ( +
+ + + {optionsWithPlaceholder.map(option => ( + + ))} + + {!!onClear && !!value && renderClearIcon()} + + + {helpText && {helpText}} + {error && error !== true && ( + {error} + )} +
+ ); + }, + ); diff --git a/src/components/TextInputFloating/TextInputFloating.tsx b/src/components/TextInputFloating/TextInputFloating.tsx deleted file mode 100644 index 33a55b296..000000000 --- a/src/components/TextInputFloating/TextInputFloating.tsx +++ /dev/null @@ -1,261 +0,0 @@ -import React, { - ChangeEvent, - forwardRef, - MouseEvent, - KeyboardEvent, - FocusEvent, - ForwardRefExoticComponent, - ReactNode, - HTMLProps, - InputHTMLAttributes, -} from 'react'; -import classNames from 'classnames'; -import { ChangeEvent as CleaveChangeEvent } from 'cleave.js/react/props'; -import { ResponsiveProp } from '../../types'; -import { generateResponsiveClasses } from '../../lib/generateResponsiveClasses'; - -import { Box, BoxProps } from '../Box/Box'; -import { HelpText } from '../HelpText/HelpText'; -import { Icon } from '../Icon/Icon'; -import { getAutoCompleteValue } from '../../lib/getAutoCompleteValue'; -import styles from './TextInputFloating.module.scss'; -import { InputValidationMessage } from '../InputValidationMessage/InputValidationMessage'; - -export type TextInputFloatingSize = 'md' | 'lg'; -export interface TextInputFloatingProps { - /** - * The input's id attribute. Used to programmatically tie the input with its label. - */ - id: string; - /** - * Custom content to be displayed above the input. If the label is hidden, will be used to set aria-label attribute. - */ - label: string; - /** - * Callback function to call on change event. - */ - onChange: ( - event: ChangeEvent | CleaveChangeEvent, - ) => void; - /** - * The text value of the input. Required since our Input is a controlled component. - */ - value: InputHTMLAttributes['value']; - /** - * Automatically focus the input when the page is loaded. - */ - autoFocus?: boolean; - /** - * Custom class to be added to standard input classes. - */ - className?: string; - /** - * Mark the input field as invalid and display a validation message. - * Pass a string or node to render a validation message below the input. - */ - error?: ReactNode; - /** - * Additional clarifying text to help describe the input - */ - helpText?: ReactNode; - /** - * Props passed directly to the input element of the component - */ - inputProps?: BoxProps & HTMLProps; - /** - * The input's disabled attribute - */ - isDisabled?: boolean; - /** - * The required and aria-required attributes on the input - */ - isRequired?: boolean; - /** - * The input's 'maxlength' attribute. - * NOTE: initializing the input with a value longer than the desired maxlength will not trim this value. - */ - maxLength?: number; - /** - * The input's 'name' attribute. - */ - name?: string; - /** - * Callback function to call on blur event. - */ - onBlur?: (event: FocusEvent) => void; - /** - * Callback function to call when input us cleared. When this is passed, - * the input will display an icon on the right side, for triggering this callback. - */ - onClear?: ( - event: MouseEvent | KeyboardEvent, - ) => void; - /** - * Callback function to call on focus event. - */ - onFocus?: (event: FocusEvent) => void; - /** - * The input placeholder attribute. - */ - placeholder?: string; - /** - * An input helper rendered before the input field value - */ - prefix?: ReactNode; - /** - * Visual indicator that the field is required, that gets appended to the label - */ - requiredIndicator?: ReactNode; - /** - * The size of the text input. - */ - size?: TextInputFloatingSize | ResponsiveProp; - /** - * An input helper rendered after the input field value - */ - suffix?: ReactNode; - /** - * The input 'type' value. Defaults to type 'text'. - */ - type?: InputHTMLAttributes['type']; - /** - * Additional props to be spread to rendered element - */ - [x: string]: any; // eslint-disable-line -} - -export const TextInputFloating: ForwardRefExoticComponent = forwardRef( - ( - { - id, - label, - onChange, - value, - autoComplete = false, - autoFocus = false, - error = false, - helpText, - inputProps = {}, - isDisabled = false, - isRequired = false, - maxLength = undefined, - name = '', - onBlur = undefined, - onClear = undefined, - onFocus = undefined, - prefix = undefined, - placeholder = ' ', - requiredIndicator = ' *', - suffix = undefined, - size = 'md', - type = 'text', - }, - ref, - ) => { - const responsiveClasses = generateResponsiveClasses('size', size); - - const inputWrapperClasses = classNames( - 'palmetto-components__variables__form-control', - styles['text-input-wrapper'], - ...responsiveClasses.map(c => styles[c]), - { - [styles.error]: error, - [styles.disabled]: isDisabled, - [styles['is-clearable']]: onClear, - }, - ); - - const clearBtnClasses = classNames(styles['clear-button'], styles.md); - - const renderClearIcon = (): ReactNode => { - const handleKeyPress = ( - event: KeyboardEvent, - ): void => { - if (event.keyCode === 13 && onClear) onClear(event); - }; - - return ( - - ); - }; - - const computedInputProps: TextInputFloatingProps['inputProps'] = { - ...inputProps, // These are spread first so that we don't have top level props overwritten by the user. - 'aria-required': isRequired, - 'aria-invalid': !!error, - 'aria-label': label, - 'aria-labelledby': label ? `${id}Label` : undefined, - autoComplete: getAutoCompleteValue(autoComplete), - autoFocus, - disabled: isDisabled, - id, - maxLength, - name, - onBlur, - onChange, - onFocus, - placeholder, - required: isRequired, - type, - value, - className: classNames(inputProps.className), - }; - - return ( - - - {prefix && ( - - {prefix} - - )} - - - {!!onClear && !!value && renderClearIcon()} - - - {suffix && ( - - {suffix} - - )} - - {helpText && {helpText}} - {error && error !== true && ( - {error} - )} - - ); - }, -); diff --git a/src/components/TextInputFloating/TextInputFloating.Overview.stories.mdx b/src/components/TextInputInset/TextInputInset.Overview.stories.mdx similarity index 89% rename from src/components/TextInputFloating/TextInputFloating.Overview.stories.mdx rename to src/components/TextInputInset/TextInputInset.Overview.stories.mdx index d1a39ae79..5d2fe5523 100644 --- a/src/components/TextInputFloating/TextInputFloating.Overview.stories.mdx +++ b/src/components/TextInputInset/TextInputInset.Overview.stories.mdx @@ -1,21 +1,21 @@ import { useState } from 'react'; import { action } from '@storybook/addon-actions'; import { Meta, Story, Canvas, ArgsTable } from '@storybook/addon-docs'; -import { TextInputFloating } from './TextInputFloating'; +import { TextInputInset } from './TextInputInset'; import { Box } from '../Box/Box'; import { Icon } from '../Icon/Icon'; -# TextInputFloating +# TextInputInset -Use `TextInputFloating` to create a floating label input field. It is a controlled component, which means that you need to provide a value and an `onChange` handler to update the value. +Use `TextInputInset` to create a floating label input field. It is a controlled component, which means that you need to provide a value and an `onChange` handler to update the value. @@ -24,14 +24,14 @@ Use `TextInputFloating` to create a floating label input field. It is a controll const [valuePw, setValuePw] = useState(''); return ( - setValue(event.target.value)} /> - + ### Required @@ -60,7 +60,7 @@ Use the `isRequired` prop to display an `*` after the label and set the `require const [value, setValue] = useState(''); return ( - setValue(event.target.value)} isRequired /> - { const [value, setValue] = useState(''); return ( - - setValue(event.target.value)} isRequired /> - - setValue(event.target.value)} isDisabled /> - { const [value, setValue] = useState('clear me'); return ( - - - setPrefixValue1(event.target.value)} prefix="@" /> - - setPrefixValue3(event.target.value)} suffix={} /> - - setValue1(event.target.value)} /> - = ({ ...args }) => ( - +const Template: Story = ({ ...args }) => ( + ); export const Playground = Template.bind({}); Playground.args = { - id: 'playgroundTextInputFloating ', - label: 'Playground TextInputFloating ', + id: 'playgroundTextInputInset ', + label: 'Playground TextInputInset ', helpText: 'Helpful text', - name: 'playgroundTextInputFloating ', + name: 'playgroundTextInputInset ', }; diff --git a/src/components/TextInputFloating/TextInputFloating.VisualTest.stories.tsx b/src/components/TextInputInset/TextInputInset.VisualTest.stories.tsx similarity index 79% rename from src/components/TextInputFloating/TextInputFloating.VisualTest.stories.tsx rename to src/components/TextInputInset/TextInputInset.VisualTest.stories.tsx index 96c4af67a..b1d71d5c4 100644 --- a/src/components/TextInputFloating/TextInputFloating.VisualTest.stories.tsx +++ b/src/components/TextInputInset/TextInputInset.VisualTest.stories.tsx @@ -1,18 +1,18 @@ import React, { ReactElement, useState } from 'react'; import { Meta, Story } from '@storybook/react/types-6-0'; import { within } from '@storybook/testing-library'; -import { TextInputFloating, TextInputFloatingProps } from './TextInputFloating'; +import { TextInputInset, TextInputInsetProps } from './TextInputInset'; import { Icon } from '../Icon/Icon'; import { Box } from '../Box/Box'; import { RESPONSIVE_STORY } from '../../docs/constants'; export default { - title: 'Components/TextInputFloating/Visual Regression Tests', - component: TextInputFloating, + title: 'Components/TextInputInset/Visual Regression Tests', + component: TextInputInset, } as Meta; -const Template: Story = args => ( - = args => ( + {}} // eslint-disable-line /> @@ -64,72 +64,88 @@ export const PrefixSuffixSizes: React.FC = (): ReactElement => { return ( - ) => setPrefixValue(event.target.value)} + onChange={(event: React.ChangeEvent) => + setPrefixValue(event.target.value) + } prefix="@" /> - ) => setPrefixValue2(event.target.value)} + onChange={(event: React.ChangeEvent) => + setPrefixValue2(event.target.value) + } prefix="$" suffix=".99" /> - ) => setPrefixValue3(event.target.value)} + onChange={(event: React.ChangeEvent) => + setPrefixValue3(event.target.value) + } suffix={} /> - ) => setPrefixValue4(event.target.value)} + onChange={(event: React.ChangeEvent) => + setPrefixValue4(event.target.value) + } onClear={() => setPrefixValue4('')} suffix={} /> - ) => setPrefixValue(event.target.value)} + onChange={(event: React.ChangeEvent) => + setPrefixValue(event.target.value) + } prefix="@" size="lg" /> - ) => setPrefixValue2(event.target.value)} + onChange={(event: React.ChangeEvent) => + setPrefixValue2(event.target.value) + } prefix="$" suffix=".99" size="lg" /> - ) => setPrefixValue3(event.target.value)} + onChange={(event: React.ChangeEvent) => + setPrefixValue3(event.target.value) + } suffix={} size="lg" /> - ) => setPrefixValue4(event.target.value)} + onChange={(event: React.ChangeEvent) => + setPrefixValue4(event.target.value) + } onClear={() => setPrefixValue4('')} suffix={} size="lg" diff --git a/src/components/TextInputFloating/TextInputFloating.module.scss b/src/components/TextInputInset/TextInputInset.module.scss similarity index 100% rename from src/components/TextInputFloating/TextInputFloating.module.scss rename to src/components/TextInputInset/TextInputInset.module.scss diff --git a/src/components/TextInputFloating/TextInputFloating.test.tsx b/src/components/TextInputInset/TextInputInset.test.tsx similarity index 80% rename from src/components/TextInputFloating/TextInputFloating.test.tsx rename to src/components/TextInputInset/TextInputInset.test.tsx index ef181632c..333de54a7 100644 --- a/src/components/TextInputFloating/TextInputFloating.test.tsx +++ b/src/components/TextInputInset/TextInputInset.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { render, fireEvent, screen } from '@testing-library/react'; -import { TextInputFloating } from './TextInputFloating'; +import { TextInputInset } from './TextInputInset'; const baseProps = { name: 'firstName', @@ -17,9 +17,7 @@ describe('TextInput', () => { test('onChange event fires callback function', () => { const mockedHandleChange = jest.fn(() => null); - render( - , - ); + render(); const inputElement = screen.getByDisplayValue(baseProps.value); fireEvent.change(inputElement, { target: { value: 'good bye' } }); @@ -32,7 +30,7 @@ describe('TextInput', () => { value = event.target.value; }); const { rerender } = render( - { expect(mockedHandleChange).toHaveBeenCalledTimes(1); rerender( - { describe('onClear', () => { test('onClear prop renders clear icon when input has value', () => { - render( null} />); + render( null} />); const clearButton = screen.getByTestId('text-input-clear-button'); expect(clearButton).toBeInTheDocument(); }); @@ -68,9 +66,7 @@ describe('TextInput', () => { test('onClear event fires callback function', () => { const mockedHandleClear = jest.fn(() => null); - render( - , - ); + render(); const clearButton = screen.getByTestId('text-input-clear-button'); expect(clearButton).toBeInTheDocument(); @@ -86,9 +82,7 @@ describe('TextInput', () => { describe('onFocus', () => { test('Input fires onFocus callback', () => { const mockedHandleFocus = jest.fn(); - render( - , - ); + render(); const inputElement = screen.getByDisplayValue(baseProps.value); fireEvent.focus(inputElement); expect(mockedHandleFocus).toBeCalledTimes(1); @@ -98,7 +92,7 @@ describe('TextInput', () => { describe('onBlur', () => { test('Input fires onBlur callback', () => { const mockedHandleBlur = jest.fn(); - render(); + render(); const inputElement = screen.getByDisplayValue(baseProps.value); fireEvent.focus(inputElement); fireEvent.blur(inputElement); @@ -110,7 +104,7 @@ describe('TextInput', () => { describe('States', () => { describe('Label', () => { test('it renders a label', () => { - render(); + render(); const labelElement = screen.getByText(baseProps.label); @@ -120,7 +114,7 @@ describe('TextInput', () => { describe('Autofocused', () => { test('Input autofocuses if "autoFocus" prop is set to true', () => { - render(); + render(); const inputElement = screen.getByDisplayValue(baseProps.value); expect(document.activeElement).toEqual(inputElement); }); @@ -128,19 +122,19 @@ describe('TextInput', () => { describe('Autocomplete', () => { test('Input correctly assigns autocomplete value of "on" when bool true is provided', () => { - render(); + render(); const inputElement = screen.getByDisplayValue(baseProps.value); expect(inputElement).toHaveAttribute('autocomplete', 'on'); }); test('Input correctly assigns autocomplete value of "off" when bool false is provided', () => { - render(); + render(); const inputElement = screen.getByDisplayValue('hello'); expect(inputElement).toHaveAttribute('autocomplete', 'off'); }); test('Input correctly assigns autocomplete specific value when provided', () => { - render(); + render(); const inputElement = screen.getByDisplayValue('hello'); expect(inputElement).toHaveAttribute('autocomplete', 'email'); }); @@ -148,7 +142,7 @@ describe('TextInput', () => { describe('Required', () => { test('it correctly assigns the "aria-required" attribute when "isRequired" prop is true', () => { - render(); + render(); const inputElement = screen.getByDisplayValue('hello'); @@ -158,7 +152,7 @@ describe('TextInput', () => { describe('Error', () => { test('Input correctly displays error message if provided', () => { - render(); + render(); const validationMessageElement = screen.getByText('You silly goose'); @@ -170,11 +164,7 @@ describe('TextInput', () => { describe('Help Text', () => { test('Input renders help text', async () => { const { getByText } = render( - , + , ); expect(getByText('i am help text')).toBeDefined(); @@ -183,7 +173,7 @@ describe('TextInput', () => { describe('Max Length', () => { test('Input correctly passes maxlength property if prop is passed', async () => { - render(); + render(); const inputElement = screen.getByLabelText(baseProps.label); expect(inputElement).toBeInTheDocument(); @@ -195,11 +185,7 @@ describe('TextInput', () => { describe('Name', () => { test('Input correctly passes name property if prop is passed', async () => { render( - , + , ); const inputElement = screen.getByLabelText(baseProps.label); @@ -211,7 +197,7 @@ describe('TextInput', () => { describe('Aria-labelledby', () => { test('assigns the "aria-labelledby" attribute and renders label with correct id, when label is provided', () => { - render(); + render(); const inputElement = screen.getByDisplayValue(baseProps.value); expect(inputElement).toHaveAttribute( 'aria-labelledby', @@ -223,12 +209,12 @@ describe('TextInput', () => { describe('Prefix and Suffix', () => { test('renders the prefix if specified', () => { - render(); + render(); expect(screen.getByText('prefixValue')).toBeInTheDocument(); }); test('renders the suffix if specified', () => { - render(); + render(); expect(screen.getByText('suffixValue')).toBeInTheDocument(); }); }); diff --git a/src/components/TextInputInset/TextInputInset.tsx b/src/components/TextInputInset/TextInputInset.tsx new file mode 100644 index 000000000..b4989037e --- /dev/null +++ b/src/components/TextInputInset/TextInputInset.tsx @@ -0,0 +1,262 @@ +import React, { + ChangeEvent, + forwardRef, + MouseEvent, + KeyboardEvent, + FocusEvent, + ForwardRefExoticComponent, + ReactNode, + HTMLProps, + InputHTMLAttributes, +} from 'react'; +import classNames from 'classnames'; +import { ChangeEvent as CleaveChangeEvent } from 'cleave.js/react/props'; +import { ResponsiveProp } from '../../types'; +import { generateResponsiveClasses } from '../../lib/generateResponsiveClasses'; + +import { Box, BoxProps } from '../Box/Box'; +import { HelpText } from '../HelpText/HelpText'; +import { Icon } from '../Icon/Icon'; +import { getAutoCompleteValue } from '../../lib/getAutoCompleteValue'; +import styles from './TextInputInset.module.scss'; +import { InputValidationMessage } from '../InputValidationMessage/InputValidationMessage'; + +export type TextInputInsetSize = 'md' | 'lg'; +export interface TextInputInsetProps { + /** + * The input's id attribute. Used to programmatically tie the input with its label. + */ + id: string; + /** + * Custom content to be displayed above the input. If the label is hidden, will be used to set aria-label attribute. + */ + label: string; + /** + * Callback function to call on change event. + */ + onChange: ( + event: ChangeEvent | CleaveChangeEvent, + ) => void; + /** + * The text value of the input. Required since our Input is a controlled component. + */ + value: InputHTMLAttributes['value']; + /** + * Automatically focus the input when the page is loaded. + */ + autoFocus?: boolean; + /** + * Custom class to be added to standard input classes. + */ + className?: string; + /** + * Mark the input field as invalid and display a validation message. + * Pass a string or node to render a validation message below the input. + */ + error?: ReactNode; + /** + * Additional clarifying text to help describe the input + */ + helpText?: ReactNode; + /** + * Props passed directly to the input element of the component + */ + inputProps?: BoxProps & HTMLProps; + /** + * The input's disabled attribute + */ + isDisabled?: boolean; + /** + * The required and aria-required attributes on the input + */ + isRequired?: boolean; + /** + * The input's 'maxlength' attribute. + * NOTE: initializing the input with a value longer than the desired maxlength will not trim this value. + */ + maxLength?: number; + /** + * The input's 'name' attribute. + */ + name?: string; + /** + * Callback function to call on blur event. + */ + onBlur?: (event: FocusEvent) => void; + /** + * Callback function to call when input us cleared. When this is passed, + * the input will display an icon on the right side, for triggering this callback. + */ + onClear?: ( + event: MouseEvent | KeyboardEvent, + ) => void; + /** + * Callback function to call on focus event. + */ + onFocus?: (event: FocusEvent) => void; + /** + * The input placeholder attribute. + */ + placeholder?: string; + /** + * An input helper rendered before the input field value + */ + prefix?: ReactNode; + /** + * Visual indicator that the field is required, that gets appended to the label + */ + requiredIndicator?: ReactNode; + /** + * The size of the text input. + */ + size?: TextInputInsetSize | ResponsiveProp; + /** + * An input helper rendered after the input field value + */ + suffix?: ReactNode; + /** + * The input 'type' value. Defaults to type 'text'. + */ + type?: InputHTMLAttributes['type']; + /** + * Additional props to be spread to rendered element + */ + [x: string]: any; // eslint-disable-line +} + +export const TextInputInset: ForwardRefExoticComponent = + forwardRef( + ( + { + id, + label, + onChange, + value, + autoComplete = false, + autoFocus = false, + error = false, + helpText, + inputProps = {}, + isDisabled = false, + isRequired = false, + maxLength = undefined, + name = '', + onBlur = undefined, + onClear = undefined, + onFocus = undefined, + prefix = undefined, + placeholder = ' ', + requiredIndicator = ' *', + suffix = undefined, + size = 'md', + type = 'text', + }, + ref, + ) => { + const responsiveClasses = generateResponsiveClasses('size', size); + + const inputWrapperClasses = classNames( + 'palmetto-components__variables__form-control', + styles['text-input-wrapper'], + ...responsiveClasses.map(c => styles[c]), + { + [styles.error]: error, + [styles.disabled]: isDisabled, + [styles['is-clearable']]: onClear, + }, + ); + + const clearBtnClasses = classNames(styles['clear-button'], styles.md); + + const renderClearIcon = (): ReactNode => { + const handleKeyPress = ( + event: KeyboardEvent, + ): void => { + if (event.keyCode === 13 && onClear) onClear(event); + }; + + return ( + + ); + }; + + const computedInputProps: TextInputInsetProps['inputProps'] = { + ...inputProps, // These are spread first so that we don't have top level props overwritten by the user. + 'aria-required': isRequired, + 'aria-invalid': !!error, + 'aria-label': label, + 'aria-labelledby': label ? `${id}Label` : undefined, + autoComplete: getAutoCompleteValue(autoComplete), + autoFocus, + disabled: isDisabled, + id, + maxLength, + name, + onBlur, + onChange, + onFocus, + placeholder, + required: isRequired, + type, + value, + className: classNames(inputProps.className), + }; + + return ( + + + {prefix && ( + + {prefix} + + )} + + + {!!onClear && !!value && renderClearIcon()} + + + {suffix && ( + + {suffix} + + )} + + {helpText && {helpText}} + {error && error !== true && ( + {error} + )} + + ); + }, + ); diff --git a/src/components/TextareaInputFloating/TextareaInputFloating.Overview.stories.mdx b/src/components/TextareaInputInset/TextareaInputInset.Overview.stories.mdx similarity index 87% rename from src/components/TextareaInputFloating/TextareaInputFloating.Overview.stories.mdx rename to src/components/TextareaInputInset/TextareaInputInset.Overview.stories.mdx index da0b592a4..cd57a20c0 100644 --- a/src/components/TextareaInputFloating/TextareaInputFloating.Overview.stories.mdx +++ b/src/components/TextareaInputInset/TextareaInputInset.Overview.stories.mdx @@ -1,21 +1,21 @@ import { useState } from 'react'; import { action } from '@storybook/addon-actions'; import { Meta, Story, Canvas, ArgsTable } from '@storybook/addon-docs'; -import { TextareaInputFloating } from './TextareaInputFloating'; +import { TextareaInputInset } from './TextareaInputInset'; import { Box } from '../Box/Box'; import { Icon } from '../Icon/Icon'; -# TextareaInputFloating +# TextareaInputInset -Use `TextareaInputFloating` to create a floating label input field. It is a controlled component, which means that you need to provide a value and an `onChange` handler to update the value. +Use `TextareaInputInset` to create a floating label input field. It is a controlled component, which means that you need to provide a value and an `onChange` handler to update the value. @@ -24,7 +24,7 @@ Use `TextareaInputFloating` to create a floating label input field. It is a cont const [valuePw, setValuePw] = useState(''); return ( - + ### Required @@ -51,7 +51,7 @@ Use the `isRequired` prop to display an `*` after the label and set the `require const [value, setValue] = useState(''); return ( - setValue(event.target.value)} isRequired /> - { const [value, setValue] = useState(''); return ( - - setValue(event.target.value)} isRequired /> - - setValue(event.target.value)} isDisabled /> - - setValue(event.target.value)} /> - - setValue(event.target.value)} resize="both" /> - = ({ ...args }) => ( - +const Template: Story = ({ ...args }) => ( + ); export const Playground = Template.bind({}); Playground.args = { - id: 'playgroundTextareaInputFloating ', - label: 'Playground TextareaInputFloating ', + id: 'playgroundTextareaInputInset ', + label: 'Playground TextareaInputInset ', helpText: 'Helpful text', - name: 'playgroundTextareaInputFloating ', + name: 'playgroundTextareaInputInset ', }; diff --git a/src/components/TextareaInputFloating/TextareaInputFloating.VisualTest.stories.tsx b/src/components/TextareaInputInset/TextareaInputInset.VisualTest.stories.tsx similarity index 79% rename from src/components/TextareaInputFloating/TextareaInputFloating.VisualTest.stories.tsx rename to src/components/TextareaInputInset/TextareaInputInset.VisualTest.stories.tsx index 00fe99320..ae83ba1d5 100644 --- a/src/components/TextareaInputFloating/TextareaInputFloating.VisualTest.stories.tsx +++ b/src/components/TextareaInputInset/TextareaInputInset.VisualTest.stories.tsx @@ -2,18 +2,18 @@ import React from 'react'; import { Meta, Story } from '@storybook/react/types-6-0'; import { within } from '@storybook/testing-library'; import { - TextareaInputFloating, - TextareaInputFloatingProps, -} from './TextareaInputFloating'; + TextareaInputInset, + TextareaInputInsetProps, +} from './TextareaInputInset'; import { RESPONSIVE_STORY } from '../../docs/constants'; export default { - title: 'Components/TextareaInputFloating/Visual Regression Tests', - component: TextareaInputFloating, + title: 'Components/TextareaInputInset/Visual Regression Tests', + component: TextareaInputInset, } as Meta; -const Template: Story = args => ( - = args => ( + {}} // eslint-disable-line /> diff --git a/src/components/TextareaInputFloating/TextareaInputFloating.module.scss b/src/components/TextareaInputInset/TextareaInputInset.module.scss similarity index 100% rename from src/components/TextareaInputFloating/TextareaInputFloating.module.scss rename to src/components/TextareaInputInset/TextareaInputInset.module.scss diff --git a/src/components/TextareaInputFloating/TextareaInputFloating.test.tsx b/src/components/TextareaInputInset/TextareaInputInset.test.tsx similarity index 83% rename from src/components/TextareaInputFloating/TextareaInputFloating.test.tsx rename to src/components/TextareaInputInset/TextareaInputInset.test.tsx index 3c11f49ad..11ff2d614 100644 --- a/src/components/TextareaInputFloating/TextareaInputFloating.test.tsx +++ b/src/components/TextareaInputInset/TextareaInputInset.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { render, fireEvent, screen } from '@testing-library/react'; -import { TextareaInputFloating } from './TextareaInputFloating'; +import { TextareaInputInset } from './TextareaInputInset'; const baseProps = { name: 'firstName', @@ -18,10 +18,7 @@ describe('TextInput', () => { const mockedHandleChange = jest.fn(() => null); render( - , + , ); const inputElement = screen.getByDisplayValue(baseProps.value); @@ -35,7 +32,7 @@ describe('TextInput', () => { value = event.target.value; }); const { rerender } = render( - { expect(mockedHandleChange).toHaveBeenCalledTimes(1); rerender( - { test('Input fires onFocus callback', () => { const mockedHandleFocus = jest.fn(); render( - , + , ); const inputElement = screen.getByDisplayValue(baseProps.value); fireEvent.focus(inputElement); @@ -76,9 +73,7 @@ describe('TextInput', () => { describe('onBlur', () => { test('Input fires onBlur callback', () => { const mockedHandleBlur = jest.fn(); - render( - , - ); + render(); const inputElement = screen.getByDisplayValue(baseProps.value); fireEvent.focus(inputElement); fireEvent.blur(inputElement); @@ -90,7 +85,7 @@ describe('TextInput', () => { describe('States', () => { describe('Label', () => { test('it renders a label', () => { - render(); + render(); const labelElement = screen.getByText(baseProps.label); @@ -100,7 +95,7 @@ describe('TextInput', () => { describe('Autofocused', () => { test('Input autofocuses if "autoFocus" prop is set to true', () => { - render(); + render(); const inputElement = screen.getByDisplayValue(baseProps.value); expect(document.activeElement).toEqual(inputElement); }); @@ -108,19 +103,19 @@ describe('TextInput', () => { describe('Autocomplete', () => { test('Input correctly assigns autocomplete value of "on" when bool true is provided', () => { - render(); + render(); const inputElement = screen.getByDisplayValue(baseProps.value); expect(inputElement).toHaveAttribute('autocomplete', 'on'); }); test('Input correctly assigns autocomplete value of "off" when bool false is provided', () => { - render(); + render(); const inputElement = screen.getByDisplayValue('hello'); expect(inputElement).toHaveAttribute('autocomplete', 'off'); }); test('Input correctly assigns autocomplete specific value when provided', () => { - render(); + render(); const inputElement = screen.getByDisplayValue('hello'); expect(inputElement).toHaveAttribute('autocomplete', 'email'); }); @@ -128,7 +123,7 @@ describe('TextInput', () => { describe('Required', () => { test('it correctly assigns the "aria-required" attribute when "isRequired" prop is true', () => { - render(); + render(); const inputElement = screen.getByDisplayValue('hello'); @@ -138,9 +133,7 @@ describe('TextInput', () => { describe('Error', () => { test('Input correctly displays error message if provided', () => { - render( - , - ); + render(); const validationMessageElement = screen.getByText('You silly goose'); @@ -152,7 +145,7 @@ describe('TextInput', () => { describe('Help Text', () => { test('Input renders help text', async () => { const { getByText } = render( - { describe('Max Length', () => { test('Input correctly passes maxlength property if prop is passed', async () => { - render(); + render(); const inputElement = screen.getByLabelText(baseProps.label); expect(inputElement).toBeInTheDocument(); @@ -177,7 +170,7 @@ describe('TextInput', () => { describe('Name', () => { test('Input correctly passes name property if prop is passed', async () => { render( - { describe('Aria-labelledby', () => { test('assigns the "aria-labelledby" attribute and renders label with correct id, when label is provided', () => { - render(); + render(); const inputElement = screen.getByDisplayValue(baseProps.value); expect(inputElement).toHaveAttribute( 'aria-labelledby', diff --git a/src/components/TextareaInputFloating/TextareaInputFloating.tsx b/src/components/TextareaInputInset/TextareaInputInset.tsx similarity index 54% rename from src/components/TextareaInputFloating/TextareaInputFloating.tsx rename to src/components/TextareaInputInset/TextareaInputInset.tsx index 52d678967..a5ece773a 100644 --- a/src/components/TextareaInputFloating/TextareaInputFloating.tsx +++ b/src/components/TextareaInputInset/TextareaInputInset.tsx @@ -15,11 +15,11 @@ import { generateResponsiveClasses } from '../../lib/generateResponsiveClasses'; import { Box, BoxProps } from '../Box/Box'; import { HelpText } from '../HelpText/HelpText'; import { getAutoCompleteValue } from '../../lib/getAutoCompleteValue'; -import styles from './TextareaInputFloating.module.scss'; +import styles from './TextareaInputInset.module.scss'; import { InputValidationMessage } from '../InputValidationMessage/InputValidationMessage'; -export type TextareaInputFloatingSize = 'md' | 'lg'; -export interface TextareaInputFloatingProps { +export type TextareaInputInsetSize = 'md' | 'lg'; +export interface TextareaInputInsetProps { /** * The input's id attribute. Used to programmatically tie the input with its label. */ @@ -107,7 +107,7 @@ export interface TextareaInputFloatingProps { /** * The size of the text input. */ - size?: TextareaInputFloatingSize | ResponsiveProp; + size?: TextareaInputInsetSize | ResponsiveProp; /** * An input helper rendered after the input field value */ @@ -122,94 +122,95 @@ export interface TextareaInputFloatingProps { [x: string]: any; // eslint-disable-line } -export const TextareaInputFloating: ForwardRefExoticComponent = forwardRef( - ( - { - id, - label, - onChange, - value, - autoComplete = false, - autoFocus = false, - className, - error = false, - helpText, - inputProps = {}, - isDisabled = false, - isRequired = false, - maxLength = undefined, - name = '', - onBlur = undefined, - onFocus = undefined, - placeholder = ' ', - requiredIndicator = ' *', - resize = 'vertical', - rows = 5, - size = 'md', - type = 'text', - }, - ref, - ) => { - const responsiveClasses = generateResponsiveClasses('size', size); - - const inputWrapperClasses = classNames( - 'palmetto-components__variables__form-control', - styles['text-input-wrapper'], - ...responsiveClasses.map(c => styles[c]), +export const TextareaInputInset: ForwardRefExoticComponent = + forwardRef( + ( { - [styles.disabled]: isDisabled, + id, + label, + onChange, + value, + autoComplete = false, + autoFocus = false, + className, + error = false, + helpText, + inputProps = {}, + isDisabled = false, + isRequired = false, + maxLength = undefined, + name = '', + onBlur = undefined, + onFocus = undefined, + placeholder = ' ', + requiredIndicator = ' *', + resize = 'vertical', + rows = 5, + size = 'md', + type = 'text', }, - ); + ref, + ) => { + const responsiveClasses = generateResponsiveClasses('size', size); - const computedInputProps: TextareaInputFloatingProps['inputProps'] = { - ...inputProps, // These are spread first so that we don't have top level props overwritten by the user. - 'aria-required': isRequired, - 'aria-invalid': !!error, - 'aria-label': label, - 'aria-labelledby': label ? `${id}Label` : undefined, - autoComplete: getAutoCompleteValue(autoComplete), - autoFocus, - className: classNames(styles[`textarea-resize-${resize}`], { - [styles.error]: error, - }), - disabled: isDisabled, - id, - maxLength, - name, - onBlur, - onChange, - onFocus, - placeholder, - required: isRequired, - rows, - type, - value, - }; + const inputWrapperClasses = classNames( + 'palmetto-components__variables__form-control', + styles['text-input-wrapper'], + ...responsiveClasses.map(c => styles[c]), + { + [styles.disabled]: isDisabled, + }, + ); - return ( - - - - + + + + {helpText && {helpText}} + {error && error !== true && ( + {error} + )} - {helpText && {helpText}} - {error && error !== true && ( - {error} - )} - - ); - }, -); + ); + }, + ); diff --git a/src/components/index.ts b/src/components/index.ts index 8d0d39367..078719b6e 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -41,16 +41,16 @@ export * from './RadioGroup/RadioGroup'; export * from './ResponsiveProvider/ResponsiveProvider'; export * from './SelectInput/SelectInput'; export * from './SelectInputNative/SelectInputNative'; -export * from './SelectInputNativeFloating/SelectInputNativeFloating'; +export * from './SelectInputNativeInset/SelectInputNativeInset'; export * from './Spinner/Spinner'; export * from './TabPanels/TabPanels'; export * from './Tabs/Tabs'; export * from './TabsSlider/TabsSlider'; export * from './Table/Table'; export * from './TextInput/TextInput'; -export * from './TextInputFloating/TextInputFloating'; +export * from './TextInputInset/TextInputInset'; export * from './TextareaInput/TextareaInput'; -export * from './TextareaInputFloating/TextareaInputFloating'; +export * from './TextareaInputInset/TextareaInputInset'; export * from './TextLink/TextLink'; export * from './ThemeProvider/ThemeProvider'; export * from './TimePicker/TimePicker'; diff --git a/src/docs/FormTheming.stories.mdx b/src/docs/FormTheming.stories.mdx index 9603273ca..5bccc6d55 100644 --- a/src/docs/FormTheming.stories.mdx +++ b/src/docs/FormTheming.stories.mdx @@ -1,15 +1,15 @@ import { Meta, Story, Canvas } from '@storybook/addon-docs'; import { useState } from 'react'; import { TextInput } from '../components/TextInput/TextInput'; -import { TextInputFloating } from '../components/TextInputFloating/TextInputFloating'; +import { TextInputInset } from '../components/TextInputInset/TextInputInset'; import { CheckboxInput } from '../components/CheckboxInput/CheckboxInput'; import { SelectInput } from '../components/SelectInput/SelectInput'; import { SelectInputNative } from '../components/SelectInputNative/SelectInputNative'; -import { SelectInputNativeFloating } from '../components/SelectInputNativeFloating/SelectInputNativeFloating'; +import { SelectInputNativeInset } from '../components/SelectInputNativeInset/SelectInputNativeInset'; import { OptionTileGroup } from '../components/OptionTileGroup/OptionTileGroup'; import { RadioGroup } from '../components/RadioGroup/RadioGroup'; import { TextareaInput } from '../components/TextareaInput/TextareaInput'; -import { TextareaInputFloating } from '../components/TextareaInputFloating/TextareaInputFloating'; +import { TextareaInputInset } from '../components/TextareaInputInset/TextareaInputInset'; import { Toggle } from '../components/Toggle/Toggle'; import { Button } from '../components/Button/Button'; import { Box } from '../components/Box/Box'; @@ -96,16 +96,16 @@ Form controls share tokens since they are designed to have a consistent appearan isChecked={isThemed} onChange={e => setIsThemed(e.target.checked)} /> - handleChange('emptyValue', event.target.value)} /> - handleChange('textValue', event.target.value)} /> @@ -123,9 +123,9 @@ Form controls share tokens since they are designed to have a consistent appearan options={options} value={themeExampleValues.selectValue} /> - handleChange('selectValue', event.target.value)} options={options} value={themeExampleValues.selectValue} @@ -149,10 +149,10 @@ Form controls share tokens since they are designed to have a consistent appearan value={themeExampleValues.textareaValue} onChange={e => handleChange('textareaValue', e.target.value)} /> - handleChange('textareaValue', e.target.value)} /> @@ -277,5 +277,4 @@ The following tokens are shared by all form controls to maintain a consistent ap })()} -import {TextareaInputFloating} from -'src/components/TextareaInputFloating/TextareaInputFloating'; +import {TextareaInputInset} from 'src/components/TextareaInputInset/TextareaInputInset'; diff --git a/src/stories/FormControls.VisualTests.stories.tsx b/src/stories/FormControls.VisualTests.stories.tsx index 6cbc298c4..2592165c9 100644 --- a/src/stories/FormControls.VisualTests.stories.tsx +++ b/src/stories/FormControls.VisualTests.stories.tsx @@ -4,7 +4,7 @@ import { Box } from '../components/Box/Box'; import { SelectInput } from '../components/SelectInput/SelectInput'; import { SelectInputNative } from '../components/SelectInputNative/SelectInputNative'; import { TextInput } from '../components/TextInput/TextInput'; -import { TextInputFloating } from '../components/TextInputFloating/TextInputFloating'; +import { TextInputInset } from '../components/TextInputInset/TextInputInset'; import { Toggle } from '../components/Toggle/Toggle'; export default { @@ -47,9 +47,9 @@ const Template: Story = args => { - handleChange('textInputMd', event.target.value)} @@ -57,9 +57,9 @@ const Template: Story = args => { /> - -## Floating Inputs +## Inset Inputs -Try not to mix Floating style inputs with regular inputs in the same form. +Try not to mix Inset style inputs with regular inputs in the same form. - + {() => { const [formValues, setFormValues] = useState({ emailInput: '', @@ -48,13 +48,13 @@ Try not to mix Floating style inputs with regular inputs in the same form. }; return ( - handleChange('emailInput', event.target.value)} /> - - handleChange('notesInput', event.target.value)} /> - handleChange('selectInput', event.target.value)} From e4d5c169222b8aeed54782664bf06793d2e6eeb9 Mon Sep 17 00:00:00 2001 From: nathanyoung <1447339+nathanyoung@users.noreply.github.com> Date: Wed, 6 Mar 2024 15:29:41 -0800 Subject: [PATCH 3/3] lint --- .../SelectInputNativeInset.tsx | 237 +++++++++-------- .../TextInputInset.VisualTest.stories.tsx | 32 +-- .../TextInputInset/TextInputInset.tsx | 249 +++++++++--------- .../TextareaInputInset/TextareaInputInset.tsx | 171 ++++++------ 4 files changed, 335 insertions(+), 354 deletions(-) diff --git a/src/components/SelectInputNativeInset/SelectInputNativeInset.tsx b/src/components/SelectInputNativeInset/SelectInputNativeInset.tsx index e11116316..fd0d1359b 100644 --- a/src/components/SelectInputNativeInset/SelectInputNativeInset.tsx +++ b/src/components/SelectInputNativeInset/SelectInputNativeInset.tsx @@ -33,7 +33,7 @@ export interface SelectInputNativeInsetProps { /** * List of options for the select input. */ - options: { value: string | number; label: string | number }[]; + options: { value: string | number; label: string | number; }[]; /** * Callback function to call on change event. */ @@ -112,129 +112,128 @@ export interface SelectInputNativeInsetProps { [x: string]: any; // eslint-disable-line } -export const SelectInputNativeInset: ForwardRefExoticComponent = - forwardRef( - ( - { - id, - label, - onChange, - value, - autoComplete = false, - autoFocus = false, - error = false, - helpText, - inputProps = {}, - isDisabled = false, - isRequired = false, - name = '', - onBlur = undefined, - onClear = undefined, - onFocus = undefined, - options, - placeholder = 'Select...', - requiredIndicator = ' *', - size = 'md', - }, - ref, - ) => { - const placeholderOption = { value: '', label: placeholder }; - const optionsWithPlaceholder = [{ ...placeholderOption }, ...options]; - - const responsiveClasses = generateResponsiveClasses('size', size); +export const SelectInputNativeInset: ForwardRefExoticComponent = forwardRef( + ( + { + id, + label, + onChange, + value, + autoComplete = false, + autoFocus = false, + error = false, + helpText, + inputProps = {}, + isDisabled = false, + isRequired = false, + name = '', + onBlur = undefined, + onClear = undefined, + onFocus = undefined, + options, + placeholder = 'Select...', + requiredIndicator = ' *', + size = 'md', + }, + ref, + ) => { + const placeholderOption = { value: '', label: placeholder }; + const optionsWithPlaceholder = [{ ...placeholderOption }, ...options]; - const inputWrapperClasses = classNames( - 'palmetto-components__variables__form-control', - styles['text-input-wrapper'], - ...responsiveClasses.map(c => styles[c]), - { - [styles.error]: error, - [styles.disabled]: isDisabled, - [styles['is-clearable']]: onClear, - }, - ); + const responsiveClasses = generateResponsiveClasses('size', size); - const clearBtnClasses = classNames(styles['clear-button'], styles.md); + const inputWrapperClasses = classNames( + 'palmetto-components__variables__form-control', + styles['text-input-wrapper'], + ...responsiveClasses.map(c => styles[c]), + { + [styles.error]: error, + [styles.disabled]: isDisabled, + [styles['is-clearable']]: onClear, + }, + ); - const renderClearIcon = (): ReactNode => { - const handleKeyPress = ( - event: KeyboardEvent, - ): void => { - if (event.keyCode === 13 && onClear) onClear(event); - }; + const clearBtnClasses = classNames(styles['clear-button'], styles.md); - return ( - - ); - }; - - const computedInputProps: SelectInputNativeInsetProps['inputProps'] = { - ...inputProps, // These are spread first so that we don't have top level props overwritten by the user. - 'aria-required': isRequired, - 'aria-invalid': !!error, - 'aria-label': label, - 'aria-labelledby': label ? `${id}Label` : undefined, - autoComplete: getAutoCompleteValue(autoComplete), - autoFocus, - disabled: isDisabled, - id, - name, - onBlur, - onChange, - onFocus, - required: isRequired, - value: value ?? '', - className: classNames(inputProps.className), + const renderClearIcon = (): ReactNode => { + const handleKeyPress = ( + event: KeyboardEvent, + ): void => { + if (event.keyCode === 13 && onClear) onClear(event); }; return ( -
- - - {optionsWithPlaceholder.map(option => ( - - ))} - - {!!onClear && !!value && renderClearIcon()} - - - {helpText && {helpText}} - {error && error !== true && ( - {error} - )} -
+ ); - }, - ); + }; + + const computedInputProps: SelectInputNativeInsetProps['inputProps'] = { + ...inputProps, // These are spread first so that we don't have top level props overwritten by the user. + 'aria-required': isRequired, + 'aria-invalid': !!error, + 'aria-label': label, + 'aria-labelledby': label ? `${id}Label` : undefined, + autoComplete: getAutoCompleteValue(autoComplete), + autoFocus, + disabled: isDisabled, + id, + name, + onBlur, + onChange, + onFocus, + required: isRequired, + value: value ?? '', + className: classNames(inputProps.className), + }; + + return ( +
+ + + {optionsWithPlaceholder.map(option => ( + + ))} + + {!!onClear && !!value && renderClearIcon()} + + + {helpText && {helpText}} + {error && error !== true && ( + {error} + )} +
+ ); + }, +); diff --git a/src/components/TextInputInset/TextInputInset.VisualTest.stories.tsx b/src/components/TextInputInset/TextInputInset.VisualTest.stories.tsx index b1d71d5c4..912537330 100644 --- a/src/components/TextInputInset/TextInputInset.VisualTest.stories.tsx +++ b/src/components/TextInputInset/TextInputInset.VisualTest.stories.tsx @@ -68,18 +68,14 @@ export const PrefixSuffixSizes: React.FC = (): ReactElement => { id="prefixSuffix5" value={prefixValue} label="Prefix with Value" - onChange={(event: React.ChangeEvent) => - setPrefixValue(event.target.value) - } + onChange={(event: React.ChangeEvent) => setPrefixValue(event.target.value)} prefix="@" /> ) => - setPrefixValue2(event.target.value) - } + onChange={(event: React.ChangeEvent) => setPrefixValue2(event.target.value)} prefix="$" suffix=".99" /> @@ -88,9 +84,7 @@ export const PrefixSuffixSizes: React.FC = (): ReactElement => { value={prefixValue3} label="Suffix" placeholder="Contact name" - onChange={(event: React.ChangeEvent) => - setPrefixValue3(event.target.value) - } + onChange={(event: React.ChangeEvent) => setPrefixValue3(event.target.value)} suffix={} /> { value={prefixValue4} label="Suffix with Clear" placeholder="Contact name" - onChange={(event: React.ChangeEvent) => - setPrefixValue4(event.target.value) - } + onChange={(event: React.ChangeEvent) => setPrefixValue4(event.target.value)} onClear={() => setPrefixValue4('')} suffix={} /> @@ -110,9 +102,7 @@ export const PrefixSuffixSizes: React.FC = (): ReactElement => { id="prefixSuffix9" value={prefixValue} label="Prefix with Value" - onChange={(event: React.ChangeEvent) => - setPrefixValue(event.target.value) - } + onChange={(event: React.ChangeEvent) => setPrefixValue(event.target.value)} prefix="@" size="lg" /> @@ -120,9 +110,7 @@ export const PrefixSuffixSizes: React.FC = (): ReactElement => { id="prefixSuffix10" value={prefixValue2} label="Prefix and Suffix" - onChange={(event: React.ChangeEvent) => - setPrefixValue2(event.target.value) - } + onChange={(event: React.ChangeEvent) => setPrefixValue2(event.target.value)} prefix="$" suffix=".99" size="lg" @@ -132,9 +120,7 @@ export const PrefixSuffixSizes: React.FC = (): ReactElement => { value={prefixValue3} label="Suffix" placeholder="Contact name" - onChange={(event: React.ChangeEvent) => - setPrefixValue3(event.target.value) - } + onChange={(event: React.ChangeEvent) => setPrefixValue3(event.target.value)} suffix={} size="lg" /> @@ -143,9 +129,7 @@ export const PrefixSuffixSizes: React.FC = (): ReactElement => { value={prefixValue4} label="Suffix with Clear" placeholder="Contact name" - onChange={(event: React.ChangeEvent) => - setPrefixValue4(event.target.value) - } + onChange={(event: React.ChangeEvent) => setPrefixValue4(event.target.value)} onClear={() => setPrefixValue4('')} suffix={} size="lg" diff --git a/src/components/TextInputInset/TextInputInset.tsx b/src/components/TextInputInset/TextInputInset.tsx index b4989037e..071dbc8d7 100644 --- a/src/components/TextInputInset/TextInputInset.tsx +++ b/src/components/TextInputInset/TextInputInset.tsx @@ -124,139 +124,138 @@ export interface TextInputInsetProps { [x: string]: any; // eslint-disable-line } -export const TextInputInset: ForwardRefExoticComponent = - forwardRef( - ( +export const TextInputInset: ForwardRefExoticComponent = forwardRef( + ( + { + id, + label, + onChange, + value, + autoComplete = false, + autoFocus = false, + error = false, + helpText, + inputProps = {}, + isDisabled = false, + isRequired = false, + maxLength = undefined, + name = '', + onBlur = undefined, + onClear = undefined, + onFocus = undefined, + prefix = undefined, + placeholder = ' ', + requiredIndicator = ' *', + suffix = undefined, + size = 'md', + type = 'text', + }, + ref, + ) => { + const responsiveClasses = generateResponsiveClasses('size', size); + + const inputWrapperClasses = classNames( + 'palmetto-components__variables__form-control', + styles['text-input-wrapper'], + ...responsiveClasses.map(c => styles[c]), { - id, - label, - onChange, - value, - autoComplete = false, - autoFocus = false, - error = false, - helpText, - inputProps = {}, - isDisabled = false, - isRequired = false, - maxLength = undefined, - name = '', - onBlur = undefined, - onClear = undefined, - onFocus = undefined, - prefix = undefined, - placeholder = ' ', - requiredIndicator = ' *', - suffix = undefined, - size = 'md', - type = 'text', + [styles.error]: error, + [styles.disabled]: isDisabled, + [styles['is-clearable']]: onClear, }, - ref, - ) => { - const responsiveClasses = generateResponsiveClasses('size', size); - - const inputWrapperClasses = classNames( - 'palmetto-components__variables__form-control', - styles['text-input-wrapper'], - ...responsiveClasses.map(c => styles[c]), - { - [styles.error]: error, - [styles.disabled]: isDisabled, - [styles['is-clearable']]: onClear, - }, - ); - - const clearBtnClasses = classNames(styles['clear-button'], styles.md); + ); - const renderClearIcon = (): ReactNode => { - const handleKeyPress = ( - event: KeyboardEvent, - ): void => { - if (event.keyCode === 13 && onClear) onClear(event); - }; + const clearBtnClasses = classNames(styles['clear-button'], styles.md); - return ( - - ); - }; - - const computedInputProps: TextInputInsetProps['inputProps'] = { - ...inputProps, // These are spread first so that we don't have top level props overwritten by the user. - 'aria-required': isRequired, - 'aria-invalid': !!error, - 'aria-label': label, - 'aria-labelledby': label ? `${id}Label` : undefined, - autoComplete: getAutoCompleteValue(autoComplete), - autoFocus, - disabled: isDisabled, - id, - maxLength, - name, - onBlur, - onChange, - onFocus, - placeholder, - required: isRequired, - type, - value, - className: classNames(inputProps.className), + const renderClearIcon = (): ReactNode => { + const handleKeyPress = ( + event: KeyboardEvent, + ): void => { + if (event.keyCode === 13 && onClear) onClear(event); }; return ( - - - {prefix && ( - - {prefix} - - )} - + + + ); + }; + + const computedInputProps: TextInputInsetProps['inputProps'] = { + ...inputProps, // These are spread first so that we don't have top level props overwritten by the user. + 'aria-required': isRequired, + 'aria-invalid': !!error, + 'aria-label': label, + 'aria-labelledby': label ? `${id}Label` : undefined, + autoComplete: getAutoCompleteValue(autoComplete), + autoFocus, + disabled: isDisabled, + id, + maxLength, + name, + onBlur, + onChange, + onFocus, + placeholder, + required: isRequired, + type, + value, + className: classNames(inputProps.className), + }; + + return ( + + + {prefix && ( + + {prefix} + + )} + + + {!!onClear && !!value && renderClearIcon()} + + + {suffix && ( + + {suffix} - {helpText && {helpText}} - {error && error !== true && ( - {error} )} - ); - }, - ); + {helpText && {helpText}} + {error && error !== true && ( + {error} + )} + + ); + }, +); diff --git a/src/components/TextareaInputInset/TextareaInputInset.tsx b/src/components/TextareaInputInset/TextareaInputInset.tsx index a5ece773a..94db2d230 100644 --- a/src/components/TextareaInputInset/TextareaInputInset.tsx +++ b/src/components/TextareaInputInset/TextareaInputInset.tsx @@ -122,95 +122,94 @@ export interface TextareaInputInsetProps { [x: string]: any; // eslint-disable-line } -export const TextareaInputInset: ForwardRefExoticComponent = - forwardRef( - ( +export const TextareaInputInset: ForwardRefExoticComponent = forwardRef( + ( + { + id, + label, + onChange, + value, + autoComplete = false, + autoFocus = false, + className, + error = false, + helpText, + inputProps = {}, + isDisabled = false, + isRequired = false, + maxLength = undefined, + name = '', + onBlur = undefined, + onFocus = undefined, + placeholder = ' ', + requiredIndicator = ' *', + resize = 'vertical', + rows = 5, + size = 'md', + type = 'text', + }, + ref, + ) => { + const responsiveClasses = generateResponsiveClasses('size', size); + + const inputWrapperClasses = classNames( + 'palmetto-components__variables__form-control', + styles['text-input-wrapper'], + ...responsiveClasses.map(c => styles[c]), { - id, - label, - onChange, - value, - autoComplete = false, - autoFocus = false, - className, - error = false, - helpText, - inputProps = {}, - isDisabled = false, - isRequired = false, - maxLength = undefined, - name = '', - onBlur = undefined, - onFocus = undefined, - placeholder = ' ', - requiredIndicator = ' *', - resize = 'vertical', - rows = 5, - size = 'md', - type = 'text', + [styles.disabled]: isDisabled, }, - ref, - ) => { - const responsiveClasses = generateResponsiveClasses('size', size); + ); - const inputWrapperClasses = classNames( - 'palmetto-components__variables__form-control', - styles['text-input-wrapper'], - ...responsiveClasses.map(c => styles[c]), - { - [styles.disabled]: isDisabled, - }, - ); + const computedInputProps: TextareaInputInsetProps['inputProps'] = { + ...inputProps, // These are spread first so that we don't have top level props overwritten by the user. + 'aria-required': isRequired, + 'aria-invalid': !!error, + 'aria-label': label, + 'aria-labelledby': label ? `${id}Label` : undefined, + autoComplete: getAutoCompleteValue(autoComplete), + autoFocus, + className: classNames(styles[`textarea-resize-${resize}`], { + [styles.error]: error, + }), + disabled: isDisabled, + id, + maxLength, + name, + onBlur, + onChange, + onFocus, + placeholder, + required: isRequired, + rows, + type, + value, + }; - const computedInputProps: TextareaInputInsetProps['inputProps'] = { - ...inputProps, // These are spread first so that we don't have top level props overwritten by the user. - 'aria-required': isRequired, - 'aria-invalid': !!error, - 'aria-label': label, - 'aria-labelledby': label ? `${id}Label` : undefined, - autoComplete: getAutoCompleteValue(autoComplete), - autoFocus, - className: classNames(styles[`textarea-resize-${resize}`], { - [styles.error]: error, - }), - disabled: isDisabled, - id, - maxLength, - name, - onBlur, - onChange, - onFocus, - placeholder, - required: isRequired, - rows, - type, - value, - }; - - return ( - - + + + - ); - }, - ); + {helpText && {helpText}} + {error && error !== true && ( + {error} + )} + + ); + }, +);