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

Add TextArea component #400

Merged
merged 47 commits into from
Jul 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
7f43af0
TextField component
mkernohanbc Jun 13, 2024
6afaefa
fork Text Field component into Text Area, replacing <Input> with <Tex…
mkernohanbc Jun 26, 2024
89e4f96
stage TextArea on vite
mkernohanbc Jun 26, 2024
a922dd7
remove unused size styles
mkernohanbc Jun 26, 2024
2152c26
group textfield and textarea together in storybook
mkernohanbc Jun 26, 2024
b7ece20
add error state story
mkernohanbc Jun 26, 2024
5c22928
front matter for textarea docs
mkernohanbc Jun 27, 2024
30b1a3e
add demo form to vite app
mkernohanbc Jun 27, 2024
88ffc05
add min-height to input field
mkernohanbc Jul 2, 2024
aabd76b
first pass at adding character counter
mkernohanbc Jul 3, 2024
4c5a845
style tweak to fix accessibility vio
mkernohanbc Jul 8, 2024
1c4b2b2
move error state icon out of input field and into error message slot
mkernohanbc Jul 9, 2024
920f720
fix typo in iconError class name
mkernohanbc Jul 10, 2024
96caaa4
add new stories for TextArea
mkernohanbc Jul 10, 2024
7d1b6e4
rework structure and styling of description and char count
mkernohanbc Jul 12, 2024
7862e3d
removes an empty div if neither maxLength nor description are present
mkernohanbc Jul 15, 2024
340e2e0
new icon implementation to fix typescript error
mkernohanbc Jul 15, 2024
c81318d
rework positioning logic for description and counter slots
mkernohanbc Jul 15, 2024
b510c5f
CSS cleanup
mkernohanbc Jul 16, 2024
b763129
Revert "add demo form to vite app"
mkernohanbc Jul 16, 2024
8ef5d58
Revert "group textfield and textarea together in storybook"
mkernohanbc Jul 16, 2024
b6c025f
Revert "TextField component"
mkernohanbc Jul 16, 2024
0b8bc37
Add work in progress TextArea maxLength counter idea
ty2k Jul 16, 2024
ebfbe72
Merge pull request #405 from bcgov/feature/textarea-counter-idea
mkernohanbc Jul 16, 2024
bbcba9e
add comment to new charCount function
mkernohanbc Jul 17, 2024
7ae8882
add missing colour styles and docs note on placeholder text
mkernohanbc Jul 17, 2024
4e165df
expand storybook docs
mkernohanbc Jul 17, 2024
2a58854
reorder stories and add isRequired case
mkernohanbc Jul 17, 2024
4f91ae2
syntax cleanup
mkernohanbc Jul 17, 2024
284d5d6
Merge branch 'main' into feature/textarea
mkernohanbc Jul 18, 2024
1423f3e
add warning icon as a react component
mkernohanbc Jul 18, 2024
95c8ac3
changes to warning icon SVG
mkernohanbc Jul 18, 2024
05f9a25
change icon to SvgExclamationIcon
mkernohanbc Jul 18, 2024
e51075e
fix naming error in SVG URL
mkernohanbc Jul 22, 2024
187086f
bump storybook to 8.2.5
mkernohanbc Jul 22, 2024
583dc65
correct typo
mkernohanbc Jul 22, 2024
09183ac
fix errors in static SVG
mkernohanbc Jul 22, 2024
d19ccdb
remove unnecessary styling from react SVG
mkernohanbc Jul 22, 2024
3f97840
icon styling
mkernohanbc Jul 22, 2024
40bfbdb
update textarea story settings
mkernohanbc Jul 22, 2024
49038e3
clean up description field styling
mkernohanbc Jul 22, 2024
d072d78
reimplement error icon using react component
mkernohanbc Jul 22, 2024
77af9b6
clean up styling and fix spacing issue
mkernohanbc Jul 22, 2024
1f718f0
Revert "bump storybook to 8.2.5"
mkernohanbc Jul 22, 2024
ffe3945
simplify styling of required label
mkernohanbc Jul 22, 2024
3bbe419
remove hardcoded IDs from SVG
mkernohanbc Jul 22, 2024
cbe0370
update default SVG ID
mkernohanbc Jul 22, 2024
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
2 changes: 2 additions & 0 deletions packages/react-components/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
SelectPage,
TagGroupPage,
TooltipPage,
TextAreaPage,
TextFieldPage,
} from "@/pages";

