diff --git a/.changeset/beige-swans-glow.md b/.changeset/beige-swans-glow.md new file mode 100644 index 000000000..181d561ca --- /dev/null +++ b/.changeset/beige-swans-glow.md @@ -0,0 +1,7 @@ +--- +"@lux-design-system/design-tokens": minor +--- + +In deze commit: + +- Nieuwe tokens: utrecht component button group diff --git a/.changeset/good-windows-smash.md b/.changeset/good-windows-smash.md new file mode 100644 index 000000000..d137c4b5f --- /dev/null +++ b/.changeset/good-windows-smash.md @@ -0,0 +1,8 @@ +--- +"@lux-design-system/components-react": major +--- + +In deze commit: + +- Nieuw component: LuxPreHeading +- Nieuw component: LuxHeadingGroup diff --git a/.changeset/hungry-books-hug.md b/.changeset/hungry-books-hug.md new file mode 100644 index 000000000..3f60bf717 --- /dev/null +++ b/.changeset/hungry-books-hug.md @@ -0,0 +1,5 @@ +--- +"@lux-design-system/components-react": minor +--- + +Nieuw component: LuxAlert diff --git a/.changeset/nervous-eels-happen.md b/.changeset/nervous-eels-happen.md new file mode 100644 index 000000000..f1b2618d4 --- /dev/null +++ b/.changeset/nervous-eels-happen.md @@ -0,0 +1,5 @@ +--- +"@lux-design-system/components-react": minor +--- + +Nieuw component: Form Field diff --git a/.changeset/rotten-eyes-poke.md b/.changeset/rotten-eyes-poke.md new file mode 100644 index 000000000..49c758351 --- /dev/null +++ b/.changeset/rotten-eyes-poke.md @@ -0,0 +1,5 @@ +--- +"@lux-design-system/components-react": minor +--- + +Nieuw component: Lux Form Field Text Input diff --git a/.changeset/thick-cats-agree.md b/.changeset/thick-cats-agree.md new file mode 100644 index 000000000..563288833 --- /dev/null +++ b/.changeset/thick-cats-agree.md @@ -0,0 +1,5 @@ +--- +"@lux-design-system/components-react": minor +--- + +Nieuw component: Form Field Error Message diff --git a/.changeset/violet-frogs-report.md b/.changeset/violet-frogs-report.md new file mode 100644 index 000000000..23489a551 --- /dev/null +++ b/.changeset/violet-frogs-report.md @@ -0,0 +1,5 @@ +--- +"@lux-design-system/components-react": minor +--- + +Nieuw component: Form Field Description diff --git a/.lux.stylelintrc.json b/.lux.stylelintrc.json index b21b4e4fa..165d431ac 100644 --- a/.lux.stylelintrc.json +++ b/.lux.stylelintrc.json @@ -14,7 +14,7 @@ "order/properties-alphabetical-order": null, "scss/dollar-variable-pattern": "^(lux)-[a-z0-9-]+$", "scss/percent-placeholder-pattern": "^(lux)-[a-z0-9-]+$", - "custom-property-pattern": "^_?(lux)-[a-z0-9-]+$", + "custom-property-pattern": "^_?(lux|utrecht)-[a-z0-9-]+$", "selector-class-pattern": "^(lux)-[a-z0-9_-]+|(force-state)--[a-z]+$", "keyframes-name-pattern": "^(lux)-[a-z0-9-]+$" } diff --git a/packages/components-react/package.json b/packages/components-react/package.json index 9f76fa788..5b24a2910 100644 --- a/packages/components-react/package.json +++ b/packages/components-react/package.json @@ -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", diff --git a/packages/components-react/src/alert/Alert.css b/packages/components-react/src/alert/Alert.css new file mode 100644 index 000000000..d5dbcbadb --- /dev/null +++ b/packages/components-react/src/alert/Alert.css @@ -0,0 +1,20 @@ +.lux-alert { + --utrecht-heading-1-color: var(--utrecht-alert-color); + --utrecht-heading-1-font-size: var(--lux-alert-heading-font-size); + --utrecht-heading-1-line-height: var(--lux-alert-heading-font-size); + --utrecht-heading-2-color: var(--utrecht-alert-color); + --utrecht-heading-2-font-size: var(--lux-alert-heading-font-size); + --utrecht-heading-2-line-height: var(--lux-alert-heading-font-size); + --utrecht-heading-3-color: var(--utrecht-alert-color); + --utrecht-heading-3-font-size: var(--lux-alert-heading-font-size); + --utrecht-heading-3-line-height: var(--lux-alert-heading-font-size); + --utrecht-heading-4-color: var(--utrecht-alert-color); + --utrecht-heading-4-font-size: var(--lux-alert-heading-font-size); + --utrecht-heading-4-line-height: var(--lux-alert-heading-font-size); + --utrecht-heading-5-color: var(--utrecht-alert-color); + --utrecht-heading-5-font-size: var(--lux-alert-heading-font-size); + --utrecht-heading-5-line-height: var(--lux-alert-heading-font-size); + --utrecht-heading-6-color: var(--utrecht-alert-color); + --utrecht-heading-6-font-size: var(--lux-alert-heading-font-size); + --utrecht-heading-6-line-height: var(--lux-alert-heading-font-size); +} diff --git a/packages/components-react/src/alert/Alert.tsx b/packages/components-react/src/alert/Alert.tsx new file mode 100644 index 000000000..125385553 --- /dev/null +++ b/packages/components-react/src/alert/Alert.tsx @@ -0,0 +1,60 @@ +import { + Alert as UtrechtAlert, + AlertProps as UtrechtAlertProps, + AlertType as UtrechtAlertType, +} from '@utrecht/component-library-react/dist/css-module'; +import './Alert.css'; + +type AlertType = Exclude | 'success'; + +export interface LuxAlertProps extends Omit { + type: AlertType; +} + +//TODO replace icons in #308 +const InfoIcon = () => ( + + + +); +const SuccessIcon = () => ( + + + +); +const WarningIcon = () => ( + + + +); +const ErrorIcon = () => ( + + + +); + +export const LuxAlert = (props: LuxAlertProps) => { + const { children, type, className, ...otherProps } = props; + const utrechtAlertType: UtrechtAlertType = type === 'success' ? 'ok' : type; + + const icons = { + info: InfoIcon, + success: SuccessIcon, + warning: WarningIcon, + error: ErrorIcon, + }; + + const Icon = icons[type]; + const icon = Icon ? : <>; + + return ( + + {children} + + ); +}; diff --git a/packages/components-react/src/alert/test/Alert.spec.tsx b/packages/components-react/src/alert/test/Alert.spec.tsx new file mode 100644 index 000000000..85bf7ea66 --- /dev/null +++ b/packages/components-react/src/alert/test/Alert.spec.tsx @@ -0,0 +1,99 @@ +import { describe, expect, it } from '@jest/globals'; +import { render } from '@testing-library/react'; +import { LuxHeading1, LuxParagraph } from '../../index'; +import { LuxAlert } from '../Alert'; + +describe('Alert', () => { + it('renders an info alert', () => { + const { container } = render( + + Heading + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam quis massa lorem. Ut laoreet varius rhoncus. + + , + ); + + const alert = container.querySelector(':only-child'); + expect(alert).toBeInTheDocument(); + + expect(alert).toHaveClass('utrecht-alert--info'); + }); + + it('renders an success alert', () => { + const { container } = render( + + Heading + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam quis massa lorem. Ut laoreet varius rhoncus. + + , + ); + + const alert = container.querySelector(':only-child'); + expect(alert).toBeInTheDocument(); + + expect(alert).toHaveClass('utrecht-alert--ok'); + }); + + it('renders an warning alert', () => { + const { container } = render( + + Heading + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam quis massa lorem. Ut laoreet varius rhoncus. + + , + ); + + const alert = container.querySelector(':only-child'); + expect(alert).toBeInTheDocument(); + + expect(alert).toHaveClass('utrecht-alert--warning'); + }); + + it('renders an error alert', () => { + const { container } = render( + + Heading + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam quis massa lorem. Ut laoreet varius rhoncus. + + , + ); + + const alert = container.querySelector(':only-child'); + expect(alert).toBeInTheDocument(); + + expect(alert).toHaveClass('utrecht-alert--error'); + }); + + it('can have an additional class name', () => { + const { container } = render( + + Heading + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam quis massa lorem. Ut laoreet varius rhoncus. + + , + ); + + const alert = container.querySelector(':only-child'); + + expect(alert).toHaveClass('custom-alert'); + + expect(alert).toHaveClass('lux-alert'); + }); + + it('can have extra properties', () => { + const { container } = render( + , + ); + + const alert = container.querySelector(':only-child'); + + expect(alert).not.toBeVisible(); + }); +}); diff --git a/packages/components-react/src/button/Button.tsx b/packages/components-react/src/button/Button.tsx index 9f5c5b168..9a19ef344 100644 --- a/packages/components-react/src/button/Button.tsx +++ b/packages/components-react/src/button/Button.tsx @@ -25,7 +25,7 @@ const ICON_POSITIONS: { [key: string]: string } = { export const LuxButton = (props: LuxButtonProps) => { const { size, icon: iconNode, iconPosition, ...otherProps } = props; - const className = `lux-button ${size !== undefined ? SIZE_CLASSNAME[size] : ''}`; + const className = `lux-button ${size !== undefined ? SIZE_CLASSNAME[size] : ''} ${otherProps.className || ''}`; const positionedIcon = React.Children.map(iconNode, (iconElement) => { if (!iconElement) { diff --git a/packages/components-react/src/form-field-description/FormFieldDescription.tsx b/packages/components-react/src/form-field-description/FormFieldDescription.tsx new file mode 100644 index 000000000..6778978ee --- /dev/null +++ b/packages/components-react/src/form-field-description/FormFieldDescription.tsx @@ -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 = { + 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 { + 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 ; +}; + +LuxFormFieldDescription.displayName = 'LuxFormFieldDescription'; diff --git a/packages/components-react/src/form-field-description/test/FormFieldDescription.spec.tsx b/packages/components-react/src/form-field-description/test/FormFieldDescription.spec.tsx new file mode 100644 index 000000000..588013a0b --- /dev/null +++ b/packages/components-react/src/form-field-description/test/FormFieldDescription.spec.tsx @@ -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(Test Description); + + const description = screen.getByText('Test Description'); + expect(description).toBeInTheDocument(); + }); + + it('applies the base class', () => { + render(Test Description); + + const description = screen.getByText('Test Description'); + expect(description).toHaveClass('utrecht-form-field-description'); + }); + + it('can have an additional class name', () => { + render(Test Description); + + 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(Test Description); + + const description = screen.getByTestId('test-description'); + expect(description).toBeInTheDocument(); + }); + it('renders content with a paragraph', () => { + render( + +

This is a paragraph

