Skip to content

Commit

Permalink
feat(react): Added FormField, its subcomponents and FormFieldTextbox (#…
Browse files Browse the repository at this point in the history
…306)

# Contents
De volgende componenten toegevoegd: 
- FormField
- FormFieldLabel
- FormFieldDescription
- FormFieldErrorMessage
- Textbox
- FormFieldTextbox

## Checklist
- [X] New features/components and bugfixes are covered by tests
- [X] Changesets are created
- [X] Definition of Done is checked

---------

Co-authored-by: Vlad Afanasev <[email protected]>
Co-authored-by: Jaap-Hein Wester <[email protected]>
  • Loading branch information
3 people authored Nov 8, 2024
1 parent c834cc4 commit 2ac36c4
Show file tree
Hide file tree
Showing 35 changed files with 2,053 additions and 6 deletions.
5 changes: 5 additions & 0 deletions .changeset/nervous-eels-happen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@lux-design-system/components-react": minor
---

Nieuw component: Form Field
5 changes: 5 additions & 0 deletions .changeset/rotten-eyes-poke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@lux-design-system/components-react": minor
---

Nieuw component: Lux Form Field Text Input
5 changes: 5 additions & 0 deletions .changeset/thick-cats-agree.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@lux-design-system/components-react": minor
---

Nieuw component: Form Field Error Message
5 changes: 5 additions & 0 deletions .changeset/violet-frogs-report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@lux-design-system/components-react": minor
---

Nieuw component: Form Field Description
2 changes: 1 addition & 1 deletion packages/components-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
"dist/"
],
"dependencies": {
"@utrecht/component-library-css": "6.0.0",
"@utrecht/component-library-css": "6.1.0",
"@utrecht/component-library-react": "7.1.0",
"clsx": "2.1.1",
"date-fns": "3.6.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import {
FormFieldDescription as UtrechtFormFieldDescription,
FormFieldDescriptionProps as UtrechtFormFieldDescriptionProps,
} from '@utrecht/component-library-react/dist/css-module';
import clsx from 'clsx';

const FORM_FIELD_DESCRIPTION_CLASSES: Record<LuxFormFieldDescriptionAppearance, string> = {
valid: 'utrecht-form-field-description--valid',
invalid: 'utrecht-form-field-description--invalid',
};

export type LuxFormFieldDescriptionAppearance = 'valid' | 'invalid';
// Extend the Utrecht props but omit valid and invalid since we're replacing them
export interface LuxFormFieldDescriptionProps extends Omit<UtrechtFormFieldDescriptionProps, 'valid' | 'invalid'> {
appearance?: LuxFormFieldDescriptionAppearance;
}

export const LuxFormFieldDescription = (props: LuxFormFieldDescriptionProps) => {
const { appearance, className, ...restProps } = props;

const classNames = clsx(
{
[FORM_FIELD_DESCRIPTION_CLASSES.valid]: appearance === 'valid',
[FORM_FIELD_DESCRIPTION_CLASSES.invalid]: appearance === 'invalid',
},
className,
);

return <UtrechtFormFieldDescription {...restProps} className={classNames} />;
};

LuxFormFieldDescription.displayName = 'LuxFormFieldDescription';
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { describe, expect, it } from '@jest/globals';
import { render, screen } from '@testing-library/react';
import { LuxFormFieldDescription } from '../FormFieldDescription';

