Skip to content
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

feat: Add TextArea Component #1095

Merged
merged 18 commits into from
Mar 6, 2024
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
1 change: 1 addition & 0 deletions packages/css/src/components/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

/* Append here */
@import "./tabs/tabs";
@import "./text-area/text-area";
@import "./column/column";
@import "./margin/margin";
@import "./gap/gap";
Expand Down
17 changes: 17 additions & 0 deletions packages/css/src/components/text-area/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<!-- @license CC0-1.0 -->

# TextArea
alimpens marked this conversation as resolved.
Show resolved Hide resolved

A form field that allows the user to input text over multiple lines.

## Guidelines

- Use a TextArea when users need to enter more than 1 sentence of text, such as a comment or description.
- The height of the TextArea should be appropriate for the information to be entered.
- A TextArea must have a label, and in most cases, this label should be visible.
- Use `spellcheck="false"` for fields that may contain sensitive information, such as passwords and personal data.
Some browser extensions for spell-checking send this information to external servers.

## Accessibility

- [WCAG 2.1](https://www.w3.org/WAI/WCAG21/Techniques/html/H91.html) requires that the TextArea has a label or title attribute.
70 changes: 70 additions & 0 deletions packages/css/src/components/text-area/text-area.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/**
* @license EUPL-1.2+
* Copyright Gemeente Amsterdam
*/

@mixin reset {
box-sizing: border-box;
margin-block: 0;
-webkit-text-size-adjust: 100%;
}

.amsterdam-text-area {
border: none;
box-shadow: var(--amsterdam-text-area-box-shadow);
color: var(--amsterdam-text-area-color);
font-family: var(--amsterdam-text-area-font-family);
font-size: var(--amsterdam-text-area-font-size);
font-weight: var(--amsterdam-text-area-font-weight);
line-height: var(--amsterdam-text-area-line-height);
max-width: 100%;
min-height: var(--amsterdam-text-area-min-height);
alimpens marked this conversation as resolved.
Show resolved Hide resolved
outline-offset: var(--amsterdam-text-area-outline-offset);
padding-block: var(--amsterdam-text-area-padding-block);
padding-inline: var(--amsterdam-text-area-padding-inline);
touch-action: manipulation;
width: 100%;

@include reset;

&:hover {
box-shadow: var(--amsterdam-text-area-hover-box-shadow);
}
}

.amsterdam-text-area::placeholder {
color: var(--amsterdam-text-area-placeholder-color);
}

.amsterdam-text-area:disabled {
background-color: var(--amsterdam-text-area-disabled-background-color);
box-shadow: var(--amsterdam-text-area-disabled-box-shadow);
color: var(--amsterdam-text-area-disabled-color);
cursor: var(--amsterdam-text-area-disabled-cursor);
}

.amsterdam-text-area:invalid,
.amsterdam-text-area[aria-invalid="true"] {
box-shadow: var(--amsterdam-text-area-invalid-box-shadow);

&:hover {
// TODO: this should be the (currently non-existent) dark red hover color
box-shadow: var(--amsterdam-text-area-invalid-hover-box-shadow);
}
}

.amsterdam-text-area--resize-none {
resize: none;
}

.amsterdam-text-area--resize-horizontal {
resize: inline;
}

.amsterdam-text-area--resize-vertical {
resize: block;
}

.amsterdam-text-area--cols {
width: auto;
}
5 changes: 5 additions & 0 deletions packages/react/src/TextArea/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<!-- @license CC0-1.0 -->

# React TextArea component
alimpens marked this conversation as resolved.
Show resolved Hide resolved

[TextArea documentation](../../../css/src/components/text-area/README.md)
106 changes: 106 additions & 0 deletions packages/react/src/TextArea/TextArea.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { createRef, useState } from 'react'
import { TextArea } from './TextArea'
import '@testing-library/jest-dom'

describe('Text area', () => {
it('renders', () => {
render(<TextArea />)

const component = screen.getByRole('textbox')

expect(component).toBeInTheDocument()
expect(component).toBeVisible()
})

it('renders a design system BEM class name', () => {
render(<TextArea />)

const component = screen.getByRole('textbox')

expect(component).toHaveClass('amsterdam-text-area')
})

it('renders an additional class name', () => {
render(<TextArea className="extra" />)

const component = screen.getByRole('textbox')

expect(component).toHaveClass('amsterdam-text-area extra')
})

alimpens marked this conversation as resolved.
Show resolved Hide resolved
it('renders a textarea with horizontal resize', () => {
render(<TextArea resize="horizontal" />)

const component = screen.getByRole('textbox')

expect(component).toHaveClass('amsterdam-text-area amsterdam-text-area--resize-horizontal')
})

it('renders a textarea with vertical resize', () => {
render(<TextArea resize="vertical" />)

const component = screen.getByRole('textbox')

expect(component).toHaveClass('amsterdam-text-area amsterdam-text-area--resize-vertical')
})

it('renders a textarea with no resize', () => {
render(<TextArea resize="none" />)

const component = screen.getByRole('textbox')

expect(component).toHaveClass('amsterdam-text-area amsterdam-text-area--resize-none')
})

it('renders a textarea with cols class when cols prop is provided', () => {
render(<TextArea cols={10} />)

const component = screen.getByRole('textbox')

expect(component).toHaveClass('amsterdam-text-area amsterdam-text-area--cols')
})

it('can be controlled', async () => {
function ControlledComponent() {
const [value, setValue] = useState('Hello')

return <TextArea value={value} onChange={(e) => setValue(e.target.value)} />
}

render(<ControlledComponent />)

const componentText = screen.getByDisplayValue('Hello')
expect(componentText).toBeInTheDocument()

const component = screen.getByRole('textbox')
if (component) {
await userEvent.type(component, ', World!')
}

const newComponentText = screen.getByDisplayValue('Hello, World!')
expect(newComponentText).toBeInTheDocument()
})

it('should not update the value when disabled', async () => {
render(<TextArea disabled defaultValue="Hello" />)

const component = screen.getByRole('textbox')
if (component) {
await userEvent.type(component, ', World!')
}

expect(component).toHaveValue('Hello')
})

it('supports ForwardRef in React', () => {
const ref = createRef<HTMLTextAreaElement>()

const { container } = render(<TextArea ref={ref} />)

const component = container.querySelector(':only-child')

expect(ref.current).toBe(component)
})
})
29 changes: 29 additions & 0 deletions packages/react/src/TextArea/TextArea.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* @license EUPL-1.2+
* Copyright Gemeente Amsterdam
*/

import clsx from 'clsx'
import { forwardRef } from 'react'
import type { ForwardedRef, TextareaHTMLAttributes } from 'react'

export type TextAreaProps = TextareaHTMLAttributes<HTMLTextAreaElement> & {
resize?: 'none' | 'horizontal' | 'vertical'
}

export const TextArea = forwardRef(
({ className, resize, ...restProps }: TextAreaProps, ref: ForwardedRef<HTMLTextAreaElement>) => (
<textarea
{...restProps}
ref={ref}
className={clsx(
'amsterdam-text-area',
resize && `amsterdam-text-area--resize-${resize}`,
restProps.cols && 'amsterdam-text-area--cols',
className,
)}
/>
),
)

TextArea.displayName = 'TextArea'
2 changes: 2 additions & 0 deletions packages/react/src/TextArea/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { TextArea } from './TextArea'
export type { TextAreaProps } from './TextArea'
1 change: 1 addition & 0 deletions packages/react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

/* Append here */
export * from './Tabs'
export * from './TextArea'
export * from './Column'
export * from './Fieldset'
export * from './LinkList'
Expand Down
36 changes: 36 additions & 0 deletions proprietary/tokens/src/components/amsterdam/text-area.tokens.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"amsterdam": {
"text-area": {
"box-shadow": { "value": "inset 0 0 0 1px {amsterdam.color.primary-black}" },
"color": { "value": "{amsterdam.color.primary-black}" },
"font-family": { "value": "{amsterdam.typography.font-family}" },
"font-size": { "value": "{amsterdam.typography.text-level.6.font-size}" },
"font-weight": { "value": "{amsterdam.typography.font-weight.normal}" },
"line-height": { "value": "{amsterdam.typography.text-level.6.line-height}" },
"min-height": {
"value": "calc({amsterdam.typography.text-level.6.line-height} * 1em + 2 * {amsterdam.text-area.padding-block})"
},
"outline-offset": { "value": "{amsterdam.focus.outline-offset}" },
"padding-block": { "value": "0.5rem" },
"padding-inline": { "value": "1rem" },
"disabled": {
"background-color": { "value": "{amsterdam.color.primary-white}" },
"box-shadow": { "value": "inset 0 0 0 1px {amsterdam.color.neutral-grey2}" },
"color": { "value": "{amsterdam.color.neutral-grey2}" },
"cursor": { "value": "{amsterdam.action.disabled.cursor}" }
},
"hover": {
"box-shadow": { "value": "inset 0 0 0 2px {amsterdam.color.primary-black}" }
},
"invalid": {
"box-shadow": { "value": "inset 0 0 0 1px {amsterdam.color.primary-red}" },
"hover": {
"box-shadow": { "value": "inset 0 0 0 2px {amsterdam.color.primary-red}" }
}
},
"placeholder": {
"color": { "value": "{amsterdam.color.neutral-grey3}" }
}
}
}
}
31 changes: 31 additions & 0 deletions storybook/src/components/TextArea/TextArea.docs.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Canvas, Controls, Markdown, Meta, Primary } from "@storybook/blocks";
import * as TextAreaStories from "./TextArea.stories.tsx";
import README from "../../../../packages/css/src/components/text-area/README.md?raw";

<Meta of={TextAreaStories} />

<Markdown>{README}</Markdown>

<Primary />

<Controls />

## Vertical Resize

<Canvas of={TextAreaStories.VerticalResize} />

## Horizontal Resize

<Canvas of={TextAreaStories.HorizontalResize} />

alimpens marked this conversation as resolved.
Show resolved Hide resolved
## No Resize

<Canvas of={TextAreaStories.NoResize} />

## Invalid

<Canvas of={TextAreaStories.Invalid} />

## Disabled

<Canvas of={TextAreaStories.Disabled} />
Loading
Loading