Skip to content

Commit

Permalink
✨ Copy over the TextField form component from the SDK
Browse files Browse the repository at this point in the history
Uses the underlying NL DS utrecht components, but without any styling
for now. This sets up the MVP pure-react form rendering to be used with
the Formio component definitions.
  • Loading branch information
sergei-maertens committed Dec 22, 2024
1 parent ea285fa commit ddba1bf
Show file tree
Hide file tree
Showing 12 changed files with 369 additions and 6 deletions.
30 changes: 30 additions & 0 deletions .storybook/decorators.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type {Decorator} from '@storybook/react';
import {Formik} from 'formik';

export const withFormik: Decorator = (Story, context) => {
const isDisabled = context.parameters?.formik?.disable ?? false;
if (isDisabled) {
return <Story />;
}
const initialValues = context.parameters?.formik?.initialValues || {};
const initialErrors = context.parameters?.formik?.initialErrors || {};
const initialTouched = context.parameters?.formik?.initialTouched || {};
const wrapForm = context.parameters?.formik?.wrapForm ?? true;
return (
<Formik
initialValues={initialValues}
initialErrors={initialErrors}
initialTouched={initialTouched}
enableReinitialize
onSubmit={(values, formikHelpers) => console.log(values, formikHelpers)}
>
{wrapForm ? (
<form id="storybook-withFormik-decorator-form" data-testid="storybook-formik-form">
<Story />
</form>
) : (
<Story />
)}
</Formik>
);
};
49 changes: 44 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
"typescript": "^5.7.2"
},
"dependencies": {
"@utrecht/component-library-react": "^1.0.0-alpha.353",
"clsx": "^2.1.0",
"formik": "^2.4.5"
}
Expand Down
22 changes: 22 additions & 0 deletions src/components/forms/HelpText.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import {FormFieldDescription} from '@utrecht/component-library-react';
import clsx from 'clsx';

export type HelpTextProps = {
children: React.ReactNode;
};

const HelpText: React.FC<HelpTextProps> = ({children}) => {
if (!children) return null;
return (
<FormFieldDescription
className={clsx(
'utrecht-form-field-description--openforms-helptext',
'utrecht-form-field__description'
)}
>
{children}
</FormFieldDescription>
);
};

export default HelpText;
50 changes: 50 additions & 0 deletions src/components/forms/Label.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import {FormLabel, Paragraph} from '@utrecht/component-library-react';
import clsx from 'clsx';

export interface LabelContentProps {
id: string;
children: React.ReactNode;
isDisabled?: boolean;
isRequired?: boolean;
type?: string;
}

export const LabelContent: React.FC<LabelContentProps> = ({
id,
isDisabled = false,
isRequired = false,
type,
children,
}) => {
// TODO: add support for required-fields-with-asterisk configuration (via context)
return (
<FormLabel
htmlFor={id}
disabled={isDisabled}
className={clsx({
'utrecht-form-label--openforms': true,
'utrecht-form-label--openforms-required': isRequired,
[`utrecht-form-label--${type}`]: type,
})}
>
{children}
</FormLabel>
);
};

export interface LabelProps {
id: string;
children: React.ReactNode;
isDisabled?: boolean;
isRequired?: boolean;
}

const Label: React.FC<LabelProps> = ({id, isRequired = false, isDisabled = false, children}) => (
<Paragraph className="utrecht-form-field__label">
<LabelContent id={id} isRequired={isRequired} isDisabled={isDisabled}>
{children}
</LabelContent>
</Paragraph>
);

export default Label;
32 changes: 32 additions & 0 deletions src/components/forms/TextField/TextField.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import {ArgTypes, Canvas, Meta, Story} from '@storybook/blocks';

import * as TextFieldStories from './TextField.stories';

<Meta of={TextFieldStories} />

# TextField

A text-based input field with label, help text and validation errors.

The value of the field is tracked in the Formik parent state, linked through the `name` prop. You
can use `lodash.get` syntax for the name, e.g. `foo.bar` will look up the key `bar` inside the
object `foo`.

<Canvas of={TextFieldStories.Default} />

## Props

<ArgTypes of={TextFieldStories} />

## Validation errors

Validation errors are tracked in the Formik state and displayed if any are present.

<Story of={TextFieldStories.ValidationError} />

## No asterisks

The backend can be configured to treat fields as required by default and instead mark optional
fields explicitly, through the `ConfigContext`.

{/* <Story of={TextFieldStories.NoAsterisks} /> */}
84 changes: 84 additions & 0 deletions src/components/forms/TextField/TextField.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import {Meta, StoryObj} from '@storybook/react';
import {expect, userEvent, within} from '@storybook/test';

import {withFormik} from '@/sb-decorators';

import TextField from './TextField';

export default {
title: 'Internal API / Forms / TextField',
component: TextField,
decorators: [withFormik],
parameters: {
formik: {
initialValues: {
test: '',
},
},
},
} satisfies Meta<typeof TextField>;

type Story = StoryObj<typeof TextField>;

export const Default: Story = {
args: {
name: 'test',
label: 'test',
description: 'This is a custom description',
isDisabled: false,
isRequired: true,
},
play: async ({canvasElement}) => {
const canvas = within(canvasElement);
await expect(canvas.getByRole('textbox')).toBeVisible();
await expect(canvas.getByText('test')).toBeVisible();
await expect(canvas.getByText('This is a custom description')).toBeVisible();
// Check if clicking on the label focuses the input
const label = canvas.getByText('test');
await userEvent.click(label);
await expect(canvas.getByRole('textbox')).toHaveFocus();
},
};

export const ValidationError: Story = {
name: 'Validation error',
parameters: {
formik: {
initialValues: {
textinput: 'some text',
},
initialErrors: {
textinput: 'invalid',
},
initialTouched: {
textinput: true,
},
},
},
args: {
name: 'textinput',
label: 'Text field',
description: 'Description above the errors',
isDisabled: false,
isRequired: true,
},
play: async ({canvasElement}) => {
const canvas = within(canvasElement);
await expect(canvas.getByText('invalid')).toBeVisible();
},
};

// export const NoAsterisks = {
// name: 'No asterisk for required',
// decorators: [ConfigDecorator],
// parameters: {
// config: {
// requiredFieldsWithAsterisk: false,
// },
// },
// args: {
// name: 'test',
// label: 'Default required',
// isRequired: true,
// },
// };
Loading

0 comments on commit ddba1bf

Please sign in to comment.