-
Notifications
You must be signed in to change notification settings - Fork 32
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
9 changed files
with
396 additions
and
0 deletions.
There are no files selected for viewing
33 changes: 33 additions & 0 deletions
33
packages/react-component-library/src/components/TextAreaE/TextAreaE.stories.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' |
201 changes: 201 additions & 0 deletions
201
packages/react-component-library/src/components/TextAreaE/TextAreaE.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
}) | ||
}) | ||
}) | ||
}) |
86 changes: 86 additions & 0 deletions
86
packages/react-component-library/src/components/TextAreaE/TextAreaE.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' |
1 change: 1 addition & 0 deletions
1
packages/react-component-library/src/components/TextAreaE/index.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './TextAreaE' |
24 changes: 24 additions & 0 deletions
24
packages/react-component-library/src/components/TextAreaE/partials/StyledInput.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
` |
25 changes: 25 additions & 0 deletions
25
packages/react-component-library/src/components/TextAreaE/partials/StyledLabel.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
`} | ||
` |
14 changes: 14 additions & 0 deletions
14
packages/react-component-library/src/components/TextAreaE/partials/StyledOuterWrapper.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
` |
Oops, something went wrong.