Skip to content

Commit

Permalink
feat(textarea): add textarea component
Browse files Browse the repository at this point in the history
  • Loading branch information
RobinWijnant committed Aug 17, 2020
1 parent 2ded6bc commit 1582e16
Show file tree
Hide file tree
Showing 8 changed files with 249 additions and 2 deletions.
2 changes: 1 addition & 1 deletion src/components/Input/Input.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ A Input can be used to let the user insert text using key strokes. A crucial for

## Best practices

- Inputs should be wrapped in a FormField or should be preceded by another component serving as a label // TODO Link to FormField
- Inputs should be wrapped in a [FormField](/form-field) or should be preceded by another component serving as a label

## Examples

Expand Down
2 changes: 1 addition & 1 deletion src/components/Input/Input.module.css
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
.input {
width: inherit;
background-color: var(--color-neutral-1);
border-radius: var(--border-radius-small);
border: 1px solid var(--color-neutral-4);
padding: 4.5px 14px;
width: 100%;
color: var(--color-neutral-9);
}

.input::placeholder {
Expand Down
40 changes: 40 additions & 0 deletions src/components/TextArea/TextArea.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
---
name: TextArea
menu: Components
route: /text-area
---

import { Playground, Props } from 'docz';
import { TextArea } from '../../index.ts'

# TextArea

A Input can be used to let the user insert text using key strokes. A crucial form item for building interactive apps.

## Best practices

- TextAreas should be wrapped in a [FormField](/form-field) or should be preceded by another component serving as a label

## Examples

### Basic usage

<Playground>
<TextArea name="description" />
</Playground>

### Listening for changes

<Playground>
<TextArea name="changeLogger" onChange={(event) => console.log(`LOGGER: ${event.target.value}`)} />
</Playground>

### Input with error

<Playground>
<TextArea name="invalidTextArea" isInvalid />
</Playground>

## API

<Props of={TextArea} />
26 changes: 26 additions & 0 deletions src/components/TextArea/TextArea.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
.textarea {
background-color: var(--color-neutral-1);
border-radius: var(--border-radius-small);
border: 1px solid var(--color-neutral-4);
padding: 4.5px 14px;
width: 100%;
color: var(--color-neutral-9);
resize: vertical;
}

.textarea::placeholder {
color: var(--color-neutral-6);
}

.textarea:focus {
outline: none;
box-shadow: 0 0 0 3px var(--color-focus);
}

.containsError {
border-color: var(--color-error);
}

.containsError:focus {
box-shadow: 0 0 0 3px var(--color-error-light);
}
103 changes: 103 additions & 0 deletions src/components/TextArea/TextArea.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import React, { ComponentProps } from 'react';
import { fireEvent, render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

import Input from './TextArea';

describe('TextArea', () => {
const value = 'It was nice!';
const defaultButtonProps: ComponentProps<typeof Input> = {
name: 'comment',
};

test('default snapshot', () => {
const component = <Input {...defaultButtonProps} />;
const { asFragment } = render(component);
expect(asFragment()).toMatchSnapshot();
});

test('value prop', () => {
const component = <Input {...defaultButtonProps} value={value} onChange={() => {}} />;
const { getByTestId } = render(component);
const inputElement = getByTestId('textarea-comment');

expect(inputElement.innerHTML).toBe(value);
});

test('onChange prop', async () => {
const onChangeFn = jest.fn();
const component = <Input {...defaultButtonProps} onChange={onChangeFn} />;
const { getByTestId } = render(component);
const inputElement = getByTestId('textarea-comment');

await userEvent.type(inputElement, value);

expect(onChangeFn).toHaveBeenCalledTimes(value.length);
expect((inputElement as any).value).toBe(value);
});

test('onBlur prop', async () => {
const onBlurFn = jest.fn();
const component = <Input {...defaultButtonProps} onBlur={onBlurFn} />;
const { getByTestId } = render(component);
const inputElement = getByTestId('textarea-comment');

fireEvent.blur(inputElement);

expect(onBlurFn).toHaveBeenCalledTimes(1);
});

test('placeholder prop', () => {
const component = <Input {...defaultButtonProps} placeholder="First name" />;
const { getByTestId } = render(component);
const inputElement = getByTestId('textarea-comment');

expect(inputElement.getAttribute('placeholder')).toBe('First name');
});

test('errorMessage prop', () => {
const component = <Input {...defaultButtonProps} isInvalid />;
const { getByTestId } = render(component);
const inputElement = getByTestId('textarea-comment');

expect(inputElement.classList).toContain('containsError');
});

test('spellCheck prop', () => {
const component = <Input {...defaultButtonProps} spellCheck />;
const { getByTestId } = render(component);
const inputElement = getByTestId('textarea-comment');

expect(inputElement.hasAttribute('spellcheck')).toBe(true);
expect(inputElement.getAttribute('spellcheck')).not.toBe(false);
});

test('autoComplete prop', () => {
const component = <Input {...defaultButtonProps} autoComplete />;
const { getByTestId } = render(component);
const inputElement = getByTestId('textarea-comment');

expect(inputElement.getAttribute('autocomplete')).toBe('on');
});

test('maxLength prop', () => {
const maxLength = 3;
const component = <Input {...defaultButtonProps} maxLength={3} />;
const { getByTestId } = render(component);
const inputElement = getByTestId('textarea-comment');

expect(inputElement.getAttribute('maxlength')).toBe(maxLength.toString());
});

test('className prop', () => {
const className = 'center';
const component = <Input {...defaultButtonProps} className={className} />;
const { getByTestId } = render(component);
const containerElement = getByTestId('textarea-comment');

const renderedClassNames = containerElement.className.split(' ');
expect(renderedClassNames).toContain(className);
// className in prop should be the last in the row
expect(renderedClassNames.indexOf(className)).toBe(renderedClassNames.length - 1);
});
});
64 changes: 64 additions & 0 deletions src/components/TextArea/TextArea.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import React, { ChangeEventHandler, FocusEventHandler } from 'react';
import classNames from 'classnames';

import cssReset from '../../css-reset.module.css';
import styles from './TextArea.module.css';

interface Props {
name: string;
value?: string;
onChange?: ChangeEventHandler<HTMLTextAreaElement>;
onBlur?: FocusEventHandler<HTMLTextAreaElement>;
placeholder?: string;
errorMessage?: string;
spellCheck?: boolean;
autoComplete?: boolean;
maxLength?: number;
className?: string;
isInvalid?: boolean;
}

const TextArea = React.forwardRef<HTMLTextAreaElement, Props>(
(
{
name,
value,
onChange,
onBlur,
placeholder,
isInvalid = false,
spellCheck = false,
autoComplete = false,
maxLength,
className,
}: Props,
ref,
) => {
const textareaClassNames = classNames(
cssReset.ventura,
styles.textarea,
{
[styles.containsError]: Boolean(isInvalid),
},
className,
);

return (
<textarea
name={name}
value={value}
onChange={onChange}
placeholder={placeholder}
spellCheck={spellCheck}
autoComplete={autoComplete ? 'on' : 'off'}
maxLength={maxLength}
ref={ref}
className={textareaClassNames}
data-testid={`textarea-${name}`}
onBlur={onBlur}
/>
);
},
);

export default TextArea;
13 changes: 13 additions & 0 deletions src/components/TextArea/__snapshots__/TextArea.test.tsx.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`TextArea default snapshot 1`] = `
<DocumentFragment>
<textarea
autocomplete="off"
class="ventura textarea"
data-testid="textarea-comment"
name="comment"
spellcheck="false"
/>
</DocumentFragment>
`;
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ export { default as Input } from './components/Input/Input';
export { default as Modal } from './components/Modal/Modal';
export { default as Radio } from './components/Radio/Radio';
export { default as Table } from './components/Table/Table';
export { default as TextArea } from './components/TextArea/TextArea';

export * from 'react-feather';

0 comments on commit 1582e16

Please sign in to comment.