Skip to content

Commit

Permalink
feat(components): add textarea (#53)
Browse files Browse the repository at this point in the history
  • Loading branch information
danielsimao authored Feb 5, 2024
1 parent d88fd24 commit 7a3bbba
Show file tree
Hide file tree
Showing 8 changed files with 346 additions and 11 deletions.
48 changes: 41 additions & 7 deletions packages/components/src/Input/BaseInput.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,36 @@
import { Sizes } from '@interlay/theme';
import { FocusEvent, forwardRef, InputHTMLAttributes, ReactNode, useEffect, useRef, useState } from 'react';
import { Sizes, Spacing } from '@interlay/theme';
import {
FocusEvent,
forwardRef,
InputHTMLAttributes,
ReactNode,
TextareaHTMLAttributes,
useEffect,
useRef,
useState
} from 'react';

import { ElementTypeProp } from '../utils/types';
import { Field, FieldProps, useFieldProps } from '../Field';
import { HelperTextProps } from '../HelperText';
import { LabelProps } from '../Label';
import { hasError } from '../utils/input';

import { Adornment, StyledBaseInput } from './Input.style';

// TODO: might need to consolidate this later
interface HTMLInputProps extends ElementTypeProp {
elementType?: 'input';
inputProps: InputHTMLAttributes<HTMLInputElement>;
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
}

interface HTMLTextAreaProps extends ElementTypeProp {
elementType?: 'textarea';
inputProps: TextareaHTMLAttributes<HTMLTextAreaElement>;
onChange?: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
}

type Props = {
label?: ReactNode;
labelProps?: LabelProps;
Expand All @@ -18,11 +41,10 @@ type Props = {
defaultValue?: string | ReadonlyArray<string> | number;
size?: Sizes;
isInvalid?: boolean;
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
minHeight?: Spacing;
onFocus?: (e: FocusEvent<Element>) => void;
onBlur?: (e: FocusEvent<Element>) => void;
inputProps: InputHTMLAttributes<HTMLInputElement>;
};
} & (HTMLInputProps | HTMLTextAreaProps);

type InheritAttrs = Omit<
HelperTextProps &
Expand All @@ -33,7 +55,17 @@ type BaseInputProps = Props & InheritAttrs;

const BaseInput = forwardRef<HTMLInputElement, BaseInputProps>(
(
{ startAdornment, endAdornment, bottomAdornment, size = 'medium', isInvalid, inputProps, ...props },
{
startAdornment,
endAdornment,
bottomAdornment,
size = 'medium',
isInvalid,
inputProps,
minHeight,
elementType = 'input',
...props
},
ref
): JSX.Element => {
const endAdornmentRef = useRef<HTMLDivElement>(null);
Expand All @@ -54,12 +86,14 @@ const BaseInput = forwardRef<HTMLInputElement, BaseInputProps>(
<Field {...fieldProps}>
{startAdornment && <Adornment $position='left'>{startAdornment}</Adornment>}
<StyledBaseInput
ref={ref}
ref={ref as any}
$adornments={{ bottom: !!bottomAdornment, left: !!startAdornment, right: !!endAdornment }}
$endAdornmentWidth={endAdornmentWidth}
$hasError={error}
$isDisabled={!!inputProps.disabled}
$minHeight={minHeight}
$size={size}
as={elementType}
{...inputProps}
/>
{bottomAdornment && <Adornment $position='bottom'>{bottomAdornment}</Adornment>}
Expand Down
7 changes: 6 additions & 1 deletion packages/components/src/Input/Input.style.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import styled from 'styled-components';
import { theme } from '@interlay/theme';
import { Spacing, theme } from '@interlay/theme';
import { Placement, Sizes } from '@interlay/theme';

type BaseInputProps = {
Expand All @@ -8,6 +8,7 @@ type BaseInputProps = {
$isDisabled: boolean;
$hasError: boolean;
$endAdornmentWidth: number;
$minHeight?: Spacing;
};

type AdornmentProps = {
Expand Down Expand Up @@ -60,6 +61,10 @@ const StyledBaseInput = styled.input<BaseInputProps>`
}};
padding-bottom: ${({ $adornments }) => ($adornments.bottom ? theme.spacing.spacing6 : theme.spacing.spacing2)};
min-height: ${({ $minHeight, as }) =>
$minHeight ? theme.spacing[$minHeight] : as === 'textarea' && theme.spacing.spacing16};
resize: ${({ as }) => as === 'textarea' && 'vertical'};
&:hover:not(:disabled):not(:focus) {
border: ${(props) => !props.$isDisabled && !props.$hasError && theme.border.focus};
}
Expand Down
10 changes: 9 additions & 1 deletion packages/components/src/Input/Input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,20 @@ type Props = {
value?: string;
defaultValue?: string;
onValueChange?: (value: string) => void;
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
};

type InheritAttrs = Omit<BaseInputProps, keyof Props | 'errorMessageProps' | 'descriptionProps' | 'inputProps'>;
type InheritAttrs = Omit<
BaseInputProps,
keyof Props | 'errorMessageProps' | 'descriptionProps' | 'inputProps' | 'elementType'
>;

type AriaAttrs = Omit<AriaTextFieldOptions<'input'>, (keyof Props & InheritAttrs) | 'onChange'>;

type InputProps = Props & InheritAttrs & AriaAttrs;

const elementType = 'input';

const Input = forwardRef<HTMLInputElement, InputProps>(
({ isInvalid, onValueChange, onChange, ...props }, ref): JSX.Element => {
const inputRef = useDOMRef(ref);
Expand All @@ -25,6 +31,7 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
{
...props,
isInvalid: isInvalid || !!props.errorMessage,
inputElementType: elementType,
onChange: onValueChange
},
inputRef
Expand All @@ -34,6 +41,7 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
<BaseInput
ref={inputRef}
descriptionProps={descriptionProps}
elementType={elementType}
errorMessageProps={errorMessageProps}
inputProps={mergeProps(inputProps, { onChange })}
labelProps={labelProps}
Expand Down
12 changes: 10 additions & 2 deletions packages/components/src/NumberInput/NumberInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,21 @@ type Props = {
value?: string | number;
defaultValue?: string | number;
onValueChange?: (value: string | number) => void;
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
inputMode?: 'numeric' | 'decimal';
};

type InheritAttrs = Omit<BaseInputProps, keyof Props | 'errorMessageProps' | 'descriptionProps' | 'inputProps'>;
type InheritAttrs = Omit<
BaseInputProps,
keyof Props | 'errorMessageProps' | 'descriptionProps' | 'inputProps' | 'elementType'
>;

type AriaAttrs = Omit<AriaTextFieldOptions<'input'>, keyof (Props & InheritAttrs)>;

type NumberInputProps = Props & InheritAttrs & AriaAttrs;

const elementType = 'input';

// FIXME: some event are running duplicate
const NumberInput = forwardRef<HTMLInputElement, NumberInputProps>(
(
Expand Down Expand Up @@ -56,7 +62,8 @@ const NumberInput = forwardRef<HTMLInputElement, NumberInputProps>(
inputMode,
isInvalid: isInvalid || !!props.errorMessage,
value: value,
autoComplete: 'off'
autoComplete: 'off',
inputElementType: elementType
},
inputRef
);
Expand All @@ -72,6 +79,7 @@ const NumberInput = forwardRef<HTMLInputElement, NumberInputProps>(
ref={inputRef}
autoCorrect='off'
descriptionProps={descriptionProps}
elementType={elementType}
errorMessageProps={errorMessageProps}
inputProps={mergeProps(inputProps, { onChange: handleChange })}
labelProps={labelProps}
Expand Down
98 changes: 98 additions & 0 deletions packages/components/src/TextArea/TextArea.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { Meta, StoryFn, StoryObj } from '@storybook/react';
import { useState } from 'react';
import { InformationCircle } from '@interlay/icons';

import { Flex, Span } from '..';

import { TextArea, TextAreaProps } from '.';

export default {
title: 'Forms/TextArea',
component: TextArea,
parameters: {
layout: 'centered'
},
args: {
label: 'Address'
}
} as Meta<typeof TextArea>;

export const Default: StoryObj<TextAreaProps> = {};

export const Controlled: StoryFn<TextAreaProps> = (args) => {
const [state, setState] = useState<string>();

return <TextArea {...args} value={state} onChange={(e) => setState(e.target.value)} />;
};

export const DefaultValue: StoryObj<TextAreaProps> = {
args: {
defaultValue: 'Sesame Street'
}
};

export const WithDescription: StoryObj<TextAreaProps> = {
args: {
description: 'Please enter your street address'
}
};

export const WithErrorMessage: StoryObj<TextAreaProps> = {
args: {
errorMessage: 'Please enter your street address'
}
};

export const WithMultipleErrorMessage: StoryObj<TextAreaProps> = {
args: {
errorMessage: ['Please enter your street address', 'Please enter your street address']
}
};

export const SideLabel: StoryObj<TextAreaProps> = {
args: {
labelPosition: 'side'
}
};

export const MaxWidth: StoryObj<TextAreaProps> = {
args: {
maxWidth: 'spacing28'
}
};

export const MinHeight: StoryObj<TextAreaProps> = {
args: {
minHeight: 'spacing28'
}
};

export const Adornments: StoryFn<TextAreaProps> = (args) => (
<Flex direction='column'>
<TextArea {...args} label='Start Adornment' startAdornment={<InformationCircle />} />
<TextArea {...args} endAdornment={<InformationCircle />} label='End Adornment' />
<TextArea
{...args}
bottomAdornment={
<Span color='tertiary' size='xs'>
$0.00
</Span>
}
label='Bottom Adornment'
/>
</Flex>
);

export const Sizes: StoryFn<TextAreaProps> = (args) => (
<Flex direction='column'>
<TextArea {...args} label='Small' size='small' />
<TextArea {...args} label='Medium' />
<TextArea {...args} label='Large' size='large' />
</Flex>
);

export const Disabled: StoryObj<TextAreaProps> = {
args: {
isDisabled: true
}
};
57 changes: 57 additions & 0 deletions packages/components/src/TextArea/TextArea.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { useDOMRef } from '@interlay/hooks';
import { AriaTextFieldOptions, useTextField } from '@react-aria/textfield';
import { mergeProps } from '@react-aria/utils';
import { forwardRef } from 'react';

import { BaseInput, BaseInputProps } from '../Input';

type Props = {
value?: string;
defaultValue?: string;
onValueChange?: (value: string) => void;
onChange?: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
};

type InheritAttrs = Omit<
BaseInputProps,
keyof Props | 'errorMessageProps' | 'descriptionProps' | 'inputProps' | 'elementType'
>;

type AriaAttrs = Omit<AriaTextFieldOptions<'textarea'>, (keyof Props & InheritAttrs) | 'onChange'>;

type TextAreaProps = Props & InheritAttrs & AriaAttrs;

const elementType = 'textarea';

const TextArea = forwardRef<HTMLTextAreaElement, TextAreaProps>(
({ isInvalid, onValueChange, onChange, ...props }, ref): JSX.Element => {
const inputRef = useDOMRef(ref);
// We are specifing `validationState` so that when there are errors, `aria-invalid` is set to `true`
const { inputProps, descriptionProps, errorMessageProps, labelProps } = useTextField(
{
...props,
isInvalid: isInvalid || !!props.errorMessage,
inputElementType: elementType,
onChange: onValueChange
},
inputRef
);

return (
<BaseInput
ref={inputRef as any}
descriptionProps={descriptionProps}
elementType={elementType}
errorMessageProps={errorMessageProps}
inputProps={mergeProps(inputProps, { onChange })}
labelProps={labelProps}
{...props}
/>
);
}
);

TextArea.displayName = 'TextArea';

export { TextArea };
export type { TextAreaProps };
Loading

0 comments on commit 7a3bbba

Please sign in to comment.