describe('Form Field Description', () => {
it('renders a basic description', () => {
render(<LuxFormFieldDescription>Test Description</LuxFormFieldDescription>);

const description = screen.getByText('Test Description');
expect(description).toBeInTheDocument();
});

it('applies the base class', () => {
render(<LuxFormFieldDescription>Test Description</LuxFormFieldDescription>);

const description = screen.getByText('Test Description');
expect(description).toHaveClass('utrecht-form-field-description');
});

it('can have an additional class name', () => {
render(<LuxFormFieldDescription className="custom-class">Test Description</LuxFormFieldDescription>);

const description = screen.getByText('Test Description');
expect(description).toHaveClass('utrecht-form-field-description');
expect(description).toHaveClass('custom-class');
});

it('passes through other HTML attributes', () => {
render(<LuxFormFieldDescription data-testid="test-description">Test Description</LuxFormFieldDescription>);

const description = screen.getByTestId('test-description');
expect(description).toBeInTheDocument();
});
it('renders content with a paragraph', () => {
render(
<LuxFormFieldDescription data-testid="rich-text-description">
<p>This is a paragraph</p>
</LuxFormFieldDescription>,
);
const description = screen.getByTestId('rich-text-description');
expect(description).toBeInTheDocument();

const paragraph = description.querySelector('p');
expect(paragraph).toBeInTheDocument();
expect(paragraph).toHaveTextContent('This is a paragraph');
});
it('renders rich text content', () => {
render(
<LuxFormFieldDescription data-testid="rich-text-description">
<strong>Bold</strong> Description
</LuxFormFieldDescription>,
);
const description = screen.getByTestId('rich-text-description');
expect(description).toBeInTheDocument();

const boldText = description.querySelector('strong');
expect(boldText).toBeInTheDocument();
expect(boldText).toHaveTextContent('Bold');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import {
FormFieldErrorMessage as UtrechtFormFieldErrorMessage,
FormFieldErrorMessageProps as UtrechtFormFieldErrorMessageProps,
} from '@utrecht/component-library-react/dist/css-module';
import clsx from 'clsx';
import { ForwardedRef, forwardRef, PropsWithChildren } from 'react';

const FORM_FIELD_ERROR_MESSAGE_CLASSES: { [key: string]: string } = {
distanced: 'utrecht-form-field-error-message--distanced',
};

export type LuxFormFieldErrorMessageProps = UtrechtFormFieldErrorMessageProps & {
distanced?: boolean;
};

export const LuxFormFieldErrorMessage = forwardRef(
(
{ children, className, distanced, ...restProps }: PropsWithChildren<LuxFormFieldErrorMessageProps>,
ref: ForwardedRef<HTMLParagraphElement>,
) => {
const classNames = clsx(
{
[FORM_FIELD_ERROR_MESSAGE_CLASSES.distanced]: distanced,
},
className,
);

return (
<UtrechtFormFieldErrorMessage {...restProps} ref={ref} className={classNames}>
{children}
</UtrechtFormFieldErrorMessage>
);
},
);

LuxFormFieldErrorMessage.displayName = 'LuxFormFieldErrorMessage';
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { describe, expect, it } from '@jest/globals';
import { render, screen } from '@testing-library/react';
import { LuxFormFieldErrorMessage } from '../FormFieldErrorMessage';

describe('Form Field Error Message', () => {
it('renders a basic error message', () => {
render(<LuxFormFieldErrorMessage>Test Error Message</LuxFormFieldErrorMessage>);

const errorMessage = screen.getByText('Test Error Message');
expect(errorMessage).toBeInTheDocument();
});

it('applies the base class', () => {
render(<LuxFormFieldErrorMessage>Test Error Message</LuxFormFieldErrorMessage>);

const errorMessage = screen.getByText('Test Error Message');
expect(errorMessage).toHaveClass('utrecht-form-field-error-message');
});

it('can have an additional class name', () => {
render(<LuxFormFieldErrorMessage className="custom-class">Test Error Message</LuxFormFieldErrorMessage>);

const errorMessage = screen.getByText('Test Error Message');
expect(errorMessage).toHaveClass('utrecht-form-field-error-message');
expect(errorMessage).toHaveClass('custom-class');
});

it('applies the distanced class when distanced prop is true', () => {
render(<LuxFormFieldErrorMessage distanced>Test Error Message</LuxFormFieldErrorMessage>);

const errorMessage = screen.getByText('Test Error Message');
expect(errorMessage).toHaveClass('utrecht-form-field-error-message--distanced');
});

it('passes through other HTML attributes', () => {
render(<LuxFormFieldErrorMessage data-testid="test-error-message">Test Error Message</LuxFormFieldErrorMessage>);

const errorMessage = screen.getByTestId('test-error-message');
expect(errorMessage).toBeInTheDocument();
});

it('renders rich text content', () => {
render(
<LuxFormFieldErrorMessage data-testid="rich-text-error-message">
<strong>Error:</strong> Invalid input
</LuxFormFieldErrorMessage>,
);
const errorMessage = screen.getByTestId('rich-text-error-message');
expect(errorMessage).toBeInTheDocument();

const strongText = errorMessage.querySelector('strong');
expect(strongText).toBeInTheDocument();
expect(strongText).toHaveTextContent('Error:');
});
});
41 changes: 41 additions & 0 deletions packages/components-react/src/form-field-label/FormFieldLabel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { FormLabel as UtrechtFormLabel } from '@utrecht/component-library-react/dist/css-module';
import clsx from 'clsx';
import { ForwardedRef, forwardRef, LabelHTMLAttributes, PropsWithChildren } from 'react';

const FORM_LABEL_CLASSES: { [key: string]: string } = {
checkbox: 'utrecht-form-label--checkbox',
radio: 'utrecht-form-label--radio',
disabled: 'utrecht-form-label--disabled',
checked: 'utrecht-form-label--checked',
};

export interface LuxFormFieldLabelProps extends LabelHTMLAttributes<HTMLLabelElement> {
type?: 'checkbox' | 'radio';
disabled?: boolean;
checked?: boolean;
}

export const LuxFormFieldLabel = forwardRef(
(
{ children, className, type, disabled, checked, ...restProps }: PropsWithChildren<LuxFormFieldLabelProps>,
ref: ForwardedRef<HTMLLabelElement>,
) => {
const classNames = clsx(
{
[FORM_LABEL_CLASSES.radio]: type === 'radio',
[FORM_LABEL_CLASSES.checkbox]: type === 'checkbox',
[FORM_LABEL_CLASSES.disabled]: disabled,
[FORM_LABEL_CLASSES.checked]: checked,
},
className,
);

return (
<UtrechtFormLabel {...restProps} ref={ref} className={classNames}>
{children}
</UtrechtFormLabel>
);
},
);

LuxFormFieldLabel.displayName = 'LuxFormFieldLabel';
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { describe, expect, it } from '@jest/globals';
import { render, screen } from '@testing-library/react';
import { LuxFormFieldLabel } from '../FormFieldLabel';

describe('Form Field Label', () => {
it('renders a basic label', () => {
render(<LuxFormFieldLabel htmlFor="test-input">Test Label</LuxFormFieldLabel>);

const label = screen.getByText('Test Label');
expect(label).toBeInTheDocument();
expect(label).toHaveAttribute('for', 'test-input');
});

it('applies the correct class for checkbox type', () => {
render(<LuxFormFieldLabel type="checkbox">Checkbox Label</LuxFormFieldLabel>);

const label = screen.getByText('Checkbox Label');
expect(label).toHaveClass('utrecht-form-label--checkbox');
});

it('applies the correct class for radio type', () => {
render(<LuxFormFieldLabel type="radio">Radio Label</LuxFormFieldLabel>);

const label = screen.getByText('Radio Label');
expect(label).toHaveClass('utrecht-form-label--radio');
});

it('applies the disabled class when disabled prop is true', () => {
render(<LuxFormFieldLabel disabled>Disabled Label</LuxFormFieldLabel>);

const label = screen.getByText('Disabled Label');
expect(label).toHaveClass('utrecht-form-label--disabled');
});

it('applies the checked class when checked prop is true', () => {
render(<LuxFormFieldLabel checked>Checked Label</LuxFormFieldLabel>);

const label = screen.getByText('Checked Label');
expect(label).toHaveClass('utrecht-form-label--checked');
});

it('can have an additional class name', () => {
render(<LuxFormFieldLabel className="custom-class">Custom Label</LuxFormFieldLabel>);

const label = screen.getByText('Custom Label');
expect(label).toHaveClass('custom-class');
expect(label).toHaveClass('utrecht-form-label');
});

it('passes through other HTML attributes', () => {
render(<LuxFormFieldLabel data-testid="test-label">Test Label</LuxFormFieldLabel>);

const label = screen.getByTestId('test-label');
expect(label).toBeInTheDocument();
});

it('renders rich text content', () => {
render(
<LuxFormFieldLabel data-testid="rich-text-label">
<strong>Bold</strong> Label
</LuxFormFieldLabel>,
);
const label = screen.getByTestId('rich-text-label');
expect(label).toBeInTheDocument();

const boldText = label.querySelector('strong');
expect(boldText).toBeInTheDocument();
expect(boldText).toHaveTextContent('Bold');
});

it('combines multiple classes correctly', () => {
render(
<LuxFormFieldLabel type="checkbox" disabled checked className="custom-class">
Complex Label
</LuxFormFieldLabel>,
);

const label = screen.getByText('Complex Label');
expect(label).toHaveClass('utrecht-form-label');
expect(label).toHaveClass('utrecht-form-label--checkbox');
expect(label).toHaveClass('utrecht-form-label--disabled');
expect(label).toHaveClass('utrecht-form-label--checked');
expect(label).toHaveClass('custom-class');
});
});
Loading

0 comments on commit 2ac36c4

Please sign in to comment.