-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
✨ [#52] Add formio textarea component
This was originally planned to use for the itemsExpression in the selectboxes component, but turned out to not be easy to manage to validate for valid JSON and eventually replaced with the already existing (courtesy of Laurens) JSONEdit component. Since we need to eventually implement the TextArea builder component anyway, it will be of good use later and was decided to keep it.
- Loading branch information
1 parent
e0ec215
commit 44cd6fe
Showing
4 changed files
with
203 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
import {expect} from '@storybook/jest'; | ||
import {Meta, StoryObj} from '@storybook/react'; | ||
import {userEvent, within} from '@storybook/testing-library'; | ||
|
||
import {withFormik} from '@/sb-decorators'; | ||
|
||
import TextArea from './textarea'; | ||
|
||
export default { | ||
title: 'Formio/Components/TextArea', | ||
component: TextArea, | ||
decorators: [withFormik], | ||
parameters: { | ||
modal: {noModal: true}, | ||
formik: {initialValues: {'my-textarea': 'initial value'}}, | ||
}, | ||
args: { | ||
name: 'my-textarea', | ||
}, | ||
} as Meta<typeof TextArea>; | ||
|
||
type Story = StoryObj<typeof TextArea>; | ||
|
||
export const Required: Story = { | ||
args: { | ||
required: true, | ||
label: 'A required textarea', | ||
}, | ||
}; | ||
|
||
export const WithoutLabel: Story = { | ||
args: { | ||
label: '', | ||
}, | ||
}; | ||
|
||
export const WithToolTip: Story = { | ||
args: { | ||
label: 'With tooltip', | ||
tooltip: 'Hiya!', | ||
required: false, | ||
}, | ||
}; | ||
|
||
export const Multiple: Story = { | ||
args: { | ||
label: 'Multiple inputs', | ||
description: 'Array of strings instead of a single string value', | ||
multiple: true, | ||
}, | ||
|
||
parameters: { | ||
formik: { | ||
initialValues: {'my-textarea': ['first value']}, | ||
}, | ||
}, | ||
|
||
play: async ({canvasElement}) => { | ||
const canvas = within(canvasElement); | ||
|
||
// check that new items can be added | ||
await userEvent.click(canvas.getByRole('button', {name: 'Add another'})); | ||
const input1 = canvas.getByTestId('input-my-textarea[0]'); | ||
await expect(input1).toHaveDisplayValue('first value'); | ||
await userEvent.clear(input1); | ||
await userEvent.type(input1, 'Foo'); | ||
await expect(input1).toHaveDisplayValue('Foo'); | ||
|
||
const input2 = canvas.getByTestId('input-my-textarea[1]'); | ||
await expect(input2).toHaveDisplayValue(''); | ||
|
||
// the label & description should be rendered only once, even with > 1 inputs | ||
await expect(canvas.queryAllByText('Multiple inputs')).toHaveLength(1); | ||
await expect( | ||
canvas.queryAllByText('Array of strings instead of a single string value') | ||
).toHaveLength(1); | ||
|
||
// finally, it should be possible delete rows again | ||
const removeButtons = await canvas.findAllByRole('button', {name: 'Remove item'}); | ||
await expect(removeButtons).toHaveLength(2); | ||
await userEvent.click(removeButtons[0]); | ||
await expect(canvas.getByTestId('input-my-textarea[0]')).toHaveDisplayValue(''); | ||
await expect(canvas.queryByTestId('input-my-textarea[1]')).not.toBeInTheDocument(); | ||
}, | ||
}; | ||
|
||
export const WithErrors: Story = { | ||
args: { | ||
label: 'With errors', | ||
}, | ||
|
||
parameters: { | ||
formik: { | ||
initialValues: {'my-textarea': ''}, | ||
initialErrors: {'my-textarea': 'Example error', 'other-field': 'Other error'}, | ||
}, | ||
}, | ||
|
||
play: async ({canvasElement}) => { | ||
const canvas = within(canvasElement); | ||
await expect(canvas.queryByText('Other error')).not.toBeInTheDocument(); | ||
await expect(canvas.queryByText('Example error')).toBeInTheDocument(); | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
import clsx from 'clsx'; | ||
import {Field, useFormikContext} from 'formik'; | ||
import {useContext, useRef} from 'react'; | ||
|
||
import {RenderContext} from '@/context'; | ||
import CharCount from '@/utils/charcount'; | ||
import {ErrorList, useValidationErrors} from '@/utils/errors'; | ||
|
||
import Component from './component'; | ||
import Description from './description'; | ||
import {withMultiple} from './multiple'; | ||
|
||
export interface TextAreaProps { | ||
name: string; | ||
label?: React.ReactNode; | ||
required?: boolean; | ||
tooltip?: string; | ||
description?: React.ReactNode; | ||
showCharCount?: boolean; | ||
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void; | ||
} | ||
|
||
export const TextArea: React.FC<JSX.IntrinsicElements['textarea'] & TextAreaProps> = ({ | ||
name, | ||
label, | ||
required = false, | ||
tooltip = '', | ||
description = '', | ||
showCharCount = false, | ||
onChange, | ||
...props | ||
}) => { | ||
const {getFieldProps, getFieldMeta} = useFormikContext(); | ||
const {value, onChange: formikOnChange} = getFieldProps<string | undefined>(name); | ||
const {touched} = getFieldMeta<string | undefined>(name); | ||
const {errors, hasErrors} = useValidationErrors(name); | ||
// const [{value}, {touched}] = useField<string | undefined>(name); | ||
const inputRef = useRef<HTMLInputElement>(null); | ||
const {bareInput} = useContext(RenderContext); | ||
|
||
const htmlId = `editform-${name}`; | ||
if (value === undefined && props.value === undefined) { | ||
props = {...props, value: ''}; | ||
} | ||
|
||
const inputField = ( | ||
<Field | ||
innerRef={inputRef} | ||
name={name} | ||
id={htmlId} | ||
as="textarea" | ||
type="textarea" | ||
className={clsx('form-control', {'is-invalid': hasErrors})} | ||
data-testid={`input-${name}`} | ||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => { | ||
formikOnChange(event); | ||
onChange?.(event); | ||
}} | ||
{...props} | ||
/> | ||
); | ||
|
||
const hasFocus = inputRef.current === document.activeElement; | ||
const charCount = showCharCount && (touched || hasFocus) && value && <CharCount value={value} />; | ||
|
||
// 'bare input' is actually a little bit more than just the input, looking at the | ||
// vanillay formio implementation. | ||
if (bareInput) { | ||
return ( | ||
<> | ||
{inputField} | ||
{charCount} | ||
<ErrorList errors={errors} /> | ||
</> | ||
); | ||
} | ||
|
||
// default-mode, wrapping the field with label, description etc. | ||
return ( | ||
<Component | ||
type="textarea" | ||
field={name} | ||
required={required} | ||
htmlId={htmlId} | ||
label={label} | ||
tooltip={tooltip} | ||
> | ||
<div>{inputField}</div> | ||
{charCount} | ||
{description && <Description text={description} />} | ||
</Component> | ||
); | ||
}; | ||
|
||
// make the TextArea component 'multiple' capable | ||
export const TextAreaMultiple = withMultiple(TextArea, ''); | ||
export default TextAreaMultiple; |