From 59a646a4c22c2ca0c338f4296d444e5a7c27e9e9 Mon Sep 17 00:00:00 2001 From: Lena Rashkovan Date: Mon, 16 Sep 2024 11:38:48 +0200 Subject: [PATCH 1/8] feat(input): replace base input with react-aria component --- src/components/experimental/Input/Input.tsx | 30 +++++++++------------ 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/src/components/experimental/Input/Input.tsx b/src/components/experimental/Input/Input.tsx index 62489c76..c9d6e6b7 100644 --- a/src/components/experimental/Input/Input.tsx +++ b/src/components/experimental/Input/Input.tsx @@ -1,11 +1,11 @@ import React, { ReactElement } from 'react'; +import { Input as BaseInput, InputProps as BaseInputProps } from 'react-aria-components'; import styled from 'styled-components'; import { useGeneratedId } from '../../../utils/hooks'; -import { theme } from '../../../essentials/experimental/theme'; +import { theme } from '../../../essentials/experimental'; +import { getSemanticValue } from '../../../essentials/experimental/cssVariables'; -const TextInput = styled.input.attrs(() => ({ - type: 'text' -}))` +const StyledInput = styled(BaseInput)` border: none; background-color: unset; outline: none; @@ -39,7 +39,7 @@ const Label = styled.label<{ $shouldDisplace: boolean; $shouldLabelAnimate?: boo top: ${theme.space[1]}; font-size: ${theme.fontSizes[0]}; transform: translate3d(1px, 0 ,0); - color: hsla(347, 41%, 50%, 1); // var(--sys-color-interactive, #B44B61); + color: ${getSemanticValue('interactive')} `} `; @@ -48,10 +48,10 @@ const Wrapper = styled.div` font-family: ${theme.fonts.normal}; - background-color: hsla(0, 6%, 99%, 1); // var(--sys-color-surface, #FCFCFC); - border-width: 0.0625rem; // 1px + background-color: ${getSemanticValue('surface')}; + border-width: 0.0625rem; border-style: solid; - border-color: hsla(0, 6%, 82%, 1); // var(--sys-color-divider, #D4CECE); + border-color: ${getSemanticValue('outline-variant')}; border-radius: ${theme.radii[4]}; padding-left: ${theme.space[4]}; @@ -63,21 +63,17 @@ const Wrapper = styled.div` overflow: hidden; &:hover { - border-color: hsla(0, 6%, 47%, 1); // var(--sys-color-outline, #7F7171); + border-color: ${getSemanticValue('outline')}; } &:focus-within { - // var(--sys-color-interactive, #B44B61); - outline: hsla(347, 41%, 50%, 1) solid 0.125rem; + outline: ${getSemanticValue('interactive')} solid 0.125rem; outline-offset: -0.125rem; } `; -export interface InputProps { +export interface InputProps extends BaseInputProps { label: string; - placeholder: string; - id?: string; - onChange?: (value: string) => void; } function Input({ label, placeholder, id: providedId, onChange, ...rest }: InputProps): ReactElement { @@ -87,7 +83,7 @@ function Input({ label, placeholder, id: providedId, onChange, ...rest }: InputP const [shouldLabelDisplace, setShouldLabelDisplace] = React.useState(false); const handleChange = (e: React.ChangeEvent) => { - onChange(e.target.value); + onChange(e); setValue(prevState => { if (!prevState) { @@ -117,7 +113,7 @@ function Input({ label, placeholder, id: providedId, onChange, ...rest }: InputP return ( - Date: Mon, 16 Sep 2024 17:15:36 +0200 Subject: [PATCH 2/8] feat(text-field): add basic styles and structure --- src/components/experimental/Input/Input.tsx | 132 ------------- .../experimental/Input/docs/Input.stories.tsx | 21 -- .../experimental/TextField/TextField.tsx | 183 ++++++++++++++++++ .../TextField/docs/TextField.stories.tsx | 68 +++++++ src/components/experimental/index.ts | 2 +- 5 files changed, 252 insertions(+), 154 deletions(-) delete mode 100644 src/components/experimental/Input/Input.tsx delete mode 100644 src/components/experimental/Input/docs/Input.stories.tsx create mode 100644 src/components/experimental/TextField/TextField.tsx create mode 100644 src/components/experimental/TextField/docs/TextField.stories.tsx diff --git a/src/components/experimental/Input/Input.tsx b/src/components/experimental/Input/Input.tsx deleted file mode 100644 index c9d6e6b7..00000000 --- a/src/components/experimental/Input/Input.tsx +++ /dev/null @@ -1,132 +0,0 @@ -import React, { ReactElement } from 'react'; -import { Input as BaseInput, InputProps as BaseInputProps } from 'react-aria-components'; -import styled from 'styled-components'; -import { useGeneratedId } from '../../../utils/hooks'; -import { theme } from '../../../essentials/experimental'; -import { getSemanticValue } from '../../../essentials/experimental/cssVariables'; - -const StyledInput = styled(BaseInput)` - border: none; - background-color: unset; - outline: none; - - font-size: ${theme.fontSizes[1]}; - font-weight: ${theme.fontWeights.medium}; - line-height: ${theme.lineHeights[1]}; - - padding-top: ${theme.space[6]}; - padding-bottom: ${theme.space[2]}; - - display: block; - width: 100%; -`; - -const Label = styled.label<{ $shouldDisplace: boolean; $shouldLabelAnimate?: boolean }>` - position: absolute; - top: 50%; - left: ${theme.space[4]}; - font-size: ${theme.fontSizes[1]}; - line-height: ${theme.lineHeights[0]}; - - transform: translate3d(0, calc(-${theme.lineHeights[0]} / 2), 0); - transform-origin: 0; - - transition: top 0.2s ease, font-size 0.2s ease, transform 0.2s ease; - - ${props => - props.$shouldDisplace && - ` - top: ${theme.space[1]}; - font-size: ${theme.fontSizes[0]}; - transform: translate3d(1px, 0 ,0); - color: ${getSemanticValue('interactive')} - `} -`; - -const Wrapper = styled.div` - box-sizing: content-box; - - font-family: ${theme.fonts.normal}; - - background-color: ${getSemanticValue('surface')}; - border-width: 0.0625rem; - border-style: solid; - border-color: ${getSemanticValue('outline-variant')}; - border-radius: ${theme.radii[4]}; - - padding-left: ${theme.space[4]}; - padding-right: ${theme.space[4]}; - display: flex; - align-items: end; - - position: relative; - overflow: hidden; - - &:hover { - border-color: ${getSemanticValue('outline')}; - } - - &:focus-within { - outline: ${getSemanticValue('interactive')} solid 0.125rem; - outline-offset: -0.125rem; - } -`; - -export interface InputProps extends BaseInputProps { - label: string; -} - -function Input({ label, placeholder, id: providedId, onChange, ...rest }: InputProps): ReactElement { - const id = useGeneratedId(providedId); - - const [value, setValue] = React.useState(); - const [shouldLabelDisplace, setShouldLabelDisplace] = React.useState(false); - - const handleChange = (e: React.ChangeEvent) => { - onChange(e); - - setValue(prevState => { - if (!prevState) { - setShouldLabelDisplace(true); - } - return e.target.value; - }); - }; - - const handleFocus = () => { - if (placeholder) { - return; - } - setShouldLabelDisplace(true); - }; - - const handleBlur = () => { - if (!value) { - setShouldLabelDisplace(false); - return; - } - if (placeholder || value) { - return; - } - setShouldLabelDisplace(false); - }; - - return ( - - - - - ); -} - -export { Input }; diff --git a/src/components/experimental/Input/docs/Input.stories.tsx b/src/components/experimental/Input/docs/Input.stories.tsx deleted file mode 100644 index 8913501a..00000000 --- a/src/components/experimental/Input/docs/Input.stories.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { StoryObj, Meta } from '@storybook/react'; -import { Input } from '../Input'; - -const meta: Meta = { - title: 'Experimental/Components/Input', - component: Input, - args: { - label: 'Passenger name' - }, - argTypes: { - label: { - description: 'The label the text field' - } - } -}; - -export default meta; - -type Story = StoryObj; - -export const Default: Story = {}; diff --git a/src/components/experimental/TextField/TextField.tsx b/src/components/experimental/TextField/TextField.tsx new file mode 100644 index 00000000..cb4e1649 --- /dev/null +++ b/src/components/experimental/TextField/TextField.tsx @@ -0,0 +1,183 @@ +import React, { ReactElement } from 'react'; +import { + Input, + InputProps, + Label, + TextField as BaseTextField, + TextFieldProps as BaseTextFieldProps, + Text, + TextArea +} from 'react-aria-components'; +import styled, { css } from 'styled-components'; +import { get } from '../../../utils/experimental/themeGet'; +import { getSemanticValue } from '../../../essentials/experimental/cssVariables'; +import { textStyles } from '../Text/Text'; + +// TODO +// [] make a flying label (should be up if in focus, has placeholder or is filled) +// [x] style helper text +// [x] add counter +// [x] style error text +// [] add autogrow +// [] add clear control +// [] add arrow control +// [] add an optional icon + +const StyledInputSource = styled.div` + border: none; + background-color: unset; + outline: none; + + padding-top: ${get('space.6')}; + padding-bottom: ${get('space.2')}; + + display: block; + width: 100%; + + caret-color: ${getSemanticValue('interactive')}; + ${textStyles.variants.body1} + + &::placeholder { + color: ${getSemanticValue('on-surface-variant')}; + } +`; + +const StyledTextArea = styled(TextArea).attrs({ rows: 1 })` + resize: none; +`; + +const StyledLabel = styled(Label)` + position: absolute; + left: ${get('space.4')}; + top: 50%; + color: ${getSemanticValue('on-surface')}; + + ${textStyles.variants.body1} + + transform: translate3d(0, calc(-${textStyles.variants.body1.lineHeight} / 2), 0); + transform-origin: 0; + + transition: top 0.2s ease, font-size 0.2s ease, transform 0.2s ease; +`; + +const TopLine = styled.div` + box-sizing: content-box; + + background-color: ${getSemanticValue('surface')}; + border-width: 0.0625rem; + border-style: solid; + border-color: ${getSemanticValue('outline-variant')}; + border-radius: ${get('radii.4')}; + + padding-left: ${get('space.4')}; + padding-right: ${get('space.4')}; + display: flex; + align-items: end; + min-width: 315px; + + position: relative; + overflow: hidden; + + &:hover { + border-color: ${getSemanticValue('outline')}; + } + + &:focus-within { + outline: ${getSemanticValue('interactive')} solid 0.125rem; + outline-offset: -0.125rem; + } + + &:focus-within ${StyledLabel} { + top: ${get('space.1')}; + transform: translate3d(1px, 0, 0); + color: ${getSemanticValue('interactive')}; + + ${textStyles.variants.label2} + } +`; + +const BottomLine = styled.footer` + display: grid; + grid-template-areas: 'message counter'; + justify-content: space-between; + gap: ${get('space.2')}; + + padding: ${get('space.1')} ${get('space.3')} ${get('space.0')}; + + color: ${getSemanticValue('on-surface-variant')}; + ${textStyles.variants.label2} + + &:empty { + display: none; + } +`; + +const Counter = styled.span` + grid-area: counter; +`; + +const Wrapper = styled(BaseTextField)` + padding: ${get('space.2')} ${get('space.0')}; + + ${props => + props.isInvalid && + css` + color: ${getSemanticValue('negative')}; + + ${StyledLabel}, + ${BottomLine} { + color: currentColor; + } + + ${TopLine} { + border-color: currentColor; + } + `} + + ${props => + props.isDisabled && + css` + opacity: 0.38; + `} +`; + +export interface TextFieldProps extends BaseTextFieldProps { + label: string; + placeholder?: string; + description?: string; + errorMessage?: string; + multiline?: boolean; +} + +function TextField({ + label, + description, + errorMessage, + placeholder, + multiline = false, + ...props +}: TextFieldProps): ReactElement { + const [text, setText] = React.useState(''); + + const handleChange = (value: string) => { + setText(value); + props.onChange?.(value); + }; + + return ( + + + {label} + + + + {(description || errorMessage) && ( + {errorMessage || description} + )} + {Boolean(props.maxLength) && {`${text.length} / ${props.maxLength}`}} + + + ); +} + +export { TextField }; diff --git a/src/components/experimental/TextField/docs/TextField.stories.tsx b/src/components/experimental/TextField/docs/TextField.stories.tsx new file mode 100644 index 00000000..f0687e15 --- /dev/null +++ b/src/components/experimental/TextField/docs/TextField.stories.tsx @@ -0,0 +1,68 @@ +import { StoryObj, Meta } from '@storybook/react'; +import { TextField } from '../TextField'; + +const meta: Meta = { + title: 'Experimental/Components/TextField', + component: TextField, + args: { + label: 'Passenger name' + }, + argTypes: { + label: { + description: 'The label of the text field' + }, + maxLength: { + description: 'The maximum length of the text field (optional)', + control: 'number' + }, + isDisabled: { + control: 'boolean' + }, + isInvalid: { + control: 'boolean' + } + } +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = {}; + +export const WithPlaceholder: Story = { + args: { + placeholder: 'Placeholder' + } +}; + +export const WithDescription: Story = { + args: { + description: 'Helper text' + } +}; + +export const WithMaxLength: Story = { + args: { + maxLength: 999 + } +}; + +export const Disabled: Story = { + args: { + isDisabled: true + } +}; + +export const Invalid: Story = { + args: { + isInvalid: true + } +}; + +export const InvalidWithMessage: Story = { + args: { + isInvalid: true, + errorMessage: 'Error text' + } +}; diff --git a/src/components/experimental/index.ts b/src/components/experimental/index.ts index 9676d74e..6a04f4a4 100644 --- a/src/components/experimental/index.ts +++ b/src/components/experimental/index.ts @@ -1,4 +1,4 @@ export { Chip } from './Chip/Chip'; export { Text } from './Text/Text'; export { Button } from './Button/Button'; -export { Input } from './Input/Input'; +export { TextField } from './TextField/TextField'; From de14a7d99d8436fbbc5a71c6f5763c87906de571 Mon Sep 17 00:00:00 2001 From: Lena Rashkovan Date: Mon, 16 Sep 2024 17:21:39 +0200 Subject: [PATCH 3/8] feat(text-field): add a flying label --- .../experimental/TextField/TextField.tsx | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/components/experimental/TextField/TextField.tsx b/src/components/experimental/TextField/TextField.tsx index cb4e1649..c81822cf 100644 --- a/src/components/experimental/TextField/TextField.tsx +++ b/src/components/experimental/TextField/TextField.tsx @@ -14,7 +14,7 @@ import { getSemanticValue } from '../../../essentials/experimental/cssVariables' import { textStyles } from '../Text/Text'; // TODO -// [] make a flying label (should be up if in focus, has placeholder or is filled) +// [x] make a flying label (should be up if in focus, has placeholder or is filled) // [x] style helper text // [x] add counter // [x] style error text @@ -46,7 +46,14 @@ const StyledTextArea = styled(TextArea).attrs({ rows: 1 })` resize: none; `; -const StyledLabel = styled(Label)` +const flyingStyles = css` + top: ${get('space.1')}; + transform: translate3d(1px, 0, 0); + + ${textStyles.variants.label2} +`; + +const StyledLabel = styled(Label)<{ $flying: boolean }>` position: absolute; left: ${get('space.4')}; top: 50%; @@ -58,6 +65,8 @@ const StyledLabel = styled(Label)` transform-origin: 0; transition: top 0.2s ease, font-size 0.2s ease, transform 0.2s ease; + + ${props => props.$flying && flyingStyles} `; const TopLine = styled.div` @@ -88,11 +97,9 @@ const TopLine = styled.div` } &:focus-within ${StyledLabel} { - top: ${get('space.1')}; - transform: translate3d(1px, 0, 0); color: ${getSemanticValue('interactive')}; - ${textStyles.variants.label2} + ${flyingStyles} } `; @@ -167,8 +174,8 @@ function TextField({ return ( - {label} - + {label} + {(description || errorMessage) && ( From 91c5206d76ed326587c04e1daf9071d29057be18 Mon Sep 17 00:00:00 2001 From: Lena Rashkovan Date: Mon, 16 Sep 2024 17:21:56 +0200 Subject: [PATCH 4/8] feat(text-field): add a flying label --- src/components/experimental/TextField/TextField.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/experimental/TextField/TextField.tsx b/src/components/experimental/TextField/TextField.tsx index c81822cf..ff971575 100644 --- a/src/components/experimental/TextField/TextField.tsx +++ b/src/components/experimental/TextField/TextField.tsx @@ -174,7 +174,7 @@ function TextField({ return ( - {label} + 0)}>{label} From 7c4ed8a22b60a02e387100df4a6ba04b885187fe Mon Sep 17 00:00:00 2001 From: Lena Rashkovan Date: Wed, 18 Sep 2024 13:05:50 +0200 Subject: [PATCH 5/8] feat(text-field): add clearing --- .../experimental/TextField/ClearButton.tsx | 25 +++ .../experimental/TextField/Field.ts | 41 ++++ .../experimental/TextField/Label.ts | 25 +++ .../experimental/TextField/TextField.tsx | 181 ++++++++---------- .../TextField/docs/TextField.stories.tsx | 35 ++++ 5 files changed, 210 insertions(+), 97 deletions(-) create mode 100644 src/components/experimental/TextField/ClearButton.tsx create mode 100644 src/components/experimental/TextField/Field.ts create mode 100644 src/components/experimental/TextField/Label.ts diff --git a/src/components/experimental/TextField/ClearButton.tsx b/src/components/experimental/TextField/ClearButton.tsx new file mode 100644 index 00000000..f78dd949 --- /dev/null +++ b/src/components/experimental/TextField/ClearButton.tsx @@ -0,0 +1,25 @@ +import React, { ReactElement } from 'react'; +import styled from 'styled-components'; +import { Button as BaseButton, ButtonProps as BaseButtonProps } from 'react-aria-components'; +import { getSemanticValue } from '../../../essentials/experimental/cssVariables'; +import { XCrossCircleIcon } from '../../../icons'; + +const StyledButton = styled(BaseButton)` + appearance: none; + background: none; + border: none; + display: flex; + margin: 0; + padding: 0; + color: ${getSemanticValue('on-surface-variant')}; +`; + +function ClearButton(props: BaseButtonProps): ReactElement { + return ( + + + + ); +} + +export { ClearButton }; diff --git a/src/components/experimental/TextField/Field.ts b/src/components/experimental/TextField/Field.ts new file mode 100644 index 00000000..d925165e --- /dev/null +++ b/src/components/experimental/TextField/Field.ts @@ -0,0 +1,41 @@ +import { Input as BaseInput, TextArea as BaseTextArea } from 'react-aria-components'; +import styled, { css } from 'styled-components'; +import { getSemanticValue } from '../../../essentials/experimental/cssVariables'; +import { textStyles } from '../Text/Text'; + +const fieldStyles = css` + border: none; + background-color: unset; + outline: none; + + display: block; + width: 100%; + padding: 0; + + caret-color: ${getSemanticValue('interactive')}; + color: ${getSemanticValue('on-surface')}; + + ${textStyles.variants.body1} + + &::placeholder { + color: ${getSemanticValue('on-surface-variant')}; + } +`; + +// TODO: Implement autogrow +export const TextArea = styled(BaseTextArea).attrs({ rows: 1 })` + ${fieldStyles}; + + resize: none; + min-height: ${textStyles.variants.body1.lineHeight}; +`; + +export const Input = styled(BaseInput)` + ${fieldStyles} + + &[type='search'] { + &::-webkit-search-cancel-button { + display: none; + } + } +`; diff --git a/src/components/experimental/TextField/Label.ts b/src/components/experimental/TextField/Label.ts new file mode 100644 index 00000000..997b7574 --- /dev/null +++ b/src/components/experimental/TextField/Label.ts @@ -0,0 +1,25 @@ +import { Label as BaseLabel } from 'react-aria-components'; +import styled, { css } from 'styled-components'; +import { textStyles } from '../Text/Text'; + +export const flyingLabelStyles = css` + top: 0; + transform: translate3d(0, 0, 0); + + ${textStyles.variants.label2} +`; + +export const Label = styled(BaseLabel)<{ $flying: boolean }>` + position: absolute; + top: 50%; + color: currentColor; + + ${textStyles.variants.body1} + + transform: translate3d(0, calc(-${textStyles.variants.body1.lineHeight} / 2), 0); + transform-origin: 0; + + transition: top 200ms ease, font-size 200ms ease, transform 200ms ease; + + ${props => props.$flying && flyingLabelStyles} +`; diff --git a/src/components/experimental/TextField/TextField.tsx b/src/components/experimental/TextField/TextField.tsx index ff971575..6c5f40e9 100644 --- a/src/components/experimental/TextField/TextField.tsx +++ b/src/components/experimental/TextField/TextField.tsx @@ -1,117 +1,73 @@ -import React, { ReactElement } from 'react'; -import { - Input, - InputProps, - Label, - TextField as BaseTextField, - TextFieldProps as BaseTextFieldProps, - Text, - TextArea -} from 'react-aria-components'; -import styled, { css } from 'styled-components'; +import React, { ReactElement, RefObject } from 'react'; +import { TextField as BaseTextField, TextFieldProps as BaseTextFieldProps, Text } from 'react-aria-components'; +import styled from 'styled-components'; import { get } from '../../../utils/experimental/themeGet'; import { getSemanticValue } from '../../../essentials/experimental/cssVariables'; import { textStyles } from '../Text/Text'; +import { VisuallyHidden } from '../../VisuallyHidden/VisuallyHidden'; +import { ClearButton } from './ClearButton'; +import { Label, flyingLabelStyles } from './Label'; +import { TextArea, Input } from './Field'; -// TODO -// [x] make a flying label (should be up if in focus, has placeholder or is filled) -// [x] style helper text -// [x] add counter -// [x] style error text -// [] add autogrow -// [] add clear control -// [] add arrow control -// [] add an optional icon - -const StyledInputSource = styled.div` - border: none; - background-color: unset; - outline: none; - - padding-top: ${get('space.6')}; - padding-bottom: ${get('space.2')}; - - display: block; - width: 100%; - - caret-color: ${getSemanticValue('interactive')}; - ${textStyles.variants.body1} - - &::placeholder { - color: ${getSemanticValue('on-surface-variant')}; - } -`; - -const StyledTextArea = styled(TextArea).attrs({ rows: 1 })` - resize: none; -`; - -const flyingStyles = css` - top: ${get('space.1')}; - transform: translate3d(1px, 0, 0); +const defaultAriaStrings = { + clearFieldButton: 'Clear field', + messageFieldIsCleared: 'The field is cleared' +}; - ${textStyles.variants.label2} -`; - -const StyledLabel = styled(Label)<{ $flying: boolean }>` - position: absolute; - left: ${get('space.4')}; - top: 50%; - color: ${getSemanticValue('on-surface')}; - - ${textStyles.variants.body1} - - transform: translate3d(0, calc(-${textStyles.variants.body1.lineHeight} / 2), 0); - transform-origin: 0; - - transition: top 0.2s ease, font-size 0.2s ease, transform 0.2s ease; +const InnerWrapper = styled.div` + width: 100%; + padding-top: ${get('space.4')}; - ${props => props.$flying && flyingStyles} + position: relative; + overflow: hidden; `; const TopLine = styled.div` box-sizing: content-box; + color: ${getSemanticValue('on-surface-variant')}; background-color: ${getSemanticValue('surface')}; border-width: 0.0625rem; border-style: solid; border-color: ${getSemanticValue('outline-variant')}; border-radius: ${get('radii.4')}; - padding-left: ${get('space.4')}; - padding-right: ${get('space.4')}; + padding: ${get('space.2')} ${get('space.3')} ${get('space.2')} ${get('space.4')}; display: flex; - align-items: end; - min-width: 315px; + align-items: start; + gap: ${get('space.3')}; - position: relative; - overflow: hidden; + & > :not(${InnerWrapper}) { + flex-shrink: 0; + padding-top: ${get('space.2')}; + } &:hover { border-color: ${getSemanticValue('outline')}; + color: ${getSemanticValue('on-surface')}; } &:focus-within { + color: ${getSemanticValue('interactive')}; outline: ${getSemanticValue('interactive')} solid 0.125rem; outline-offset: -0.125rem; - } - &:focus-within ${StyledLabel} { - color: ${getSemanticValue('interactive')}; - - ${flyingStyles} + ${Label} { + ${flyingLabelStyles} + } } `; const BottomLine = styled.footer` display: grid; - grid-template-areas: 'message counter'; + grid-template-areas: '. counter'; justify-content: space-between; gap: ${get('space.2')}; padding: ${get('space.1')} ${get('space.3')} ${get('space.0')}; color: ${getSemanticValue('on-surface-variant')}; + ${textStyles.variants.label2} &:empty { @@ -126,34 +82,41 @@ const Counter = styled.span` const Wrapper = styled(BaseTextField)` padding: ${get('space.2')} ${get('space.0')}; - ${props => - props.isInvalid && - css` + &[data-disabled] { + opacity: 0.38; + + ${TopLine} { + pointer-events: none; + } + } + + &[data-invalid] { + ${Label}, + ${BottomLine} { color: ${getSemanticValue('negative')}; + } - ${StyledLabel}, - ${BottomLine} { - color: currentColor; - } - - ${TopLine} { - border-color: currentColor; - } - `} - - ${props => - props.isDisabled && - css` - opacity: 0.38; - `} + ${TopLine} { + border-color: ${getSemanticValue('negative')}; + } + } `; export interface TextFieldProps extends BaseTextFieldProps { label: string; + leadingIcon?: React.ReactNode; + actionIcon?: React.ReactNode; placeholder?: string; description?: string; errorMessage?: string; multiline?: boolean; + /** + * If you project supports multiple languages, it is recommended to pass translated labels to these properties + */ + ariaStrings?: { + clearFieldButton: string; + messageFieldIsCleared: string; + }; } function TextField({ @@ -161,10 +124,14 @@ function TextField({ description, errorMessage, placeholder, + leadingIcon, + actionIcon, multiline = false, + ariaStrings = defaultAriaStrings, ...props }: TextFieldProps): ReactElement { - const [text, setText] = React.useState(''); + const [text, setText] = React.useState(props.defaultValue || props.value || ''); + const ref = React.useRef(null); const handleChange = (value: string) => { setText(value); @@ -172,10 +139,30 @@ function TextField({ }; return ( - + - 0)}>{label} - + {leadingIcon} + + + {multiline ? ( +