Skip to content
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
99 changes: 99 additions & 0 deletions src/components/text-area/TextArea.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import * as React from 'react';
import classNames from 'classnames';
import uniqueId from 'lodash/uniqueId';

import Label from '../label';
import Tooltip, { TooltipPosition, TooltipTheme, type TooltipProps } from '../tooltip';

import './TextArea.scss';

export interface TextAreaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
/** Add a class to the component */
className?: string;
/** Description shown below the label */
description?: React.ReactNode;
/** Error message shown in the error tooltip */
error?: React.ReactNode;
/** Renders error tooltip at the specified position (positions are those from Tooltip) */
errorTooltipPosition?: NonNullable<TooltipProps['position']>;
/** Hides the label */
hideLabel?: boolean;
/** Hides (optional) text from the label */
hideOptionalLabel?: boolean;
/** Makes the text area value required */
isRequired?: boolean;
/** Is text area resizable */
isResizable?: boolean;
/** Label displayed for the text area */
label: React.ReactNode;
/** Ref to the underlying textarea element. @TODO: eventually rename to innerRef for consistancy across all form elements */
textareaRef?: React.Ref<HTMLTextAreaElement>;
/** A CSS class for the tooltip's tether element component */
tooltipTetherClassName?: string;
}

const TextArea = ({
className = '',
description,
error,
errorTooltipPosition,
hideLabel,
hideOptionalLabel,
isRequired,
isResizable,
label,
textareaRef,
tooltipTetherClassName,
...rest
}: TextAreaProps) => {
const hasError = !!error;
const classes = classNames(className, 'text-area-container', {
'show-error': hasError,
});

const errorMessageID = React.useRef(uniqueId('errorMessage')).current;
const descriptionID = React.useRef(uniqueId('description')).current;

const ariaAttrs = {
'aria-invalid': hasError,
'aria-required': isRequired,
'aria-errormessage': errorMessageID,
'aria-describedby': description ? descriptionID : undefined,
};
Comment thread
bonchevskyi marked this conversation as resolved.

return (
<div className={classes}>
<Label hideLabel={hideLabel} showOptionalText={!hideOptionalLabel && !isRequired} text={label}>
<>
{!!description && (
<div id={descriptionID} className="text-area-description">
{description}
</div>
)}
<Tooltip
isShown={hasError}
position={errorTooltipPosition || TooltipPosition.BOTTOM_LEFT}
tetherElementClassName={tooltipTetherClassName}
text={error || ''}
theme={TooltipTheme.ERROR}
>
<textarea
ref={textareaRef}
required={isRequired}
style={{ resize: isResizable ? '' : 'none' } as React.CSSProperties}
{...ariaAttrs}
{...rest}
/>
</Tooltip>
<span id={errorMessageID} className="accessibility-hidden" role="alert">
{error}
</span>
</>
</Label>
</div>
);
};

TextArea.displayName = 'TextArea';

export default TextArea;
29 changes: 29 additions & 0 deletions src/components/text-area/TextAreaField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import * as React from 'react';
import getProp from 'lodash/get';
import type { FieldProps } from 'formik';

import TextAreaPrimitive from './TextArea';
import type { TextAreaProps } from './TextArea';

export interface TextAreaFieldProps extends Omit<TextAreaProps, 'form'>, FieldProps {
/** Ref forwarded to the underlying textarea element as textareaRef */
innerRef?: (instance: HTMLTextAreaElement | null) => void;
}

const TextAreaField = ({ field, form, innerRef, isRequired, ...rest }: TextAreaFieldProps) => {
const { name } = field;
const { errors, touched } = form;
const isTouched = getProp(touched, name);
const error = isTouched ? getProp(errors, name) : null;
return (
<TextAreaPrimitive
{...field}
{...rest}
textareaRef={innerRef}
error={error as React.ReactNode}
hideOptionalLabel={isRequired}
/>
Comment thread
bonchevskyi marked this conversation as resolved.
);
};

export default TextAreaField;
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { mount, shallow } from 'enzyme';
import * as React from 'react';
import { mount, shallow } from 'enzyme';
import TetherComponent from 'react-tether';

import TextArea from '../TextArea';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
// @flow

import * as React from 'react';
import { shallow } from 'enzyme';

import TextAreaField from '../TextAreaField';

describe('components/text-area/TextAreaField', () => {
const getWrapper = (props = {}) => shallow(<TextAreaField {...props} />);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const getWrapper = (props: any = {}) => shallow(<TextAreaField {...props} />);

test('should render properly', () => {
const wrapper = getWrapper({
Expand Down
4 changes: 4 additions & 0 deletions src/components/text-area/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export { default } from './TextArea';
export { default as TextAreaField } from './TextAreaField';
export type { TextAreaProps } from './TextArea';
export type { TextAreaFieldProps } from './TextAreaField';
Comment thread
bonchevskyi marked this conversation as resolved.
Loading