+
, + ); + 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( + + Bold Description + , + ); + const description = screen.getByTestId('rich-text-description'); + expect(description).toBeInTheDocument(); + + const boldText = description.querySelector('strong'); + expect(boldText).toBeInTheDocument(); + expect(boldText).toHaveTextContent('Bold'); + }); +}); diff --git a/packages/components-react/src/form-field-error-message/FormFieldErrorMessage.tsx b/packages/components-react/src/form-field-error-message/FormFieldErrorMessage.tsx new file mode 100644 index 000000000..b13012b35 --- /dev/null +++ b/packages/components-react/src/form-field-error-message/FormFieldErrorMessage.tsx @@ -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, + ref: ForwardedRef, + ) => { + const classNames = clsx( + { + [FORM_FIELD_ERROR_MESSAGE_CLASSES.distanced]: distanced, + }, + className, + ); + + return ( + + {children} + + ); + }, +); + +LuxFormFieldErrorMessage.displayName = 'LuxFormFieldErrorMessage'; diff --git a/packages/components-react/src/form-field-error-message/test/FormFieldErrorMessage.spec.tsx b/packages/components-react/src/form-field-error-message/test/FormFieldErrorMessage.spec.tsx new file mode 100644 index 000000000..9aebbd44f --- /dev/null +++ b/packages/components-react/src/form-field-error-message/test/FormFieldErrorMessage.spec.tsx @@ -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(Test Error Message); + + const errorMessage = screen.getByText('Test Error Message'); + expect(errorMessage).toBeInTheDocument(); + }); + + it('applies the base class', () => { + render(Test Error Message); + + const errorMessage = screen.getByText('Test Error Message'); + expect(errorMessage).toHaveClass('utrecht-form-field-error-message'); + }); + + it('can have an additional class name', () => { + render(Test Error Message); + + 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(Test Error Message); + + const errorMessage = screen.getByText('Test Error Message'); + expect(errorMessage).toHaveClass('utrecht-form-field-error-message--distanced'); + }); + + it('passes through other HTML attributes', () => { + render(Test Error Message); + + const errorMessage = screen.getByTestId('test-error-message'); + expect(errorMessage).toBeInTheDocument(); + }); + + it('renders rich text content', () => { + render( + + Error: Invalid input + , + ); + const errorMessage = screen.getByTestId('rich-text-error-message'); + expect(errorMessage).toBeInTheDocument(); + + const strongText = errorMessage.querySelector('strong'); + expect(strongText).toBeInTheDocument(); + expect(strongText).toHaveTextContent('Error:'); + }); +}); diff --git a/packages/components-react/src/form-field-label/FormFieldLabel.tsx b/packages/components-react/src/form-field-label/FormFieldLabel.tsx new file mode 100644 index 000000000..d4415b3ea --- /dev/null +++ b/packages/components-react/src/form-field-label/FormFieldLabel.tsx @@ -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 { + type?: 'checkbox' | 'radio'; + disabled?: boolean; + checked?: boolean; +} + +export const LuxFormFieldLabel = forwardRef( + ( + { children, className, type, disabled, checked, ...restProps }: PropsWithChildren, + ref: ForwardedRef, + ) => { + 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 ( + + {children} + + ); + }, +); + +LuxFormFieldLabel.displayName = 'LuxFormFieldLabel'; diff --git a/packages/components-react/src/form-field-label/test/FormFieldLabel.spec.tsx b/packages/components-react/src/form-field-label/test/FormFieldLabel.spec.tsx new file mode 100644 index 000000000..b46ac8e8c --- /dev/null +++ b/packages/components-react/src/form-field-label/test/FormFieldLabel.spec.tsx @@ -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(Test Label); + + const label = screen.getByText('Test Label'); + expect(label).toBeInTheDocument(); + expect(label).toHaveAttribute('for', 'test-input'); + }); + + it('applies the correct class for checkbox type', () => { + render(Checkbox Label); + + const label = screen.getByText('Checkbox Label'); + expect(label).toHaveClass('utrecht-form-label--checkbox'); + }); + + it('applies the correct class for radio type', () => { + render(Radio Label); + + const label = screen.getByText('Radio Label'); + expect(label).toHaveClass('utrecht-form-label--radio'); + }); + + it('applies the disabled class when disabled prop is true', () => { + render(Disabled Label); + + const label = screen.getByText('Disabled Label'); + expect(label).toHaveClass('utrecht-form-label--disabled'); + }); + + it('applies the checked class when checked prop is true', () => { + render(Checked Label); + + const label = screen.getByText('Checked Label'); + expect(label).toHaveClass('utrecht-form-label--checked'); + }); + + it('can have an additional class name', () => { + render(Custom Label); + + const label = screen.getByText('Custom Label'); + expect(label).toHaveClass('custom-class'); + expect(label).toHaveClass('utrecht-form-label'); + }); + + it('passes through other HTML attributes', () => { + render(Test Label); + + const label = screen.getByTestId('test-label'); + expect(label).toBeInTheDocument(); + }); + + it('renders rich text content', () => { + render( + + Bold Label + , + ); + 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( + + Complex Label + , + ); + + 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'); + }); +}); diff --git a/packages/components-react/src/form-field-textbox/FormFieldTextbox.tsx b/packages/components-react/src/form-field-textbox/FormFieldTextbox.tsx new file mode 100644 index 000000000..b7e55d392 --- /dev/null +++ b/packages/components-react/src/form-field-textbox/FormFieldTextbox.tsx @@ -0,0 +1,122 @@ +import { TextboxTypes } from '@utrecht/component-library-react/dist/Textbox'; +import { FormFieldTextboxProps as UtrechtFormFieldTextboxProps } from '@utrecht/component-library-react/dist/css-module'; +import clsx from 'clsx'; +import { useId } from 'react'; +import { LuxFormField } from '../form-field/FormField'; +import { + LuxFormFieldDescription, + type LuxFormFieldDescriptionAppearance, +} from '../form-field-description/FormFieldDescription'; +import { LuxFormFieldErrorMessage } from '../form-field-error-message/FormFieldErrorMessage'; +import { LuxFormFieldLabel } from '../form-field-label/FormFieldLabel'; +import { type Direction, LuxTextbox } from '../textbox/Textbox'; + +export type LuxFormFieldTextboxProps = UtrechtFormFieldTextboxProps & { + appearance?: LuxFormFieldDescriptionAppearance; + distanced?: boolean; + inputDir?: Direction; +}; + +export const LuxFormFieldTextbox = ({ + label, + type, + description, + errorMessage, + disabled, + invalid, + appearance, + distanced, + className, + inputRef, + inputDir, + ...restProps +}: LuxFormFieldTextboxProps) => { + const inputId = useId(); + const descriptionId = useId(); + const errorMessageId = useId(); + + const labelNode = + typeof label === 'string' ? ( + + {label} + + ) : ( + label + ); + const descriptionNode = + typeof description === 'string' ? ( + + {description} + + ) : ( + description + ); + const errorMessageNode = + typeof errorMessage === 'string' ? ( + + {errorMessage} + + ) : ( + errorMessage + ); + + // TODO: naar utils + function pick(obj: T, paths: Array): Pick { + const ret = {} as Pick; + for (const k of paths) { + ret[k] = obj[k]; + } + return ret; + } + + const textBoxAttrs = pick(restProps, [ + 'autoComplete', + 'min', + 'max', + 'minLength', + 'maxLength', + 'pattern', + 'placeholder', + 'readOnly', + 'required', + 'inputRequired', + 'value', + 'defaultValue', + 'list', + 'size', + 'step', + 'onFocus', + 'onBlur', + 'onInput', + 'onChange', + ]); + + const formFieldAttrs = pick(restProps, ['children']); + + return ( + + } + className={className} + {...formFieldAttrs} + /> + ); +}; diff --git a/packages/components-react/src/form-field-textbox/test/form-field-textbox.spec.tsx b/packages/components-react/src/form-field-textbox/test/form-field-textbox.spec.tsx new file mode 100644 index 000000000..c03efa4c3 --- /dev/null +++ b/packages/components-react/src/form-field-textbox/test/form-field-textbox.spec.tsx @@ -0,0 +1,51 @@ +import { describe, expect, it } from '@jest/globals'; +import { render, screen } from '@testing-library/react'; +import { LuxFormFieldTextbox } from '../FormFieldTextbox'; + +describe('Form Field Textbox', () => { + it('renders a basic form field textbox with label and input', () => { + render(); + + expect(screen.getByText('Name')).toBeInTheDocument(); + expect(screen.getByRole('textbox')).toBeInTheDocument(); + }); + + it('applies the base class', () => { + render(); + + const formField = screen.getByText('Name').closest('.utrecht-form-field'); + expect(formField).toHaveClass('utrecht-form-field'); + }); + + it('can have an additional class name', () => { + render(); + + 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(); + + expect(screen.getByText('Enter your full name')).toBeInTheDocument(); + }); + + it('renders error message when invalid and error message provided', () => { + render(); + + expect(screen.getByText('Name is required')).toBeInTheDocument(); + }); + + it('adds the correct attributes to the Textbox', () => { + render(); + + const textbox = screen.getByRole('textbox'); + + expect(textbox).toBeInTheDocument(); + expect(textbox).toHaveAttribute('autocomplete', 'on'); + expect(textbox).toHaveAttribute('dir', 'rtl'); + expect(textbox).toHaveAttribute('aria-required', 'true'); + expect(textbox).not.toHaveAttribute('required'); + }); +}); diff --git a/packages/components-react/src/form-field/FormField.tsx b/packages/components-react/src/form-field/FormField.tsx new file mode 100644 index 000000000..d7d943d6a --- /dev/null +++ b/packages/components-react/src/form-field/FormField.tsx @@ -0,0 +1,19 @@ +import { + FormField as UtrechtFormField, + FormFieldProps as UtrechtFormFieldProps, +} from '@utrecht/component-library-react/dist/css-module'; +import { ForwardedRef, forwardRef } from 'react'; + +export const LuxFormField = forwardRef( + ({ children, className, ...restProps }: UtrechtFormFieldProps, ref: ForwardedRef) => { + return ( + + {children} + + ); + }, +); + +export type LuxFormFieldProps = UtrechtFormFieldProps; + +LuxFormField.displayName = 'LuxFormField'; diff --git a/packages/components-react/src/form-field/test/FormField.spec.tsx b/packages/components-react/src/form-field/test/FormField.spec.tsx new file mode 100644 index 000000000..92d4b5e27 --- /dev/null +++ b/packages/components-react/src/form-field/test/FormField.spec.tsx @@ -0,0 +1,60 @@ +import { describe, expect, it } from '@jest/globals'; +import { render, screen } from '@testing-library/react'; +import { LuxFormField } from '../FormField'; + +describe('Form Field', () => { + it('renders a basic form field with label and input', () => { + render(} />); + + expect(screen.getByText('Name')).toBeInTheDocument(); + expect(screen.getByRole('textbox')).toBeInTheDocument(); + }); + + it('applies the base class', () => { + render(} />); + + const formField = screen.getByText('Name').closest('.utrecht-form-field'); + expect(formField).toHaveClass('utrecht-form-field'); + }); + + it('can have an additional class name', () => { + render(} 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(} description="Enter your full name" />); + + expect(screen.getByText('Enter your full name')).toBeInTheDocument(); + }); + + it('renders error message when invalid and error message provided', () => { + render(} invalid={true} errorMessage="Name is required" />); + + expect(screen.getByText('Name is required')).toBeInTheDocument(); + }); + + it('applies the correct class for checkbox type', () => { + render(} type="checkbox" />); + + const formField = screen.getByText('Accept terms').closest('.utrecht-form-field'); + expect(formField).toHaveClass('utrecht-form-field--checkbox'); + }); + + it('applies the correct class for radio type', () => { + render(} type="radio" />); + + const formField = screen.getByText('Gender').closest('.utrecht-form-field'); + expect(formField).toHaveClass('utrecht-form-field--radio'); + }); + + it('applies the correct class for text type', () => { + render(} type="text" />); + + const formField = screen.getByText('Name').closest('.utrecht-form-field'); + expect(formField).toHaveClass('utrecht-form-field--text'); + }); +}); diff --git a/packages/components-react/src/heading-group/HeadingGroup.tsx b/packages/components-react/src/heading-group/HeadingGroup.tsx new file mode 100644 index 000000000..dd76f3f4e --- /dev/null +++ b/packages/components-react/src/heading-group/HeadingGroup.tsx @@ -0,0 +1,5 @@ +import { HeadingGroup, type HeadingGroupProps } from '@utrecht/component-library-react/dist/css-module'; + +HeadingGroup.displayName = 'LuxHeadingGroup'; +export const LuxHeadingGroup = HeadingGroup; +export type LuxHeadingGroupProps = HeadingGroupProps; diff --git a/packages/components-react/src/heading-group/test/HeadingGroup.spec.tsx b/packages/components-react/src/heading-group/test/HeadingGroup.spec.tsx new file mode 100644 index 000000000..f9d988ece --- /dev/null +++ b/packages/components-react/src/heading-group/test/HeadingGroup.spec.tsx @@ -0,0 +1,46 @@ +import { describe, expect, it } from '@jest/globals'; +import { render, screen } from '@testing-library/react'; +import { LuxHeadingGroup } from '../HeadingGroup'; + +describe('Heading', () => { + it('renders a HeadingGroup', () => { + render( + +

Lux Pre-Heading

+

Lux Heading

+
, + ); + + const heading = screen.getByText('Lux Heading'); + expect(heading).toBeInTheDocument(); + const preHeading = screen.getByText('Lux Pre-Heading'); + expect(preHeading).toBeInTheDocument(); + }); + it('can be hidden', () => { + const { container } = render( + , + ); + + const headingGroup = container.querySelector(':only-child'); + + expect(headingGroup).not.toBeVisible(); + }); + + it('can have an additional class name', () => { + const { container } = render( + +

Lux Pre-Heading

+

Lux Heading

+
, + ); + + const headingGroup = container.querySelector(':only-child'); + + expect(headingGroup).toHaveClass('large'); + + expect(headingGroup).toHaveClass('utrecht-heading-group'); + }); +}); diff --git a/packages/components-react/src/index.ts b/packages/components-react/src/index.ts index 32a0f7eae..baf8b63c8 100644 --- a/packages/components-react/src/index.ts +++ b/packages/components-react/src/index.ts @@ -1,3 +1,4 @@ +export { LuxAlert, type LuxAlertProps } from './alert/Alert'; export { LuxButton, type LuxButtonProps } from './button/Button'; export { LuxDocument, type LuxDocumentProps } from './document/Document'; export { @@ -10,6 +11,20 @@ export { LuxHeading6, type LuxHeadingProps, } from './heading/Heading'; +export { LuxHeadingGroup, type LuxHeadingGroupProps } from './heading-group/HeadingGroup'; +export { LuxFormField, type LuxFormFieldProps } from './form-field/FormField'; +export { + LuxFormFieldDescription, + type LuxFormFieldDescriptionProps, +} from './form-field-description/FormFieldDescription'; +export { LuxFormFieldLabel, type LuxFormFieldLabelProps } from './form-field-label/FormFieldLabel'; +export { + LuxFormFieldErrorMessage, + type LuxFormFieldErrorMessageProps, +} from './form-field-error-message/FormFieldErrorMessage'; +export { LuxTextbox, INPUT_TYPES, type LuxTextboxProps } from './textbox/Textbox'; +export { LuxFormFieldTextbox, type LuxFormFieldTextboxProps } from './form-field-textbox/FormFieldTextbox'; export { LuxParagraph, type LuxParagraphProps } from './paragraph/Paragraph'; +export { LuxPreHeading, type LuxPreHeadingProps } from './pre-heading/PreHeading'; export { LuxSection, type LuxSectionProps } from './section/Section'; export { LuxSelect, LuxSelectOption, type LuxSelectProps, type LuxSelectOptionProps } from './select/Select'; diff --git a/packages/components-react/src/pre-heading/PreHeading.tsx b/packages/components-react/src/pre-heading/PreHeading.tsx new file mode 100644 index 000000000..4d178a268 --- /dev/null +++ b/packages/components-react/src/pre-heading/PreHeading.tsx @@ -0,0 +1,6 @@ +import { PreHeading, type PreHeadingProps } from '@utrecht/component-library-react/dist/css-module'; + +PreHeading.displayName = 'LuxPreHeading'; +export const LuxPreHeading = PreHeading; + +export type LuxPreHeadingProps = PreHeadingProps; diff --git a/packages/components-react/src/pre-heading/test/PreHeading.spec.tsx b/packages/components-react/src/pre-heading/test/PreHeading.spec.tsx new file mode 100644 index 000000000..e4668fa36 --- /dev/null +++ b/packages/components-react/src/pre-heading/test/PreHeading.spec.tsx @@ -0,0 +1,43 @@ +import { describe, expect, it } from '@jest/globals'; +import { render, screen } from '@testing-library/react'; +import { LuxPreHeading } from '../PreHeading'; + +describe('Heading', () => { + it('renders a PreHeading', () => { + render(Lux PreHeading); + + const preHeading = screen.getByText('Lux PreHeading'); + expect(preHeading).toBeInTheDocument(); + }); + + it('renders rich text content', () => { + const { container } = render( + + Lux PreHeading + , + ); + + const preHeading = container.querySelector(':only-child'); + const richText = preHeading?.querySelector('strong'); + + expect(richText).toBeInTheDocument(); + }); + + it('can be hidden', () => { + const { container } = render(); + + const preHeading = container.querySelector(':only-child'); + + expect(preHeading).not.toBeVisible(); + }); + + it('can have an additional class name', () => { + render(Lux PreHeading); + + const preHeading = screen.getByText('Lux PreHeading'); + + expect(preHeading).toHaveClass('large'); + + expect(preHeading).toHaveClass('utrecht-pre-heading'); + }); +}); diff --git a/packages/components-react/src/textbox/Textbox.tsx b/packages/components-react/src/textbox/Textbox.tsx new file mode 100644 index 000000000..0bc825511 --- /dev/null +++ b/packages/components-react/src/textbox/Textbox.tsx @@ -0,0 +1,102 @@ +import { Textbox as UtrechtTextbox } from '@utrecht/component-library-react/dist/css-module'; +import clsx from 'clsx'; +import { ForwardedRef, forwardRef, InputHTMLAttributes, PropsWithChildren } from 'react'; + +const TEXTBOX_CLASSES: { [key: string]: string } = { + invalid: 'utrecht-textbox--invalid', + disabled: 'utrecht-textbox--disabled', + readOnly: 'utrecht-textbox--read-only', + focus: 'utrecht-textbox--focus', + focusVisible: 'utrecht-textbox--focus-visible', + required: 'utrecht-textbox--required', +}; + +export const INPUT_TYPES = { + TEXT: 'text', + EMAIL: 'email', + URL: 'url', + TEL: 'tel', + SEARCH: 'search', + PASSWORD: 'password', + NUMBER: 'number', + DATE: 'date', + DATETIME_LOCAL: 'datetime-local', + MONTH: 'month', + TIME: 'time', + WEEK: 'week', +} as const; + +export type InputType = (typeof INPUT_TYPES)[keyof typeof INPUT_TYPES]; +export type Direction = 'ltr' | 'rtl' | 'auto'; + +export interface LuxTextboxProps extends Omit, 'type'> { + type?: InputType; + invalid?: boolean; + dir?: Direction; + focus?: boolean; + focusVisible?: boolean; + autoComplete?: string; + minLength?: number; + placeholderDir?: Direction; + spellCheck?: boolean; +} + +export const LuxTextbox = forwardRef( + ( + { + className, + type = INPUT_TYPES.TEXT, + invalid, + disabled, + readOnly, + dir, + focus, + focusVisible, + required, + value, + autoComplete, + minLength, + placeholder, + placeholderDir, + spellCheck, + ...restProps + }: PropsWithChildren, + ref: ForwardedRef, + ) => { + const classes = clsx( + { + [TEXTBOX_CLASSES.invalid]: invalid, + [TEXTBOX_CLASSES.disabled]: disabled, + [TEXTBOX_CLASSES.readOnly]: readOnly, + [TEXTBOX_CLASSES.focus]: focus, + [TEXTBOX_CLASSES.focusVisible]: focusVisible, + [TEXTBOX_CLASSES.required]: required, + }, + className, + ); + + return ( + + ); + }, +); + +LuxTextbox.displayName = 'LuxTextbox'; diff --git a/packages/components-react/src/textbox/test/Textbox.spec.tsx b/packages/components-react/src/textbox/test/Textbox.spec.tsx new file mode 100644 index 000000000..cad2f6e01 --- /dev/null +++ b/packages/components-react/src/textbox/test/Textbox.spec.tsx @@ -0,0 +1,83 @@ +import { describe, expect, it } from '@jest/globals'; +import { render, screen } from '@testing-library/react'; +import { LuxTextbox } from '../Textbox'; + +describe('Form Field Text Input', () => { + it('renders a basic text input', () => { + render(); + const input = screen.getByPlaceholderText('Enter text'); + expect(input).toBeInTheDocument(); + expect(input).toHaveClass('utrecht-textbox'); + }); + + it('applies the correct class for invalid state', () => { + render(); + const input = screen.getByPlaceholderText('Invalid input'); + expect(input).toHaveClass('utrecht-textbox--invalid'); + }); + + it('applies the correct class and property for disabled state', () => { + render(); + const input = screen.getByPlaceholderText('Disabled input'); + expect(input).toHaveClass('utrecht-textbox--disabled'); + expect(input).toBeDisabled(); + }); + + it('applies the correct class for read-only state', () => { + render(); + const input = screen.getByPlaceholderText('Read-only input'); + expect(input).toHaveClass('utrecht-textbox--read-only'); + }); + + it('can have an additional class name', () => { + render(); + const input = screen.getByPlaceholderText('Custom input'); + expect(input).toHaveClass('custom-class'); + expect(input).toHaveClass('utrecht-textbox'); + }); + + it('passes through other HTML attributes', () => { + render(); + const input = screen.getByTestId('test-input'); + expect(input).toBeInTheDocument(); + }); + + it('combines multiple classes correctly', () => { + render(); + const input = screen.getByPlaceholderText('Complex input'); + expect(input).toHaveClass('utrecht-textbox'); + expect(input).toHaveClass('utrecht-textbox--invalid'); + expect(input).toHaveClass('utrecht-textbox--disabled'); + expect(input).toHaveClass('utrecht-textbox--read-only'); + expect(input).toHaveClass('custom-class'); + }); + + it('renders with different input types', () => { + const types = ['text', 'email', 'password', 'number', 'date']; + types.forEach((type) => { + render(); + const input = screen.getByPlaceholderText(`${type} input`) as HTMLInputElement; + expect(input).toHaveAttribute('type', type); + }); + }); + + it('handles focus and focus-visible states', () => { + render(); + const input = screen.getByPlaceholderText('Focused input'); + expect(input).toHaveClass('utrecht-textbox--focus'); + expect(input).toHaveClass('utrecht-textbox--focus-visible'); + }); + + it('handles required state', () => { + render(); + const input = screen.getByPlaceholderText('Required input'); + expect(input).toHaveClass('utrecht-textbox--required'); + expect(input).toBeRequired(); + }); + + it('applies correct direction', () => { + render(); + const input = screen.getByPlaceholderText('RTL input'); + expect(input).toHaveAttribute('dir', 'rtl'); + }); +}); diff --git a/packages/storybook/config/main.ts b/packages/storybook/config/main.ts index 0b1c5cc42..e713ea863 100644 --- a/packages/storybook/config/main.ts +++ b/packages/storybook/config/main.ts @@ -4,6 +4,7 @@ import { mergeConfig } from 'vite'; const config: StorybookConfig = { stories: ['../src/**/*stories.@(js|jsx|ts|tsx)', '../src/**/*.mdx'], addons: [ + '@geometricpanda/storybook-addon-badges', '@storybook/addon-a11y', '@storybook/addon-backgrounds', '@storybook/addon-themes', diff --git a/packages/storybook/config/preview.tsx b/packages/storybook/config/preview.tsx index 48e855079..b3ee1f78d 100644 --- a/packages/storybook/config/preview.tsx +++ b/packages/storybook/config/preview.tsx @@ -1,3 +1,4 @@ +import { BADGE_LOCATION } from '@geometricpanda/storybook-addon-badges'; import { LuxDocument } from '@lux-design-system/components-react'; import { defineCustomElements } from '@lux-design-system/web-components-stencil/loader/index.js'; import { withThemeByClassName } from '@storybook/addon-themes'; @@ -7,6 +8,14 @@ import '@lux-design-system/font/src/index.scss'; import './themes'; import '../src/styles/theme.css'; +/* eslint-disable no-unused-vars */ +export enum BADGES { + WIP = 'wip', + CANARY = 'canary', + LATEST = 'latest', +} +/* eslint-enable */ + defineCustomElements(); const preview: Preview = { @@ -66,6 +75,34 @@ const preview: Preview = { }, ], }, + badgesConfig: { + [BADGES.WIP]: { + location: [BADGE_LOCATION.TOOLBAR], + title: 'W.I.P.', + tooltip: 'Work in progress', + }, + [BADGES.CANARY]: { + styles: { + backgroundColor: '#FFFF8F', // Canary Yellow + borderColor: '#bfbf6b', + textTransform: 'none', + }, + location: [BADGE_LOCATION.TOOLBAR_EXTRA], + title: 'Canary', + tooltip: 'Alleen beschikbaar in de canary release packages', + }, + [BADGES.LATEST]: { + styles: { + backgroundColor: '#4cbb17', + borderColor: '#398c11', + color: '#181920', // Contrast Ratio: 7.03:1 + textTransform: 'none', + }, + location: [BADGE_LOCATION.TOOLBAR_EXTRA], + title: 'Latest', + tooltip: 'Beschikbaar in de latest release package', + }, + }, chromatic: { disable: true, // Enable on story level. Not every story needs to be tested. diff --git a/packages/storybook/package.json b/packages/storybook/package.json index 0484bb234..dd8f57489 100644 --- a/packages/storybook/package.json +++ b/packages/storybook/package.json @@ -25,6 +25,7 @@ }, "devDependencies": { "@chromatic-com/storybook": "1.6.1", + "@geometricpanda/storybook-addon-badges": "2.0.5", "@lux-design-system/assets": "workspace:*", "@lux-design-system/components-css": "workspace:*", "@lux-design-system/components-react": "workspace:*", @@ -52,10 +53,17 @@ "@types/react-dom": "18.3.0", "@utrecht/alert-css": "1.1.0", "@utrecht/button-css": "1.2.0", + "@utrecht/form-field-css": "1.3.0", + "@utrecht/form-field-description-css": "1.3.0", + "@utrecht/form-field-error-message-css": "1.3.1", + "@utrecht/form-label-css": "1.3.0", "@utrecht/heading-css": "1.2.0", + "@utrecht/pre-heading-css": "1.2.0", + "@utrecht/heading-group-css": "1.2.0", "@utrecht/select-css": "1.2.0", "@utrecht/link-css": "1.1.0", "@utrecht/paragraph-css": "1.1.0", + "@utrecht/textbox-css": "1.3.1", "@vitejs/plugin-react": "4.3.1", "chromatic": "11.7.0", "react": "18.3.1", diff --git a/packages/storybook/src/react-components/alert/alert.mdx b/packages/storybook/src/react-components/alert/alert.mdx new file mode 100644 index 000000000..926c03052 --- /dev/null +++ b/packages/storybook/src/react-components/alert/alert.mdx @@ -0,0 +1,27 @@ +import { Canvas, Controls, Markdown, Meta } from "@storybook/blocks"; +import markdown from "@utrecht/alert-css/README.md?raw"; +import * as AlertStories from "./alert.stories.tsx"; +import { CitationDocumentation } from "../../utils/CitationDocumentation.tsx"; + + + +# Alert + + + +{markdown} + +## Playground + + + + +## Variants + + + + + diff --git a/packages/storybook/src/react-components/alert/alert.stories.tsx b/packages/storybook/src/react-components/alert/alert.stories.tsx new file mode 100644 index 000000000..c739fe5d7 --- /dev/null +++ b/packages/storybook/src/react-components/alert/alert.stories.tsx @@ -0,0 +1,113 @@ +import { LuxAlert, type LuxAlertProps, LuxHeading1, LuxParagraph } from '@lux-design-system/components-react'; +import tokens from '@lux-design-system/design-tokens/dist/index.json'; +import type { Meta, StoryObj } from '@storybook/react'; + +type Story = StoryObj; + +const meta = { + title: 'React Components/Alert', + id: 'react-components-alert', + component: LuxAlert, + subcomponents: {}, + parameters: { + tokens, + tokensPrefix: 'utrecht-alert', + }, + argTypes: { + type: { + description: 'Type modifier', + control: 'select', + options: ['info', 'success', 'warning', 'error'], + }, + children: { + name: 'content (children)', + description: 'Alert inhoud', + control: 'object', + table: { + type: { + summary: 'HTML Content', + }, + }, + }, + }, +} satisfies Meta; + +export default meta; + +const AlertTemplate: Story = { + args: { + type: 'info', + children: ( + <> + Heading + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam quis massa lorem. Ut laoreet varius rhoncus. + + + ), + }, + render: ({ children, ...args }: LuxAlertProps) => {children}, +}; + +export const Playground: Story = { + ...AlertTemplate, + name: 'Playground', + parameters: { + docs: { + sourceState: 'shown', + }, + }, + tags: ['!autodocs'], +}; + +export const InfoAlert: Story = { + ...AlertTemplate, + args: { + ...AlertTemplate.args, + type: 'info', + }, + parameters: { + docs: { + sourceState: 'shown', + }, + }, +}; + +export const SuccessAlert: Story = { + ...AlertTemplate, + args: { + ...AlertTemplate.args, + type: 'success', + }, + parameters: { + docs: { + sourceState: 'shown', + }, + }, +}; + +export const WarningAlert: Story = { + ...AlertTemplate, + args: { + ...AlertTemplate.args, + type: 'warning', + }, + parameters: { + docs: { + sourceState: 'shown', + }, + }, +}; + +export const ErrorAlert: Story = { + ...AlertTemplate, + args: { + ...AlertTemplate.args, + type: 'error', + }, + parameters: { + docs: { + sourceState: 'shown', + }, + }, +}; diff --git a/packages/storybook/src/react-components/button/button.mdx b/packages/storybook/src/react-components/button/button.mdx index 950e7d0a8..d77052c7c 100644 --- a/packages/storybook/src/react-components/button/button.mdx +++ b/packages/storybook/src/react-components/button/button.mdx @@ -9,7 +9,7 @@ import { CitationDocumentation } from "../../utils/CitationDocumentation.tsx"; {markdown} diff --git a/packages/storybook/src/react-components/form-field-description/form-field-description.mdx b/packages/storybook/src/react-components/form-field-description/form-field-description.mdx new file mode 100644 index 000000000..124ea418e --- /dev/null +++ b/packages/storybook/src/react-components/form-field-description/form-field-description.mdx @@ -0,0 +1,74 @@ +import { Canvas, Controls, Description, Markdown, Meta } from "@storybook/blocks"; +import markdown from "@utrecht/form-field-description-css/README.md?raw"; +import * as FormFieldDescriptionStories from "./form-field-description.stories"; +import { CitationDocumentation } from "../../utils/CitationDocumentation"; + + + +# Lux Form Field Description + + + +{markdown} + +## Opmerkingen + +- De component gebruikt een `appearance` prop om de validatiestatus van het gekoppelde formulierelement weer te geven. +- De `appearance` prop kan worden ingesteld op 'valid' of 'invalid' om de juiste validatiestatus te tonen. +- De classnames `utrecht-form-field-description--valid` en `utrecht-form-field-description--invalid` worden automatisch toegepast op basis van de `appearance` prop. +- Wanneer geen `appearance` is ingesteld, wordt de standaard weergave gebruikt. + +## Playground + + + + +## Varianten + +### Standaard Beschrijving + + + + +### Geldige Status + + + + +### Ongeldige Status + + + + +### Lange Beschrijving + + + + +## Properties + +- `children`: De inhoud van de beschrijving (ReactNode) +- `appearance`: De weergavestatus van de beschrijving ('valid' | 'invalid' | undefined) +- `className`: Extra CSS-klassen voor styling + +## Voorbeelden + +```tsx +// Standaard gebruik + + Voer uw gegevens in + + +// Met geldige status + + Uw invoer voldoet aan de vereisten + + +// Met ongeldige status + + Controleer uw invoer + +``` diff --git a/packages/storybook/src/react-components/form-field-description/form-field-description.stories.tsx b/packages/storybook/src/react-components/form-field-description/form-field-description.stories.tsx new file mode 100644 index 000000000..f5cad3598 --- /dev/null +++ b/packages/storybook/src/react-components/form-field-description/form-field-description.stories.tsx @@ -0,0 +1,109 @@ +import { LuxFormFieldDescription } 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 { BADGES } from '../../../config/preview'; + +const meta = { + title: 'React Components/Form Field/Form Field Description', + id: 'react-components-form-field-description', + component: LuxFormFieldDescription, + parameters: { + badges: [BADGES.WIP, BADGES.CANARY], + tokens, + tokensPrefix: 'react-form-field-description', + docs: { + description: { + component: 'A description component for form fields that provides additional context or validation feedback.', + }, + }, + }, + argTypes: { + children: { + control: 'text', + description: 'The content of the description', + table: { + type: { summary: 'ReactNode' }, + }, + }, + appearance: { + control: 'select', + options: [undefined, 'valid', 'invalid'], + description: 'Sets the appearance state of the description', + table: { + type: { summary: "'valid' | 'invalid' | undefined" }, + defaultValue: { summary: 'undefined' }, + }, + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Playground: Story = { + args: { + children: 'This is a form field description', + }, + parameters: { + docs: { + description: { + story: 'Interactive playground for the description component.', + }, + }, + }, +}; + +export const Default: Story = { + args: { + children: 'Enter your full name', + }, + parameters: { + docs: { + description: { + story: 'Default description without any validation state.', + }, + }, + }, +}; + +export const Valid: Story = { + args: { + children: 'Your input meets the requirements', + appearance: 'valid', + }, + parameters: { + docs: { + description: { + story: 'Description with valid appearance.', + }, + }, + }, +}; + +export const Invalid: Story = { + args: { + children: 'Please check the input requirements', + appearance: 'invalid', + }, + parameters: { + docs: { + description: { + story: 'Description with invalid appearance.', + }, + }, + }, +}; + +export const LongDescription: Story = { + args: { + children: + 'This is a longer description that provides more detailed information about what is expected in this form field. It can span multiple lines if needed.', + }, + parameters: { + docs: { + description: { + story: 'Example of a longer description text.', + }, + }, + }, +}; diff --git a/packages/storybook/src/react-components/form-field-error-message/form-field-error-message.mdx b/packages/storybook/src/react-components/form-field-error-message/form-field-error-message.mdx new file mode 100644 index 000000000..94440c2cf --- /dev/null +++ b/packages/storybook/src/react-components/form-field-error-message/form-field-error-message.mdx @@ -0,0 +1,40 @@ +import { Canvas, Controls, Description, Markdown, Meta } from "@storybook/blocks"; +import markdown from "@utrecht/form-field-error-message-css/README.md?raw"; +import * as FormFieldErrorMessageStories from "./form-field-error-message.stories"; +import { CitationDocumentation } from "../../utils/CitationDocumentation"; + + + +# Lux Form Field Error Message + + + +{markdown} + +## Opmerkingen + +- Gebruik dit component voor foutmeldingen bij formuliervelden die geen valide invoer hebben. +- Dit component is meestal een onderdeel van het _form field_ component. +- Gebruik een `id` attribuut op dit element, zodat je met `aria-describedby` op de _form control_ met `aria-invalid="true"` een koppeling kunt maken. +- Gebruik in HTML in plaats van `aria-live="polite"` het attribuut `role="status"`. +- Gebruik in HTML in plaats van `aria-live="assertive"` het attribuut `role="alert"`. + +## Playground + + + + +## Varianten + +### Default Form Field Error Message + + + + +### Distanced Error Message + + + diff --git a/packages/storybook/src/react-components/form-field-error-message/form-field-error-message.stories.tsx b/packages/storybook/src/react-components/form-field-error-message/form-field-error-message.stories.tsx new file mode 100644 index 000000000..12f84b2e8 --- /dev/null +++ b/packages/storybook/src/react-components/form-field-error-message/form-field-error-message.stories.tsx @@ -0,0 +1,74 @@ +import { LuxFormFieldErrorMessage, type LuxFormFieldErrorMessageProps } 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 { forwardRef, PropsWithChildren } from 'react'; +import { BADGES } from '../../../config/preview'; + +const WrappedLuxFormFieldErrorMessage = forwardRef< + HTMLParagraphElement, + PropsWithChildren +>((props, ref) => ); + +WrappedLuxFormFieldErrorMessage.displayName = 'WrappedLuxFormFieldErrorMessage'; + +const meta = { + title: 'React Components/Form Field/Form Field Error Message', + id: 'react-components-form-field-error-message', + component: WrappedLuxFormFieldErrorMessage, + parameters: { + badges: [BADGES.WIP, BADGES.CANARY], + tokens, + tokensPrefix: 'react-form-field-error-message', + }, + argTypes: { + children: { + control: 'text', + description: 'The content of the form field error message', + }, + distanced: { + control: 'boolean', + description: 'Whether the error message should be distanced from the form field', + }, + id: { + control: 'text', + description: 'Unique identifier to associate it with a form input', + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +const FormFieldErrorMessageTemplate: Story = { + render: (args) => , +}; + +export const Playground: Story = { + ...FormFieldErrorMessageTemplate, + args: { + children: 'Your password must contain at least 16 characters.', + id: 'error-message-1', + }, + parameters: { + docs: { + sourceState: 'shown', + }, + }, +}; + +export const Default: Story = { + ...FormFieldErrorMessageTemplate, + args: { + children: 'Your password must contain at least 16 characters.', + id: 'error-message-2', + }, +}; + +export const Distanced: Story = { + ...FormFieldErrorMessageTemplate, + args: { + children: 'Your password must contain at least 16 characters.', + id: 'error-message-3', + distanced: true, + }, +}; diff --git a/packages/storybook/src/react-components/form-field-label/form-field-label.mdx b/packages/storybook/src/react-components/form-field-label/form-field-label.mdx new file mode 100644 index 000000000..f9416f34a --- /dev/null +++ b/packages/storybook/src/react-components/form-field-label/form-field-label.mdx @@ -0,0 +1,53 @@ +import { Canvas, Controls, Description, Markdown, Meta } from "@storybook/blocks"; +import markdown from "@utrecht/form-label-css/README.md?raw"; +import * as FormFieldLabelStories from "./form-field-label.stories"; +import { CitationDocumentation } from "../../utils/CitationDocumentation"; + + + +# Lux Form Field Label + + + +{markdown} + +## Opmerkingen + +- De property `type` kan de waarden `checkbox` of `radio` krijgen om specifieke stijlen toe te passen. +- De properties `disabled` en `checked` zijn toegevoegd om de status van het gekoppelde formulierelement weer te geven. +- De classnames `lux-form-label--${type}`, `lux-form-label--disabled`, en `lux-form-label--checked` worden automatisch toegepast op basis van de properties. + +## Playground + + + + +## Form Field Label varianten + +### Default Form Label + + + + +### Checkbox Label + + + + +### Radio Label + + + + +### Uitgeschakeld Label + + + + +### Aangevinkt Label + + + diff --git a/packages/storybook/src/react-components/form-field-label/form-field-label.stories.tsx b/packages/storybook/src/react-components/form-field-label/form-field-label.stories.tsx new file mode 100644 index 000000000..b597e13c6 --- /dev/null +++ b/packages/storybook/src/react-components/form-field-label/form-field-label.stories.tsx @@ -0,0 +1,112 @@ +import { LuxFormFieldLabel as FormFieldLabel, LuxFormFieldLabelProps } 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 { type LabelHTMLAttributes, type PropsWithChildren } from 'react'; +import { BADGES } from '../../../config/preview'; + +const LuxFormFieldLabel = ( + props: PropsWithChildren & LabelHTMLAttributes, +) => ; + +type Story = StoryObj; + +const meta = { + title: 'React Components/Form Field/Form Field Label', + id: 'react-components-form-field-label', + component: LuxFormFieldLabel, + subcomponents: {}, + parameters: { + badges: [BADGES.WIP, BADGES.CANARY], + tokens, + tokensPrefix: 'react-form-label', + }, + argTypes: { + children: { + name: 'content', + description: 'React text', + control: 'text', + table: { + type: { + summary: 'HTML Content', + }, + }, + }, + type: { + control: 'select', + options: [undefined, 'checkbox', 'radio'], + }, + disabled: { + control: 'boolean', + }, + checked: { + control: 'boolean', + }, + }, +} satisfies Meta; + +export default meta; + +const FormFieldLabelTemplate: Story = { + render: ({ children, ...args }) => {children}, +}; + +const textTemplate = (name = 'Form Label') => `${name}`; + +export const Playground: Story = { + ...FormFieldLabelTemplate, + name: 'Playground', + args: { + children: textTemplate(), + }, + parameters: { + docs: { + sourceState: 'shown', + }, + }, + tags: ['!autodocs'], +}; + +export const Default: Story = { + ...FormFieldLabelTemplate, + name: 'Default Form Label', + args: { + children: textTemplate('Default Form Label'), + }, +}; + +export const CheckboxLabel: Story = { + ...FormFieldLabelTemplate, + name: 'Checkbox Label', + args: { + children: textTemplate('Checkbox Label'), + type: 'checkbox', + }, +}; + +export const RadioLabel: Story = { + ...FormFieldLabelTemplate, + name: 'Radio Label', + args: { + children: textTemplate('Radio Label'), + type: 'radio', + }, +}; + +export const DisabledLabel: Story = { + ...FormFieldLabelTemplate, + name: 'Disabled Label', + args: { + children: textTemplate('Disabled Label'), + disabled: true, + }, +}; + +export const CheckedLabel: Story = { + ...FormFieldLabelTemplate, + name: 'Checked Label', + args: { + children: textTemplate('Checked Label'), + checked: true, + type: 'checkbox', + }, +}; diff --git a/packages/storybook/src/react-components/form-field-textbox/form-field-textbox.mdx b/packages/storybook/src/react-components/form-field-textbox/form-field-textbox.mdx new file mode 100644 index 000000000..cc320aaa2 --- /dev/null +++ b/packages/storybook/src/react-components/form-field-textbox/form-field-textbox.mdx @@ -0,0 +1,11 @@ +import { Canvas, Controls, Meta } from "@storybook/blocks"; +import * as FormFieldTextboxStories from "./form-field-textbox.stories"; + + + +# Lux Form Field Textbox + +## Playground + + + diff --git a/packages/storybook/src/react-components/form-field-textbox/form-field-textbox.stories.tsx b/packages/storybook/src/react-components/form-field-textbox/form-field-textbox.stories.tsx new file mode 100644 index 000000000..47b26387c --- /dev/null +++ b/packages/storybook/src/react-components/form-field-textbox/form-field-textbox.stories.tsx @@ -0,0 +1,56 @@ +import { LuxFormFieldTextbox, type LuxFormFieldTextboxProps } 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 { BADGES } from '../../../config/preview'; +import FormFieldDescriptionMeta from '../form-field-description/form-field-description.stories'; +import FormFieldErrorMessageMeta from '../form-field-error-message/form-field-error-message.stories'; +import TextboxMeta from '../textbox/textbox.stories'; + +const meta = { + title: 'React Components/Form Field/Form Field Textbox', + id: 'react-components-form-field-form-field-textbox', + component: LuxFormFieldTextbox, + parameters: { + badges: [BADGES.WIP, BADGES.CANARY], + tokens, + tokensPrefix: 'utrecht-form-field-textbox', + }, + argTypes: { + ...TextboxMeta.argTypes, + appearance: { + ...FormFieldDescriptionMeta.argTypes.appearance, + }, + distanced: { + ...FormFieldErrorMessageMeta.argTypes.distanced, + }, + disabled: { + type: 'boolean', + }, + errorMessage: { + if: { + arg: 'invalid', + truthy: true, + }, + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Playground: Story = { + name: 'Playground', + args: { + label: 'Form Field Textbox', + description: 'Textbox in een FormField', + errorMessage: 'Zo kan het ook een ErrorMessage hebben', + invalid: false, + appearance: undefined, + }, + parameters: { + docs: { + sourceState: 'shown', + }, + }, + tags: ['!autodocs'], +}; diff --git a/packages/storybook/src/react-components/form-field/form-field.mdx b/packages/storybook/src/react-components/form-field/form-field.mdx new file mode 100644 index 000000000..0204b9ad8 --- /dev/null +++ b/packages/storybook/src/react-components/form-field/form-field.mdx @@ -0,0 +1,78 @@ +import LinkTo from "@storybook/addon-links/react"; +import { Canvas, Controls, Description, Meta } from "@storybook/blocks"; +import mdAnatomy from "@utrecht/form-field-css/docs/anatomy.nl.md?raw"; +import mdWcag from "@utrecht/form-field-css/docs/wcag.nl.md?raw"; +import * as FormFieldStories from "./form-field.stories"; +import { CitationDocumentation } from "../../utils/CitationDocumentation"; +import { MarkdownDocumentation } from "../../utils/MarkdownDocumentation"; +import FormFieldTextboxMeta from "../form-field-textbox/form-field-textbox.stories"; + + + +# Form Field + + + +{mdAnatomy} + +## Wanneer gebruik je dit component? + +Dit component gebruik je als een van de voorgedefinieerde FormField*Type*'s niet voldoen. Bekijk ook: + +- + FormFieldTextbox + + +**Let op:** Op dit moment rendert het de foutmelding onder het input. + +{mdWcag} + +## Playground + + + + +## Varianten + +### Textbox + + + + +### Textbox met beschrijving + + + + +### Textbox met foutmelding + + + + +## Gebruik + +```jsx +} + description="Kies een unieke gebruikersnaam" + type="text" +/> + +} + type="checkbox" +/> + +} + errorMessage="Voer een geldig e-mailadres in" + invalid={true} + type="text" +/> +``` diff --git a/packages/storybook/src/react-components/form-field/form-field.stories.tsx b/packages/storybook/src/react-components/form-field/form-field.stories.tsx new file mode 100644 index 000000000..9cdc137e0 --- /dev/null +++ b/packages/storybook/src/react-components/form-field/form-field.stories.tsx @@ -0,0 +1,143 @@ +import { + LuxFormField, + LuxFormFieldDescription, + LuxFormFieldErrorMessage, + LuxFormFieldLabel, + LuxParagraph, + LuxTextbox, +} 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 { BADGES } from '../../../config/preview'; + +const meta = { + title: 'React Components/Form Field', + id: 'react-components-form-field', + component: LuxFormField, + parameters: { + badges: [BADGES.WIP, BADGES.CANARY], + tokens, + tokensPrefix: 'react-form-field', + }, + argTypes: { + input: { + control: { + type: 'select', + }, + description: 'De Input van het Form Field, bijv ``.', + options: ['Textbox'], + mapping: { + Textbox: , + }, + table: { type: { summary: 'ReactNode' } }, + }, + type: { + control: 'select', + options: ['text', 'checkbox', 'radio'], + description: 'Het type van het Form Field.', + table: { + defaultValue: { summary: 'text' }, + }, + }, + invalid: { + control: 'boolean', + description: 'Zet het Form Field in `invalid` status.', + table: { + defaultValue: { summary: 'false' }, + }, + }, + errorMessage: { + if: { + arg: 'invalid', + truthy: true, + }, + }, + children: { + type: 'string', + description: 'Extra content, of als properties niet gebruikt worden, de content.', + table: { type: { summary: 'ReactNode' } }, + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +const FormFieldTemplate: Story = { + args: { + input: 'Textbox', + invalid: false, + label: 'Form Field Label', + description: 'Form Field Description', + errorMessage: 'Form Field Error Message', + }, + render: ({ ...args }) => { + const { label, description, errorMessage, invalid, children, ...restArgs } = args; + + const labelNode = {label}; + const descriptionNode = ( + + {description} + + ); + const errorMessageNode = invalid ? {errorMessage} : undefined; + + return ( + + {children} + + ); + }, +}; + +export const Playground: Story = { + name: 'Playground', + ...FormFieldTemplate, + args: { + ...FormFieldTemplate.args, + }, + parameters: { + docs: { + sourceState: 'shown', + }, + }, + tags: ['!autodocs'], +}; + +export const FormFieldTextbox: Story = { + name: 'Form Field met Textbox', + ...FormFieldTemplate, + args: { + ...FormFieldTemplate.args, + label: 'Voornaam', + description: undefined, + }, +}; + +export const FormFieldDescription: Story = { + name: 'Form Field met beschrijving', + ...FormFieldTemplate, + args: { + ...FormFieldTemplate.args, + label: 'Achternaam', + description: 'Gebruikt u de naam van uw partner? Vul dan ook uw eigen achternaam in.', + }, +}; + +export const FormFieldError: Story = { + name: 'Form Field met een foutmelding', + ...FormFieldTemplate, + args: { + ...FormFieldTemplate.args, + label: 'Nederlandse IBAN', + description: undefined, + errorMessage: 'Een Nederlandse IBAN begint altijd met NL', + invalid: true, + }, +}; diff --git a/packages/storybook/src/react-components/heading-group/heading-group.mdx b/packages/storybook/src/react-components/heading-group/heading-group.mdx new file mode 100644 index 000000000..30351e5a3 --- /dev/null +++ b/packages/storybook/src/react-components/heading-group/heading-group.mdx @@ -0,0 +1,20 @@ +import { Canvas, Controls, Markdown, Meta } from "@storybook/blocks"; +import markdown from "@utrecht/heading-group-css/README.md?raw"; +import * as HeadingGroupStories from "./heading-group.stories"; +import { CitationDocumentation } from "../../utils/CitationDocumentation"; + + + +# Heading-Group + + + +{markdown} + +## Playground + + + diff --git a/packages/storybook/src/react-components/heading-group/heading-group.stories.tsx b/packages/storybook/src/react-components/heading-group/heading-group.stories.tsx new file mode 100644 index 000000000..f3aa23fb2 --- /dev/null +++ b/packages/storybook/src/react-components/heading-group/heading-group.stories.tsx @@ -0,0 +1,49 @@ +import { LuxHeading, LuxHeadingGroup, LuxPreHeading } from '@lux-design-system/components-react'; +import tokens from '@lux-design-system/design-tokens/dist/index.json'; +import type { Meta, StoryObj } from '@storybook/react'; + +type Story = StoryObj; + +const meta = { + title: 'React Components/Heading-group', + id: 'react-components-heading-group', + component: LuxHeadingGroup, + subcomponents: {}, + parameters: { + tokens, + tokensPrefix: 'react-heading-group', + }, + argTypes: { + children: { + name: 'content', + description: 'React text', + table: { + type: { + summary: 'HTML Content', + }, + }, + }, + }, +} satisfies Meta; + +export default meta; + +const headingText = "Pa's wijze lynx bezag vroom het fikse aquaduct!"; + +const HeadingGroupTemplate: Story = { + render: ({ ...args }) => {args['children']}, +}; + +export const Playground: Story = { + ...HeadingGroupTemplate, + name: 'Playground', + parameters: { + docs: { + sourceState: 'shown', + }, + }, + args: { + children: [{headingText}, {headingText}], + }, + tags: ['!autodocs'], +}; diff --git a/packages/storybook/src/react-components/pre-heading/pre-heading.mdx b/packages/storybook/src/react-components/pre-heading/pre-heading.mdx new file mode 100644 index 000000000..20e37d071 --- /dev/null +++ b/packages/storybook/src/react-components/pre-heading/pre-heading.mdx @@ -0,0 +1,20 @@ +import { Canvas, Controls, Markdown, Meta } from "@storybook/blocks"; +import markdown from "@utrecht/pre-heading-css/README.md?raw"; +import * as PreHeadingStories from "./pre-heading.stories"; +import { CitationDocumentation } from "../../utils/CitationDocumentation"; + + + +# Pre-Heading + + + +{markdown} + +## Playground + + + diff --git a/packages/storybook/src/react-components/pre-heading/pre-heading.stories.tsx b/packages/storybook/src/react-components/pre-heading/pre-heading.stories.tsx new file mode 100644 index 000000000..a6bb672c6 --- /dev/null +++ b/packages/storybook/src/react-components/pre-heading/pre-heading.stories.tsx @@ -0,0 +1,50 @@ +import { LuxPreHeading } from '@lux-design-system/components-react'; +import tokens from '@lux-design-system/design-tokens/dist/index.json'; +import type { Meta, StoryObj } from '@storybook/react'; + +type Story = StoryObj; + +const meta = { + title: 'React Components/Pre-heading', + id: 'react-components-pre-heading', + component: LuxPreHeading, + subcomponents: {}, + parameters: { + tokens, + tokensPrefix: 'react-pre-heading', + }, + argTypes: { + children: { + name: 'content', + description: 'React text', + control: 'text', + table: { + type: { + summary: 'HTML Content', + }, + }, + }, + }, +} satisfies Meta; + +export default meta; + +const preHeadingText = "Pa's wijze lynx bezag vroom het fikse aquaduct!"; + +const PreHeadingTemplate: Story = { + render: ({ ...args }) => {args['children']}, + args: { + children: preHeadingText, + }, +}; + +export const Playground: Story = { + ...PreHeadingTemplate, + name: 'Playground', + parameters: { + docs: { + sourceState: 'shown', + }, + }, + tags: ['!autodocs'], +}; diff --git a/packages/storybook/src/react-components/textbox/textbox.mdx b/packages/storybook/src/react-components/textbox/textbox.mdx new file mode 100644 index 000000000..3a402e98a --- /dev/null +++ b/packages/storybook/src/react-components/textbox/textbox.mdx @@ -0,0 +1,40 @@ +import LinkTo from "@storybook/addon-links/react"; +import { Canvas, Controls, Markdown, Meta } from "@storybook/blocks"; +import markdown from "@utrecht/textbox-css/README.md?raw"; +import * as TextboxStories from "./textbox.stories"; +import { CitationDocumentation } from "../../utils/CitationDocumentation"; +import FormFieldTextboxMeta from "../form-field-textbox/form-field-textbox.stories"; + + + +# Textbox + +Lux Textbox is gebaseerd op Utrecht Textbox. Dit is alleen het input-element, over het algemeen wordt deze binnen het FormField component gebruikt. +Zie ook het FormFieldTextbox-component. + + + +{markdown} + +## Playground + + + + +## Gebruik + +De Lux Form Field Text Input component kan worden gebruikt voor het invoeren van tekst in formulieren. Het ondersteunt verschillende types en staten. + +```jsx +import { LuxTextbox } from '@lux-design-system/components-react'; + + + + + + + +``` diff --git a/packages/storybook/src/react-components/textbox/textbox.stories.tsx b/packages/storybook/src/react-components/textbox/textbox.stories.tsx new file mode 100644 index 000000000..c5647736b --- /dev/null +++ b/packages/storybook/src/react-components/textbox/textbox.stories.tsx @@ -0,0 +1,348 @@ +import { INPUT_TYPES, LuxTextbox, LuxTextboxProps } 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 { forwardRef, InputHTMLAttributes, PropsWithChildren } from 'react'; +import { BADGES } from '../../../config/preview'; + +// Create a wrapper component to handle the forwardRef +const WrappedLuxTextbox = forwardRef< + HTMLInputElement, + PropsWithChildren & InputHTMLAttributes +>((props, ref) => ); + +WrappedLuxTextbox.displayName = 'LuxTextbox'; + +const meta = { + title: 'React Components/Form Field/Textbox', + id: 'react-components-form-field-textbox', + component: WrappedLuxTextbox, + parameters: { + badges: [BADGES.WIP, BADGES.CANARY], + tokens, + tokensPrefix: 'utrecht-textbox', + }, + argTypes: { + type: { + control: 'select', + options: Object.values(INPUT_TYPES), + }, + dir: { + control: 'select', + options: ['auto', 'ltr', 'rtl'], + }, + disabled: { + control: 'boolean', + }, + focusVisible: { + control: 'boolean', + }, + invalid: { + control: 'boolean', + }, + readOnly: { + control: 'boolean', + }, + required: { + control: 'boolean', + }, + value: { + control: 'text', + }, + autoComplete: { + control: 'select', + options: [ + '', + 'additional-name', + 'address-level1', + 'address-level2', + 'address-level3', + 'address-level4', + 'address-line1', + 'address-line2', + 'address-line3', + 'bday', + 'bday-day', + 'bday-month', + 'bday-year', + 'cc-additional-name', + 'cc-csc', + 'cc-exp', + 'cc-exp-month', + 'cc-exp-year', + 'cc-family-name', + 'cc-given-name', + 'cc-name', + 'cc-number', + 'cc-type', + 'country', + 'country-name', + 'current-password', + 'email', + 'family-name', + 'fax', + 'given-name', + 'home', + 'honorific-prefix', + 'honorific-suffix', + 'impp', + 'language', + 'mobile', + 'name', + 'new-password', + 'nickname', + 'one-time-code', + 'organization', + 'organization-title', + 'pager', + 'photo', + 'postal-code', + 'sex', + 'street-address', + 'tel', + 'tel-area-code', + 'tel-country-code', + 'tel-extension', + 'tel-local', + 'tel-local-prefix', + 'tel-local-suffix', + 'tel-national', + 'transaction-amount', + 'transaction-currency', + 'url', + 'username', + 'work', + ], + }, + minLength: { + control: 'number', + }, + placeholder: { + control: 'text', + }, + placeholderDir: { + control: 'select', + options: ['auto', 'ltr', 'rtl'], + }, + spellCheck: { + control: 'select', + options: ['', 'true', 'false'], + }, + focus: { + control: 'boolean', + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +const TextInputTemplate: Story = { + render: (args) => , +}; + +export const Playground: Story = { + ...TextInputTemplate, + args: { + placeholder: 'Enter text', + type: INPUT_TYPES.TEXT, + }, + parameters: { + docs: { + sourceState: 'shown', + }, + }, +}; + +export const Default: Story = { + ...TextInputTemplate, + args: { + placeholder: 'Default text input', + }, +}; + +export const Naam: Story = { + ...TextInputTemplate, + args: { + value: 'Sjors van Amerongen', + autoComplete: 'name', + }, +}; +export const Voornaam: Story = { + ...TextInputTemplate, + args: { + value: 'Sjors', + autoComplete: 'given-name', + }, +}; +export const Achternaam: Story = { + ...TextInputTemplate, + args: { + value: 'van Amerongen', + autoComplete: 'family-name', + }, +}; +export const Voorvoegsel: Story = { + ...TextInputTemplate, + args: { + value: 'van Amerongen', + autoComplete: 'name-prefix', + }, +}; + +export const Emailadres: Story = { + ...TextInputTemplate, + args: { + type: INPUT_TYPES.EMAIL, + placeholder: 'Vul een e-mailadres in', + autoComplete: 'email', + }, +}; +export const Website: Story = { + ...TextInputTemplate, + args: { + type: INPUT_TYPES.URL, + autoComplete: 'email', + value: 'https://google.com', + }, +}; + +export const Wachtwoord: Story = { + ...TextInputTemplate, + args: { + type: INPUT_TYPES.PASSWORD, + autoComplete: 'current-password', + }, +}; +export const WachtwoordKiezen: Story = { + ...TextInputTemplate, + args: { + type: INPUT_TYPES.PASSWORD, + autoComplete: 'new-password', + }, +}; + +export const Organisatienaam: Story = { + ...TextInputTemplate, + args: { + autoComplete: 'organization', + }, +}; +export const Huisnummer: Story = { + ...TextInputTemplate, + args: { + maxLength: 5, + min: 1, + max: 99999, + pattern: '[0-9]+', + type: 'number', + value: 42, + }, +}; +export const Huisletter: Story = { + ...TextInputTemplate, + args: { + maxLength: 1, + min: 1, + max: 99999, + type: 'number', + pattern: '[A-Za-z]+', + value: 'Q', + }, +}; + +export const DatumSelecteren: Story = { + ...TextInputTemplate, + args: { + type: INPUT_TYPES.DATE, + placeholder: 'Selecteer datum', + }, +}; + +export const MaandSelecteren: Story = { + ...TextInputTemplate, + args: { + type: INPUT_TYPES.MONTH, + placeholder: 'Selecteer maand', + }, +}; + +export const TijdSelecteren: Story = { + ...TextInputTemplate, + args: { + type: INPUT_TYPES.TIME, + placeholder: 'Selecteer tijd', + }, +}; + +export const WeekSelecteren: Story = { + ...TextInputTemplate, + args: { + type: INPUT_TYPES.WEEK, + placeholder: 'Selecteer week', + }, +}; + +export const Zoekveld: Story = { + ...TextInputTemplate, + args: { + type: INPUT_TYPES.SEARCH, + placeholder: 'Zoeken...', + }, +}; + +export const Telefoonnummer: Story = { + ...TextInputTemplate, + args: { + type: INPUT_TYPES.TEL, + autoComplete: 'tel', + value: '+31 30 286 00 00', + }, +}; + +export const Disabled: Story = { + ...TextInputTemplate, + args: { + disabled: true, + placeholder: 'Disabled input', + }, +}; + +export const Invalid: Story = { + ...TextInputTemplate, + args: { + invalid: true, + placeholder: 'Invalid input', + }, +}; + +export const ReadOnly: Story = { + ...TextInputTemplate, + args: { + readOnly: true, + value: 'Read-only input', + }, +}; + +export const Required: Story = { + ...TextInputTemplate, + args: { + required: true, + placeholder: 'Required field', + }, +}; + +export const FocusVisible: Story = { + ...TextInputTemplate, + args: { + focus: true, + placeholder: 'Focused input', + }, +}; + +export const RTL: Story = { + ...TextInputTemplate, + args: { + dir: 'rtl', + placeholderDir: 'rtl', + }, +}; diff --git a/packages/storybook/src/utils/MarkdownDocumentation.tsx b/packages/storybook/src/utils/MarkdownDocumentation.tsx new file mode 100644 index 000000000..5393cb331 --- /dev/null +++ b/packages/storybook/src/utils/MarkdownDocumentation.tsx @@ -0,0 +1,12 @@ +import { Markdown } from '@storybook/blocks'; + +type MarkdownProps = { + children: string; +}; + +/** + * Renders a component, but with the headings 1 level down + */ +export const MarkdownDocumentation = ({ children }: MarkdownProps) => ( + {children.replace(/^(#+) (.+)/gim, '#$1 $2')} +); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index aad4dc0c7..5a3454f4f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -111,8 +111,8 @@ importers: packages/components-react: dependencies: '@utrecht/component-library-css': - specifier: 6.0.0 - version: 6.0.0 + specifier: 6.1.0 + version: 6.1.0 '@utrecht/component-library-react': specifier: 7.1.0 version: 7.1.0(@babel/runtime@7.25.0)(date-fns@3.6.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -241,6 +241,9 @@ importers: '@chromatic-com/storybook': specifier: 1.6.1 version: 1.6.1(react@18.3.1) + '@geometricpanda/storybook-addon-badges': + specifier: 2.0.5 + version: 2.0.5(@storybook/blocks@8.2.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.2.7(@babel/preset-env@7.25.3(@babel/core@7.25.2))))(@storybook/components@8.2.7(storybook@8.2.7(@babel/preset-env@7.25.3(@babel/core@7.25.2))))(@storybook/manager-api@8.2.7(storybook@8.2.7(@babel/preset-env@7.25.3(@babel/core@7.25.2))))(@storybook/preview-api@8.2.7(storybook@8.2.7(@babel/preset-env@7.25.3(@babel/core@7.25.2))))(@storybook/theming@8.2.8(storybook@8.2.7(@babel/preset-env@7.25.3(@babel/core@7.25.2))))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@lux-design-system/assets': specifier: workspace:* version: link:../../proprietary/assets @@ -319,18 +322,39 @@ importers: '@utrecht/button-css': specifier: 1.2.0 version: 1.2.0 + '@utrecht/form-field-css': + specifier: 1.3.0 + version: 1.3.0 + '@utrecht/form-field-description-css': + specifier: 1.3.0 + version: 1.3.0 + '@utrecht/form-field-error-message-css': + specifier: 1.3.1 + version: 1.3.1 + '@utrecht/form-label-css': + specifier: 1.3.0 + version: 1.3.0 '@utrecht/heading-css': specifier: 1.2.0 version: 1.2.0 + '@utrecht/heading-group-css': + specifier: 1.2.0 + version: 1.2.0 '@utrecht/link-css': specifier: 1.1.0 version: 1.1.0 '@utrecht/paragraph-css': specifier: 1.1.0 version: 1.1.0 + '@utrecht/pre-heading-css': + specifier: 1.2.0 + version: 1.2.0 '@utrecht/select-css': specifier: 1.2.0 version: 1.2.0 + '@utrecht/textbox-css': + specifier: 1.3.1 + version: 1.3.1 '@vitejs/plugin-react': specifier: 4.3.1 version: 4.3.1(vite@5.3.5(@types/node@22.7.4)(sass@1.77.8)(terser@5.29.2)) @@ -1760,9 +1784,28 @@ packages: peerDependencies: react: ^18.0.0 + '@geometricpanda/storybook-addon-badges@2.0.5': + resolution: {integrity: sha512-FH56ly6ZhltjyKQWxUKORP67BxhL9FMJRByS5lqKZpeP8J2MMsMXG7eQmFXKcZGQORfVQye+1uYYWXweDOiFTQ==} + peerDependencies: + '@storybook/blocks': ^8.3.0 + '@storybook/components': ^8.3.0 + '@storybook/core-events': ^8.3.0 + '@storybook/manager-api': ^8.3.0 + '@storybook/preview-api': ^8.3.0 + '@storybook/theming': ^8.3.0 + '@storybook/types': ^8.3.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + '@humanwhocodes/config-array@0.11.14': resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} engines: {node: '>=10.10.0'} + deprecated: Use @eslint/config-array instead '@humanwhocodes/module-importer@1.0.1': resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} @@ -1770,6 +1813,7 @@ packages: '@humanwhocodes/object-schema@2.0.2': resolution: {integrity: sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==} + deprecated: Use @eslint/object-schema instead '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} @@ -2739,8 +2783,8 @@ packages: '@utrecht/button-css@1.2.0': resolution: {integrity: sha512-aYqnmuT5HOshv8Kr9IvBsJef+2KNKRNwLPQHxNC2fSGXWDFzROAeChUr0A1Ylt45aAApAD/bxxIHHBAeS1PwEA==} - '@utrecht/component-library-css@6.0.0': - resolution: {integrity: sha512-N+sCB7GSKOeuM06EYmVVbg2JA6t739Z9beXKuqu7Xuvb+0tjQDzPdgzm1vEtTqCMC5L8U1wO7v0HO1d0KlejQw==} + '@utrecht/component-library-css@6.1.0': + resolution: {integrity: sha512-+2qarCIgsNpLpxOcG5Rw3WLqNBASoWJFHMI4RlZJm5JTFfnhnl2wC/ylK23wOOooLNNCmsGrLdvSHHrEThJynw==} '@utrecht/component-library-react@7.1.0': resolution: {integrity: sha512-TPYDkuGWKfvhkdFBPtVfUMEXjqqabSia++Ewf2FyRYuCSud/ZxWCkw53Pf7HXlEloAngQMc/BbrJB4f2Ok9B+Q==} @@ -2765,18 +2809,39 @@ packages: '@utrecht/document-css@1.1.0': resolution: {integrity: sha512-navpa20l9U2c/gMDNzZ83MF2/VfXJBXVIGw6CoZ7s3uNbR92H6MAvvSn29C/Kg9QGjDAhDd7P5dIyjQ1KrwJfg==} + '@utrecht/form-field-css@1.3.0': + resolution: {integrity: sha512-AcMdfFMznH/h1RYwqSij93AR35Wv7ov/1pk6jEZXegCyvqPQi1L636QYNBmPN+HyZJUXymRbA85gXONVIRoMBg==} + + '@utrecht/form-field-description-css@1.3.0': + resolution: {integrity: sha512-Be38ejgdCGmEpLdlJTwhqqQPUbRufjKXT9NNqxrVrHnsqfdmNkXaFXQmRTHVBE3I4Jp+JhG+Qsnw6nghytQKTA==} + + '@utrecht/form-field-error-message-css@1.3.1': + resolution: {integrity: sha512-G8NLWwzL4LuCODpOyMH4fuisgYGBJ+YiBT55w+HVbUR9SyrIBL2gWs3jNyEnTFF4HtjCTUHSS/n/CZqqrq+Yhg==} + + '@utrecht/form-label-css@1.3.0': + resolution: {integrity: sha512-EwjgD5Rw0LjJffv2dyAwhHwD0cuMjAPfM/H4395ldPuACnYiqavOF2IB0l0fB8nMvj3XC/VO2Q2m15neocXssg==} + '@utrecht/heading-css@1.2.0': resolution: {integrity: sha512-d+H5TfhOKYN+YMau3l/4DR/K3mYjYGbZvxfzflxMD2HnHgOxgYC9YvH/FQysE19mKbepGZPzV1rMYoW/vcwmvg==} + '@utrecht/heading-group-css@1.2.0': + resolution: {integrity: sha512-EfLu0TxBZRw0TeEMX61ygs2wWubVjMVAWVfDnYHhoqgX5Wq1tmz0lR4TJuxWgVAyiIuTFOObJtS7x35k0/yPAg==} + '@utrecht/link-css@1.1.0': resolution: {integrity: sha512-L/R5rGvA2RrXJFw/k2l2U7ZgeJuz92pdFDbW1Rtw+7XBK5Z/e+YMpnlDL8fx5VmFk95xO4Jmgv1j6BYiI6vnqA==} '@utrecht/paragraph-css@1.1.0': resolution: {integrity: sha512-HxkYL/W0tkHngucdBFHOK8t7OmUmlnz4uIXP4GBXdA299Hp+RGb/1vaH5A2RaIN0P7/4v6EkuV4lquOcHx0K0Q==} + '@utrecht/pre-heading-css@1.2.0': + resolution: {integrity: sha512-64r4F/uXyEnQkquCdP5hSQsSfqg0i4tME0/yQ02q1EkU3grUXSebIYi2nwD0Cq2weBBNdyelNwdI2f/slNMMVA==} + '@utrecht/select-css@1.2.0': resolution: {integrity: sha512-bgW5aTjsh47jPxzwHrqJDimQjogIQexjTXX02RutMxmfrGEUrZdL2VrI9zWuRknU9PN93DOnu1hr656fIMLk5w==} + '@utrecht/textbox-css@1.3.1': + resolution: {integrity: sha512-O0ouypWFt3SQRIrtUoEw5jnHdnHmT1b9AX8lQmoGPHRP0nbg/pRNasNRca9JAxAzc8I6dGe9KyDF94utmjNL+A==} + '@utrecht/web-component-library-stencil@2.0.0': resolution: {integrity: sha512-tl4YctoEi9nzSrbFLgmIm/BOJzke82NF7TJcmNgzQhBDmWykZNbeNHdx7CE07+TmMR81ZWs8s/umiTCTC6pRUQ==} @@ -4068,6 +4133,7 @@ packages: eslint@8.57.0: resolution: {integrity: sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. hasBin: true espree@9.6.1: @@ -9137,6 +9203,17 @@ snapshots: '@utrecht/components': 4.0.0 react: 18.3.1 + '@geometricpanda/storybook-addon-badges@2.0.5(@storybook/blocks@8.2.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.2.7(@babel/preset-env@7.25.3(@babel/core@7.25.2))))(@storybook/components@8.2.7(storybook@8.2.7(@babel/preset-env@7.25.3(@babel/core@7.25.2))))(@storybook/manager-api@8.2.7(storybook@8.2.7(@babel/preset-env@7.25.3(@babel/core@7.25.2))))(@storybook/preview-api@8.2.7(storybook@8.2.7(@babel/preset-env@7.25.3(@babel/core@7.25.2))))(@storybook/theming@8.2.8(storybook@8.2.7(@babel/preset-env@7.25.3(@babel/core@7.25.2))))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@storybook/blocks': 8.2.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.2.7(@babel/preset-env@7.25.3(@babel/core@7.25.2))) + '@storybook/components': 8.2.7(storybook@8.2.7(@babel/preset-env@7.25.3(@babel/core@7.25.2))) + '@storybook/manager-api': 8.2.7(storybook@8.2.7(@babel/preset-env@7.25.3(@babel/core@7.25.2))) + '@storybook/preview-api': 8.2.7(storybook@8.2.7(@babel/preset-env@7.25.3(@babel/core@7.25.2))) + '@storybook/theming': 8.2.8(storybook@8.2.7(@babel/preset-env@7.25.3(@babel/core@7.25.2))) + optionalDependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + '@humanwhocodes/config-array@0.11.14': dependencies: '@humanwhocodes/object-schema': 2.0.2 @@ -10403,7 +10480,7 @@ snapshots: '@utrecht/button-css@1.2.0': {} - '@utrecht/component-library-css@6.0.0': {} + '@utrecht/component-library-css@6.1.0': {} '@utrecht/component-library-react@7.1.0(@babel/runtime@7.25.0)(date-fns@3.6.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: @@ -10421,14 +10498,28 @@ snapshots: '@utrecht/document-css@1.1.0': {} + '@utrecht/form-field-css@1.3.0': {} + + '@utrecht/form-field-description-css@1.3.0': {} + + '@utrecht/form-field-error-message-css@1.3.1': {} + + '@utrecht/form-label-css@1.3.0': {} + '@utrecht/heading-css@1.2.0': {} + '@utrecht/heading-group-css@1.2.0': {} + '@utrecht/link-css@1.1.0': {} '@utrecht/paragraph-css@1.1.0': {} + '@utrecht/pre-heading-css@1.2.0': {} + '@utrecht/select-css@1.2.0': {} + '@utrecht/textbox-css@1.3.1': {} + '@utrecht/web-component-library-stencil@2.0.0': dependencies: '@stencil/core': 4.18.3 diff --git a/proprietary/design-tokens/src/imported/$metadata.json b/proprietary/design-tokens/src/imported/$metadata.json index 9b075d8ff..d8246953c 100644 --- a/proprietary/design-tokens/src/imported/$metadata.json +++ b/proprietary/design-tokens/src/imported/$metadata.json @@ -52,6 +52,7 @@ "nl/utrecht-radio-button", "nl/utrecht-select", "nl/utrecht-text-input", + "nl/utrecht-button-group", "nl/utrecht-checkbox", "nl/utrecht-textarea" ] diff --git a/proprietary/design-tokens/src/imported/nl/utrecht-button-group.json b/proprietary/design-tokens/src/imported/nl/utrecht-button-group.json new file mode 100644 index 000000000..8bddf79f3 --- /dev/null +++ b/proprietary/design-tokens/src/imported/nl/utrecht-button-group.json @@ -0,0 +1,26 @@ +{ + "utrecht": { + "button-group": { + "background-color": { + "value": "{lux.color.none}", + "type": "color" + }, + "column-gap": { + "value": "{lux.space.200}", + "type": "spacing" + }, + "padding-block-end": { + "value": "{lux.space.0}", + "type": "spacing" + }, + "padding-block-start": { + "value": "{lux.space.0}", + "type": "spacing" + }, + "row-gap": { + "value": "{lux.space.100}", + "type": "spacing" + } + } + } +} \ No newline at end of file