Skip to content

Commit

Permalink
feat(TextareaField): add TextArea base component and TextareaField (#…
Browse files Browse the repository at this point in the history
…1493)

- create TextArea base component
- for now exporting, but we would want to treat this similar to how we do the input base component
- will sync with latest design before merging
- create TextareaField component (augmenting with label, a11y changes, etc.)
- specify examples for updating height using rows
- handle disabled on base component
  • Loading branch information
booc0mtaco authored Feb 22, 2023
1 parent 5abd719 commit f2ba31d
Show file tree
Hide file tree
Showing 11 changed files with 624 additions and 1 deletion.
16 changes: 16 additions & 0 deletions src/components/TextArea/TextArea.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
@import '../../design-tokens/mixins.css';

/*------------------------------------*\
    # TEXTAREA
\*------------------------------------*/

/**
* Default input styles
*/
.textarea {
@mixin inputStyles;
}

.textarea--disabled:focus {
outline: none;
}
56 changes: 56 additions & 0 deletions src/components/TextArea/TextArea.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import clsx from 'clsx';
import React, { forwardRef } from 'react';
import styles from './TextArea.module.css';

export interface Props
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
/**
* CSS class names that can be appended to the component
*/
className?: string;
/**
* Text default contents of the field
*/
children?: string;
/**
* Whether the disabled stat is active
*/
disabled?: boolean;
/**
* Whether the error state is active
*/
isError?: boolean;
}

/**
* Base component, applying styles to a <textarea> tag
*/
export const TextArea = forwardRef<HTMLTextAreaElement, Props>(
(
{
className,
children,
disabled,
defaultValue = '',
isError = false,
...other
},
ref,
) => {
const componentClassName = clsx(
styles['textarea'],
isError && styles['error'],
disabled && styles['textarea--disabled'],
className,
);

return (
<textarea
className={componentClassName}
defaultValue={children || defaultValue}
ref={ref}
{...other}
></textarea>
);
},
);
1 change: 1 addition & 0 deletions src/components/TextArea/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { TextArea as default } from './TextArea';
21 changes: 21 additions & 0 deletions src/components/TextareaField/TextareaField.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*------------------------------------*\
# TEXTAREA FIELD
\*------------------------------------*/

/**
* Wraps the Label and the optional/required indicator.
*/
.textarea-field__overline {
display: flex;
justify-content: space-between;
margin-bottom: var(--eds-size-half);
color: var(--eds-theme-color-form-label);
}

.textarea-field__overline--no-label {
justify-content: flex-end;
}

.textarea-field__overline--disabled {
color: var(--eds-theme-color-text-disabled);
}
83 changes: 83 additions & 0 deletions src/components/TextareaField/TextareaField.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { BADGE } from '@geometricpanda/storybook-addon-badges';
import type { StoryObj, Meta } from '@storybook/react';
import React from 'react';

import { TextareaField } from './TextareaField';

export default {
title: 'Components/TextareaField',
component: TextareaField,
args: {
placeholder: 'Enter long-form text here',
defaultValue: `Lorem ipsum, dolor sit amet consectetur adipisicing elit. Id neque nemo
dicta rerum commodi et fugiat quo optio veniam! Ea odio corporis nemo
praesentium, commodi eligendi asperiores quis dolorum porro.`,
label: 'Textarea Field',
rows: 5,
fieldNote: 'Longer Field description',
},
parameters: {
badges: [BADGE.BETA],
},
} as Meta<Args>;

type Args = React.ComponentProps<typeof TextareaField>;

export const Default: StoryObj<Args> = {
render: (args) => (
<TextareaField aria-label="Text Label" {...args}></TextareaField>
),
};

export const UsingChildren: StoryObj<Args> = {
args: {
children: `Lorem ipsum, dolor sit amet consectetur adipisicing elit. Id neque nemo
dicta rerum commodi et fugiat quo optio veniam! Ea odio corporis nemo
praesentium, commodi eligendi asperiores quis dolorum porro.`,
defaultValue: '',
},
};

export const WithNoDefaultValue: StoryObj<Args> = {
args: {
defaultValue: '',
},
};

