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

fea: add TextArea component #2107

Merged
merged 8 commits into from
May 7, 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
2 changes: 2 additions & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@
"@types/autosize": "^4.0.1",
"@types/babel__standalone": "^7.1.7",
"@types/body-scroll-lock": "^3.1.0",
"@types/jest-axe": "^3.5.9",
"@types/lodash": "^4.14.184",
"@types/lodash-es": "^4.17.6",
"@types/react": "^18.0.25",
Expand Down Expand Up @@ -221,6 +222,7 @@
"execa": "^5.1.1",
"identity-obj-proxy": "^3.0.0",
"jest": "^29.7.0",
"jest-axe": "^8.0.0",
"jest-environment-jsdom": "^29.7.0",
"js-sha256": "^0.9.0",
"json-loader": "^0.5.7",
Expand Down
3 changes: 3 additions & 0 deletions packages/core/setupTests.ts
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
import { toHaveNoViolations } from "jest-axe";
import "@testing-library/jest-dom";

expect.extend(toHaveNoViolations);
93 changes: 93 additions & 0 deletions packages/core/src/components/TextArea/TextArea.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
@import "../../styles/typography";
@import "~monday-ui-style/dist/mixins";

.textAreaWrapper {
display: flex;
flex-direction: column;
width: 100%;
gap: var(--spacing-xs);
@include smoothing-text;
@include vibe-text(text2, normal);

.label {
display: inline-flex;
gap: var(--spacing-xs);

&.required::after {
content: "*";
color: var(--color-error);
}
}

.helpText {
color: var(--secondary-text-color);
}

.textArea {
resize: vertical;
border: 1px solid var(--ui-border-color);
border-radius: var(--border-radius-small);
padding: var(--spacing-small) var(--spacing-medium);
outline: none;
talkor marked this conversation as resolved.
Show resolved Hide resolved

&:hover {
border-color: var(--primary-text-color);
}

&:active,
&:focus,
&:focus-within {
border-color: var(--primary-color);
}

&.small {
@include vibe-text(text2, normal);
}

&.large {
@include vibe-text(text1, normal);
}
}

&.success {
.textArea {
border-color: var(--color-success);
}

.helpText {
color: var(--color-success);
}
}

&.error {
.textArea {
border-color: var(--color-error);
}

.helpText {
color: var(--color-error);
}
}

&.disabled {
.textArea,
.textArea:hover {
color: var(--disabled-text-color);
background-color: var(--disabled-background-color);
border-color: var(--disabled-text-color);
}

.helpText {
color: var(--disabled-text-color);
}
}

&.readOnly {
.textArea,
.textArea:hover {
background-color: var(--allgrey-background-color);
border-color: transparent;
resize: none;
}
}
}
81 changes: 81 additions & 0 deletions packages/core/src/components/TextArea/TextArea.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import React, { forwardRef } from "react";
import cx from "classnames";
import { getTestId } from "../../tests/test-ids-utils";
import { ComponentDefaultTestId } from "../../tests/constants";
import styles from "./TextArea.module.scss";
import { TextAreaProps, TextAreaSize } from "./TextArea.types";
import Text from "../Text/Text";

const DEFAULT_ROWS: Record<TextAreaSize, number> = {
small: 3,
large: 4
};

const TextArea = forwardRef(
(
{
size = "small",
rows,
label,
helpText,
success,
error,
className,
"data-testid": dataTestId,
id,
disabled,
readOnly,
value,
onChange,
"aria-label": ariaLabel,
required
}: TextAreaProps,
ref: React.ForwardedRef<HTMLTextAreaElement>
talkor marked this conversation as resolved.
Show resolved Hide resolved
) => {
const numRows = rows || DEFAULT_ROWS[size];
const helpTextId = helpText && `${id}-help-text`;

return (
<div
className={cx(
styles.textAreaWrapper,
{
[styles.error]: error,
[styles.success]: success,
[styles.disabled]: disabled,
[styles.readOnly]: readOnly
},
className
)}
data-testid={dataTestId || getTestId(ComponentDefaultTestId.TEXT_AREA, id)}
>
{label && (
<label className={cx(styles.label, { [styles.required]: required })} htmlFor={id}>
{label}
</label>
)}
<textarea
id={id}
ref={ref}
disabled={disabled}
readOnly={readOnly}
required={required}
rows={numRows}
className={cx(styles.textArea, [styles[size]])}
talkor marked this conversation as resolved.
Show resolved Hide resolved
value={value}
onChange={onChange}
aria-invalid={error}
aria-label={ariaLabel}
aria-describedby={helpTextId ?? undefined}
talkor marked this conversation as resolved.
Show resolved Hide resolved
/>
{helpText && (
<Text className={cx(styles.helpText)} color={Text.colors.INHERIT} id={helpTextId}>
{helpText}
</Text>
)}
</div>
);
}
);

