Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test(components-react): add visual baseline to all formfield components #347

Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@
outline: none;
}

&:not(#{&}--disabled):has(.utrecht-form-field__input .utrecht-checkbox:hover)::before {
&--with-target:not(#{&}--disabled):has(.utrecht-form-field__input .utrecht-checkbox:hover)::before {
border-color: var(--lux-form-field-checkbox-hover-inner-border-color);
background-color: var(--lux-form-field-checkbox-hover-inner-background-color);
color: var(--lux-form-field-checkbox-hover-inner-color);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import clsx from 'clsx';
import { useId } from 'react';
import { LuxCheckbox } from '../checkbox/Checkbox';
import { LuxCheckbox, LuxCheckboxProps } from '../checkbox/Checkbox';
import { LuxFormField, LuxFormFieldProps } from '../form-field/FormField';
import {
LuxFormFieldDescription,
Expand All @@ -9,14 +9,16 @@ import {
import { LuxFormFieldErrorMessage } from '../form-field-error-message/FormFieldErrorMessage';
import { LuxFormFieldLabel } from '../form-field-label/FormFieldLabel';
import './FormFieldCheckbox.scss';
import { pick } from '../utils/object';

export type LuxFormFieldCheckboxProps = LuxFormFieldProps & {
checked?: boolean;
disabled?: boolean;
appearance?: LuxFormFieldDescriptionAppearance;
withTarget?: boolean;
distanced?: boolean;
};
export type LuxFormFieldCheckboxProps = LuxFormFieldProps &
LuxCheckboxProps & {
checked?: boolean;
disabled?: boolean;
appearance?: LuxFormFieldDescriptionAppearance;
withTarget?: boolean;
distanced?: boolean;
};

export const LuxFormFieldCheckbox = ({
label,
Expand Down Expand Up @@ -73,6 +75,17 @@ export const LuxFormFieldCheckbox = ({
errorMessage
);

const checkBoxAttrs = pick(restProps, [
'required',
'inputRequired',
'value',
'defaultValue',
'onFocus',
'onBlur',
'onInput',
'onChange',
]);

return (
<LuxFormField
type="checkbox"
Expand All @@ -81,7 +94,14 @@ export const LuxFormFieldCheckbox = ({
errorMessage={errorMessageNode}
invalid={invalid}
input={
<LuxCheckbox id={inputId} disabled={disabled} invalid={invalid} checked={checked} withTarget={withTarget} />
<LuxCheckbox
id={inputId}
disabled={disabled}
invalid={invalid}
checked={checked}
withTarget={withTarget}
{...checkBoxAttrs}
/>
}
className={clsx('lux-form-field-checkbox', {
'lux-form-field-checkbox--invalid': invalid,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { describe, expect, it } from '@jest/globals';
import { render, screen } from '@testing-library/react';
import { LuxFormFieldCheckbox } from '../FormFieldCheckbox';

describe('Form Field Checkbox', () => {
it('renders a basic form field checkbox with label and input', () => {
render(<LuxFormFieldCheckbox label="Name" />);

expect(screen.getByText('Name')).toBeInTheDocument();
expect(screen.getByRole('checkbox')).toBeInTheDocument();
});

it('applies the base class', () => {
render(<LuxFormFieldCheckbox label="Name" />);

const formField = screen.getByText('Name').closest('.utrecht-form-field');
expect(formField).toHaveClass('utrecht-form-field');
});

it('can have an additional class name', () => {
render(<LuxFormFieldCheckbox label="Name" className="custom-class" />);

const formField = screen.getByText('Name').closest('.utrecht-form-field');
expect(formField).toHaveClass('utrecht-form-field');
expect(formField).toHaveClass('custom-class');
});

it('renders description when provided', () => {
render(<LuxFormFieldCheckbox label="Name" description="Enter your full name" />);

expect(screen.getByText('Enter your full name')).toBeInTheDocument();
});

it('renders error message when invalid and error message provided', () => {
render(<LuxFormFieldCheckbox label="Name" invalid={true} errorMessage="Name is required" />);

expect(screen.getByText('Name is required')).toBeInTheDocument();
});

it('adds the correct attributes to the Checkbox', () => {
render(<LuxFormFieldCheckbox label="Name" checked required />);

const checkbox = screen.getByRole('checkbox');

expect(checkbox).toBeInTheDocument();
expect(checkbox).toBeChecked();
expect(checkbox).toHaveAttribute('aria-required', 'true');
expect(checkbox).not.toHaveAttribute('required');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ import React, { ForwardedRef, forwardRef } from 'react';
import './FormFieldRadioGroup.css';
import { LuxFormFieldRadioOption } from '../form-field-radio-option/FormFieldRadioOption';

type RadioOptionValue = string | number;

interface RadioOption {
value: string;
value: RadioOptionValue;
label: string;
disabled?: boolean;
description?: React.ReactNode;
Expand All @@ -16,7 +18,7 @@ export interface LuxFormFieldRadioGroupProps {
description?: string;
errorMessage?: string;
options: RadioOption[];
value?: string;
value?: RadioOptionValue;
invalid?: boolean;
required?: boolean;
className?: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ import {
} from '../form-field-description/FormFieldDescription';
import { LuxFormFieldErrorMessage } from '../form-field-error-message/FormFieldErrorMessage';
import { LuxFormFieldLabel } from '../form-field-label/FormFieldLabel';
import { LuxSelect, type LuxSelectProps } from '../select/Select';
import { LuxSelect, LuxSelectOption, type LuxSelectOptionProps, type LuxSelectProps } from '../select/Select';

export type LuxFormFieldSelectOptionsProps = LuxSelectOptionProps & {
label?: string | number;
};

export interface LuxFormFieldSelectProps
extends Omit<UtrechtFormFieldProps, 'type' | 'onBlur' | 'onChange' | 'onFocus'>,
Expand All @@ -27,13 +31,15 @@ export interface LuxFormFieldSelectProps
| 'size'
| 'value'
> {
options?: LuxFormFieldSelectOptionsProps[];
appearance?: LuxFormFieldDescriptionAppearance;
distanced?: boolean;
inputRef?: Ref<HTMLSelectElement>;
}

export const LuxFormFieldSelect = ({
label,
options,
description,
errorMessage,
disabled,
Expand Down Expand Up @@ -114,10 +120,17 @@ export const LuxFormFieldSelect = ({
}) || undefined
}
{...selectAttrs}
/>
>
{options
? options.map((option) => (
<LuxSelectOption {...option} key={option.value}>
{option.label}
</LuxSelectOption>
))
: formFieldAttrs['children']}
</LuxSelect>
}
className={className}
{...formFieldAttrs}
/>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
import { LuxFormFieldErrorMessage } from '../form-field-error-message/FormFieldErrorMessage';
import { LuxFormFieldLabel } from '../form-field-label/FormFieldLabel';
import { type Direction, LuxTextbox } from '../textbox/Textbox';
import { pick } from '../utils/object';

export type LuxFormFieldTextboxProps = UtrechtFormFieldTextboxProps & {
appearance?: LuxFormFieldDescriptionAppearance;
Expand Down Expand Up @@ -60,15 +61,6 @@ export const LuxFormFieldTextbox = ({
errorMessage
);

// TODO: naar utils
function pick<T extends object, U extends keyof T>(obj: T, paths: Array<U>): Pick<T, U> {
const ret = {} as Pick<T, U>;
for (const k of paths) {
ret[k] = obj[k];
}
return ret;
}

const textBoxAttrs = pick(restProps, [
'autoComplete',
'min',
Expand Down
6 changes: 5 additions & 1 deletion packages/components-react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@ export {
type LuxFormFieldErrorMessageProps,
} from './form-field-error-message/FormFieldErrorMessage';
export { LuxFormFieldLabel, type LuxFormFieldLabelProps } from './form-field-label/FormFieldLabel';
export { LuxFormFieldSelect, type LuxFormFieldSelectProps } from './form-field-select/FormFieldSelect';
export {
LuxFormFieldSelect,
type LuxFormFieldSelectProps,
type LuxFormFieldSelectOptionsProps,
} from './form-field-select/FormFieldSelect';
export { LuxFormFieldTextbox, type LuxFormFieldTextboxProps } from './form-field-textbox/FormFieldTextbox';
export { LuxParagraph, type LuxParagraphProps } from './paragraph/Paragraph';
export {
Expand Down
7 changes: 4 additions & 3 deletions packages/components-react/src/select/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@ import {
} from '@utrecht/component-library-react/dist/css-module';
import './Select.css';
import clsx from 'clsx';
import { forwardRef } from 'react';
import { ForwardedRef, forwardRef } from 'react';

export type LuxSelectProps = UtrechtSelectProps;
export type LuxSelectOptionProps = UtrechtSelectOptionProps;

export const LuxSelect = forwardRef((props: LuxSelectProps) => {
export const LuxSelect = forwardRef((props: LuxSelectProps, ref: ForwardedRef<HTMLSelectElement>) => {
const { className, ...restProps } = props;

return <UtrechtSelect className={clsx(className, 'lux-select')} {...restProps} />;
return <UtrechtSelect ref={ref} className={clsx(className, 'lux-select')} {...restProps} />;
});

export const LuxSelectOption = SelectOption;
Expand Down
17 changes: 17 additions & 0 deletions packages/components-react/src/utils/object.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* Pick only certain keys form object.
*
* @export
* @template {object} T
* @template {keyof T} U
* @param {T} obj Object to pick from
* @param {Array<U>} keys Keys to pick
* @returns {Pick<T, U>} Object containing only picked keys
*/
export function pick<T extends object, U extends keyof T>(obj: T, keys: Array<U>): Pick<T, U> {
const ret = {} as Pick<T, U>;
for (const k of keys) {
ret[k] = obj[k];
}
return ret;
}
33 changes: 33 additions & 0 deletions packages/storybook/config/components.ts
remypar5 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {
LuxButton,
LuxDocument,
LuxHeading1,
LuxHeading2,
LuxHeading3,
LuxHeading4,
LuxHeading5,
LuxHeading6,
LuxHeadingGroup,
LuxLink,
LuxParagraph,
LuxSection,
LuxSelect,
LuxTextbox,
} from '@lux-design-system/components-react';

export default {
a: LuxLink,
body: LuxDocument,
button: LuxButton,
h1: LuxHeading1,
h2: LuxHeading2,
h3: LuxHeading3,
h4: LuxHeading4,
h5: LuxHeading5,
h6: LuxHeading6,
hgroup: LuxHeadingGroup,
input: LuxTextbox,
p: LuxParagraph,
section: LuxSection,
select: LuxSelect,
};
21 changes: 21 additions & 0 deletions packages/storybook/src/react-components/checkbox/visual/States.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { LuxCheckbox } from '@lux-design-system/components-react';

export const VisualStates = () => (
<>
<div>
<LuxCheckbox /> Default
</div>
<div>
<LuxCheckbox checked /> Checked
</div>
<div>
<LuxCheckbox disabled /> Disabled
</div>
<div>
<LuxCheckbox checked disabled /> Checked & Disabled
</div>
<div>
<LuxCheckbox withTarget /> With Target
</div>
</>
);
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { LuxFormFieldCheckbox, type LuxFormFieldCheckboxProps } from '@lux-design-system/components-react';
import tokens from '@lux-design-system/design-tokens/dist/index.json';
import type { Meta, StoryObj } from '@storybook/react';
import { VisualStates } from './visual/States';
import { BADGES } from '../../../config/preview';
import { createDesignTokensStory, createVisualRegressionStory, VisualRegressionWrapper } from '../../utils';
import CheckboxMeta from '../checkbox/checkbox.stories';
import FormFieldDescriptionMeta from '../form-field-description/form-field-description.stories';
import FormFieldErrorMessageMeta from '../form-field-error-message/form-field-error-message.stories';
Expand Down Expand Up @@ -91,6 +93,14 @@ export const WithTarget: Story = {
...Playground.args,
withTarget: true,
},
parameters: {
docs: {
description: {
story:
'Met `withTarget` wordt het hele component (behalve de foutmelding) een klikdoel. _Let op:_ dit kan voor gebruikers onverwacht zijn.',
},
},
},
};

export const withLongTexts: Story = {
Expand All @@ -105,3 +115,18 @@ export const withLongTexts: Story = {
withTarget: true,
},
};

export const DesignTokens = createDesignTokensStory(meta);

export const Visual = createVisualRegressionStory(() => (
<>
<h4 className="utrecht-heading-3">Light</h4>
<VisualRegressionWrapper className={`lux-theme--logius-light`}>
remypar5 marked this conversation as resolved.
Show resolved Hide resolved
<VisualStates />
</VisualRegressionWrapper>
<h4 className="utrecht-heading-3">Dark</h4>
<VisualRegressionWrapper className={`lux-theme--logius-dark`}>
<VisualStates />
</VisualRegressionWrapper>
</>
));
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { LuxFormFieldCheckbox } from '@lux-design-system/components-react';

export const VisualStates = () => (
<>
<LuxFormFieldCheckbox label="Label" />
<LuxFormFieldCheckbox label="Label" checked />
<LuxFormFieldCheckbox label="Label" description="Description" />
<h5 className="utrecht-heading-4">Hover &amp; Focus</h5>
<div className="pseudo-hover-all">
<LuxFormFieldCheckbox label="Label" />
</div>
<div className="pseudo-focus-all pseudo-focus-visible-all">
<LuxFormFieldCheckbox label="Label" />
</div>
<h5 className="utrecht-heading-4">Invalid</h5>
<LuxFormFieldCheckbox label="Label" errorMessage="Error Message" invalid />
<LuxFormFieldCheckbox label="Label" description="Description" errorMessage="Error Message" invalid />
<h5 className="utrecht-heading-4">Disabled</h5>
<LuxFormFieldCheckbox label="Label" disabled />
<LuxFormFieldCheckbox label="Label" disabled checked />
<LuxFormFieldCheckbox label="Label" description="Description" disabled />
</>
);
Loading
Loading