Skip to content

Commit

Permalink
feat(TextareaField): add TextArea base component and TextareaField
Browse files Browse the repository at this point in the history
- 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.)
  • Loading branch information
booc0mtaco committed Feb 21, 2023
1 parent 5abd719 commit 6b39903
Show file tree
Hide file tree
Showing 11 changed files with 426 additions and 2 deletions.
12 changes: 12 additions & 0 deletions src/components/TextArea/TextArea.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
@import '../../design-tokens/mixins.css';

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

/**
* Default input styles
*/
.textarea {
@mixin inputStyles;
}
48 changes: 48 additions & 0 deletions src/components/TextArea/TextArea.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
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, defaultValue = '', isError = false, ...other },
ref,
) => {
const componentClassName = clsx(
styles['textarea'],
isError && styles['error'],
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';
19 changes: 19 additions & 0 deletions src/components/TextareaField/TextareaField.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*------------------------------------*\
# 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);
}
58 changes: 58 additions & 0 deletions src/components/TextareaField/TextareaField.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
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: {
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',
width: '100',
height: '100',
fieldNote: 'Test',
},
parameters: {
layout: 'centered',
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 WhenDisabled: StoryObj<Args> = {
args: {
disabled: true,
},
parameters: {
axe: {
// Disabled input does not need to meet color contrast
disabledRules: ['color-contrast'],
},
},
};

export const WhenInvalid: StoryObj<Args> = {
args: {
maxLength: 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);
});
130 changes: 130 additions & 0 deletions src/components/TextareaField/TextareaField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
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}
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 6b39903

Please sign in to comment.