From adc6d8416d20b8bd385761ec6336637e9764545e Mon Sep 17 00:00:00 2001 From: Scott Rees <6165315+reesscot@users.noreply.github.com> Date: Fri, 18 Feb 2022 14:06:22 -0700 Subject: [PATCH] feature: add `TextAreaField` primitive (#1356) --- README.md | 1 + docs/src/data/links.ts | 5 + .../TextAreaFieldPropControls.tsx | 257 ++++++++++++++++++ .../pages/components/textareafield/demo.tsx | 213 +++++++++++++++ .../DefaultRequiredTextAreaFieldExample.tsx | 10 + .../examples/DefaultTextAreaExample.tsx | 7 + .../examples/RequiredTextAreaFieldExample.tsx | 39 +++ .../TextAreaFieldDescriptiveExample.tsx | 20 ++ .../examples/TextAreaFieldStatesExample.tsx | 18 ++ .../TextAreaFieldStylePropsExample.tsx | 23 ++ .../TextAreaFieldValidationErrorExample.tsx | 13 + .../TextAreaFieldVariationExample.tsx | 10 + .../examples/TextAreaMaxLengthExample.tsx | 11 + .../examples/TextAreaResizableExample.tsx | 12 + .../examples/TextAreaRowsExample.tsx | 11 + .../examples/TextAreaSizeExample.tsx | 23 ++ .../textareafield/examples/index.ts | 12 + .../components/textareafield/index.page.mdx | 9 + .../pages/components/textareafield/react.mdx | 231 ++++++++++++++++ .../textareafield/useTextAreaFieldProps.tsx | 109 ++++++++ docs/src/pages/components/textfield/react.mdx | 10 +- docs/src/styles/index.scss | 1 + .../styles/primitives/textAreaFieldStyles.css | 6 + packages/react/__tests__/exports.ts | 2 + .../__tests__/useDeprecationWarning.test.tsx | 32 +++ .../react/src/hooks/useDeprecationWarning.ts | 22 ++ .../TextAreaField/TextAreaField.tsx | 76 ++++++ .../__tests__/TextAreaField.test.tsx | 238 ++++++++++++++++ .../src/primitives/TextAreaField/index.ts | 1 + .../src/primitives/TextField/TextField.tsx | 7 + .../TextField/__tests__/TextField.test.tsx | 11 + packages/react/src/primitives/components.ts | 1 + .../react/src/primitives/shared/constants.ts | 1 + packages/react/src/primitives/types/index.ts | 1 + .../src/primitives/types/textAreaField.ts | 8 + .../react/src/primitives/types/textField.ts | 11 +- .../theme/css/component/textAreaField.scss | 3 + packages/ui/src/theme/css/styles.scss | 1 + 38 files changed, 1463 insertions(+), 3 deletions(-) create mode 100644 docs/src/pages/components/textareafield/TextAreaFieldPropControls.tsx create mode 100644 docs/src/pages/components/textareafield/demo.tsx create mode 100644 docs/src/pages/components/textareafield/examples/DefaultRequiredTextAreaFieldExample.tsx create mode 100644 docs/src/pages/components/textareafield/examples/DefaultTextAreaExample.tsx create mode 100644 docs/src/pages/components/textareafield/examples/RequiredTextAreaFieldExample.tsx create mode 100644 docs/src/pages/components/textareafield/examples/TextAreaFieldDescriptiveExample.tsx create mode 100644 docs/src/pages/components/textareafield/examples/TextAreaFieldStatesExample.tsx create mode 100644 docs/src/pages/components/textareafield/examples/TextAreaFieldStylePropsExample.tsx create mode 100644 docs/src/pages/components/textareafield/examples/TextAreaFieldValidationErrorExample.tsx create mode 100644 docs/src/pages/components/textareafield/examples/TextAreaFieldVariationExample.tsx create mode 100644 docs/src/pages/components/textareafield/examples/TextAreaMaxLengthExample.tsx create mode 100644 docs/src/pages/components/textareafield/examples/TextAreaResizableExample.tsx create mode 100644 docs/src/pages/components/textareafield/examples/TextAreaRowsExample.tsx create mode 100644 docs/src/pages/components/textareafield/examples/TextAreaSizeExample.tsx create mode 100644 docs/src/pages/components/textareafield/examples/index.ts create mode 100644 docs/src/pages/components/textareafield/index.page.mdx create mode 100644 docs/src/pages/components/textareafield/react.mdx create mode 100644 docs/src/pages/components/textareafield/useTextAreaFieldProps.tsx create mode 100644 docs/src/styles/primitives/textAreaFieldStyles.css create mode 100644 packages/react/src/hooks/__tests__/useDeprecationWarning.test.tsx create mode 100644 packages/react/src/hooks/useDeprecationWarning.ts create mode 100644 packages/react/src/primitives/TextAreaField/TextAreaField.tsx create mode 100644 packages/react/src/primitives/TextAreaField/__tests__/TextAreaField.test.tsx create mode 100644 packages/react/src/primitives/TextAreaField/index.ts create mode 100644 packages/react/src/primitives/types/textAreaField.ts create mode 100644 packages/ui/src/theme/css/component/textAreaField.scss diff --git a/README.md b/README.md index 09c16d996be..91916f4d4d2 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,7 @@ Amplify UI is an open-source UI library with cloud-connected components that are | Table | ✅ | | | Tabs | ✅ | | | Text | ✅ | | +| TextAreaField | ✅ | | | TextField | ✅ | | | ToggleButton | ✅ | | | View | ✅ | | diff --git a/docs/src/data/links.ts b/docs/src/data/links.ts index f2f1e6d26f5..a529f37ce6d 100644 --- a/docs/src/data/links.ts +++ b/docs/src/data/links.ts @@ -96,6 +96,11 @@ export const feedbackComponents: ComponentNavItem[] = [ ].sort(sortByLabel); export const inputComponents = [ + { + href: '/components/textareafield', + label: 'TextArea Field', + body: `The TextAreaField form primitive can be used allow users to input multiline text content.`, + }, { href: '/components/textfield', label: 'Text Field', diff --git a/docs/src/pages/components/textareafield/TextAreaFieldPropControls.tsx b/docs/src/pages/components/textareafield/TextAreaFieldPropControls.tsx new file mode 100644 index 00000000000..ba1111b0835 --- /dev/null +++ b/docs/src/pages/components/textareafield/TextAreaFieldPropControls.tsx @@ -0,0 +1,257 @@ +import * as React from 'react'; +import { + Flex, + TextField, + SelectField, + SwitchField, + TextAreaFieldProps, +} from '@aws-amplify/ui-react'; + +export interface TextAreaFieldControlsProps extends TextAreaFieldProps { + setAutoComplete: ( + value: React.SetStateAction + ) => void; + setDefaultValue: ( + value: React.SetStateAction + ) => void; + setDescriptiveText: ( + value: React.SetStateAction + ) => void; + setErrorMessage: ( + value: React.SetStateAction + ) => void; + setHasError: ( + value: React.SetStateAction + ) => void; + setIsDisabled: ( + value: React.SetStateAction + ) => void; + setIsReadOnly: ( + value: React.SetStateAction + ) => void; + setIsRequired: ( + value: React.SetStateAction + ) => void; + setLabel: (value: React.SetStateAction) => void; + setLabelHidden: ( + value: React.SetStateAction + ) => void; + setMaxLength: ( + value: React.SetStateAction + ) => void; + setName: (value: React.SetStateAction) => void; + setPlaceholder: ( + value: React.SetStateAction + ) => void; + setSize: (value: React.SetStateAction) => void; + setRows: (value: React.SetStateAction) => void; + setValue: (value: React.SetStateAction) => void; + setVariation: ( + value: React.SetStateAction + ) => void; +} + +interface TextAreaFieldControlsInterface { + (props: TextAreaFieldControlsProps): JSX.Element; +} + +export const TextAreaFieldPropControls: TextAreaFieldControlsInterface = ({ + autoComplete, + defaultValue, + descriptiveText, + errorMessage, + hasError, + isDisabled, + isReadOnly, + isRequired, + label, + labelHidden, + maxLength, + name, + placeholder, + rows, + setAutoComplete, + setDefaultValue, + setDescriptiveText, + setErrorMessage, + setHasError, + setIsDisabled, + setIsReadOnly, + setIsRequired, + setLabel, + setLabelHidden, + setMaxLength, + setName, + setPlaceholder, + setRows, + setSize, + setValue, + setVariation, + size, + value, + variation, +}) => { + return ( + + { + setAutoComplete(event.target.value); + }} + label="autocomplete" + /> + { + setDefaultValue(event.target.value); + }} + label="defaultValue" + /> + { + setLabel(event.target.value); + }} + label="label" + /> + { + setName(event.target.value); + }} + label="name" + /> + { + setPlaceholder(event.target.value); + }} + label="placeholder" + /> + { + setRows(event.target.value); + }} + label="rows" + /> + { + setMaxLength(event.target.value); + }} + label="maxLength" + /> + { + setDescriptiveText(event.target.value); + }} + label="descriptiveText" + /> + { + setErrorMessage(event.target.value); + }} + label="errorMessage" + /> + { + setValue(event.target.value); + }} + label="value" + /> + + { + setHasError(!hasError); + }} + label="hasError" + /> + { + setLabelHidden(!labelHidden); + }} + label="labelHidden" + /> + { + setIsDisabled(!isDisabled); + }} + label="isDisabled" + /> + { + setIsReadOnly(!isReadOnly); + }} + label="isReadOnly" + /> + { + setIsRequired(!isRequired); + }} + label="isRequired" + /> + + + + setSize(event.target.value as TextAreaFieldProps['size']) + } + label="size" + > + + + + + + setVariation(event.target.value as TextAreaFieldProps['variation']) + } + label="variation" + > + + + + ); +}; diff --git a/docs/src/pages/components/textareafield/demo.tsx b/docs/src/pages/components/textareafield/demo.tsx new file mode 100644 index 00000000000..44b27a1d35d --- /dev/null +++ b/docs/src/pages/components/textareafield/demo.tsx @@ -0,0 +1,213 @@ +import * as React from 'react'; + +import { + TextAreaField, + Flex, + FlexContainerStyleProps, + TextAreaFieldProps, +} from '@aws-amplify/ui-react'; + +import { Demo } from '@/components/Demo'; +import { useTextAreaFieldProps } from './useTextAreaFieldProps'; +import { GetFieldControls } from '../shared/GetFieldControls'; +import { useFlexContainerStyleProps } from '../shared/useFlexContainerStyleProps'; +import { TextAreaFieldPropControls } from './TextAreaFieldPropControls'; + +export const TextAreaFieldDemo = () => { + const flexStyleProps = useFlexContainerStyleProps({ + alignItems: '', + alignContent: '', + direction: 'column', + gap: '', + justifyContent: '', + wrap: 'nowrap', + }); + const textFieldProps = useTextAreaFieldProps({ + autoComplete: 'off', + defaultValue: null, + descriptiveText: 'Enter a valid last name', + errorMessage: '', + hasError: false, + isDisabled: false, + isReadOnly: false, + isRequired: false, + label: 'Last name', + labelHidden: false, + name: 'last_name', + placeholder: 'Baggins', + rows: 3, + maxLength: null, + size: 'small', + value: null, + variation: null, + }); + const FlexPropControls = GetFieldControls({ + typeName: 'Flex', + fields: flexStyleProps, + }); + + const [ + [alignItems], + [alignContent], + [direction], + [gap], + [justifyContent], + [wrap], + ] = flexStyleProps; + + const { + autoComplete, + defaultValue, + hasError, + label, + descriptiveText, + errorMessage, + isDisabled, + isReadOnly, + isRequired, + labelHidden, + placeholder, + size, + rows, + maxLength, + value, + name, + variation, + } = textFieldProps; + + const code = + ` console.info(e.currentTarget.value)} + onInput={(e) => console.info('input fired:', e.currentTarget.value)} + onCopy={(e) => console.info('onCopy fired:', e.currentTarget.value)} + onCut={(e) => console.info('onCut fired:', e.currentTarget.value)} + onPaste={(e) => console.info('onPaste fired:', e.currentTarget.value)} + onSelect={(e) => + console.info( + 'onSelect fired:', + e.currentTarget.value.substring( + e.currentTarget.selectionStart, + e.currentTarget.selectionEnd + ) + ) + } +/>`; + + return ( + + + {FlexPropControls} + + } + > + console.info(e.currentTarget.value)} + onInput={(e) => console.info('input fired:', e.currentTarget.value)} + onCopy={(e) => console.info('onCopy fired:', e.currentTarget.value)} + onCut={(e) => console.info('onCut fired:', e.currentTarget.value)} + onPaste={(e) => console.info('onPaste fired:', e.currentTarget.value)} + onSelect={(e) => + console.info( + 'onSelect fired:', + e.currentTarget.value.substring( + e.currentTarget.selectionStart, + e.currentTarget.selectionEnd + ) + ) + } + /> + + ); +}; diff --git a/docs/src/pages/components/textareafield/examples/DefaultRequiredTextAreaFieldExample.tsx b/docs/src/pages/components/textareafield/examples/DefaultRequiredTextAreaFieldExample.tsx new file mode 100644 index 00000000000..09b03a7913d --- /dev/null +++ b/docs/src/pages/components/textareafield/examples/DefaultRequiredTextAreaFieldExample.tsx @@ -0,0 +1,10 @@ +import { Button, Flex, TextAreaField } from '@aws-amplify/ui-react'; + +export const DefaultRequiredTextAreaFieldExample = () => { + return ( + + + + + ); +}; diff --git a/docs/src/pages/components/textareafield/examples/DefaultTextAreaExample.tsx b/docs/src/pages/components/textareafield/examples/DefaultTextAreaExample.tsx new file mode 100644 index 00000000000..8699e246cdd --- /dev/null +++ b/docs/src/pages/components/textareafield/examples/DefaultTextAreaExample.tsx @@ -0,0 +1,7 @@ +import { TextAreaField } from '@aws-amplify/ui-react'; + +export const DefaultTextAreaExample = () => { + return ( + + ); +}; diff --git a/docs/src/pages/components/textareafield/examples/RequiredTextAreaFieldExample.tsx b/docs/src/pages/components/textareafield/examples/RequiredTextAreaFieldExample.tsx new file mode 100644 index 00000000000..fff70b8c78e --- /dev/null +++ b/docs/src/pages/components/textareafield/examples/RequiredTextAreaFieldExample.tsx @@ -0,0 +1,39 @@ +import { Button, Flex, Text, TextAreaField } from '@aws-amplify/ui-react'; + +export const RequiredTextAreaFieldExample = () => { + return ( + + + Essay Question #1 + + {' '} + (required) + + + } + isRequired={true} + /> + + Required + + } + isRequired={true} + /> + + + ); +}; diff --git a/docs/src/pages/components/textareafield/examples/TextAreaFieldDescriptiveExample.tsx b/docs/src/pages/components/textareafield/examples/TextAreaFieldDescriptiveExample.tsx new file mode 100644 index 00000000000..28e1f917613 --- /dev/null +++ b/docs/src/pages/components/textareafield/examples/TextAreaFieldDescriptiveExample.tsx @@ -0,0 +1,20 @@ +import { Text, TextAreaField, View } from '@aws-amplify/ui-react'; + +export const TextAreaFieldDescriptiveExample = () => { + return ( + + + Please enter a USPS validated address + + } + /> + + ); +}; diff --git a/docs/src/pages/components/textareafield/examples/TextAreaFieldStatesExample.tsx b/docs/src/pages/components/textareafield/examples/TextAreaFieldStatesExample.tsx new file mode 100644 index 00000000000..517417981d1 --- /dev/null +++ b/docs/src/pages/components/textareafield/examples/TextAreaFieldStatesExample.tsx @@ -0,0 +1,18 @@ +import { Flex, TextAreaField } from '@aws-amplify/ui-react'; + +export const TextAreaFieldStatesExample = () => { + return ( + + + + + ); +}; diff --git a/docs/src/pages/components/textareafield/examples/TextAreaFieldStylePropsExample.tsx b/docs/src/pages/components/textareafield/examples/TextAreaFieldStylePropsExample.tsx new file mode 100644 index 00000000000..b23b34f6ed1 --- /dev/null +++ b/docs/src/pages/components/textareafield/examples/TextAreaFieldStylePropsExample.tsx @@ -0,0 +1,23 @@ +import { Text, TextAreaField, useTheme } from '@aws-amplify/ui-react'; + +export const TextAreaFieldStylePropsExample = () => { + const { tokens } = useTheme(); + return ( + + Address: + + } + backgroundColor={tokens.colors.background.secondary} + color={tokens.colors.black} + width="400px" + /> + ); +}; diff --git a/docs/src/pages/components/textareafield/examples/TextAreaFieldValidationErrorExample.tsx b/docs/src/pages/components/textareafield/examples/TextAreaFieldValidationErrorExample.tsx new file mode 100644 index 00000000000..c686a100f89 --- /dev/null +++ b/docs/src/pages/components/textareafield/examples/TextAreaFieldValidationErrorExample.tsx @@ -0,0 +1,13 @@ +import { Flex, TextAreaField } from '@aws-amplify/ui-react'; + +export const TextAreaFieldValidationErrorExample = () => { + return ( + + + + ); +}; diff --git a/docs/src/pages/components/textareafield/examples/TextAreaFieldVariationExample.tsx b/docs/src/pages/components/textareafield/examples/TextAreaFieldVariationExample.tsx new file mode 100644 index 00000000000..22bdf7f08be --- /dev/null +++ b/docs/src/pages/components/textareafield/examples/TextAreaFieldVariationExample.tsx @@ -0,0 +1,10 @@ +import { TextAreaField } from '@aws-amplify/ui-react'; + +export const TextAreaFieldVariationExample = () => { + return ( + <> + + + + ); +}; diff --git a/docs/src/pages/components/textareafield/examples/TextAreaMaxLengthExample.tsx b/docs/src/pages/components/textareafield/examples/TextAreaMaxLengthExample.tsx new file mode 100644 index 00000000000..d9e94af92e0 --- /dev/null +++ b/docs/src/pages/components/textareafield/examples/TextAreaMaxLengthExample.tsx @@ -0,0 +1,11 @@ +import { TextAreaField } from '@aws-amplify/ui-react'; + +export const TextAreaMaxLengthExample = () => { + return ( + + ); +}; diff --git a/docs/src/pages/components/textareafield/examples/TextAreaResizableExample.tsx b/docs/src/pages/components/textareafield/examples/TextAreaResizableExample.tsx new file mode 100644 index 00000000000..c79d5df0876 --- /dev/null +++ b/docs/src/pages/components/textareafield/examples/TextAreaResizableExample.tsx @@ -0,0 +1,12 @@ +import { TextAreaField } from '@aws-amplify/ui-react'; + +export const TextAreaResizableExample = () => { + return ( + + ); +}; diff --git a/docs/src/pages/components/textareafield/examples/TextAreaRowsExample.tsx b/docs/src/pages/components/textareafield/examples/TextAreaRowsExample.tsx new file mode 100644 index 00000000000..691ef374724 --- /dev/null +++ b/docs/src/pages/components/textareafield/examples/TextAreaRowsExample.tsx @@ -0,0 +1,11 @@ +import { TextAreaField } from '@aws-amplify/ui-react'; + +export const TextAreaRowsExample = () => { + return ( + + ); +}; diff --git a/docs/src/pages/components/textareafield/examples/TextAreaSizeExample.tsx b/docs/src/pages/components/textareafield/examples/TextAreaSizeExample.tsx new file mode 100644 index 00000000000..5b90dca569d --- /dev/null +++ b/docs/src/pages/components/textareafield/examples/TextAreaSizeExample.tsx @@ -0,0 +1,23 @@ +import { Flex, TextAreaField } from '@aws-amplify/ui-react'; + +export const TextAreaSizeExample = () => { + return ( + + + + + + + ); +}; diff --git a/docs/src/pages/components/textareafield/examples/index.ts b/docs/src/pages/components/textareafield/examples/index.ts new file mode 100644 index 00000000000..0de8a0941be --- /dev/null +++ b/docs/src/pages/components/textareafield/examples/index.ts @@ -0,0 +1,12 @@ +export { DefaultTextAreaExample } from './DefaultTextAreaExample'; +export { DefaultRequiredTextAreaFieldExample } from './DefaultRequiredTextAreaFieldExample'; +export { RequiredTextAreaFieldExample } from './RequiredTextAreaFieldExample'; +export { TextAreaMaxLengthExample } from './TextAreaMaxLengthExample'; +export { TextAreaResizableExample } from './TextAreaResizableExample'; +export { TextAreaRowsExample } from './TextAreaRowsExample'; +export { TextAreaSizeExample } from './TextAreaSizeExample'; +export { TextAreaFieldDescriptiveExample } from './TextAreaFieldDescriptiveExample'; +export { TextAreaFieldStatesExample } from './TextAreaFieldStatesExample'; +export { TextAreaFieldStylePropsExample } from './TextAreaFieldStylePropsExample'; +export { TextAreaFieldValidationErrorExample } from './TextAreaFieldValidationErrorExample'; +export { TextAreaFieldVariationExample } from './TextAreaFieldVariationExample'; diff --git a/docs/src/pages/components/textareafield/index.page.mdx b/docs/src/pages/components/textareafield/index.page.mdx new file mode 100644 index 00000000000..937ff8e26a6 --- /dev/null +++ b/docs/src/pages/components/textareafield/index.page.mdx @@ -0,0 +1,9 @@ +--- +title: TextAreaField +description: The TextAreaField form primitive can be used allow users to input multiline text content. +isPrimitive: true +--- + +import { Fragment } from '@/components/Fragment'; + +{({ platform }) => import(`./${platform}.mdx`)} diff --git a/docs/src/pages/components/textareafield/react.mdx b/docs/src/pages/components/textareafield/react.mdx new file mode 100644 index 00000000000..0ddf1c9e0d1 --- /dev/null +++ b/docs/src/pages/components/textareafield/react.mdx @@ -0,0 +1,231 @@ +import { TextAreaField } from '@aws-amplify/ui-react'; + +import { TextAreaFieldDemo } from './demo'; +import { + DefaultRequiredTextAreaFieldExample, + DefaultTextAreaExample, + RequiredTextAreaFieldExample, + TextAreaMaxLengthExample, + TextAreaResizableExample, + TextAreaRowsExample, + TextAreaSizeExample, + TextAreaFieldDescriptiveExample, + TextAreaFieldStatesExample, + TextAreaFieldStylePropsExample, + TextAreaFieldValidationErrorExample, + TextAreaFieldVariationExample, +} from './examples'; +import { Example, ExampleCode } from '@/components/Example'; +import { Fragment } from '@/components/Fragment'; + +## Demo + + + +## Usage + +Import the `TextAreaField` component and styles and provide a `label` for accessibility/usability. + + + + + ```jsx file=./examples/DefaultTextAreaExample.tsx + + ``` + + + + +### Accessibility + +The form primitives are accessible by default. A matching `label` HTML element will be connected to the form control -- simply provide a `label` prop with a `string` or `ReactNode`. If no `id` is provided, one will be automatically generated and connected to both `label` and form control elements. + +### Resizeable + +For a resizeable multiline field, use `resize` prop. Common values are `horizontal`, `vertical`, `both`. See [MDN resize docs](https://developer.mozilla.org/en-US/docs/Web/CSS/resize) for supported values. + + + + + ```jsx file=./examples/TextAreaResizableExample.tsx + + ``` + + + + +### Size + +To change the general size, use the `size` prop. Available options are `small`, none (default), and `large`. + + + + + ```jsx file=./examples/TextAreaSizeExample.tsx + + ``` + + + + +### Rows + +To change the number of rows of text displayed, use the `rows` prop with desired number. + + + + + ```jsx file=./examples/TextAreaRowsExample.tsx + + ``` + + + + +### Maximum length + +To enforce a maximum length of multiline text, use the `maxLength` prop. + + + + + ```jsx file=./examples/TextAreaMaxLengthExample.tsx + + ``` + + + + +### Variations + +There are two variation styles available: default and `quiet`. + + + + + ```jsx file=./examples/TextAreaFieldVariationExample.tsx + ``` + + + + +### Descriptive text + +To provide additional descriptive text of requirements of the field, use the `descriptiveText` field: + + + + + ```jsx file=./examples/TextAreaFieldDescriptiveExample.tsx + + ``` + + + + +### States + +The available `TextAreaField` states include `isDisabled` and `isReadonly`. A disabled field will be not be focusable not mutable and will not be submitted with form data. A readonly field cannot be edited by the user. + + + + + ```jsx file=./examples/TextAreaFieldStatesExample.tsx + + ``` + + + + +### Required fields + +Use the `isRequired` prop to specify a required field. + + + + + ```jsx file=./examples/DefaultRequiredTextAreaFieldExample.tsx + + ``` + + + + +There is no default styling for required fields. Customize the `label` or `descriptiveText` to instruct the form user of the required field. + + + + + ```jsx file=./examples/RequiredTextAreaFieldExample.tsx + + ``` + + + + +### Validation error styling + +Use the `hasError` and `errorMessage` fields to mark a `TextAreaField` with a validation error. + + + + + ```jsx file=./examples/TextAreaFieldValidationErrorExample.tsx + + ``` + + + + +## Styling + +### Global styling + +To override styling on all TextAreaField primitives, you can set the Amplify CSS variables with the built-in `.amplify-textareafield` class. + + + + + ```css + /* styles.css */ + .amplify-textareafield { + --amplify-components-fieldcontrol-border-color: rebeccapurple; + } + ``` + + + +### Local styling + +To override styling on a specific TextAreaField, you can use a class selector or style props. + +_Using a class selector:_ + + + + + ```css + /* styles.css */ + .custom-textareafield-class .amplify-textarea { + border-radius: 0; + } + ``` + + + +_Using style props:_ + +Flex styling props will be applied to the TextAreaField wrapping Flex component, whereas other style props will be applied to the field. This allows us to change the layout of the label and field, while also styling the input field. + + + + + ```jsx file=./examples/TextAreaFieldStylePropsExample.tsx + + ``` + + + diff --git a/docs/src/pages/components/textareafield/useTextAreaFieldProps.tsx b/docs/src/pages/components/textareafield/useTextAreaFieldProps.tsx new file mode 100644 index 00000000000..f44329742b1 --- /dev/null +++ b/docs/src/pages/components/textareafield/useTextAreaFieldProps.tsx @@ -0,0 +1,109 @@ +import { TextAreaFieldProps } from '@aws-amplify/ui-react'; +import * as React from 'react'; + +export const useTextAreaFieldProps = (initialValues: TextAreaFieldProps) => { + const [autoComplete, setAutoComplete] = React.useState< + TextAreaFieldProps['autoComplete'] + >(initialValues.autoComplete); + + const [defaultValue, setDefaultValue] = React.useState< + TextAreaFieldProps['defaultValue'] + >(initialValues.defaultValue); + + const [hasError, setHasError] = React.useState< + TextAreaFieldProps['hasError'] + >(initialValues.hasError); + + const [label, setLabel] = React.useState( + initialValues.label + ); + + const [descriptiveText, setDescriptiveText] = React.useState< + TextAreaFieldProps['descriptiveText'] + >(initialValues.descriptiveText); + + const [errorMessage, setErrorMessage] = React.useState< + TextAreaFieldProps['errorMessage'] + >(initialValues.errorMessage); + + const [isDisabled, setIsDisabled] = React.useState< + TextAreaFieldProps['isDisabled'] + >(initialValues.isDisabled); + + const [isReadOnly, setIsReadOnly] = React.useState< + TextAreaFieldProps['isReadOnly'] + >(initialValues.isReadOnly); + + const [isRequired, setIsRequired] = React.useState< + TextAreaFieldProps['isRequired'] + >(initialValues.isRequired); + + const [labelHidden, setLabelHidden] = React.useState< + TextAreaFieldProps['labelHidden'] + >(initialValues.labelHidden); + + const [placeholder, setPlaceholder] = React.useState< + TextAreaFieldProps['placeholder'] + >(initialValues.placeholder); + + const [maxLength, setMaxLength] = React.useState< + TextAreaFieldProps['maxLength'] + >(initialValues.maxLength); + + const [name, setName] = React.useState( + initialValues.name + ); + + const [rows, setRows] = React.useState( + initialValues.rows + ); + + const [size, setSize] = React.useState( + initialValues.size + ); + + const [value, setValue] = React.useState( + initialValues.value + ); + + const [variation, setVariation] = React.useState< + TextAreaFieldProps['variation'] + >(initialValues.variation); + + return { + autoComplete, + defaultValue, + descriptiveText, + errorMessage, + hasError, + isDisabled, + isReadOnly, + isRequired, + label, + labelHidden, + maxLength, + name, + placeholder, + rows, + setAutoComplete, + setDefaultValue, + setDescriptiveText, + setErrorMessage, + setHasError, + setIsDisabled, + setIsReadOnly, + setIsRequired, + setLabel, + setLabelHidden, + setMaxLength, + setName, + setPlaceholder, + setRows, + setSize, + setValue, + setVariation, + size, + value, + variation, + }; +}; diff --git a/docs/src/pages/components/textfield/react.mdx b/docs/src/pages/components/textfield/react.mdx index 3385e26cbd7..b4fc0498748 100644 --- a/docs/src/pages/components/textfield/react.mdx +++ b/docs/src/pages/components/textfield/react.mdx @@ -1,4 +1,4 @@ -import { TextField } from '@aws-amplify/ui-react'; +import { Alert, Link, TextField } from '@aws-amplify/ui-react'; import { TextFieldDemo } from './demo'; import { @@ -87,6 +87,14 @@ There are two variation styles available: default and `quiet`. To render a multi-line `textarea`, set the `isMultiline` prop to `true`. + + Multiline functionality has been moved to{' '} + TextAreaField and will be removed in the + next major release. Please use{' '} + TextAreaField instead of TextField for + multiline text fields. + + diff --git a/docs/src/styles/index.scss b/docs/src/styles/index.scss index f31c4e03995..58943fc7f28 100644 --- a/docs/src/styles/index.scss +++ b/docs/src/styles/index.scss @@ -43,6 +43,7 @@ @import './primitives/tableStyles.css'; @import './primitives/tabsStyles.css'; @import './primitives/textFieldStyles.css'; +@import './primitives/textAreaFieldStyles.css'; @import './primitives/textStyles.css'; @import './primitives/toggleButtonStyles.css'; @import './primitives/viewStyles.css'; diff --git a/docs/src/styles/primitives/textAreaFieldStyles.css b/docs/src/styles/primitives/textAreaFieldStyles.css new file mode 100644 index 00000000000..4bf50310e3e --- /dev/null +++ b/docs/src/styles/primitives/textAreaFieldStyles.css @@ -0,0 +1,6 @@ +.globally-styled-textareafield.amplify-textareafield { + --amplify-components-fieldcontrol-border-color: rebeccapurple; +} +.custom-textareafield-class .amplify-textarea { + border-radius: 0; +} diff --git a/packages/react/__tests__/exports.ts b/packages/react/__tests__/exports.ts index 9a52e927f55..ece1d5809db 100644 --- a/packages/react/__tests__/exports.ts +++ b/packages/react/__tests__/exports.ts @@ -1391,6 +1391,7 @@ describe('@aws-amplify/ui-react', () => { "TableRow", "Tabs", "Text", + "TextAreaField", "TextField", "ToggleButton", "ToggleButtonGroup", @@ -1527,6 +1528,7 @@ describe('primitive catalog', () => { "Tabs", "TabItem", "Text", + "TextAreaField", "TextField", "ToggleButton", "ToggleButtonGroup", diff --git a/packages/react/src/hooks/__tests__/useDeprecationWarning.test.tsx b/packages/react/src/hooks/__tests__/useDeprecationWarning.test.tsx new file mode 100644 index 00000000000..25a25e36bd9 --- /dev/null +++ b/packages/react/src/hooks/__tests__/useDeprecationWarning.test.tsx @@ -0,0 +1,32 @@ +import { renderHook } from '@testing-library/react-hooks'; +import { useDeprecationWarning } from '../useDeprecationWarning'; + +const originalWarn = console.warn; + +describe('useDeprecationWarning', () => { + beforeAll(() => { + console.warn = jest.fn(); + }); + afterAll(() => { + console.warn = originalWarn; + }); + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should render message (shouldWarn true)', async () => { + const message = 'This component is deprecated, use X instead'; + + renderHook(() => useDeprecationWarning({ message })); + expect(console.warn).toHaveBeenCalledTimes(1); + expect(console.warn).toHaveBeenCalledWith(message); + }); + + it('should not render message if shouldWarn is false', async () => { + const message = 'This component is deprecated, use X instead'; + + renderHook(() => useDeprecationWarning({ message, shouldWarn: false })); + expect(console.warn).toHaveBeenCalledTimes(0); + expect(console.warn).not.toHaveBeenCalledWith(message); + }); +}); diff --git a/packages/react/src/hooks/useDeprecationWarning.ts b/packages/react/src/hooks/useDeprecationWarning.ts new file mode 100644 index 00000000000..67b76b41e2e --- /dev/null +++ b/packages/react/src/hooks/useDeprecationWarning.ts @@ -0,0 +1,22 @@ +import * as React from 'react'; + +interface UseDeprecationWarning { + shouldWarn?: boolean; + message: string; +} + +export const useDeprecationWarning = ({ + shouldWarn = true, + message, +}: UseDeprecationWarning) => { + React.useEffect(() => { + if ( + shouldWarn && + // show message on builds without Node `process` polyfill + // or with process.env.NODE_ENV not production + (!process || (process && process.env.NODE_ENV !== 'production')) + ) { + console.warn(message); + } + }, [shouldWarn, message]); +}; diff --git a/packages/react/src/primitives/TextAreaField/TextAreaField.tsx b/packages/react/src/primitives/TextAreaField/TextAreaField.tsx new file mode 100644 index 00000000000..6475852d5a5 --- /dev/null +++ b/packages/react/src/primitives/TextAreaField/TextAreaField.tsx @@ -0,0 +1,76 @@ +import * as React from 'react'; +import classNames from 'classnames'; + +import { ComponentClassNames } from '../shared/constants'; +import { FieldDescription, FieldErrorMessage } from '../Field'; +import { Flex } from '../Flex'; +import { Label } from '../Label'; +import { Primitive } from '../types'; +import { splitPrimitiveProps } from '../shared/styleUtils'; +import { TextArea } from '../TextArea'; +import { TextAreaFieldProps } from '../types/textAreaField'; +import { useStableId } from '../shared/utils'; + +export const DEFAULT_ROW_COUNT = 3; + +const TextAreaFieldPrimitive: Primitive = ( + props, + ref +) => { + const { + className, + descriptiveText, + errorMessage, + hasError = false, + id, + label, + labelHidden = false, + rows, + size, + testId, + ..._rest + } = props; + + const fieldId = useStableId(id); + const descriptionId = useStableId(); + + const { flexContainerStyleProps, baseStyleProps, rest } = + splitPrimitiveProps(_rest); + + return ( + + + +