export default TextArea;
58 changes: 58 additions & 0 deletions packages/core/src/components/TextArea/TextArea.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { TextareaHTMLAttributes } from "react";
import { VibeComponentProps } from "../../types";

export type TextAreaSize = "small" | "large";
type TextAreaNativeInputProps = Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, "role">;

export interface TextAreaProps extends TextAreaNativeInputProps, VibeComponentProps {
/**
* The current value of the textarea.
*/
value?: string;
/**
* Determines the size of the textarea text as well as the default row count.
*/
size?: TextAreaSize;
/**
* Indicates that the textarea has passed validation successfully, controlling
* the visual styling to convey success.
*/
success?: boolean;
/**
* Indicates an error with the current value of the textarea, controlling
* the visual styling to convey error.
*/
error?: boolean;
/**
* The number of rows in the textarea. Defaults according to the size prop.
*/
rows?: number;
/**
* Label text associated with the textarea element.
*/
label?: string;
talkor marked this conversation as resolved.
Show resolved Hide resolved
/**
* If true, the textarea becomes non-interactive
*/
disabled?: boolean;
/**
* If true, the textarea is read-only and cannot be modified by the user
*/
readOnly?: boolean;
/**
* Function to call on textarea value change.
*/
onChange?: (event: React.ChangeEvent<HTMLTextAreaElement>) => void;
/**
* Help text associated with the textarea element, typically used for guidance.
*/
helpText?: string;
/**
* Accessibility label for the textarea element.
*/
"aria-label"?: React.AriaAttributes["aria-label"];
/**
* If true, the textarea is required and must be filled out by the user.
*/
required?: boolean;
}
82 changes: 82 additions & 0 deletions packages/core/src/components/TextArea/__stories__/TextArea.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { Meta } from "@storybook/blocks";
import TextArea from "../TextArea";
import * as TextAreaStories from "./TextArea.stories";
import {
TEXT_FIELD,
DROPDOWN,
SEARCH
} from "../../../storybook/components/related-components/component-description-map";
import doImage from "./assets/do.svg";
import dontImage from "./assets/dont.svg";

<Meta of={TextAreaStories} />

# TextArea

- [Overview](#overview)
- [Props](#props)
- [Usage](#usage)
- [Variants](#variants)
- [Do's and don'ts](#dos-and-donts)
- [Related components](#related-components)
- [Feedback](#feedback)

## Overview

A field that allows users to write multiple lines of text. Text area includes a label and a field that users can type into. It can also come with helper text.

<Canvas of={TextAreaStories.Overview} />

## Props

<PropsTable />

## Usage

<UsageGuidelines
guidelines={[
"Use text area to allow users to write multiple lines of text, usually for comments or descriptions.",
"Placeholders should only be used when necessary."
]}
/>

## Variants

### Sizes

There are two sizes available: small and large.

<Canvas of={TextAreaStories.Sizes} />

### States

Text areas have all the same states as text fields.

<Canvas of={TextAreaStories.States} />

### Validation

If a required text area is left empty, use validation text to give feedback to users. The validation error state should appear after users try to submit a form.

<Canvas of={TextAreaStories.Validation} />

## Do's and Don'ts

<ComponentRules
rules={[
{
positive: {
component: <img src={doImage} />,
description: "Use text area if you want to ask an open question. Make sure the question is short and clear."
},
negative: {
component: <img src={dontImage} />,
description: "Don't use a text area if you want short and specific info - use a text field instead. "
}
}
]}
/>

## Related components

<RelatedComponents componentsNames={[TEXT_FIELD, DROPDOWN, SEARCH]} />
Loading