Expand Down Expand Up @@ -148,6 +149,7 @@ function App() {
<SelectPage />
<TagGroupPage />
<TooltipPage />
<TextAreaPage />
<TextFieldPage />
</main>
<Footer />
Expand Down
13 changes: 13 additions & 0 deletions packages/react-components/src/assets/icon-exclamation.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// This icon is available as a plain SVG at src/assets/icon-exclamation.svg
export default function SvgExclamationIcon({ id = "exclamation-icon" }) {
return (
<svg
id={id}
width="20"
height="20"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<g>
<path
d="M17.835 15.0312C18.335 15.9062 17.71 17 16.6787 17H3.33499C2.30374 17 1.67874 15.9062 2.14749 15.0312L8.83499 3.65625C9.11624 3.21875 9.55373 3 10.0225 3C10.46 3 10.8975 3.21875 11.1787 3.65625L17.835 15.0312ZM3.64749 15.5H16.3663L9.99123 4.65625L3.64749 15.5ZM10.0225 12.5625C10.5537 12.5625 10.9912 13 10.9912 13.5312C10.9912 14.0625 10.5537 14.5 10.0225 14.5C9.45998 14.5 9.02249 14.0625 9.02249 13.5312C9.02249 13 9.45998 12.5625 10.0225 12.5625ZM9.27249 7.75C9.27249 7.34375 9.58498 7 10.0225 7C10.4287 7 10.7725 7.34375 10.7725 7.75V10.75C10.7725 11.1875 10.4287 11.5 10.0225 11.5C9.58498 11.5 9.27249 11.1875 9.27249 10.75V7.75Z"
fill="currentColor"
/>
</g>
</svg>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from "./SvgExclamationIcon";
129 changes: 129 additions & 0 deletions packages/react-components/src/components/TextArea/TextArea.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
.bcds-react-aria-TextArea {
display: inline-flex;
flex-direction: column;
/* Hacks for `stretch`: https://caniuse.com/mdn-css_properties_max-width_stretch */
align-self: stretch;
max-width: -moz-available;
max-width: -webkit-fill-available;
}

/* Styles for the text label above the input field */
.bcds-react-aria-TextArea--Label {
font: var(--typography-regular-small-body);
color: var(--typography-color-primary);
}

.bcds-react-aria-TextArea--Label > .required {
color: var(--typography-color-secondary);
padding: var(--layout-padding-none) var(--layout-padding-xsmall);
}

/* Styles for the description and character count slot below the input field */
.bcds-react-aria-TextArea--Description {
display: grid;
gap: var(--layout-margin-medium);
font: var(--typography-regular-small-body);
color: var(--typography-color-secondary);
}

/* Description label */
.bcds-react-aria-TextArea--Description > .text {
justify-self: flex-start;
grid-row: 1;
}

/* Character counter */
.bcds-react-aria-TextArea--Description > .counter {
justify-self: flex-end;
grid-row: 1;
}

/* Styles for the input field container */
.bcds-react-aria-TextArea--Container {
color: var(--typography-color-primary);
display: flex;
flex-direction: row;
align-items: center;
gap: var(--layout-margin-small);
background: var(--surface-color-forms-default);
box-sizing: border-box;
border: var(--layout-border-width-small) solid
var(--surface-color-border-default);
border-radius: var(--layout-border-radius-medium);
padding: var(--layout-padding-small) 12px;
margin: var(--layout-margin-small) var(--layout-margin-none);
}

/* Text input field */
.bcds-react-aria-TextArea--Input {
font: var(--typography-regular-body);
padding: var(--layout-padding-none);
color: var(--typography-color-primary);
border: none;
flex-grow: 1;
}

.bcds-rect-aria-TextArea--Input::placeholder {
color: var(--typography-color-placeholder);
}

/* Hover and focus states */
.bcds-react-aria-TextArea--Container
> .bcds-react-aria-TextArea--Input[data-focused] {
outline: none;
}

.bcds-react-aria-TextArea--Container:focus-within {
border-radius: var(--layout-border-radius-large);
border: var(--layout-border-width-small) solid
var(--surface-color-border-active);
outline: solid var(--layout-border-width-medium)
var(--surface-color-border-active);
outline-offset: var(--layout-margin-hair);
}

.bcds-react-aria-TextArea--Container:hover {
border-color: var(--surface-color-border-dark);
}

/* Disabled and invalid states */
.bcds-react-aria-TextArea[data-disabled]
> .bcds-react-aria-TextArea--Container {
background: var(--surface-color-forms-disabled);
cursor: not-allowed;
}

.bcds-react-aria-TextArea--Input[data-disabled] {
cursor: not-allowed;
}

.bcds-react-aria-TextArea[data-invalid] > .bcds-react-aria-TextArea--Container {
border-radius: var(--layout-border-radius-medium);
border: 1px solid var(--support-border-color-danger);
background: var(--surface-color-forms-default);
}

.bcds-react-aria-TextArea[data-readonly]
> .bcds-react-aria-TextArea--Container {
background: var(--surface-color-forms-disabled);
}

.bcds-react-aria-TextArea[data-readonly]
> .bcds-react-aria-TextArea--Container
> .bcds-react-aria-TextArea--Input {
background: var(--surface-color-forms-disabled);
}

/* Styles for the error message slot */
.bcds-react-aria-TextArea--Error {
display: flex;
font: var(--typography-regular-small-body);
color: var(--typography-color-danger);
}

.bcds-react-aria-TextArea--Error > svg {
padding-right: var(--layout-padding-xsmall);
color: var(--icons-color-danger);
width: var(--icons-size-medium);
height: var(--icons-size-medium);
}
82 changes: 82 additions & 0 deletions packages/react-components/src/components/TextArea/TextArea.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { useState } from "react";
import {
TextField as ReactAriaTextField,
TextFieldProps as ReactAriaTextFieldProps,
TextArea as ReactAriaTextArea,
Label,
FieldError,
Text,
ValidationResult,
} from "react-aria-components";

import "./TextArea.css";
import SvgExclamationIcon from "../SvgExclamationIcon";

export interface TextAreaProps extends ReactAriaTextFieldProps {
/* Sets text label above text input field */
label?: string;
/* Sets optional description text below text input field */
description?: string;
/* Used for data validation and error handling */
errorMessage?: string | ((validation: ValidationResult) => string);
}

export default function TextArea({
label,
description,
errorMessage,
maxLength,
onChange,
value,
...props
}: TextAreaProps) {
// handler for character counter, displayed when a maxLength is set
const [charCount, setCharCount] = useState(value ? value.length : 0);

function handleChange(text: string) {
if (onChange) onChange(text);
setCharCount(text.length);
}

return (
<ReactAriaTextField className={`bcds-react-aria-TextArea`} {...props}>
{({ isRequired, isInvalid }) => (
<>
{label && (
<Label className="bcds-react-aria-TextArea--Label">
{label}
{isRequired && <span className="required">(required)</span>}
</Label>
)}
<div className="bcds-react-aria-TextArea--Container">
<ReactAriaTextArea
className="bcds-react-aria-TextArea--Input"
value={value}
onChange={(e) => handleChange(e.target.value)}
/>
</div>
{description || maxLength ? (
<div className="bcds-react-aria-TextArea--Description">
{description && (
<Text slot="description" className="text">
{description}
</Text>
)}
{maxLength && (
<div className="counter">
{charCount}/{maxLength}
</div>
)}
</div>
) : null}
{isInvalid && (
<div className="bcds-react-aria-TextArea--Error">
<SvgExclamationIcon />
<FieldError>{errorMessage}</FieldError>
</div>
)}
</>
)}
</ReactAriaTextField>
);
}
2 changes: 2 additions & 0 deletions packages/react-components/src/components/TextArea/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default } from "./TextArea";
export type { TextAreaProps } from "./TextArea";
1 change: 1 addition & 0 deletions packages/react-components/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ export { default as SvgBcLogo } from "./SvgBcLogo";
export { default as Tag } from "./Tag";
export { default as TagGroup } from "./TagGroup";
export { default as TagList } from "./TagList";
export { default as TextArea } from "./TextArea";
export { default as TextField } from "./TextField";
export { default as Tooltip, TooltipTrigger } from "./Tooltip";
33 changes: 33 additions & 0 deletions packages/react-components/src/pages/TextArea/TextArea.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { useState } from "react";

import { TextArea } from "@/components";

export default function TextAreaPage() {
const [text, setText] = useState("initial text state");

function handleTextChange(text: string) {
console.log("new text value: ", text);

setText(text);
}

return (
<>
<h2>TextArea</h2>
<div>
<p>
<code>text</code>: {text}{" "}
</p>
<TextArea
label={"Controlled"}
value={text}
onChange={handleTextChange}
maxLength={500}
/>
</div>
<div>
<TextArea label={"Uncontrolled"} maxLength={500} defaultValue="Hello" />
</div>
</>
);
}
3 changes: 3 additions & 0 deletions packages/react-components/src/pages/TextArea/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import TextAreaPage from "./TextArea";

export default TextAreaPage;
3 changes: 2 additions & 1 deletion packages/react-components/src/pages/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import ButtonPage from "./Button";
import SelectPage from "./Select";
import TagGroupPage from "./TagGroup";
import TextAreaPage from "./TextArea";
import TextFieldPage from "./TextField";
import TooltipPage from "./Tooltip";

export { ButtonPage, SelectPage, TagGroupPage, TextFieldPage, TooltipPage };
export { ButtonPage, SelectPage, TagGroupPage, TextAreaPage, TextFieldPage, TooltipPage };
74 changes: 74 additions & 0 deletions packages/react-components/src/stories/TextArea.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
{/* TextArea.mdx */}

import {
Canvas,
Controls,
Meta,
Primary,
Source,
Story,
Subtitle,
} from "@storybook/blocks";

import * as TextAreaStories from "./TextArea.stories";

<Meta of={TextAreaStories} />

# Text Area

<Subtitle>
The text area component enables a user to enter multiple lines of plain text
into an interface or form.
</Subtitle>

<Source
code={`import { TextArea } from "@bcgov/design-system-react-components"; `}
language="typescript"
/>
## Usage and resources

Learn more about working with the text area component:

- [Usage and best practice guidance](https://www2.gov.bc.ca/gov/content/digital/design-system/components/text-area)
- [View the text field component in Figma](#)

This component is based on [React Aria TextField](https://react-spectrum.adobe.com/react-aria/TextField.html#multi-line).

### Validation

Default and custom data validation support is built in, using the [React Aria FieldError subcomponent](https://react-spectrum.adobe.com/react-aria/forms.html#validation).

### Placeholder

As a general rule, the `placeholder` attribute should not be used on text fields. While `placeholder` is technically supported, the use of 'ghost text' has [serious accessibility and usability issues](https://developer.mozilla.org/en-US/docs/Web/CSS/::placeholder#accessibility_concerns).

## Controls

<Primary of={TextAreaStories} />
<Controls of={TextAreaStories.TextAreaTemplate} />

## Configuration

### Maximum length

The `maxLength` prop can be used to limit the amount of text a user can input. When `maxLength` is passed, a character counter renders below the input field:

<Canvas of={TextAreaStories.TextAreaWithMaxLength} />

### States

Use `isRequired` to make a text area mandatory. A secondary label is shown above the input field:

<Canvas of={TextAreaStories.RequiredTextArea} />

Use `isDisabled` to disable a text area. A disabled text area cannot be focused or interacted with:

<Canvas of={TextAreaStories.DisabledTextArea} />

Use `isReadOnly` to lock a text area to its current value. A read-only text area can be focused and copied, but cannot be edited:

<Canvas of={TextAreaStories.ReadOnlyTextArea} />

The `isInvalid` prop should be set programmatically when an input is invalid. It renders an error icon in the input field, changes the border colour and displays an `errorMessage` (using the FieldError subcomponent):

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