Skip to content

Add Textarea component #1812

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 19 commits into from
Feb 1, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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