diff --git a/.changeset/purple-pumpkins-joke.md b/.changeset/purple-pumpkins-joke.md new file mode 100644 index 00000000..346eb062 --- /dev/null +++ b/.changeset/purple-pumpkins-joke.md @@ -0,0 +1,7 @@ +--- +"@lux-design-system/components-react": patch +--- + +In deze commit: + +Nieuw component: Form Field Checkbox diff --git a/packages/components-react/package.json b/packages/components-react/package.json index 5b24a291..7ccc5f41 100644 --- a/packages/components-react/package.json +++ b/packages/components-react/package.json @@ -25,6 +25,7 @@ "clean": "rimraf dist/ pages/", "lint": "tsc --project ./tsconfig.json --noEmit && tsc --noEmit --project ./tsconfig.test.json", "test": "mkdirp pages && cross-env NODE_OPTIONS=--experimental-vm-modules jest --verbose", + "watch:components": "vite build --watch", "watch:test": "cross-env NODE_OPTIONS=--experimental-vm-modules jest --verbose --watch" }, "main": "./dist/index.umd.js", @@ -42,6 +43,7 @@ "dependencies": { "@utrecht/component-library-css": "6.1.0", "@utrecht/component-library-react": "7.1.0", + "@utrecht/focus-ring-css": "2.3.0", "clsx": "2.1.1", "date-fns": "3.6.0", "lodash.chunk": "4.2.0" @@ -77,9 +79,9 @@ "tslib": "2.6.3", "typescript": "5.5.4", "vite": "5.3.5", + "vite-plugin-css-injected-by-js": "3.5.2", "vite-plugin-dts": "4.2.3", - "vite-plugin-runtime-config": "1.0.2", - "vite-plugin-css-injected-by-js": "3.5.2" + "vite-plugin-runtime-config": "1.0.2" }, "peerDependencies": { "react": "18", diff --git a/packages/components-react/src/checkbox/Checkbox.css b/packages/components-react/src/checkbox/Checkbox.css index 117621ee..562582f1 100644 --- a/packages/components-react/src/checkbox/Checkbox.css +++ b/packages/components-react/src/checkbox/Checkbox.css @@ -1,3 +1,9 @@ +:root { + /* Just for an example */ + --lux-checkbox-target-hover-background-color: hsla(0 0% 76% / 19%); + --lux-checkbox-target-border-radius: 50%; +} + .lux-checkbox:checked:focus { border-color: var(--lux-checkbox-checked-focus-border-color); background-color: var(--lux-checkbox-checked-focus-background-color); @@ -25,3 +31,26 @@ .lux-checkbox--disabled { cursor: not-allowed; } + +.lux-checkbox--with-target { + display: inline-grid; + grid-template-areas: "input"; + place-content: center center; +} + +.lux-checkbox--with-target::before, +.lux-checkbox--with-target::after { + grid-area: input; + min-inline-size: 44px; + min-block-size: 44px; + content: ""; +} + +.lux-checkbox--with-target::after { + z-index: -1; +} + +.lux-checkbox--with-target:hover::after { + border-radius: var(--lux-checkbox-target-border-radius); + background-color: var(--lux-checkbox-target-hover-background-color); +} diff --git a/packages/components-react/src/checkbox/Checkbox.tsx b/packages/components-react/src/checkbox/Checkbox.tsx index 5469a5ac..dac83a24 100644 --- a/packages/components-react/src/checkbox/Checkbox.tsx +++ b/packages/components-react/src/checkbox/Checkbox.tsx @@ -6,42 +6,31 @@ import './Checkbox.css'; import clsx from 'clsx'; import { ForwardedRef, forwardRef, PropsWithChildren } from 'react'; -export type LuxCheckboxProps = UtrechtCheckboxProps & { - invalid?: boolean; - name?: string; - checked?: boolean; - disabled?: boolean; - className?: string; +export type LuxCheckboxProps = Omit & { + withTarget?: boolean; }; const CLASSNAME = { checkbox: 'lux-checkbox', disabled: 'lux-checkbox--disabled', + withTarget: 'lux-checkbox--with-target', }; export const LuxCheckbox = forwardRef( ( - { disabled, className, name, checked, ...restProps }: PropsWithChildren, + { disabled, withTarget, className, ...restProps }: PropsWithChildren, ref: ForwardedRef, ) => { const combinedClassName = clsx( CLASSNAME.checkbox, { [CLASSNAME.disabled]: disabled, + [CLASSNAME.withTarget]: withTarget, }, className, ); - return ( - - ); + return ; }, ); diff --git a/packages/components-react/src/form-field-checkbox/FormFieldCheckbox.scss b/packages/components-react/src/form-field-checkbox/FormFieldCheckbox.scss new file mode 100644 index 00000000..bc6ac373 --- /dev/null +++ b/packages/components-react/src/form-field-checkbox/FormFieldCheckbox.scss @@ -0,0 +1,126 @@ +/* stylelint-disable-next-line scss/load-no-partial-leading-underscore */ +@use "@utrecht/focus-ring-css/src/_mixin.scss" as focus-ring; + +:root { + --lux-form-field-checkbox-inner-column-gap: var(--lux-space-100); + --lux-form-field-checkbox-inner-padding-inline-start: var(--lux-space-200, 0.5rem); + --lux-form-field-checkbox-inner-padding-inline-end: var(--lux-space-200, 0.5rem); + --lux-form-field-checkbox-inner-padding-block-start: var(--lux-space-200, 0.5rem); + --lux-form-field-checkbox-inner-padding-block-end: var(--lux-space-200, 0.5rem); + --lux-form-field-checkbox-inner-border-radius: var(--lux-border-radius-default); + --lux-form-field-checkbox-inner-border-width: var(--lux-border-width-default, 1px); + --lux-form-field-checkbox-inner-border-style: solid; + --lux-form-field-checkbox-inner-color: var(--lux-color-foreground-default); + --lux-form-field-checkbox-inner-border-color: var(--lux-color-brand-default); + --lux-form-field-checkbox-inner-background-color: var(--lux-color-none); + --lux-form-field-checkbox-invalid-inner-border-color: var(--lux-color-feedback-error-default); + --lux-form-field-checkbox-invalid-inner-background-color: var(--lux-color-none); + --lux-form-field-checkbox-invalid-inner-row-gap: var(--lux-space-100); + --lux-form-field-checkbox-hover-inner-color: var(--lux-color-foreground-default); + --lux-form-field-checkbox-hover-inner-border-color: var(--lux-color-brand-default); + --lux-form-field-checkbox-hover-inner-background-color: var(--lux-color-brand-subdued); + --lux-form-field-checkbox-focus-visible-inner-color: var(--lux-color-foreground-default); + --lux-form-field-checkbox-focus-visible-inner-border-color: var(--lux-color-brand-default); + --lux-form-field-checkbox-focus-visible-inner-background-color: var(--lux-color-none); + --lux-form-field-checkbox-disabled-inner-color: var(--lux-color-foreground-default); + --lux-form-field-checkbox-disabled-inner-border-color: var(--lux-color-border-subdued); + --lux-form-field-checkbox-disabled-inner-background-color: var(--lux-color-none); +} + +.lux-form-field-checkbox { + /* prettier-ignore */ + --_lux-checkbox-size: calc(var(--utrecht-checkbox-size) + var(--utrecht-checkbox-margin-inline-end) + var(--lux-form-field-checkbox-inner-padding-inline-start)); + --_lux-column-gap: var(--lux-form-field-checkbox-inner-column-gap, var(--utrecht-checkbox-margin-inline-end, 12px)); + + position: relative; + grid-template-columns: var(--_lux-checkbox-size) 100fr; + grid-template-areas: + "input label" + ". description" + "error-message error-message"; + gap: 0 var(--_lux-column-gap); + + &::before { + position: relative; + grid-row-start: label; + grid-row-end: description; + grid-column-start: input; + grid-column-end: description; + z-index: -1; + border: var(--lux-form-field-checkbox-inner-border-width) var(--lux-form-field-checkbox-inner-border-style) + var(--lux-form-field-checkbox-inner-border-color); + border-radius: var(--lux-form-field-checkbox-inner-border-radius); + pointer-events: none; + content: ""; + } + + &--disabled::before { + border-color: var(--lux-form-field-checkbox-disabled-inner-border-color); + background-color: var(--lux-form-field-checkbox-disabled-inner-background-color); + color: var(--lux-form-field-checkbox-disabled-inner-color); + } + + &--invalid::before { + border-color: var(--lux-form-field-checkbox-invalid-inner-border-color); + background-color: var(--lux-form-field-checkbox-invalid-inner-background-color); + color: var(--lux-form-field-checkbox-invalid-inner-color); + } + + &:has(:focus-visible)::before { + --_utrecht-focus-ring-box-shadow: 0 0 0 var(--utrecht-focus-outline-width, 0) + var(--utrecht-focus-inverse-outline-color, transparent); + + @include focus-ring.utrecht-focus-ring; + + z-index: -1; + border-color: var(--lux-form-field-checkbox-focus-visible-inner-border-color); + background-color: var(--lux-form-field-checkbox-focus-visible-inner-background-color); + color: var(--lux-form-field-checkbox-focus-visible-inner-color); + } + + .utrecht-checkbox:focus-visible { + outline: none; + } + + &: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); + } + + &--with-target &__label::before, + &--with-target &__description label::before { + position: absolute; + inset-block-end: 0; + inset-block-start: 0; + inset-inline-end: 0; + inset-inline-start: calc(-1 * var(--_lux-checkbox-size) - var(--_lux-column-gap)); + pointer-events: visible; + content: ""; + } + + .utrecht-form-field__label, + .utrecht-form-field__description { + position: relative; + } + + .utrecht-form-field__label, + .utrecht-form-field__input { + padding-block-start: var(--lux-form-field-checkbox-inner-padding-block-start); + } + + .utrecht-form-field__label + .utrecht-form-field__input, + .utrecht-form-field__description { + padding-block-end: var(--lux-form-field-checkbox-inner-padding-block-end); + } + + .utrecht-form-field__input { + display: grid; + place-content: baseline center; + padding-inline-start: var(--lux-form-field-checkbox-inner-padding-inline-start); + } + + .utrecht-form-field__error-message { + padding-block-start: var(--lux-form-field-checkbox-invalid-inner-row-gap); + } +} diff --git a/packages/components-react/src/form-field-checkbox/FormFieldCheckbox.tsx b/packages/components-react/src/form-field-checkbox/FormFieldCheckbox.tsx new file mode 100644 index 00000000..7c7fc442 --- /dev/null +++ b/packages/components-react/src/form-field-checkbox/FormFieldCheckbox.tsx @@ -0,0 +1,96 @@ +import clsx from 'clsx'; +import { useId } from 'react'; +import { LuxCheckbox } from '../checkbox/Checkbox'; +import { LuxFormField, LuxFormFieldProps } 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 './FormFieldCheckbox.scss'; + +export type LuxFormFieldCheckboxProps = LuxFormFieldProps & { + checked?: boolean; + disabled?: boolean; + appearance?: LuxFormFieldDescriptionAppearance; + withTarget?: boolean; + distanced?: boolean; +}; + +export const LuxFormFieldCheckbox = ({ + label, + description, + errorMessage, + checked, + disabled, + invalid, + appearance, + withTarget, + distanced, + children, + ...restProps +}: LuxFormFieldCheckboxProps) => { + 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 + ); + + return ( + + } + className={clsx('lux-form-field-checkbox', { + 'lux-form-field-checkbox--invalid': invalid, + 'lux-form-field-checkbox--disabled': disabled, + 'lux-form-field-checkbox--with-target': withTarget, + })} + {...restProps} + > + {children} + + ); +}; diff --git a/packages/components-react/src/index.ts b/packages/components-react/src/index.ts index 187b8b90..4cd4aec5 100644 --- a/packages/components-react/src/index.ts +++ b/packages/components-react/src/index.ts @@ -14,6 +14,7 @@ export { } from './heading/Heading'; export { LuxHeadingGroup, type LuxHeadingGroupProps } from './heading-group/HeadingGroup'; export { LuxFormField, type LuxFormFieldProps } from './form-field/FormField'; +export { LuxFormFieldCheckbox, type LuxFormFieldCheckboxProps } from './form-field-checkbox/FormFieldCheckbox'; export { LuxFormFieldDescription, type LuxFormFieldDescriptionProps, diff --git a/packages/storybook/src/react-components/checkbox/checkbox.mdx b/packages/storybook/src/react-components/checkbox/checkbox.mdx index 9b5b39ec..3ee7e09a 100644 --- a/packages/storybook/src/react-components/checkbox/checkbox.mdx +++ b/packages/storybook/src/react-components/checkbox/checkbox.mdx @@ -51,6 +51,6 @@ import { CitationDocumentation } from "../../utils/CitationDocumentation.tsx"; -### Focus Visible +## With Target - + diff --git a/packages/storybook/src/react-components/checkbox/checkbox.stories.tsx b/packages/storybook/src/react-components/checkbox/checkbox.stories.tsx index 96b65d12..d06132da 100644 --- a/packages/storybook/src/react-components/checkbox/checkbox.stories.tsx +++ b/packages/storybook/src/react-components/checkbox/checkbox.stories.tsx @@ -22,18 +22,16 @@ const meta = { description: 'Disabled state', control: 'boolean', }, + withTarget: { + description: 'Sets a 44px by 44px click target around the checkbox (WCAG 2.5.5)', + control: 'boolean', + }, }, } satisfies Meta; export default meta; const CheckboxTemplate: Story = { - args: { - checked: false, - disabled: false, - invalid: false, - required: false, - }, render: ({ ...args }) => , }; @@ -56,7 +54,7 @@ export const Default: Story = { export const Checked: Story = { name: 'Checked', args: { - checked: true, + defaultChecked: true, }, }; @@ -70,7 +68,7 @@ export const Disabled: Story = { export const CheckedAndDisabled: Story = { name: 'Checked and Disabled', args: { - checked: true, + defaultChecked: true, disabled: true, }, }; @@ -91,10 +89,10 @@ export const Focus: Story = { }, }; -export const FocusVisible: Story = { +export const WithTarget: Story = { ...CheckboxTemplate, - name: 'Focus Visible', - parameters: { - pseudo: { focusVisible: true }, + name: 'With Target', + args: { + withTarget: true, }, }; diff --git a/packages/storybook/src/react-components/form-field-checkbox/form-field-checkbox.mdx b/packages/storybook/src/react-components/form-field-checkbox/form-field-checkbox.mdx new file mode 100644 index 00000000..28c52afe --- /dev/null +++ b/packages/storybook/src/react-components/form-field-checkbox/form-field-checkbox.mdx @@ -0,0 +1,40 @@ +import { Canvas, Controls, Description, Meta } from "@storybook/blocks"; +import * as FormFieldCheckboxStories from "./form-field-checkbox.stories"; + + + +# Form Field Checkbox + +## Playground + + + + +## States + +### Invalid + + + + +### Disabled + + + + +## Options + +### With description + + + + +### With target + + + + +### With long texts + + + diff --git a/packages/storybook/src/react-components/form-field-checkbox/form-field-checkbox.stories.tsx b/packages/storybook/src/react-components/form-field-checkbox/form-field-checkbox.stories.tsx new file mode 100644 index 00000000..76161cd1 --- /dev/null +++ b/packages/storybook/src/react-components/form-field-checkbox/form-field-checkbox.stories.tsx @@ -0,0 +1,107 @@ +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 { BADGES } from '../../../config/preview'; +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'; + +const meta = { + title: 'React Components/Form Field/Form Field Checkbox', + id: 'react-components-form-field-form-field-checkbox', + component: LuxFormFieldCheckbox, + parameters: { + badges: [BADGES.WIP, BADGES.CANARY], + tokens, + tokensPrefix: 'utrecht-form-field-checkbox', + }, + argTypes: { + ...CheckboxMeta.argTypes, + description: { + ...FormFieldDescriptionMeta.argTypes.children, + }, + appearance: { + ...FormFieldDescriptionMeta.argTypes.appearance, + }, + distanced: { + ...FormFieldErrorMessageMeta.argTypes.distanced, + }, + disabled: { + type: 'boolean', + }, + withTarget: { + type: 'boolean', + description: 'Makes the whole inner part a click target.', + }, + errorMessage: { + if: { + arg: 'invalid', + truthy: true, + }, + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Playground: Story = { + name: 'Playground', + args: { + label: 'Label', + errorMessage: 'ErrorMessage', + invalid: false, + appearance: undefined, + }, + parameters: { + docs: { + sourceState: 'shown', + }, + }, + tags: ['!autodocs'], +}; + +export const Invalid: Story = { + name: 'Invalid', + args: { + ...Playground.args, + invalid: true, + }, +}; + +export const Disabled: Story = { + name: 'Disabled', + args: { + ...Playground.args, + disabled: true, + }, +}; + +export const WithDescription: Story = { + name: 'With Description', + args: { + ...Playground.args, + description: 'Description', + }, +}; + +export const WithTarget: Story = { + name: 'With Target', + args: { + ...Playground.args, + withTarget: true, + }, +}; + +export const withLongTexts: Story = { + name: 'With long texts', + args: { + ...Playground.args, + label: 'Lorem ipsum dolor sit amet, consectetuer adipiscing elit', + description: + 'Dolor ante id varius, aenean eu faucibus vitae malesuada. Viverra malesuada aliquam et placerat justo porta ipsum parturient.', + errorMessage: 'Cursus nostra varius efficitur lobortis aliquam lectus bibendum', + invalid: true, + withTarget: true, + }, +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a489bfbf..6c6eeb9c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -116,6 +116,9 @@ importers: '@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) + '@utrecht/focus-ring-css': + specifier: 2.3.0 + version: 2.3.0 clsx: specifier: 2.1.1 version: 2.1.1 @@ -352,15 +355,15 @@ importers: '@utrecht/pre-heading-css': specifier: 1.2.0 version: 1.2.0 + '@utrecht/radio-button-css': + specifier: 1.3.1 + version: 1.3.1 '@utrecht/select-css': specifier: 1.2.0 version: 1.2.0 '@utrecht/textbox-css': specifier: 1.3.1 version: 1.3.1 - '@utrecht/radio-button-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)) @@ -2794,6 +2797,7 @@ packages: '@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==} peerDependencies: @@ -2817,6 +2821,9 @@ packages: '@utrecht/document-css@1.1.0': resolution: {integrity: sha512-navpa20l9U2c/gMDNzZ83MF2/VfXJBXVIGw6CoZ7s3uNbR92H6MAvvSn29C/Kg9QGjDAhDd7P5dIyjQ1KrwJfg==} + '@utrecht/focus-ring-css@2.3.0': + resolution: {integrity: sha512-HYsULqosoFp9zMypvGuEaGpsl01cW6qKGIYGQa9FZCcCtwXcS0dqFEcjeZJ1yue0RJsk0QuGVBrQOXjsNPNkBg==} + '@utrecht/form-field-css@1.3.0': resolution: {integrity: sha512-AcMdfFMznH/h1RYwqSij93AR35Wv7ov/1pk6jEZXegCyvqPQi1L636QYNBmPN+HyZJUXymRbA85gXONVIRoMBg==} @@ -2844,15 +2851,15 @@ packages: '@utrecht/pre-heading-css@1.2.0': resolution: {integrity: sha512-64r4F/uXyEnQkquCdP5hSQsSfqg0i4tME0/yQ02q1EkU3grUXSebIYi2nwD0Cq2weBBNdyelNwdI2f/slNMMVA==} + '@utrecht/radio-button-css@1.3.1': + resolution: {integrity: sha512-eLO9J+OThXetZRyr777zo1DPNXwCWIoCvvlk9o2EtoLubNhzgt6ECGKN0zGoP8AwbqHxymSvvaP8SmwdpoHTbA==} + '@utrecht/select-css@1.2.0': resolution: {integrity: sha512-bgW5aTjsh47jPxzwHrqJDimQjogIQexjTXX02RutMxmfrGEUrZdL2VrI9zWuRknU9PN93DOnu1hr656fIMLk5w==} '@utrecht/textbox-css@1.3.1': resolution: {integrity: sha512-O0ouypWFt3SQRIrtUoEw5jnHdnHmT1b9AX8lQmoGPHRP0nbg/pRNasNRca9JAxAzc8I6dGe9KyDF94utmjNL+A==} - '@utrecht/radio-button-css@1.3.1': - resolution: {integrity: sha512-eLO9J+OThXetZRyr777zo1DPNXwCWIoCvvlk9o2EtoLubNhzgt6ECGKN0zGoP8AwbqHxymSvvaP8SmwdpoHTbA==} - '@utrecht/web-component-library-stencil@2.0.0': resolution: {integrity: sha512-tl4YctoEi9nzSrbFLgmIm/BOJzke82NF7TJcmNgzQhBDmWykZNbeNHdx7CE07+TmMR81ZWs8s/umiTCTC6pRUQ==} @@ -10511,6 +10518,8 @@ snapshots: '@utrecht/document-css@1.1.0': {} + '@utrecht/focus-ring-css@2.3.0': {} + '@utrecht/form-field-css@1.3.0': {} '@utrecht/form-field-description-css@1.3.0': {} @@ -10529,12 +10538,12 @@ snapshots: '@utrecht/pre-heading-css@1.2.0': {} + '@utrecht/radio-button-css@1.3.1': {} + '@utrecht/select-css@1.2.0': {} '@utrecht/textbox-css@1.3.1': {} - '@utrecht/radio-button-css@1.3.1': {} - '@utrecht/web-component-library-stencil@2.0.0': dependencies: '@stencil/core': 4.18.3