export const WhenDisabled: StoryObj<Args> = {
args: {
disabled: true,
rows: 2,
},
parameters: {
axe: {
// Disabled input does not need to meet color contrast
disabledRules: ['color-contrast'],
},
},
};

export const WhenError: StoryObj<Args> = {
args: {
isError: true,
fieldNote: 'Text should be at least 100 characters',
},
};

export const WhenInvalid: StoryObj<Args> = {
args: {
maxLength: 10,
},
};

export const WhenRequired: StoryObj<Args> = {
args: {
required: true,
},
};

export const WithADifferentSize: StoryObj<Args> = {
args: {
rows: 10,
},
};
6 changes: 6 additions & 0 deletions src/components/TextareaField/TextareaField.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { generateSnapshots } from '@chanzuckerberg/story-utils';
import * as stories from './TextareaField.stories';

describe('<TextareaField />', () => {
generateSnapshots(stories);
});
132 changes: 132 additions & 0 deletions src/components/TextareaField/TextareaField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import clsx from 'clsx';
import type { ReactNode } from 'react';
import React, { forwardRef, useId } from 'react';
import styles from './TextareaField.module.css';
import FieldNote from '../FieldNote';
import Label from '../Label';
import Text from '../Text';
import TextArea from '../TextArea';

export type Props = React.TextareaHTMLAttributes<HTMLTextAreaElement> & {
/**
* Aria-label to provide an accesible name for the text input if no visible label is provided.
*/
'aria-label'?: string;
/**
* Text content of the field upon instantiation
*/
children?: string;
/**
* CSS class names that can be appended to the component.
*/
className?: string;
/**
* Disables the field and prevents editing the contents
*/
disabled?: boolean;
/**
* Text under the textarea used to provide a description or error message to describe the input.
*/
fieldNote?: ReactNode;
/**
* HTML id for the component. Can be used with a custom Label component
*/
id?: string;
/**
* Error state of the form field
*/
isError?: boolean;
/**
* HTML label text (used in an accessory label component)
*/
label?: string;
};

/**
* BETA: This component is still a work in progress and is subject to change.
*
* `import {TextareaField} from "@chanzuckerberg/eds";`
*
* Multi-line text input field with built-in labeling and accessory text to describe
* the content. When a maximum text count is specified, component also shows relevant
* text up to the maximum.
*/
export const TextareaField = forwardRef<HTMLTextAreaElement, Props>(
(
{
'aria-describedby': ariaDescribedBy,
children,
className,
disabled,
fieldNote,
id,
isError,
label,
required,
...other
},
ref,
) => {
if (
process.env.NODE_ENV !== 'production' &&
!label &&
!other['aria-label']
) {
throw new Error('You must provide a visible label or aria-label');
}

const shouldRenderOverline = !!(label || required);
const overlineClassName = clsx(
styles['textarea-field__overline'],
!label && styles['textarea-field__overline--no-label'],
disabled && styles['textarea-field__overline--disabled'],
);

const generatedIdVar = useId();
const idVar = id || generatedIdVar;

const generatedAriaDescribedById = useId();
const ariaDescribedByVar = fieldNote
? ariaDescribedBy || generatedAriaDescribedById
: undefined;

const componentClassName = clsx(styles['textarea-field'], className);

return (
<div className={componentClassName}>
{shouldRenderOverline && (
<div className={overlineClassName}>
{label && <Label htmlFor={idVar} text={label} />}
{required && (
<Text as="p" size="sm">
Required
</Text>
)}
</div>
)}
<TextArea
aria-describedby={ariaDescribedByVar}
aria-disabled={disabled}
disabled={disabled}
id={idVar}
isError={isError}
readOnly={disabled}
ref={ref}
required={required}
{...other}
>
{children}
</TextArea>
{fieldNote && (
<FieldNote
disabled={disabled}
id={ariaDescribedByVar}
isError={isError}
>
{fieldNote}
</FieldNote>
)}
</div>
);
},
);
Loading

0 comments on commit f2ba31d

Please sign in to comment.