Skip to content

Commit

Permalink
feat(TextAreaE): Create component
Browse files Browse the repository at this point in the history
  • Loading branch information
thyhjwb6 committed Jul 1, 2021
1 parent 857dbfb commit 65bd360
Show file tree
Hide file tree
Showing 9 changed files with 396 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React from 'react'
import { Story, Meta } from '@storybook/react/types-6-0'

import { TextAreaE, TextAreaEProps } from '.'

export default {
component: TextAreaE,
title: 'Text Area (Experimental)',
parameters: {
actions: { argTypesRegex: '^on.*' },
},
} as Meta

export const Default: Story<TextAreaEProps> = (props) => (
<TextAreaE {...props} />
)

Default.args = {
name: 'textarea-default',
label: 'Example label',
}

export const Disabled: Story<TextAreaEProps> = (props) => (
<TextAreaE {...props} isDisabled label="Example label" />
)

Disabled.storyName = 'Disabled'

export const WithError: Story<TextAreaEProps> = (props) => (
<TextAreaE {...props} isInvalid label="Example label" />
)

WithError.storyName = 'With error'
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
import React from 'react'
import '@testing-library/jest-dom/extend-expect'
import { render, RenderResult } from '@testing-library/react'
import userEvent from '@testing-library/user-event'

import { FieldProps } from '../../common/FieldProps'
import { FormProps } from '../../common/FormProps'
import { TextAreaE } from '.'
import { withFormik } from '../../enhancers/withFormik'

