diff --git a/.storybook/preview.js b/.storybook/preview.js
index 0dfac9a..9ce4c16 100644
--- a/.storybook/preview.js
+++ b/.storybook/preview.js
@@ -46,6 +46,7 @@ export const parameters = {
'Image',
'Input',
'Text',
+ 'TextArea',
'Toast',
'Switch',
'Modal',
diff --git a/.storybook/storybook.requires.js b/.storybook/storybook.requires.js
index 7aef130..4d52004 100644
--- a/.storybook/storybook.requires.js
+++ b/.storybook/storybook.requires.js
@@ -35,6 +35,7 @@ const getStories = () => {
require('../src/components/Input/Input.stories.tsx'),
require('../src/components/Switch/Switch.stories.tsx'),
require('../src/components/Text/Text.stories.tsx'),
+ require('../src/components/TextArea/TextArea.stories.tsx'),
require('../src/components/TextLink/TextLink.stories.tsx'),
require('../src/components/Toast/Toast.stories.tsx'),
require('../src/components/Spinner/Spinner.stories.tsx'),
diff --git a/docs/stories/assets/textarea/TextAreaCases.png b/docs/stories/assets/textarea/TextAreaCases.png
new file mode 100644
index 0000000..0b4d5b4
Binary files /dev/null and b/docs/stories/assets/textarea/TextAreaCases.png differ
diff --git a/docs/stories/assets/textarea/TextAreaSizes.png b/docs/stories/assets/textarea/TextAreaSizes.png
new file mode 100644
index 0000000..1d2520a
Binary files /dev/null and b/docs/stories/assets/textarea/TextAreaSizes.png differ
diff --git a/docs/stories/assets/textarea/TextAreaTypes.png b/docs/stories/assets/textarea/TextAreaTypes.png
new file mode 100644
index 0000000..793fb89
Binary files /dev/null and b/docs/stories/assets/textarea/TextAreaTypes.png differ
diff --git a/docs/stories/components/textarea.stories.mdx b/docs/stories/components/textarea.stories.mdx
new file mode 100644
index 0000000..03657c4
--- /dev/null
+++ b/docs/stories/components/textarea.stories.mdx
@@ -0,0 +1,100 @@
+import { Meta } from '@storybook/addon-docs';
+import TextAreaSizes from '../assets/textarea/TextAreaSizes.png';
+import TextAreaTypes from '../assets/textarea/TextAreaTypes.png';
+import TextAreaCases from '../assets/textarea/TextAreaCases.png';
+
+
+
+# TextArea
+
+Text areas let users enter and edit text.
+
+
+
+
+
+
+## Usage
+
+```jsx
+
+```
+
+## Props
+
+#### size
+
+The size of the button.
+`large`,
+`medium`,
+`small`
+
+#### label
+
+The text or component to use for the floating label.
+
+#### labelFixed
+
+Boolean value that checks if the label is fixed.
+
+#### placeholder
+
+The string that will be rendered before text has been entered.
+
+#### disabled
+
+If true, user won't be able to interact with the component.
+
+#### helpText
+
+Helper text providing information for the textArea.
+
+#### counterText
+
+Counts the length of the textArea.
+
+#### error
+
+Boolean value indicating the error status.
+
+#### errorText
+
+Helper text showing the error in the textArea.
+
+#### maxLength
+
+Max length of content.
+
+#### maxLengthErrorText
+
+Helper text showing the error if exceed maxLength.
+
+#### style
+
+Type: `StyleProp`
+
+
+TextArea implements: <>
+
+ ...TextInput props
+
+>
diff --git a/src/components/TextArea/TextArea.stories.tsx b/src/components/TextArea/TextArea.stories.tsx
new file mode 100644
index 0000000..ad22388
--- /dev/null
+++ b/src/components/TextArea/TextArea.stories.tsx
@@ -0,0 +1,149 @@
+import React, { useState } from 'react';
+import { ComponentStory, ComponentMeta } from '@storybook/react-native';
+import Text from '../Text/Text';
+import TextArea from './TextArea';
+import Box from '../Box/Box';
+import Button from '../Button/Button';
+
+const sizeList = ['small', 'medium', 'large'];
+
+const TextAreaMeta: ComponentMeta = {
+ title: 'TextArea',
+ component: TextArea,
+ argTypes: {
+ size: {
+ options: sizeList,
+ control: { type: 'radios' },
+ },
+ },
+ args: {
+ size: 'medium',
+ label: 'Label',
+ placeholder: 'Type something...',
+ labelFixed: false,
+ helpText: 'Helper Text',
+ error: false,
+ disabled: false,
+ errorText: 'Error Text',
+ },
+};
+
+export default TextAreaMeta;
+
+type TextAreaStory = ComponentStory;
+
+export const Basic: TextAreaStory = args => (
+
+
+
+ Text Area
+
+
+
+
+
+);
+
+export const Cases: TextAreaStory = args => {
+ const [state, setState] = useState(args);
+
+ return (
+
+
+
+ Text Area Cases
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export const Sizes: TextAreaStory = () => (
+
+
+
+ Text Area Sizes
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+);
+
+export const Types: TextAreaStory = args => {
+ return (
+
+
+
+ Text Area Types
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/src/components/TextArea/TextArea.test.tsx b/src/components/TextArea/TextArea.test.tsx
new file mode 100644
index 0000000..8a58a8d
--- /dev/null
+++ b/src/components/TextArea/TextArea.test.tsx
@@ -0,0 +1,230 @@
+import React from 'react';
+import { act } from 'react-test-renderer';
+import { fireEvent, render } from '../../test-utils';
+import theme from '../../theme';
+import TextArea, { TextInputHandles } from './TextArea';
+
+describe('TextArea', () => {
+ test('should render TextArea correctly', () => {
+ // when
+ const { toJSON } = render();
+
+ // then
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ test('should render label fixed TextArea correctly', () => {
+ // when
+ const { toJSON } = render(
+ ,
+ );
+
+ // then
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ test('should render disabled TextArea correctly', () => {
+ // when
+ const { toJSON } = render();
+
+ // then
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ test('should render TextArea with help text correctly', () => {
+ // when
+ const { toJSON } = render();
+
+ // then
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ test('should render TextArea with error state correctly', () => {
+ // when
+ const { toJSON } = render();
+
+ // then
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ test('should render TextArea with error state and error text correctly', () => {
+ // when
+ const { toJSON } = render(
+ ,
+ );
+
+ // then
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ test('should render TextArea with small size correctly', () => {
+ // when
+ const { toJSON } = render();
+
+ // then
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ test('should render TextArea with medium size correctly', () => {
+ // when
+ const { toJSON } = render();
+
+ // then
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ test('should render TextArea with large size correctly', () => {
+ // when
+ const { toJSON } = render();
+
+ // then
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ test('should focus TextArea correctly', () => {
+ // when
+ const { getByTestId } = render();
+
+ // then
+ expect(getByTestId('textArea-box').props.style[0].borderColor).toBe(
+ theme.colors.neutralLighter,
+ );
+
+ // when
+ act(() => {
+ fireEvent(getByTestId('textArea'), 'onFocus');
+ });
+
+ // then
+ expect(getByTestId('textArea-box').props.style[0].borderColor).toBe(
+ theme.colors.primaryKey,
+ );
+ });
+
+ test('should not focus and blur TextArea when TextArea is disabled', () => {
+ // when
+ const { getByTestId } = render();
+
+ // then
+ expect(getByTestId('textArea-box').props.style[0].borderColor).toBe(
+ theme.colors.neutralLighter,
+ );
+
+ // when
+ act(() => {
+ const textArea = getByTestId('textArea');
+ fireEvent(textArea, 'onFocus');
+ fireEvent(textArea, 'onBlur');
+ });
+
+ // then
+ expect(getByTestId('textArea-box').props.style[0].borderColor).toBe(
+ theme.colors.neutralLighter,
+ );
+ });
+
+ test('should blur TextArea without value correctly', () => {
+ // when
+ const { getByTestId } = render();
+
+ // then
+ expect(getByTestId('textArea-box').props.style[0].borderColor).toBe(
+ theme.colors.neutralLighter,
+ );
+
+ // when
+ act(() => {
+ const textArea = getByTestId('textArea');
+ fireEvent(textArea, 'onFocus');
+ fireEvent(textArea, 'onBlur');
+ });
+
+ // then
+ expect(getByTestId('textArea-box').props.style[0].borderColor).toBe(
+ theme.colors.neutralLighter,
+ );
+ });
+
+ test('should blur TextArea with value correctly', () => {
+ // when
+ const { getByTestId } = render();
+
+ // then
+ expect(getByTestId('textArea-box').props.style[0].borderColor).toBe(
+ theme.colors.neutralLighter,
+ );
+
+ // when
+ act(() => {
+ const textArea = getByTestId('textArea');
+ fireEvent(textArea, 'onFocus');
+ fireEvent(textArea, 'onBlur');
+ });
+
+ // then
+ expect(getByTestId('textArea-box').props.style[0].borderColor).toBe(
+ theme.colors.neutralLighter,
+ );
+ });
+
+ test('should change text of TextArea without value', () => {
+ // when
+ const { getByTestId } = render();
+ const textArea = getByTestId('textArea');
+ fireEvent.changeText(textArea, 'e-mail@mail.com');
+
+ // then
+ expect(textArea.props.value).toBe('e-mail@mail.com');
+ });
+
+ test('should change text of textArea with value', () => {
+ // when
+ const { getByTestId } = render();
+ const textArea = getByTestId('textArea');
+ fireEvent.changeText(textArea, 'e-mail@mail.com');
+
+ // then
+ expect(textArea.props.value).toBe('value');
+ });
+
+ test('should not change text of TextArea when TextArea is disabled', () => {
+ // when
+ const { getByTestId } = render();
+ const textArea = getByTestId('textArea');
+ fireEvent.changeText(textArea, 'e-mail@mail.com');
+
+ // then
+ expect(textArea.props.value).not.toBe('e-mail@mail.com');
+ });
+
+ test('should change text of TextArea when TextArea has defaultValue', () => {
+ // when
+ const { getByTestId } = render(
+ ,
+ );
+ const textArea = getByTestId('textArea');
+ fireEvent.changeText(textArea, 'e-mail@mail.com');
+
+ // then
+ expect(textArea.props.value).toBe('e-mail@mail.com');
+ });
+
+ test('should access textAreas handle functions correctly', () => {
+ // given
+ const ref = React.createRef();
+
+ // when
+ render();
+
+ act(() => {
+ ref.current?.focus();
+ ref.current?.blur();
+ ref.current?.clear();
+ ref.current?.isFocused();
+ ref.current?.setNativeProps({ text: '' });
+ });
+
+ // then
+ expect(ref.current?.focus).toBeTruthy();
+ });
+});
diff --git a/src/components/TextArea/TextArea.tsx b/src/components/TextArea/TextArea.tsx
new file mode 100644
index 0000000..2fbd8a4
--- /dev/null
+++ b/src/components/TextArea/TextArea.tsx
@@ -0,0 +1,259 @@
+import React, { forwardRef, useEffect } from 'react';
+import {
+ Easing,
+ EasingFunction,
+ NativeSyntheticEvent,
+ TextInput,
+ TextInputProps,
+ TextInputFocusEventData,
+} from 'react-native';
+import theme, { Theme } from '../../theme';
+import Box from '../Box/Box';
+import { TextAreaLabel } from './TextAreaLabel';
+import { TextAreaHelpText } from './TextAreaHelpText';
+import { TextAreaCounterText } from './TextAreaCounterText';
+import { useInputRef, useInputValue, useOutlineLabelVisibility } from './hooks';
+import { getBorderColor, getPlaceholderText } from './utils';
+import {
+ backgroundColor,
+ border,
+ createRestyleComponent,
+ createVariant,
+ layout,
+ position,
+ spacing,
+ VariantProps,
+} from '@ergenekonyigit/restyle';
+
+type TextAreaProps = React.ComponentProps &
+ TextInputProps & {
+ label?: string | null;
+ labelFixed?: boolean;
+ placeholder?: string;
+ helpText?: string | null;
+ counterText?: number;
+ errorText?: string | null;
+ error?: boolean;
+ onFocus?: (e: NativeSyntheticEvent) => void;
+ onBlur?: (e: NativeSyntheticEvent) => void;
+ disabled?: boolean;
+ easing?: EasingFunction;
+ testID?: string;
+ editable?: boolean;
+ value?: string;
+ defaultValue?: string;
+ onChangeText?: (text: string) => void;
+ size?: VariantProps['variant'];
+ maxLength?: number;
+ maxLengthErrorText?: string;
+ };
+
+const sizeVariant = createVariant({
+ property: 'size',
+ themeKey: 'textAreaSizeVariants',
+});
+
+const BaseTextArea = createRestyleComponent<
+ TextAreaProps & VariantProps,
+ Theme
+>([layout, spacing, border, backgroundColor, position, sizeVariant], TextInput);
+
+export type TextInputHandles = Pick<
+ TextInput,
+ 'focus' | 'clear' | 'blur' | 'isFocused' | 'setNativeProps'
+>;
+
+const TextArea = forwardRef(
+ (
+ {
+ size = 'medium',
+ label,
+ labelFixed = false,
+ placeholder,
+ helpText,
+ counterText,
+ errorText,
+ error = false,
+ disabled = false,
+ editable = true,
+ easing = Easing.inOut(Easing.ease),
+ testID = 'textArea',
+ maxLength = 200,
+ maxLengthErrorText = 'You have exceeded the character limit.',
+ ...rest
+ }: TextAreaProps,
+ ref,
+ ) => {
+ const [focused, setFocused] = React.useState(false);
+ const [errorState, setErrorState] = React.useState(false);
+ const [length, setLength] = React.useState(0);
+
+ const innerRef = useInputRef();
+ const { value, isControlled, setUncontrolledValue } = useInputValue({
+ value: rest.value,
+ defaultValue: rest.defaultValue,
+ });
+
+ const placeholderText = getPlaceholderText({
+ label,
+ labelFixed,
+ placeholder,
+ value,
+ focused,
+ });
+
+ const borderColor = getBorderColor({
+ focused,
+ errorState,
+ });
+
+ const textAreaHeight: number = {
+ small: theme.textAreaSizeVariants.small.height,
+ medium: theme.textAreaSizeVariants.medium.height,
+ large: theme.textAreaSizeVariants.large.height,
+ }[size];
+
+ const {
+ startAnimation,
+ stopAnimation,
+ animatedViewProps,
+ animatedTextProps,
+ } = useOutlineLabelVisibility({
+ theme,
+ easing,
+ textAreaHeight,
+ focused,
+ value,
+ disabled,
+ helpText,
+ counterText,
+ errorText,
+ errorState,
+ });
+
+ useEffect(() => {
+ setErrorState(error);
+ }, [error]);
+
+ const handleFocus = (e: NativeSyntheticEvent) => {
+ if (disabled || !editable) {
+ return;
+ }
+ startAnimation();
+
+ setFocused(true);
+ setErrorState(false);
+
+ rest.onFocus?.(e);
+ };
+
+ const handleBlur = (e: NativeSyntheticEvent) => {
+ if (disabled || !editable) {
+ return;
+ }
+
+ if (!value) {
+ stopAnimation();
+ }
+
+ setFocused(false);
+ rest.onBlur?.(e);
+ };
+
+ const handleChangeText = (nextValue: string) => {
+ if (disabled || !editable) {
+ return;
+ }
+
+ if (!isControlled) {
+ setUncontrolledValue(nextValue);
+ }
+
+ setLength(nextValue.length);
+
+ rest.onChangeText?.(nextValue);
+ };
+
+ React.useImperativeHandle(ref, () => ({
+ focus: () => innerRef.current?.focus(),
+ clear: () => innerRef.current?.clear(),
+ setNativeProps: (args: Record) =>
+ innerRef.current?.setNativeProps(args),
+ isFocused: () => innerRef.current?.isFocused() || false,
+ blur: () => innerRef.current?.blur(),
+ }));
+
+ const placeHolderSize = () => {
+ if (size === 'large') {
+ return 'm';
+ } else if (size === 'medium') {
+ return 'xs';
+ } else {
+ return '2xs';
+ }
+ };
+
+ return (
+
+
+ maxLength ? 'dangerKey' : borderColor}
+ backgroundColor={disabled ? 'neutralLightest' : 'neutralFull'}
+ px="m"
+ justifyContent="flex-start"
+ zIndex="layer_0"
+ accessibilityLabel={`${testID}-box`}
+ testID={`${testID}-box`}>
+
+
+
+
+
+ maxLength ? true : errorState}
+ errorText={length > maxLength ? maxLengthErrorText : errorText}
+ />
+
+
+ maxLength ? true : errorState}
+ />
+
+
+
+ );
+ },
+);
+
+export default TextArea;
diff --git a/src/components/TextArea/TextAreaCounterText.test.tsx b/src/components/TextArea/TextAreaCounterText.test.tsx
new file mode 100644
index 0000000..171cecb
--- /dev/null
+++ b/src/components/TextArea/TextAreaCounterText.test.tsx
@@ -0,0 +1,36 @@
+import React from 'react';
+import { render } from '../../test-utils';
+import { TextAreaCounterText } from './TextAreaCounterText';
+
+describe('TextArea Counter Text', () => {
+ let counterText: number;
+ let errorState: boolean;
+
+ test('should render counter text correctly', () => {
+ // given
+ counterText = 0;
+ errorState = false;
+
+ // when
+ const { toJSON } = render(
+ ,
+ );
+
+ // then
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ test('should render null when not has error text', () => {
+ // given
+ counterText = 0;
+ errorState = false;
+
+ // when
+ const { toJSON } = render(
+ ,
+ );
+
+ // then
+ expect(toJSON()).toMatchSnapshot();
+ });
+});
diff --git a/src/components/TextArea/TextAreaCounterText.tsx b/src/components/TextArea/TextAreaCounterText.tsx
new file mode 100644
index 0000000..81f9a38
--- /dev/null
+++ b/src/components/TextArea/TextAreaCounterText.tsx
@@ -0,0 +1,29 @@
+import React from 'react';
+import Box from '../Box/Box';
+import Text from '../Text/Text';
+import { getCounterText, getTextColor } from './utils';
+
+export const TextAreaCounterText = React.memo(
+ ({
+ counterText,
+ errorState,
+ }: {
+ counterText?: number;
+ errorState: boolean;
+ }): JSX.Element | null => {
+ const textColor = getTextColor({ errorState });
+
+ const content = getCounterText({
+ counterText,
+ errorState,
+ });
+
+ return (
+
+
+ {`${content}/200`}
+
+
+ );
+ },
+);
diff --git a/src/components/TextArea/TextAreaHelpText.test.tsx b/src/components/TextArea/TextAreaHelpText.test.tsx
new file mode 100644
index 0000000..ca2fc97
--- /dev/null
+++ b/src/components/TextArea/TextAreaHelpText.test.tsx
@@ -0,0 +1,66 @@
+import React from 'react';
+import { render } from '../../test-utils';
+import { TextAreaHelpText } from './TextAreaHelpText';
+
+describe('TextArea Help Text', () => {
+ let helpText: string | null;
+ let errorText: string | null;
+ let errorState: boolean;
+
+ test('should render help text correctly', () => {
+ // given
+ helpText = 'help text';
+ errorText = 'error text';
+ errorState = false;
+
+ // when
+ const { toJSON } = render(
+ ,
+ );
+
+ // then
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ test('should render null when not has help text and error text', () => {
+ // given
+ helpText = null;
+ errorText = null;
+ errorState = false;
+
+ // when
+ const { toJSON } = render(
+ ,
+ );
+
+ // then
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ test('should render null when not has help text', () => {
+ // given
+ helpText = '';
+ errorText = 'error text';
+ errorState = false;
+
+ // when
+ const { toJSON } = render(
+ ,
+ );
+
+ // then
+ expect(toJSON()).toMatchSnapshot();
+ });
+});
diff --git a/src/components/TextArea/TextAreaHelpText.tsx b/src/components/TextArea/TextAreaHelpText.tsx
new file mode 100644
index 0000000..7ac3407
--- /dev/null
+++ b/src/components/TextArea/TextAreaHelpText.tsx
@@ -0,0 +1,36 @@
+import React from 'react';
+import Box from '../Box/Box';
+import Text from '../Text/Text';
+import { getHelpText, getTextColor } from './utils';
+
+export const TextAreaHelpText = React.memo(
+ ({
+ helpText,
+ errorState,
+ errorText,
+ }: {
+ helpText?: string | null;
+ errorState: boolean;
+ errorText?: string | null;
+ }): JSX.Element | null => {
+ if (!helpText && !errorText) {
+ return null;
+ }
+
+ const textColor = getTextColor({ errorState });
+
+ const content = getHelpText({
+ helpText,
+ errorText,
+ errorState,
+ });
+
+ return content ? (
+
+
+ {content}
+
+
+ ) : null;
+ },
+);
diff --git a/src/components/TextArea/TextAreaLabel.test.tsx b/src/components/TextArea/TextAreaLabel.test.tsx
new file mode 100644
index 0000000..f822350
--- /dev/null
+++ b/src/components/TextArea/TextAreaLabel.test.tsx
@@ -0,0 +1,116 @@
+import React from 'react';
+import { render } from '../../test-utils';
+import { TextAreaLabel } from './TextAreaLabel';
+import { AnimatedViewPropsType, AnimatedTextPropsType } from './types';
+
+describe('TextArea Label', () => {
+ let label: string | null;
+ let labelFixed: boolean;
+ let errorState: boolean;
+ let animatedViewProps: AnimatedViewPropsType;
+ let animatedTextProps: AnimatedTextPropsType;
+ let textAreaHeight: number;
+
+ beforeEach(() => {
+ label = 'Label';
+ labelFixed = false;
+ errorState = false;
+ animatedViewProps = {};
+ animatedTextProps = {};
+ textAreaHeight = 88;
+ });
+
+ test('should render null when not has a label', () => {
+ // given
+ label = null;
+
+ // when
+ const { toJSON } = render(
+ ,
+ );
+
+ // then
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ test('should render outlined label correctly', () => {
+ // when
+ const { toJSON } = render(
+ ,
+ );
+
+ // then
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ test('should render outlined label with optional correctly', () => {
+ // when
+ const { toJSON } = render(
+ ,
+ );
+
+ // then
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ test('should render fixed label correctly', () => {
+ // given
+ labelFixed = true;
+
+ // when
+ const { toJSON } = render(
+ ,
+ );
+
+ // then
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ test('should render fixed label with optional correctly', () => {
+ // given
+ labelFixed = true;
+
+ // when
+ const { toJSON } = render(
+ ,
+ );
+
+ // then
+ expect(toJSON()).toMatchSnapshot();
+ });
+});
diff --git a/src/components/TextArea/TextAreaLabel.tsx b/src/components/TextArea/TextAreaLabel.tsx
new file mode 100644
index 0000000..a7b6ccb
--- /dev/null
+++ b/src/components/TextArea/TextAreaLabel.tsx
@@ -0,0 +1,63 @@
+import React from 'react';
+import { Animated } from 'react-native';
+import Box from '../Box/Box';
+import Text from '../Text/Text';
+import { AnimatedTextPropsType, AnimatedViewPropsType } from './types';
+import { getLabelColor } from './utils';
+
+export const TextAreaLabel = React.memo(
+ ({
+ label,
+ labelFixed,
+ errorState,
+ animatedViewProps,
+ animatedTextProps,
+ }: {
+ label?: string | null;
+ labelFixed?: boolean;
+ errorState: boolean;
+ animatedViewProps: AnimatedViewPropsType;
+ animatedTextProps: AnimatedTextPropsType | any;
+ textAreaHeight: number;
+ }) => {
+ if (!label) {
+ return null;
+ }
+
+ const contentSecondaryColor = getLabelColor({ errorState });
+
+ const RenderFixedLabel = () => {
+ return (
+
+
+ {label}
+
+
+ );
+ };
+
+ const RenderOutlinedLabel = () => {
+ return (
+ <>
+
+
+
+ {label}
+
+
+
+ >
+ );
+ };
+
+ return labelFixed ? : ;
+ },
+);
diff --git a/src/components/TextArea/__snapshots__/TextArea.test.tsx.snap b/src/components/TextArea/__snapshots__/TextArea.test.tsx.snap
new file mode 100644
index 0000000..9aebdf1
--- /dev/null
+++ b/src/components/TextArea/__snapshots__/TextArea.test.tsx.snap
@@ -0,0 +1,1454 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`TextArea should render TextArea correctly 1`] = `
+
+
+
+
+ label
+
+
+
+
+
+
+
+
+
+
+
+ 0/200
+
+
+
+
+
+`;
+
+exports[`TextArea should render TextArea with error state and error text correctly 1`] = `
+
+
+
+
+ label
+
+
+
+
+
+
+
+
+
+
+ errorText
+
+
+
+
+
+
+ 0/200
+
+
+
+
+
+`;
+
+exports[`TextArea should render TextArea with error state correctly 1`] = `
+
+
+
+
+ label
+
+
+
+
+
+
+
+
+
+
+
+ 0/200
+
+
+
+
+
+`;
+
+exports[`TextArea should render TextArea with help text correctly 1`] = `
+
+
+
+
+ label
+
+
+
+
+
+
+
+
+
+
+ helpText
+
+
+
+
+
+
+ 0/200
+
+
+
+
+
+`;
+
+exports[`TextArea should render TextArea with large size correctly 1`] = `
+
+
+
+
+ label
+
+
+
+
+
+
+
+
+
+
+
+ 0/200
+
+
+
+
+
+`;
+
+exports[`TextArea should render TextArea with medium size correctly 1`] = `
+
+
+
+
+ label
+
+
+
+
+
+
+
+
+
+
+
+ 0/200
+
+
+
+
+
+`;
+
+exports[`TextArea should render TextArea with small size correctly 1`] = `
+
+
+
+
+ label
+
+
+
+
+
+
+
+
+
+
+
+ 0/200
+
+
+
+
+
+`;
+
+exports[`TextArea should render disabled TextArea correctly 1`] = `
+
+
+
+
+ label
+
+
+
+
+
+
+
+
+
+
+
+ 0/200
+
+
+
+
+
+`;
+
+exports[`TextArea should render label fixed TextArea correctly 1`] = `
+
+
+
+ label
+
+
+
+
+
+
+
+
+
+
+ 0/200
+
+
+
+
+
+`;
diff --git a/src/components/TextArea/__snapshots__/TextAreaCounterText.test.tsx.snap b/src/components/TextArea/__snapshots__/TextAreaCounterText.test.tsx.snap
new file mode 100644
index 0000000..d70e408
--- /dev/null
+++ b/src/components/TextArea/__snapshots__/TextAreaCounterText.test.tsx.snap
@@ -0,0 +1,61 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`TextArea Counter Text should render counter text correctly 1`] = `
+
+
+ 0/200
+
+
+`;
+
+exports[`TextArea Counter Text should render null when not has error text 1`] = `
+
+
+ 0/200
+
+
+`;
diff --git a/src/components/TextArea/__snapshots__/TextAreaHelpText.test.tsx.snap b/src/components/TextArea/__snapshots__/TextAreaHelpText.test.tsx.snap
new file mode 100644
index 0000000..2e72904
--- /dev/null
+++ b/src/components/TextArea/__snapshots__/TextAreaHelpText.test.tsx.snap
@@ -0,0 +1,34 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`TextArea Help Text should render help text correctly 1`] = `
+
+
+ help text
+
+
+`;
+
+exports[`TextArea Help Text should render null when not has help text 1`] = `null`;
+
+exports[`TextArea Help Text should render null when not has help text and error text 1`] = `null`;
diff --git a/src/components/TextArea/__snapshots__/TextAreaLabel.test.tsx.snap b/src/components/TextArea/__snapshots__/TextAreaLabel.test.tsx.snap
new file mode 100644
index 0000000..c3660c6
--- /dev/null
+++ b/src/components/TextArea/__snapshots__/TextAreaLabel.test.tsx.snap
@@ -0,0 +1,123 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`TextArea Label should render fixed label correctly 1`] = `
+
+
+ Label
+
+
+`;
+
+exports[`TextArea Label should render fixed label with optional correctly 1`] = `
+
+
+ Label
+
+
+`;
+
+exports[`TextArea Label should render null when not has a label 1`] = `null`;
+
+exports[`TextArea Label should render outlined label correctly 1`] = `
+
+
+
+ Label
+
+
+
+`;
+
+exports[`TextArea Label should render outlined label with optional correctly 1`] = `
+
+
+
+ Label
+
+
+
+`;
diff --git a/src/components/TextArea/hooks.test.ts b/src/components/TextArea/hooks.test.ts
new file mode 100644
index 0000000..67e82f0
--- /dev/null
+++ b/src/components/TextArea/hooks.test.ts
@@ -0,0 +1,247 @@
+import { Animated, Easing } from 'react-native';
+import theme from '../../theme';
+import { renderHook } from '../../test-utils';
+import { useInputRef, useInputValue, useOutlineLabelVisibility } from './hooks';
+
+describe('TextArea Hooks', () => {
+ test('useInputRef', () => {
+ // when
+ const { result } = renderHook(() => useInputRef());
+
+ // then
+ expect(result.current).toEqual({ current: null });
+ });
+
+ test('useInputValue value', () => {
+ // when
+ const { result } = renderHook(() =>
+ useInputValue({ value: 'value', defaultValue: undefined }),
+ );
+
+ // then
+ expect(result.current).toEqual({
+ isControlled: true,
+ setUncontrolledValue: expect.any(Function),
+ value: 'value',
+ });
+ });
+
+ test('useInputValue defaultValue', () => {
+ // when
+ const { result } = renderHook(() =>
+ useInputValue({ value: undefined, defaultValue: 'defaultValue' }),
+ );
+
+ // then
+ expect(result.current).toEqual({
+ isControlled: false,
+ setUncontrolledValue: expect.any(Function),
+ value: 'defaultValue',
+ });
+ });
+
+ test('useOutlineLabelVisibility has value and focused, disabled false', () => {
+ // when
+ const { result } = renderHook(() =>
+ useOutlineLabelVisibility({
+ theme,
+ easing: Easing.inOut(Easing.ease),
+ textAreaHeight: 88,
+ focused: false,
+ value: 'value',
+ disabled: false,
+ helpText: null,
+ errorText: null,
+ errorState: false,
+ }),
+ );
+
+ // then
+ expect(result.current.animatedViewProps).toEqual({
+ style: {
+ bottom: new Animated.Value(18),
+ height: 94,
+ left: 12,
+ position: 'absolute',
+ zIndex: 2,
+ },
+ });
+ expect(result.current.animatedTextProps).toEqual({
+ style: {
+ backgroundColor: theme.colors.neutralFull,
+ color: '#95A1B5',
+ fontFamily: 'Rubik-Regular',
+ fontSize: new Animated.Value(12),
+ height: new Animated.Value(14),
+ lineHeight: new Animated.Value(14),
+ paddingLeft: 4,
+ paddingRight: 4,
+ top: 8,
+ },
+ });
+ });
+
+ test('useOutlineLabelVisibility has value, focused and disabled', () => {
+ // when
+ const { result } = renderHook(() =>
+ useOutlineLabelVisibility({
+ theme,
+ easing: Easing.inOut(Easing.ease),
+ textAreaHeight: 88,
+ focused: true,
+ value: 'value',
+ disabled: true,
+ helpText: null,
+ errorText: null,
+ errorState: false,
+ }),
+ );
+
+ // then
+ expect(result.current.animatedViewProps).toEqual({
+ style: {
+ bottom: new Animated.Value(18),
+ height: 94,
+ left: 12,
+ position: 'absolute',
+ zIndex: 2,
+ },
+ });
+ expect(result.current.animatedTextProps).toEqual({
+ style: {
+ backgroundColor: 'transparent',
+ color: '#6E7787',
+ fontFamily: 'Rubik-Regular',
+ fontSize: new Animated.Value(12),
+ height: new Animated.Value(14),
+ lineHeight: new Animated.Value(14),
+ paddingLeft: 4,
+ paddingRight: 4,
+ top: 8,
+ },
+ });
+ });
+
+ test('useOutlineLabelVisibility has helpTextContent', () => {
+ // when
+ const { result } = renderHook(() =>
+ useOutlineLabelVisibility({
+ theme,
+ easing: Easing.inOut(Easing.ease),
+ textAreaHeight: 88,
+ focused: true,
+ value: 'value',
+ disabled: true,
+ helpText: 'helpText',
+ errorText: 'errorText',
+ errorState: false,
+ }),
+ );
+
+ // then
+ expect(result.current.animatedViewProps).toEqual({
+ style: {
+ bottom: new Animated.Value(18),
+ height: 112,
+ left: 12,
+ position: 'absolute',
+ zIndex: 2,
+ },
+ });
+ expect(result.current.animatedTextProps).toEqual({
+ style: {
+ backgroundColor: 'transparent',
+ color: '#6E7787',
+ fontFamily: 'Rubik-Regular',
+ fontSize: new Animated.Value(12),
+ height: new Animated.Value(14),
+ lineHeight: new Animated.Value(14),
+ paddingLeft: 4,
+ paddingRight: 4,
+ top: 8,
+ },
+ });
+ });
+
+ // test('useOutlineLabelVisibility has counterTextContent', () => {
+ // // when
+ // const { result } = renderHook(() =>
+ // useOutlineLabelVisibility({
+ // theme,
+ // easing: Easing.inOut(Easing.ease),
+ // textAreaHeight: 88,
+ // focused: true,
+ // value: 'value',
+ // disabled: true,
+ // helpText: 'helpText',
+ // counterText: 0,
+ // errorText: 'errorText',
+ // errorState: false,
+ // }),
+ // );
+
+ // // then
+ // expect(result.current.animatedViewProps).toEqual({
+ // style: {
+ // bottom: new Animated.Value(18),
+ // height: 112,
+ // left: 12,
+ // position: 'absolute',
+ // zIndex: 2,
+ // },
+ // });
+ // expect(result.current.animatedTextProps).toEqual({
+ // style: {
+ // backgroundColor: 'transparent',
+ // color: '#6E7787',
+ // fontFamily: 'Rubik-Regular',
+ // fontSize: new Animated.Value(12),
+ // height: new Animated.Value(14),
+ // lineHeight: new Animated.Value(14),
+ // paddingLeft: 4,
+ // paddingRight: 4,
+ // top: 8,
+ // },
+ // });
+ // });
+
+ test('useOutlineLabelVisibility has no value', () => {
+ // when
+ const { result } = renderHook(() =>
+ useOutlineLabelVisibility({
+ theme,
+ easing: Easing.inOut(Easing.ease),
+ textAreaHeight: 48,
+ focused: false,
+ disabled: false,
+ helpText: 'helpText',
+ errorText: 'errorText',
+ errorState: false,
+ }),
+ );
+
+ // then
+ expect(result.current.animatedViewProps).toEqual({
+ style: {
+ bottom: new Animated.Value(0),
+ height: 72,
+ left: 12,
+ position: 'absolute',
+ zIndex: 2,
+ },
+ });
+ expect(result.current.animatedTextProps).toEqual({
+ style: {
+ backgroundColor: theme.colors.neutralFull,
+ color: '#95A1B5',
+ fontFamily: 'Rubik-Regular',
+ fontSize: new Animated.Value(14),
+ height: new Animated.Value(16),
+ lineHeight: new Animated.Value(16),
+ paddingLeft: 4,
+ paddingRight: 4,
+ top: 8,
+ },
+ });
+ });
+});
diff --git a/src/components/TextArea/hooks.ts b/src/components/TextArea/hooks.ts
new file mode 100644
index 0000000..38a4513
--- /dev/null
+++ b/src/components/TextArea/hooks.ts
@@ -0,0 +1,194 @@
+import { useMemo, useRef, useState } from 'react';
+import { Animated, EasingFunction } from 'react-native';
+import { Theme } from '../../theme';
+import {
+ InputRefType,
+ CommonAnimatedPropsTypes,
+ AnimatedViewPropsType,
+ AnimatedTextPropsType,
+} from './types';
+import { getHelpText, getCounterText } from './utils';
+
+export const useInputRef: InputRefType = () => useRef(null);
+
+export const useInputValue = ({
+ value,
+ defaultValue,
+}: {
+ value?: string;
+ defaultValue?: string;
+}) => {
+ const isControlled = value !== undefined;
+ const validInputValue = isControlled ? value : defaultValue;
+ const [uncontrolledValue, setUncontrolledValue] = useState<
+ string | undefined
+ >(validInputValue);
+
+ return {
+ isControlled,
+ setUncontrolledValue,
+ value: isControlled ? value : uncontrolledValue,
+ };
+};
+
+export const useOutlineLabelVisibility = ({
+ theme,
+ easing,
+ textAreaHeight,
+ focused,
+ value,
+ disabled,
+ helpText,
+ counterText,
+ errorText,
+ errorState,
+}: {
+ theme: Theme;
+ easing: EasingFunction;
+ textAreaHeight: number;
+ focused: boolean;
+ value?: string;
+ disabled: boolean;
+ helpText?: string | null;
+ counterText?: number;
+ errorText?: string | null;
+ errorState: boolean;
+}) => {
+ const duration = 200;
+ const commonAnimatedProps: CommonAnimatedPropsTypes = {
+ duration,
+ useNativeDriver: false,
+ easing,
+ };
+
+ const labelFontSize: number = theme.textVariants.defaults.fontSize;
+ const labelLineHeightValue: number =
+ theme.textVariants.subtitle2Regular.fontSize;
+ const initialTopValue: number = 12;
+ const labelPositionEmptyValue = 0;
+ const labelPositionFillValue: number =
+ labelLineHeightValue / 2 + initialTopValue - 2;
+
+ const labelPositionRef = useRef(
+ new Animated.Value(
+ value ? labelPositionFillValue : labelPositionEmptyValue,
+ ),
+ ).current;
+ const fontSizeRef = useRef(
+ new Animated.Value(
+ value
+ ? theme.textVariants.subtitle4Regular.fontSize
+ : theme.textVariants.defaults.fontSize,
+ ),
+ ).current;
+ const lineHeightRef = useRef(
+ new Animated.Value(
+ value
+ ? theme.textVariants.defaults.fontSize
+ : theme.textVariants.subtitle2Regular.fontSize,
+ ),
+ ).current;
+
+ /* istanbul ignore next */
+ const startAnimation = () => {
+ Animated.parallel([
+ Animated.timing(labelPositionRef, {
+ toValue: labelPositionFillValue,
+ ...commonAnimatedProps,
+ }),
+ Animated.timing(fontSizeRef, {
+ toValue: labelFontSize - 2,
+ ...commonAnimatedProps,
+ }),
+ Animated.timing(lineHeightRef, {
+ toValue: labelLineHeightValue - 2,
+ ...commonAnimatedProps,
+ }),
+ ]).start();
+ };
+
+ /* istanbul ignore next */
+ const stopAnimation = () => {
+ Animated.parallel([
+ Animated.timing(labelPositionRef, {
+ toValue: labelPositionEmptyValue,
+ ...commonAnimatedProps,
+ }),
+ Animated.timing(fontSizeRef, {
+ toValue: labelFontSize,
+ ...commonAnimatedProps,
+ }),
+ Animated.timing(lineHeightRef, {
+ toValue: labelLineHeightValue,
+ ...commonAnimatedProps,
+ }),
+ ]).start();
+ };
+
+ let viewHeight = textAreaHeight + 6;
+ const helpTextContent = getHelpText({
+ helpText,
+ errorText,
+ errorState,
+ });
+
+ if (helpTextContent) {
+ viewHeight = textAreaHeight + 24;
+ }
+
+ const counterTextContent = getCounterText({
+ counterText,
+ errorText,
+ errorState,
+ });
+
+ if (counterTextContent) {
+ viewHeight = textAreaHeight + 24;
+ }
+
+ const animatedViewProps: AnimatedViewPropsType = useMemo(() => {
+ return {
+ style: {
+ position: 'absolute',
+ bottom: labelPositionRef,
+ left: theme.spacing.xs,
+ zIndex: 2,
+ height: viewHeight,
+ },
+ };
+ }, [labelPositionRef, theme.spacing, viewHeight]);
+
+ const animatedTextProps: AnimatedTextPropsType = useMemo(() => {
+ return {
+ style: {
+ fontFamily: theme.fonts.regular,
+ color: focused ? theme.colors.neutralDark : theme.colors.neutralLight,
+ backgroundColor: disabled ? 'transparent' : theme.colors.neutralFull,
+ height: lineHeightRef,
+ paddingLeft: theme.spacing['3xs'],
+ paddingRight: theme.spacing['3xs'],
+ top: initialTopValue - 4,
+ fontSize: fontSizeRef,
+ lineHeight: lineHeightRef,
+ },
+ };
+ }, [
+ disabled,
+ focused,
+ fontSizeRef,
+ initialTopValue,
+ lineHeightRef,
+ theme.colors.neutralDark,
+ theme.colors.neutralFull,
+ theme.colors.neutralLight,
+ theme.fonts,
+ theme.spacing,
+ ]);
+
+ return {
+ startAnimation,
+ stopAnimation,
+ animatedViewProps,
+ animatedTextProps,
+ };
+};
diff --git a/src/components/TextArea/types.ts b/src/components/TextArea/types.ts
new file mode 100644
index 0000000..0963b3d
--- /dev/null
+++ b/src/components/TextArea/types.ts
@@ -0,0 +1,16 @@
+import { EasingFunction, TextInput } from 'react-native';
+
+export type TextInputRefType = TextInput;
+
+export type InputRefType =
+ () => React.MutableRefObject;
+
+export type AnimatedViewPropsType = {};
+
+export type AnimatedTextPropsType = {};
+
+export type CommonAnimatedPropsTypes = {
+ duration: number;
+ useNativeDriver: boolean;
+ easing: EasingFunction;
+};
diff --git a/src/components/TextArea/utils.test.ts b/src/components/TextArea/utils.test.ts
new file mode 100644
index 0000000..ae29eb4
--- /dev/null
+++ b/src/components/TextArea/utils.test.ts
@@ -0,0 +1,335 @@
+import {
+ getPlaceholderText,
+ getHelpText,
+ getLabelColor,
+ getTextColor,
+ getBorderColor,
+} from './utils';
+
+describe('TextArea Utils', () => {
+ describe('getPlaceholderText', () => {
+ test('should return empty text when label and placeholder null', () => {
+ // given
+ const label = null;
+ const labelFixed = false;
+ const placeholder = null;
+ const focused = false;
+ const value = '';
+
+ // when
+ const result = getPlaceholderText({
+ label,
+ labelFixed,
+ placeholder,
+ value,
+ focused,
+ });
+
+ // then
+ expect(result).toBe('');
+ });
+
+ test('should return placeholder when label null and has placeholder', () => {
+ // given
+ const label = null;
+ const labelFixed = false;
+ const placeholder = 'placeholder';
+ const focused = false;
+ const value = '';
+
+ // when
+ const result = getPlaceholderText({
+ label,
+ labelFixed,
+ placeholder,
+ value,
+ focused,
+ });
+
+ // then
+ expect(result).toBe('placeholder');
+ });
+
+ test('should return empty text when placeholder null and has label', () => {
+ // given
+ const label = 'label';
+ const labelFixed = false;
+ const placeholder = null;
+ const focused = false;
+ const value = '';
+
+ // when
+ const result = getPlaceholderText({
+ label,
+ labelFixed,
+ placeholder,
+ value,
+ focused,
+ });
+
+ // then
+ expect(result).toBe('');
+ });
+
+ test('should return placeholder when has label, labelFixed and placeholder', () => {
+ // given
+ const label = 'label';
+ const labelFixed = true;
+ const placeholder = 'placeholder';
+ const focused = false;
+ const value = '';
+
+ // when
+ const result = getPlaceholderText({
+ label,
+ labelFixed,
+ placeholder,
+ value,
+ focused,
+ });
+
+ // then
+ expect(result).toBe('placeholder');
+ });
+
+ test('should return empty text when has label, placeholder and no value, focused', () => {
+ // given
+ const label = 'label';
+ const labelFixed = false;
+ const placeholder = 'placeholder';
+ const focused = false;
+ const value = '';
+
+ // when
+ const result = getPlaceholderText({
+ label,
+ labelFixed,
+ placeholder,
+ value,
+ focused,
+ });
+
+ // then
+ expect(result).toBe('');
+ });
+
+ test('should return empty text when has label, placeholder, value and focused', () => {
+ // given
+ const label = 'label';
+ const labelFixed = false;
+ const placeholder = 'placeholder';
+ const focused = true;
+ const value = 'value';
+
+ // whent
+ const result = getPlaceholderText({
+ label,
+ labelFixed,
+ placeholder,
+ value,
+ focused,
+ });
+
+ // then
+ expect(result).toBe('');
+ });
+
+ test('should return empty text when has label, placeholder and value', () => {
+ // given
+ const label = 'label';
+ const labelFixed = false;
+ const placeholder = 'placeholder';
+ const focused = false;
+ const value = 'value';
+
+ // whent
+ const result = getPlaceholderText({
+ label,
+ labelFixed,
+ placeholder,
+ value,
+ focused,
+ });
+
+ // then
+ expect(result).toBe('placeholder');
+ });
+
+ test('should return empty text when has label, placeholder and focused', () => {
+ // given
+ const label = 'label';
+ const labelFixed = false;
+ const placeholder = 'placeholder';
+ const focused = true;
+ const value = '';
+
+ // whent
+ const result = getPlaceholderText({
+ label,
+ labelFixed,
+ placeholder,
+ value,
+ focused,
+ });
+
+ // then
+ expect(result).toBe('placeholder');
+ });
+ });
+
+ describe('getHelpText', () => {
+ test('should return helpText when errorState false', () => {
+ // given
+ const helpText = 'helpText';
+ const errorText = null;
+ const errorState = false;
+
+ // when
+ const result = getHelpText({
+ helpText,
+ errorText,
+ errorState,
+ });
+
+ // then
+ expect(result).toBe('helpText');
+ });
+
+ test('should return helpText when errorState true and errorText null', () => {
+ // given
+ const helpText = 'helpText';
+ const errorText = null;
+ const errorState = true;
+
+ // when
+ const result = getHelpText({
+ helpText,
+ errorText,
+ errorState,
+ });
+
+ // then
+ expect(result).toBe('helpText');
+ });
+
+ test('should return errorText when errorState and errorText true', () => {
+ // given
+ const helpText = 'helpText';
+ const errorText = 'errorText';
+ const errorState = true;
+
+ // when
+ const result = getHelpText({
+ helpText,
+ errorText,
+ errorState,
+ });
+
+ // then
+ expect(result).toBe('errorText');
+ });
+ });
+
+ describe('getLabelColor', () => {
+ test('should return neutralDark when errorState false', () => {
+ // given
+ const errorState = false;
+
+ // when
+ const result = getLabelColor({
+ errorState,
+ });
+
+ // then
+ expect(result).toBe('neutralDark');
+ });
+
+ test('should return dangerKey when focused and errorState true', () => {
+ // given
+ const errorState = true;
+
+ // when
+ const result = getLabelColor({
+ errorState,
+ });
+
+ // then
+ expect(result).toBe('dangerKey');
+ });
+ });
+
+ describe('getTextColor', () => {
+ test('should return neutralLight when errorState false', () => {
+ // given
+ const errorState = false;
+
+ // when
+ const result = getTextColor({
+ errorState,
+ });
+
+ // then
+ expect(result).toBe('neutralLight');
+ });
+
+ test('should return dangerKey when focused and errorState true', () => {
+ // given
+ const errorState = true;
+
+ // when
+ const result = getTextColor({
+ errorState,
+ });
+
+ // then
+ expect(result).toBe('dangerKey');
+ });
+ });
+
+ describe('getBorderColor', () => {
+ test('should return borderColor when focused and errorState false', () => {
+ // given
+ const focused = false;
+ const errorState = false;
+
+ // when
+ const result = getBorderColor({
+ focused,
+ errorState,
+ });
+
+ // then
+ expect(result).toBe('neutralLighter');
+ });
+
+ test('should return dangerKey when focused and errorState true', () => {
+ // given
+ const focused = false;
+ const errorState = true;
+
+ // when
+ const result = getBorderColor({
+ focused,
+ errorState,
+ });
+
+ // then
+ expect(result).toBe('dangerKey');
+ });
+
+ test('should return primaryKey when focused true', () => {
+ // given
+ const focused = true;
+ const errorState = false;
+
+ // when
+ const result = getBorderColor({
+ focused,
+ errorState,
+ });
+
+ // then
+ expect(result).toBe('primaryKey');
+ });
+ });
+});
diff --git a/src/components/TextArea/utils.ts b/src/components/TextArea/utils.ts
new file mode 100644
index 0000000..9f1fd28
--- /dev/null
+++ b/src/components/TextArea/utils.ts
@@ -0,0 +1,95 @@
+export const getPlaceholderText = ({
+ label,
+ labelFixed,
+ placeholder,
+ value,
+ focused,
+}: {
+ label?: string | null;
+ labelFixed?: boolean;
+ placeholder?: string | null;
+ value?: string;
+ focused: boolean;
+}) => {
+ if (label && placeholder) {
+ if (labelFixed) {
+ return placeholder;
+ }
+
+ if (!value && !focused) {
+ return '';
+ }
+
+ if (value && focused) {
+ return '';
+ } else {
+ return placeholder;
+ }
+ }
+
+ if (!label && placeholder) {
+ return placeholder;
+ }
+
+ if (label && !placeholder) {
+ return '';
+ }
+
+ return '';
+};
+
+export const getHelpText = ({
+ helpText,
+ errorText,
+ errorState,
+}: {
+ helpText?: string | null;
+ errorText?: string | null;
+ errorState: boolean;
+}): string | null | undefined => {
+ if (errorState) {
+ if (errorText) {
+ return errorText;
+ } else {
+ return helpText;
+ }
+ }
+ return helpText;
+};
+
+export const getCounterText = ({
+ counterText,
+ errorText,
+ errorState,
+}: {
+ counterText?: number;
+ errorText?: string | null;
+ errorState: boolean;
+}): string | null | undefined | number => {
+ if (errorState) {
+ if (errorText) {
+ return errorText;
+ } else {
+ return counterText;
+ }
+ }
+ return counterText;
+};
+
+export const getLabelColor = ({ errorState }: { errorState: boolean }) => {
+ return errorState ? 'dangerKey' : 'neutralDark';
+};
+
+export const getTextColor = ({ errorState }: { errorState: boolean }) => {
+ return errorState ? 'dangerKey' : 'neutralLight';
+};
+
+export const getBorderColor = ({
+ focused,
+ errorState,
+}: {
+ focused: boolean;
+ errorState: boolean;
+}) => {
+ return focused ? 'primaryKey' : errorState ? 'dangerKey' : 'neutralLighter';
+};
diff --git a/src/index.ts b/src/index.ts
index 1e9ae26..34e44c6 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -13,6 +13,7 @@ export { default as Modal } from './components/Modal/Modal';
export { default as Spinner } from './components/Spinner/Spinner';
export { default as Switch } from './components/Switch/Switch';
export { default as Text } from './components/Text/Text';
+export { default as TextArea } from './components/TextArea/TextArea';
export { default as TextLink } from './components/TextLink/TextLink';
export { default as Toast } from './components/Toast/Toast';
diff --git a/src/theme.ts b/src/theme.ts
index 29110ec..9246389 100644
--- a/src/theme.ts
+++ b/src/theme.ts
@@ -729,6 +729,18 @@ const theme = createTheme({
xl: 40,
'2xl': 48,
},
+ textAreaSizeVariants: {
+ defaults: {},
+ small: {
+ height: 80,
+ },
+ medium: {
+ height: 88,
+ },
+ large: {
+ height: 104,
+ },
+ },
});
export type Theme = typeof theme;