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 5 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
3 changes: 2 additions & 1 deletion packages/css/src/components/index.scss
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
/**
* @license EUPL-1.2+
* Copyright (c) 2023 Gemeente Amsterdam
* Copyright (c) 2024 Gemeente Amsterdam
alimpens marked this conversation as resolved.
Show resolved Hide resolved
*/

/* Append here */
@import "./text-area/text-area";
@import "./column/column";
@import "./margin/margin";
@import "./gap/gap";
Expand Down
21 changes: 21 additions & 0 deletions packages/css/src/components/text-area/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# 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.
- Use cols and rows attributes to set the width and height of the TextArea.
alimpens marked this conversation as resolved.
Show resolved Hide resolved
- Use the experimental grow attribute to allow the TextArea to grow in height as the user types, this only works in modern browsers.
alimpens marked this conversation as resolved.
Show resolved Hide resolved
- 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 ot title attribute.
alimpens marked this conversation as resolved.
Show resolved Hide resolved

## References

- [Intrinsic & Extrinsic Sizing](https://caniuse.com/?search=form-sizing%3A%20content) browser support.
71 changes: 71 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,71 @@
/**
* @license EUPL-1.2+
* Copyright (c) 2024 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);
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: 0.5rem;
padding-inline: 1rem;
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: not-allowed;
alimpens marked this conversation as resolved.
Show resolved Hide resolved
}

.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: horizontal;
alimpens marked this conversation as resolved.
Show resolved Hide resolved
}
alimpens marked this conversation as resolved.
Show resolved Hide resolved

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

.amsterdam-text-area--grow {
/* stylelint-disable-next-line property-no-unknown */
field-sizing: content;
width: auto;
}
2 changes: 1 addition & 1 deletion packages/css/src/components/text-input/text-input.scss
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/**
* @license EUPL-1.2+
* Copyright (c) 2023 Gemeente Amsterdam
* Copyright (c) 2024 Gemeente Amsterdam
*/

@mixin reset {
Expand Down
3 changes: 3 additions & 0 deletions packages/react/src/TextArea/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# React TextArea component
alimpens marked this conversation as resolved.
Show resolved Hide resolved

[TextArea documentation](../../../css/src/components/text-area/README.md)
74 changes: 74 additions & 0 deletions packages/react/src/TextArea/TextArea.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
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', () => {
const { container } = render(<TextArea />)

const component = container.querySelector(':only-child')
alimpens marked this conversation as resolved.
Show resolved Hide resolved

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

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

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

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

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

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

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

alimpens marked this conversation as resolved.
Show resolved Hide resolved
it('should be working in a controlled state', async () => {
alimpens marked this conversation as resolved.
Show resolved Hide resolved
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)
})
})
32 changes: 32 additions & 0 deletions packages/react/src/TextArea/TextArea.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* @license EUPL-1.2+
* Copyright (c) 2024 Gemeente Amsterdam
*/

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

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

export const TextArea = forwardRef(
({ children, className, resize, grow, ...restProps }: TextAreaProps, ref: ForwardedRef<HTMLTextAreaElement>) => (
<textarea
{...restProps}
ref={ref}
className={clsx(
'amsterdam-text-area',
resize && `amsterdam-text-area--resize-${resize}`,
grow && 'amsterdam-text-area--grow',
className,
)}
>
{children}
alimpens marked this conversation as resolved.
Show resolved Hide resolved
</textarea>
),
)

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'
2 changes: 1 addition & 1 deletion packages/react/src/TextInput/TextInput.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/**
* @license EUPL-1.2+
* Copyright (c) 2023 Gemeente Amsterdam
* Copyright (c) 2024 Gemeente Amsterdam
*/

import clsx from 'clsx'
Expand Down
3 changes: 2 additions & 1 deletion packages/react/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
/**
* @license EUPL-1.2+
* Copyright (c) 2023 Gemeente Amsterdam
* Copyright (c) 2024 Gemeente Amsterdam
*/

/* Append here */
export * from './TextArea'
export * from './Column'
export * from './Fieldset'
export * from './LinkList'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"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": "2.5em" },
"outline-offset": { "value": "{amsterdam.focus.outline-offset}" },
"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}" }
},
"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}" }
}
}
}
}
34 changes: 34 additions & 0 deletions storybook/storybook-react/src/TextArea/TextArea.docs.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
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} />

## Grow

To allow the textarea to grow in width and height as the user types, set the `grow` prop to `true`.
The properties for `cols` and `rows` are ignored when `grow` is set to `true`.

<Canvas of={TextAreaStories.Grow} />

## Invalid

<Canvas of={TextAreaStories.Invalid} />

## Disabled

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