describe('TextArea', () => {
let onBlurSpy: (event: React.FormEvent) => void
let onChangeSpy: (event: React.ChangeEvent<HTMLTextAreaElement>) => void
let wrapper: RenderResult

describe('all props', () => {
beforeEach(() => {
onBlurSpy = jest.fn()
onChangeSpy = jest.fn()

wrapper = render(
<TextAreaE
className="rn-textarea--modifier"
id="id"
label="label"
name="name"
onBlur={onBlurSpy}
onChange={onChangeSpy}
value="ab"
data-arbitrary="123"
/>
)
})

it('adds the CSS modifier', () => {
expect(wrapper.getByTestId('textarea-container').classList).toContain(
'rn-textarea--modifier'
)
})

it('sets the `id` on the input', () => {
expect(wrapper.getByTestId('textarea-input')).toHaveAttribute('id', 'id')
})

it('renders the label', () => {
expect(wrapper.getByText('label')).toBeInTheDocument()
})

it('should associate the label to the field with the custom id', () => {
expect(wrapper.getByTestId('textarea-label')).toHaveAttribute('for', 'id')
})

it('sets the `name` on the input', () => {
expect(wrapper.getByTestId('textarea-input')).toHaveAttribute(
'name',
'name'
)
})

it('sets the `value` on the input', () => {
expect(wrapper.getByTestId('textarea-input')).toHaveValue('ab')
})

it('drills arbitrary props to the input', () => {
expect(wrapper.getByTestId('textarea-input')).toHaveAttribute(
'data-arbitrary',
'123'
)
})

describe('when the text area loses focus', () => {
beforeEach(() => {
wrapper.getByTestId('textarea-input').focus()
wrapper.getByTestId('textarea-input').blur()
})

it('should call the onBlur callback once', () => {
expect(onBlurSpy).toHaveBeenCalledTimes(1)
})
})

describe('when the value is changed', () => {
beforeEach(async () => {
await userEvent.type(
wrapper.getByTestId('textarea-input'),
'{backspace}'
)
})

it('should update the value', () => {
expect(wrapper.getByTestId('textarea-input')).toHaveValue('a')
})
})

describe('when the value is deleted', () => {
beforeEach(async () => {
await userEvent.type(
wrapper.getByTestId('textarea-input'),
'{backspace}{backspace}'
)
})

it('should remove the `has-content` to the container', () => {
expect(
wrapper.getByTestId('textarea-container').classList
).not.toContain('has-content')
})

it('should update the value', () => {
expect(wrapper.getByTestId('textarea-input')).toHaveValue('')
})
})
})

describe('when a Formik enhanced field has an error', () => {
let field: FieldProps
let form: FormProps

describe('and the form has not been touched', () => {
beforeEach(() => {
field = {
name: 'colour',
value: '',
onBlur: null,
onChange: jest.fn(),
}
form = {
errors: {
colour: 'Something bad',
},
touched: {},
}

const FormikTextArea = withFormik(TextAreaE)

wrapper = render(<FormikTextArea field={field} form={form} />)
})

it('should add the aria attributes', () => {
expect(wrapper.getByTestId('textarea-input')).not.toHaveAttribute(
'aria-invalid'
)
expect(wrapper.getByTestId('textarea-input')).not.toHaveAttribute(
'aria-describedby'
)
})

it('should not indicate the field has an error', () => {
expect(wrapper.queryByTestId('textarea-container')).not.toHaveClass(
'is-invalid'
)
})

it('should not show the error', () => {
expect(wrapper.queryAllByText('Something bad')).toHaveLength(0)
})
})

describe('and the form has been touched', () => {
beforeEach(() => {
field = {
name: 'colour',
value: '',
onBlur: null,
onChange: jest.fn(),
}
form = {
errors: {
colour: 'Something bad',
},
touched: {
colour: true,
},
}

const FormikTextArea = withFormik(TextAreaE)

wrapper = render(<FormikTextArea field={field} form={form} />)
})

it('should add the aria attributes', () => {
expect(wrapper.getByTestId('textarea-input')).toHaveAttribute(
'aria-invalid'
)
expect(wrapper.getByTestId('textarea-input')).toHaveAttribute(
'aria-describedby'
)
})

it('should indicate the field has an error', () => {
expect(wrapper.queryByTestId('textarea-container')).toHaveClass(
'is-invalid'
)
})

it('should show the error', () => {
expect(wrapper.getByText('Something bad')).toBeInTheDocument()
})
})
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import React, { TextareaHTMLAttributes } from 'react'

import { ComponentWithClass } from '../../common/ComponentWithClass'
import { StyledTextArea } from './partials/StyledTextArea'
import { StyledOuterWrapper } from './partials/StyledOuterWrapper'
import { StyledLabel } from './partials/StyledLabel'
import { StyledInput } from './partials/StyledInput'
import { getId } from '../../helpers'
import { InputValidationProps } from '../../common/InputValidationProps'
import { useFocus } from '../../hooks/useFocus'
import { useInputValue } from '../../hooks/useInputValue'

export interface TextAreaEProps
extends TextareaHTMLAttributes<HTMLTextAreaElement>,
ComponentWithClass,
InputValidationProps {
/**
* Toggles whether the component is disabled or not (preventing user interaction).
*/
isDisabled?: boolean
/**
* Label to display within the component.
*/
label: string
/**
* Optional handler invoked when the `onBlur` event is emitted.
*/
onBlur?: (event: React.FormEvent) => void
/**
* Optional HTML `value` attribute to apply to the component.
*/
value?: string
}

export const TextAreaE: React.FC<TextAreaEProps> = (props) => {
const {
className,
isDisabled,
isInvalid,
id = getId('text-area'),
label,
onBlur,
onChange,
value,
...rest
} = props

const { hasFocus, onLocalBlur, onLocalFocus } = useFocus(onBlur)
const { committedValue, hasValue, onValueChange } = useInputValue(value)

return (
<StyledTextArea className={className} data-testid="textarea-container">
<StyledOuterWrapper
$hasFocus={hasFocus}
$isDisabled={isDisabled}
$isInvalid={isInvalid}
>
<StyledLabel
$hasContent={hasValue}
$hasFocus={hasFocus}
data-testid="textarea-label"
htmlFor={id}
>
{label}
</StyledLabel>
<StyledInput
data-testid="textarea-input"
disabled={isDisabled}
id={id}
onBlur={onLocalBlur}
onChange={(e) => {
onValueChange(e)
if (onChange) {
onChange(e)
}
}}
onFocus={onLocalFocus}
value={committedValue}
{...rest}
/>
</StyledOuterWrapper>
</StyledTextArea>
)
}

TextAreaE.displayName = 'TextAreaE'
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './TextAreaE'
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import styled from 'styled-components'
import { selectors } from '@royalnavy/design-tokens'

const { color, fontSize } = selectors

export const StyledInput = styled.textarea`
display: block;
box-sizing: border-box;
width: 100%;
margin: 0;
border: 0;
background: none;
color: ${color('neutral', '600')};
font-size: ${fontSize('m')};
resize: vertical;
overflow: auto;
min-height: 80px;
-webkit-tap-highlight-color: transparent;
&:focus {
outline: 0;
}
`
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import styled, { css } from 'styled-components'
import { selectors } from '@royalnavy/design-tokens'

import { isIE11 } from '../../../helpers'
import {
StyledLabel as StyledLabelBase,
StyledLabelProps,
} from '../../../styled-components/partials/StyledLabel'

const { color, spacing } = selectors

export const StyledLabel = styled(StyledLabelBase)<StyledLabelProps>`
padding-bottom: ${spacing('2')};
pointer-events: none;
background-color: ${color('neutral', 'white')};
border-radius: 3px 3px 0 0;
transform: translate(10px, ${isIE11() ? '9px' : '11px'}) scale(1);
${({ $hasFocus, $hasContent }) =>
($hasFocus || $hasContent) &&
css`
transform: translate(10px, ${isIE11() ? '6px' : '4px'}) scale(0.75);
`}
`
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import styled from 'styled-components'

import { isIE11 } from '../../../helpers'
import {
StyledOuterWrapper as StyledOuterWrapperBase,
StyledOuterWrapperProps,
} from '../../../styled-components'

export const StyledOuterWrapper = styled(
StyledOuterWrapperBase
)<StyledOuterWrapperProps>`
position: relative;
padding: ${isIE11() ? '21px' : '19px'} 8px 8px 8px;
`
Loading

0 comments on commit 65bd360

Please sign in to comment.