Skip to content

Commit

Permalink
Add Textarea component (#1812)
Browse files Browse the repository at this point in the history
* feat: add TextArea component

* chore: fix lint errors

* fix: typos and content

* chore: update docs

* fix: tests

* chore: remove width and height

* chore: fix typo in docs

* docs: add ref row to TextArea

* chore: fix typo in docs

* chore: remove defaultValue

* feat: refactor TextInputWrapper to separate base styles and reuse in textarea

* fix: tests

* feat: rename TextArea to Textarea

* fix: move not-allowed to base wrapper

* fix: rename file properly
  • Loading branch information
rezrah committed Feb 1, 2022
1 parent 6a695bd commit 97bf7c6
Show file tree
Hide file tree
Showing 16 changed files with 1,740 additions and 427 deletions.
5 changes: 5 additions & 0 deletions .changeset/happy-books-attack.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@primer/react': minor
---

Add new Textarea component
187 changes: 187 additions & 0 deletions docs/content/Textarea.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
---
componentId: textarea
title: Textarea
description: Use Textarea for multi-line text input form fields
status: Alpha
source: https://github.com/primer/react/blob/main/src/Textarea.tsx
storybook: /react/storybook?path=/story/forms-textarea--default
---

import {Textarea} from '@primer/react'

<Box sx={{border: '1px solid', borderColor: 'border.default', borderRadius: 2, padding: 6, marginBottom: 3}}>
<Textarea />
</Box>

```js
import {Textarea} from '@primer/react'
```

## Examples

<Note variant="warning">

Textarea components **must always** be accompanied by a corresponding label to improve support for assistive
technologies. Examples below are provided for conciseness and may not reflect accessibility best practices.

</Note>

### Controlled mode

```javascript live noinline
const TextareaExample = () => {
// Running in controlled mode (recommended)
const [value, setValue] = React.useState('')

const handleChange = event => {
setValue(event.target.value)
}

return <Textarea placeholder="Enter a description" onChange={handleChange} value={value} />
}

render(<TextareaExample />)
```

### Uncontrolled mode

```javascript live noinline
const TextareaExample = () => {
const ref = React.useRef()

const handleSubmit = event => {
event.preventDefault()
if (!ref.current.value) {
alert(`Enter a value into the Textarea and press submit`)
return
}

alert(`Current Textarea value: ${ref.current.value}`)
}

return (
<form onSubmit={handleSubmit}>
<Textarea
cols={40}
rows={8}
ref={ref}
defaultValue="Set the initial state in uncontrolled mode using the defaultValue prop"
/>
<br />
<Button type="submit">Submit</Button>
</form>
)
}

render(<TextareaExample />)
```

### Displaying form validation state

```jsx live
<>
<Box as="label" htmlFor="success-state" sx={{display: 'block'}}>
Success state:
</Box>
<Textarea id="success-state" validationStatus="success" />
<Box as="label" htmlFor="error-state" sx={{display: 'block', mt: 5}}>
Error state:
</Box>
<Textarea id="error-state" validationStatus="error" />
</>
```

### Inactive

```jsx live
<>
<Textarea disabled>This text is inactive</Textarea>
</>
```

### Resize

By default, `Textarea` can be resized by the user vertically and horizontally. Resizing can be prevented by setting `resize` to `none`

```jsx live
<Textarea cols={40} resize="none" />
```

### Custom styling

```jsx live
<>
<Textarea sx={{marginBottom: 2}} />
<p>Custom styles like `margin` and `padding` can be applied using the `sx` prop</p>
</>
```

## Props

### Textarea

<PropsTable>
<PropsTableRow
name="required"
type="boolean"
description="Indicates to the user and assistive technologies that the field value is required"
/>
<PropsTableRow
name="cols"
type="number"
description="Specifies the visible width of a text area."
/>
<PropsTableRow
name="rows"
type="number"
description="Specifies the visible height of a text area."
/>
<PropsTableRow
name="block"
type="boolean"
defaultValue="false"
description="Expands with width of the component to fill the parent elements"
/>
<PropsTableRow
name="resize"
type="'both' | 'horizontal' | 'vertical' | 'none'"
defaultValue="'both'"
description="Changes background color to a higher contrast color"
/>
<PropsTableRow name="validationStatus"
type="'success' | 'error' | undefined"
description="Style the textarea to match the current form validation status"
/>
<PropsTableRefRow
elementType="textarea"
refType="HTMLTextAreaElement"
/>
<PropsTablePassthroughPropsRow
elementName="textarea"
passthroughPropsLink={
<Link href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea#attributes">MDN</Link>
}
/>

</PropsTable>

## Status

<ComponentChecklist
items={{
propsDocumented: true,
noUnnecessaryDeps: true,
adaptsToThemes: true,
adaptsToScreenSizes: true,
fullTestCoverage: true,
usedInProduction: false,
usageExamplesDocumented: true,
hasStorybookStories: true,
designReviewed: false,
a11yReviewed: false,
stableApi: false,
addressedApiFeedback: false,
hasDesignGuidelines: false,
hasFigmaComponent: false
}}
/>
2 changes: 2 additions & 0 deletions docs/src/@primer/gatsby-theme-doctocat/nav.yml
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@
url: /SubNav
- title: TabNav
url: /TabNav
- title: Textarea
url: /Textarea
- title: Text
url: /Text
- title: TextInput
Expand Down
3 changes: 2 additions & 1 deletion src/Checkbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import styled from 'styled-components'
import {useProvidedRefOrCreate} from './hooks'
import React, {InputHTMLAttributes, ReactElement, useLayoutEffect} from 'react'
import sx, {SxProp} from './sx'
import {FormValidationStatus} from './utils/types/FormValidationStatus'

export type CheckboxProps = {
/**
Expand All @@ -24,7 +25,7 @@ export type CheckboxProps = {
/**
* Indicates whether the checkbox validation state
*/
validationStatus?: 'error' | 'success' // TODO: hoist to Validation typings
validationStatus?: FormValidationStatus
} & InputHTMLAttributes<HTMLInputElement> &
SxProp

Expand Down
3 changes: 2 additions & 1 deletion src/Radio.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import styled from 'styled-components'
import React, {InputHTMLAttributes, ReactElement} from 'react'
import sx, {SxProp} from './sx'
import {FormValidationStatus} from './utils/types/FormValidationStatus'

export type RadioProps = {
/**
Expand Down Expand Up @@ -31,7 +32,7 @@ export type RadioProps = {
/**
* Indicates whether the radio button validation state is non-standard
*/
validationStatus?: 'error' | 'success' // TODO: hoist to Validation typings
validationStatus?: FormValidationStatus
} & InputHTMLAttributes<HTMLInputElement> &
SxProp

Expand Down
101 changes: 101 additions & 0 deletions src/Textarea.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import styled, {css} from 'styled-components'
import React, {TextareaHTMLAttributes, ReactElement} from 'react'
import {TextInputBaseWrapper} from './_TextInputWrapper'
import {FormValidationStatus} from './utils/types/FormValidationStatus'
import sx, {SxProp} from './sx'

export type TextareaProps = {
/**
* Apply inactive visual appearance to the Textarea
*/
disabled?: boolean
/**
* Indicates whether the Textarea is a required form field
*/
required?: boolean
/**
* Indicates whether the Textarea validation state
*/
validationStatus?: FormValidationStatus
/**
* Block
*/
block?: boolean
/**
* Allows resizing of the textarea
*/
resize?: 'none' | 'both' | 'horizontal' | 'vertical'
} & TextareaHTMLAttributes<HTMLTextAreaElement> &
SxProp

const StyledTextarea = styled.textarea<TextareaProps>`
border: 0;
font-size: inherit;
font-family: inherit;
background-color: transparent;
-webkit-appearance: none;
color: inherit;
width: 100%;
resize: both;
&:focus {
outline: 0;
}
${props =>
props.resize &&
css`
resize: ${props.resize};
`}
${props =>
props.disabled &&
css`
resize: none;
`}
${sx};
`

/**
* An accessible, native textarea component that supports validation states.
* This component accepts all native HTML <textarea> attributes as props.
*/
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
(
{
value,
disabled,
sx: sxProp,
required,
validationStatus,
rows = 7,
cols = 30,
resize = 'both',
block,
...rest
}: TextareaProps,
ref
): ReactElement => {
return (
<TextInputBaseWrapper sx={sxProp} validationStatus={validationStatus} disabled={disabled} block={block}>
<StyledTextarea
value={value}
resize={resize}
required={required}
aria-required={required ? 'true' : 'false'}
aria-invalid={validationStatus === 'error' ? 'true' : 'false'}
ref={ref}
disabled={disabled}
rows={rows}
cols={cols}
{...rest}
/>
</TextInputBaseWrapper>
)
}
)

Textarea.displayName = 'Textarea'

export default Textarea
6 changes: 4 additions & 2 deletions src/_InputValidation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@ interface Props {

const validationIconMap: Record<NonNullable<Props['validationStatus']>, React.ComponentType<IconProps>> = {
success: CheckCircleFillIcon,
error: AlertFillIcon
error: AlertFillIcon,
warning: AlertFillIcon
}

const validationColorMap: Record<NonNullable<Props['validationStatus']>, string> = {
success: 'success.fg',
error: 'danger.fg'
error: 'danger.fg',
warning: 'attention.fg'
}

const InputValidation: React.FC<Props> = ({children, id, validationStatus}) => {
Expand Down
Loading

0 comments on commit 97bf7c6

Please sign in to comment.