From 3b4c5ce49cb09837519e45a56cd97661126571a2 Mon Sep 17 00:00:00 2001 From: AlineNap <59806622+AlineNap@users.noreply.github.com> Date: Thu, 14 Nov 2024 13:44:43 +0100 Subject: [PATCH 1/7] Heading stijling update (#332) # Contents Wijziging tokens: Stijling van het heading component Jesse heeft volgende aangevraagd: H1, regular, zwart H2, bold, brand H3, bold, zwart H4, bold, brand - ~~[ ] New features/components and bugfixes are covered by tests~~ - [x] Changesets are created - ~~[ ] Definition of Done is checked~~ --------- Co-authored-by: Jaap-Hein Wester --- .changeset/ten-avocados-agree.md | 11 +++++++++++ .../src/imported/nl/utrecht-heading.json | 6 +++--- 2 files changed, 14 insertions(+), 3 deletions(-) create mode 100644 .changeset/ten-avocados-agree.md diff --git a/.changeset/ten-avocados-agree.md b/.changeset/ten-avocados-agree.md new file mode 100644 index 000000000..5518b9ac4 --- /dev/null +++ b/.changeset/ten-avocados-agree.md @@ -0,0 +1,11 @@ +--- +"@lux-design-system/design-tokens": major +--- + +In deze commit: + +- Gewijzigde tokens: + - utrecht heading-1: color van `brand` naar `foreground`, font-weight van `bold` naar `regular` + - utrecht heading-3: color van `brand` naar `foreground` + +**Let op:** visuele wijziging op alle thema's, H1 en H3 hebben nu een andere vormgeving. diff --git a/proprietary/design-tokens/src/imported/nl/utrecht-heading.json b/proprietary/design-tokens/src/imported/nl/utrecht-heading.json index 889ea9fd7..e78b32a64 100644 --- a/proprietary/design-tokens/src/imported/nl/utrecht-heading.json +++ b/proprietary/design-tokens/src/imported/nl/utrecht-heading.json @@ -2,7 +2,7 @@ "utrecht": { "heading-1": { "color": { - "value": "{lux.color.brand.default}", + "value": "{lux.color.foreground.default}", "type": "color" }, "font-family": { @@ -10,7 +10,7 @@ "type": "fontFamilies" }, "font-weight": { - "value": "{lux.font-weight.bold}", + "value": "{lux.font-weight.regular}", "type": "fontWeights" }, "line-height": { @@ -46,7 +46,7 @@ }, "heading-3": { "color": { - "value": "{lux.color.brand.default}", + "value": "{lux.color.foreground.default}", "type": "color" }, "font-family": { From 3636be3382258683e0f83305873355df9335089d Mon Sep 17 00:00:00 2001 From: VladAfanasev Date: Tue, 19 Nov 2024 08:29:21 +0100 Subject: [PATCH 2/7] feat: added Checkbox component (#336) Nieuw component: Checkbox --------- Co-authored-by: Vlad Afanasev --- .changeset/rude-fishes-give.md | 5 + .../src/checkbox/Checkbox.css | 27 +++++ .../src/checkbox/Checkbox.tsx | 48 +++++++++ .../src/checkbox/test/Checkbox.spec.tsx | 66 ++++++++++++ packages/components-react/src/index.ts | 1 + packages/storybook/package.json | 1 + .../react-components/checkbox/checkbox.mdx | 56 ++++++++++ .../checkbox/checkbox.stories.tsx | 100 ++++++++++++++++++ pnpm-lock.yaml | 9 +- 9 files changed, 312 insertions(+), 1 deletion(-) create mode 100644 .changeset/rude-fishes-give.md create mode 100644 packages/components-react/src/checkbox/Checkbox.css create mode 100644 packages/components-react/src/checkbox/Checkbox.tsx create mode 100644 packages/components-react/src/checkbox/test/Checkbox.spec.tsx create mode 100644 packages/storybook/src/react-components/checkbox/checkbox.mdx create mode 100644 packages/storybook/src/react-components/checkbox/checkbox.stories.tsx diff --git a/.changeset/rude-fishes-give.md b/.changeset/rude-fishes-give.md new file mode 100644 index 000000000..8fb8cd1e1 --- /dev/null +++ b/.changeset/rude-fishes-give.md @@ -0,0 +1,5 @@ +--- +"@lux-design-system/components-react": minor +--- + +Nieuw component: Checkbox diff --git a/packages/components-react/src/checkbox/Checkbox.css b/packages/components-react/src/checkbox/Checkbox.css new file mode 100644 index 000000000..117621ee8 --- /dev/null +++ b/packages/components-react/src/checkbox/Checkbox.css @@ -0,0 +1,27 @@ +.lux-checkbox:checked:focus { + border-color: var(--lux-checkbox-checked-focus-border-color); + background-color: var(--lux-checkbox-checked-focus-background-color); + color: var(--lux-checkbox-checked-focus-color); +} + +.lux-checkbox:checked:hover { + border-color: var(--lux-checkbox-checked-hover-border-color); + background-color: var(--lux-checkbox-checked-hover-background-color); + color: var(--lux-checkbox-checked-hover-color); +} + +.lux-checkbox:checked:active { + border-color: var(--lux-checkbox-checked-active-border-color); + background-color: var(--lux-checkbox-checked-active-background-color); + color: var(--lux-checkbox-checked-active-color); +} + +.lux-checkbox:checked:disabled { + border-color: var(--lux-checkbox-checked-disabled-border-color); + background-color: var(--lux-checkbox-checked-disabled-background-color); + color: var(--lux-checkbox-checked-disabled-color); +} + +.lux-checkbox--disabled { + cursor: not-allowed; +} diff --git a/packages/components-react/src/checkbox/Checkbox.tsx b/packages/components-react/src/checkbox/Checkbox.tsx new file mode 100644 index 000000000..5469a5ac9 --- /dev/null +++ b/packages/components-react/src/checkbox/Checkbox.tsx @@ -0,0 +1,48 @@ +import { + Checkbox as UtrechtCheckbox, + type CheckboxProps as UtrechtCheckboxProps, +} from '@utrecht/component-library-react/dist/css-module'; +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; +}; + +const CLASSNAME = { + checkbox: 'lux-checkbox', + disabled: 'lux-checkbox--disabled', +}; + +export const LuxCheckbox = forwardRef( + ( + { disabled, className, name, checked, ...restProps }: PropsWithChildren, + ref: ForwardedRef, + ) => { + const combinedClassName = clsx( + CLASSNAME.checkbox, + { + [CLASSNAME.disabled]: disabled, + }, + className, + ); + + return ( + + ); + }, +); + +LuxCheckbox.displayName = 'LuxCheckbox'; diff --git a/packages/components-react/src/checkbox/test/Checkbox.spec.tsx b/packages/components-react/src/checkbox/test/Checkbox.spec.tsx new file mode 100644 index 000000000..75237c31f --- /dev/null +++ b/packages/components-react/src/checkbox/test/Checkbox.spec.tsx @@ -0,0 +1,66 @@ +import { describe, expect, it } from '@jest/globals'; +import { render, screen } from '@testing-library/react'; +import { LuxCheckbox } from '../Checkbox'; + +describe('Checkbox', () => { + it('renders a checkbox', () => { + render(); + + const checkbox = screen.getByRole('checkbox'); + expect(checkbox).toBeInTheDocument(); + }); + + it('renders a checkbox with correct name attribute', () => { + render(); + + const checkbox = screen.getByRole('checkbox'); + expect(checkbox).toHaveAttribute('name', 'test-checkbox'); + }); + + it('renders a checked checkbox when checked prop is true', () => { + render(); + + const checkbox = screen.getByRole('checkbox'); + expect(checkbox).toBeChecked(); + }); + + it('renders an unchecked checkbox when checked prop is false', () => { + render(); + + const checkbox = screen.getByRole('checkbox'); + expect(checkbox).not.toBeChecked(); + }); + + it('renders a disabled checkbox when disabled prop is true', () => { + render(); + + const checkbox = screen.getByRole('checkbox'); + expect(checkbox).toBeDisabled(); + expect(checkbox).toHaveClass('lux-checkbox--disabled'); + }); + + it('applies custom className when provided', () => { + const customClass = 'custom-checkbox'; + render(); + + const checkbox = screen.getByRole('checkbox'); + expect(checkbox).toHaveClass(customClass); + }); + + it('forwards additional props to the checkbox input', () => { + render(); + + const checkbox = screen.getByRole('checkbox'); + expect(checkbox).toHaveAttribute('data-testid', 'test-id'); + expect(checkbox).toHaveAttribute('aria-label', 'test label'); + }); + + it('combines multiple classes correctly', () => { + render(); + + const checkbox = screen.getByRole('checkbox'); + expect(checkbox).toHaveClass('lux-checkbox'); + expect(checkbox).toHaveClass('lux-checkbox--disabled'); + expect(checkbox).toHaveClass('custom-class'); + }); +}); diff --git a/packages/components-react/src/index.ts b/packages/components-react/src/index.ts index 590762ec2..a53e4bd09 100644 --- a/packages/components-react/src/index.ts +++ b/packages/components-react/src/index.ts @@ -25,5 +25,6 @@ export { 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 { LuxCheckbox, type LuxCheckboxProps } from './checkbox/Checkbox'; export { LuxPreHeading, type LuxPreHeadingProps } from './pre-heading/PreHeading'; export { LuxSection, type LuxSectionProps } from './section/Section'; diff --git a/packages/storybook/package.json b/packages/storybook/package.json index 348d58228..92889296f 100644 --- a/packages/storybook/package.json +++ b/packages/storybook/package.json @@ -53,6 +53,7 @@ "@types/react-dom": "18.3.0", "@utrecht/alert-css": "1.1.0", "@utrecht/button-css": "1.2.0", + "@utrecht/checkbox-css": "1.3.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", diff --git a/packages/storybook/src/react-components/checkbox/checkbox.mdx b/packages/storybook/src/react-components/checkbox/checkbox.mdx new file mode 100644 index 000000000..9b5b39ec6 --- /dev/null +++ b/packages/storybook/src/react-components/checkbox/checkbox.mdx @@ -0,0 +1,56 @@ +import { Canvas, Controls, Markdown, Meta } from "@storybook/blocks"; +import markdown from "@utrecht/checkbox-css/README.md?raw"; +import * as CheckboxStories from "./checkbox.stories.tsx"; +import { CitationDocumentation } from "../../utils/CitationDocumentation.tsx"; + + + +# Checkbox + + + +{markdown} + +## Notes + +- The checkbox supports different states: checked, disabled, invalid, required +- All states can be combined +- The component inherits its styling from the Utrecht Design System + +## Playground + + + + +## Default + + + +## States + +### Checked + + + +### Disabled + + + +### Checked and Disabled + + + +### Hover + + + +### Focus + + + +### Focus Visible + + diff --git a/packages/storybook/src/react-components/checkbox/checkbox.stories.tsx b/packages/storybook/src/react-components/checkbox/checkbox.stories.tsx new file mode 100644 index 000000000..96b65d124 --- /dev/null +++ b/packages/storybook/src/react-components/checkbox/checkbox.stories.tsx @@ -0,0 +1,100 @@ +import { LuxCheckbox } 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/Checkbox', + id: 'react-components-checkbox', + component: LuxCheckbox, + subcomponents: {}, + parameters: { + tokens, + tokensPrefix: 'utrecht-checkbox', + }, + argTypes: { + checked: { + description: 'Checked state', + control: 'boolean', + }, + disabled: { + description: 'Disabled state', + control: 'boolean', + }, + }, +} satisfies Meta; + +export default meta; + +const CheckboxTemplate: Story = { + args: { + checked: false, + disabled: false, + invalid: false, + required: false, + }, + render: ({ ...args }) => , +}; + +export const Playground: Story = { + ...CheckboxTemplate, + name: 'Playground', + parameters: { + docs: { + sourceState: 'shown', + }, + }, + tags: ['!autodocs'], +}; + +export const Default: Story = { + name: 'Default', + args: {}, +}; + +export const Checked: Story = { + name: 'Checked', + args: { + checked: true, + }, +}; + +export const Disabled: Story = { + name: 'Disabled', + args: { + disabled: true, + }, +}; + +export const CheckedAndDisabled: Story = { + name: 'Checked and Disabled', + args: { + checked: true, + disabled: true, + }, +}; + +export const Hover: Story = { + ...CheckboxTemplate, + name: 'Hover', + parameters: { + pseudo: { hover: true }, + }, +}; + +export const Focus: Story = { + ...CheckboxTemplate, + name: 'Focus', + parameters: { + pseudo: { focus: true, focusVisible: true }, + }, +}; + +export const FocusVisible: Story = { + ...CheckboxTemplate, + name: 'Focus Visible', + parameters: { + pseudo: { focusVisible: true }, + }, +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 979f8a71b..9040fdd8c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -322,6 +322,9 @@ importers: '@utrecht/button-css': specifier: 1.2.0 version: 1.2.0 + '@utrecht/checkbox-css': + specifier: 1.3.0 + version: 1.3.0 '@utrecht/form-field-css': specifier: 1.3.0 version: 1.3.0 @@ -2780,9 +2783,11 @@ packages: '@utrecht/button-css@1.2.0': resolution: {integrity: sha512-aYqnmuT5HOshv8Kr9IvBsJef+2KNKRNwLPQHxNC2fSGXWDFzROAeChUr0A1Ylt45aAApAD/bxxIHHBAeS1PwEA==} + '@utrecht/checkbox-css@1.3.0': + resolution: {integrity: sha512-8V2mm6niueojETo8wmYaidnqZPt5HdUZhofHBwQDz7TKQcldM4Q9TD/eYt0T7SFWZTFC7XyZn/lJYetBD75Q5Q==} + '@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: @@ -10474,6 +10479,8 @@ snapshots: '@utrecht/button-css@1.2.0': {} + '@utrecht/checkbox-css@1.3.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)': From 42f75229c48ac14e5a19cf7f5452011c84340467 Mon Sep 17 00:00:00 2001 From: Jaap-Hein Wester Date: Wed, 20 Nov 2024 09:37:34 +0100 Subject: [PATCH 3/7] chore(docs): Fixes Design review button en Visual regression (#337) # Contents Fixes in documentatie nav Design review button en Visual regression story toegevoegd. ## Checklist - [x] New features/components and bugfixes are covered by tests - ~~[ ] Changesets are created~~ - ~~[ ] Definition of Done is checked~~ --------- Co-authored-by: Jaap-Hein Wester --- packages/storybook/config/preview.tsx | 2 + packages/storybook/config/themes.ts | 1 + .../src/react-components/button/button.mdx | 56 +++++++- .../button/button.stories.tsx | 136 +++++++++++++----- .../react-components/button/visual/States.tsx | 44 ++++++ .../button/visual/Variants.tsx | 11 ++ 6 files changed, 210 insertions(+), 40 deletions(-) create mode 100644 packages/storybook/src/react-components/button/visual/States.tsx create mode 100644 packages/storybook/src/react-components/button/visual/Variants.tsx diff --git a/packages/storybook/config/preview.tsx b/packages/storybook/config/preview.tsx index b3ee1f78d..5eea05ed4 100644 --- a/packages/storybook/config/preview.tsx +++ b/packages/storybook/config/preview.tsx @@ -27,6 +27,8 @@ const preview: Preview = { 'DigiD dark': 'lux-theme--digid-dark', 'Logius light': 'lux-theme--logius-light', 'Logius dark': 'lux-theme--logius-dark', + 'Mijn Aansluitingen light': 'lux-theme--eva-light', + 'Mijn Aansluitingen dark': 'lux-theme--eva-dark', 'Mijn Overheid light': 'lux-theme--mijnoverheid-light', 'Mijn Overheid dark': 'lux-theme--mijnoverheid-dark', 'NLdoc light': 'lux-theme--nldoc-light', diff --git a/packages/storybook/config/themes.ts b/packages/storybook/config/themes.ts index 24088da33..085a2a1a5 100644 --- a/packages/storybook/config/themes.ts +++ b/packages/storybook/config/themes.ts @@ -5,5 +5,6 @@ */ import '@lux-design-system/design-tokens/dist/digid/index-theme.css'; import '@lux-design-system/design-tokens/dist/logius/index-theme.css'; +import '@lux-design-system/design-tokens/dist/eva/index-theme.css'; import '@lux-design-system/design-tokens/dist/mijnoverheid/index-theme.css'; import '@lux-design-system/design-tokens/dist/nldoc/index-theme.css'; diff --git a/packages/storybook/src/react-components/button/button.mdx b/packages/storybook/src/react-components/button/button.mdx index d77052c7c..e4f8e4c81 100644 --- a/packages/storybook/src/react-components/button/button.mdx +++ b/packages/storybook/src/react-components/button/button.mdx @@ -1,4 +1,4 @@ -import { Canvas, Controls, Markdown, Meta } from "@storybook/blocks"; +import { Canvas, Controls, Description, Markdown, Meta } from "@storybook/blocks"; import markdown from "@utrecht/button-css/README.md?raw"; import * as ButtonStories from "./button.stories.tsx"; import { CitationDocumentation } from "../../utils/CitationDocumentation.tsx"; @@ -12,6 +12,8 @@ import { CitationDocumentation } from "../../utils/CitationDocumentation.tsx"; url="https://nl-design-system.github.io/utrecht/storybook-css/index.html?path=/docs/css-button--docs" /> +{/* TODO: New way of doc md */} + {markdown} ## Opmerkingen @@ -24,26 +26,68 @@ import { CitationDocumentation } from "../../utils/CitationDocumentation.tsx"; -## Small Button - - - ## Variants +### Primary / primary action button + + + +### Secondary / secondary action button + + + +### Tertiary / subtle button + + -## Button states +### Small Button + + + + +

States

+ +### Active + + +### Focus + + + +### Hover + + + +### Disabled + + + +### Busy + + + +### Toggle / Pressed + + ## Button With Icon +### At start position + + + +### At end position + + diff --git a/packages/storybook/src/react-components/button/button.stories.tsx b/packages/storybook/src/react-components/button/button.stories.tsx index 931bb6747..cce9b6c71 100644 --- a/packages/storybook/src/react-components/button/button.stories.tsx +++ b/packages/storybook/src/react-components/button/button.stories.tsx @@ -2,6 +2,10 @@ import { LuxButton } from '@lux-design-system/components-react'; import tokens from '@lux-design-system/design-tokens/dist/index.json'; import { useArgs } from '@storybook/preview-api'; import type { Meta, StoryFn, StoryObj } from '@storybook/react'; +import tokensDefinition from '@utrecht/button-css/src/tokens.json'; +import { InteractiveStates, PropertyStates } from './visual/States'; +import { Appearances, Sizes } from './visual/Variants'; +import { createDesignTokensStory, createVisualRegressionStory, VisualRegressionWrapper } from '../../utils'; type Story = StoryObj; @@ -12,6 +16,7 @@ const meta = { subcomponents: {}, parameters: { tokens, + tokensDefinition, tokensPrefix: 'utrecht-button', }, argTypes: { @@ -29,6 +34,11 @@ const meta = { description: 'Icon Position modifier', control: 'select', options: [undefined, 'start', 'end'], + table: { + defaultValue: { + summary: 'start', + }, + }, }, busy: { description: 'Busy indicator', @@ -40,19 +50,19 @@ const meta = { }, icon: { description: 'Icon Node', - control: 'object', + control: 'boolean', table: { type: { - summary: 'HTML Content', + summary: 'HTML/SVG Content / React Node', + detail: 'Use the boolean switch to show an Icon', }, }, }, - label: { - description: 'Label Node', - control: 'object', + children: { + description: 'Label (children)', table: { type: { - summary: 'HTML Content', + summary: 'HTML Content / React Node', }, }, }, @@ -63,8 +73,8 @@ export default meta; //TODO replace icon in #308 const ExampleIcon = ( - - + + ); @@ -76,9 +86,13 @@ const ButtonTemplate: Story = { icon: undefined, pressed: false, busy: false, - label: 'Klik hier!', + children: 'Button', }, - render: ({ ...args }) => {args['children']}, + render: ({ icon, children, ...args }: { icon: boolean; children: any; args: unknown }) => ( + + {children} + + ), }; const AllButtonVariantsTemplate: Story = { @@ -116,24 +130,12 @@ export const Playground: Story = { tags: ['!autodocs'], }; -export const SmallButton: Story = { - ...ButtonTemplate, - args: { - ...ButtonTemplate.args, - size: 'small', - }, - parameters: { - docs: { - sourceState: 'shown', - }, - }, -}; - export const Primary: Story = { + ...ButtonTemplate, name: 'Primary', args: { + ...ButtonTemplate.args, appearance: 'primary-action-button', - children: 'Primary Button', }, parameters: { docs: { @@ -145,10 +147,11 @@ export const Primary: Story = { }; export const Secondary: Story = { + ...ButtonTemplate, name: 'Secondary', args: { + ...ButtonTemplate.args, appearance: 'secondary-action-button', - children: 'Secondary Button', }, parameters: { docs: { @@ -160,10 +163,11 @@ export const Secondary: Story = { }; export const Tertiary: Story = { + ...ButtonTemplate, name: 'Tertiary', args: { + ...ButtonTemplate.args, appearance: 'subtle-button', - children: 'Tertiary Button', }, parameters: { docs: { @@ -174,6 +178,22 @@ export const Tertiary: Story = { }, }; +export const SmallButton: Story = { + ...ButtonTemplate, + name: 'Small', + args: { + ...ButtonTemplate.args, + size: 'small', + }, + parameters: { + docs: { + description: { + story: 'Een kleine variant zet je met `size="small"`.', + }, + }, + }, +}; + export const Active: Story = { ...AllButtonVariantsTemplate, name: 'Active', @@ -181,7 +201,7 @@ export const Active: Story = { pseudo: { active: true }, }, args: { - children: 'Active Button', + ...ButtonTemplate.args, }, }; @@ -192,7 +212,7 @@ export const Hover: Story = { pseudo: { hover: true }, }, args: { - children: 'Hover Button', + ...ButtonTemplate.args, }, }; @@ -203,7 +223,7 @@ export const Focus: Story = { pseudo: { focus: true, focusVisible: true }, }, args: { - children: 'Focus Button', + ...ButtonTemplate.args, }, }; @@ -211,7 +231,7 @@ export const Disabled: Story = { ...AllButtonVariantsTemplate, name: 'Disabled', args: { - children: 'Disabled Button', + ...ButtonTemplate.args, disabled: true, }, }; @@ -220,14 +240,14 @@ export const Busy: Story = { ...AllButtonVariantsTemplate, name: 'Busy', args: { - children: 'Busy Button', + ...ButtonTemplate.args, busy: true, }, parameters: { docs: { description: { - story: - 'Een busy button zet je met het `busy`-attribute (`true`/`false`, default: `undefined`). Toont een `wait` cursor en `aria-busy`-attribute.', + story: `Een busy button zet je met het \`busy\`-attribute (\`true\`/\`false\`, default: \`undefined\`). Toont een \`wait\` cursor en \`aria-busy\`-attribute. Dit gebruik je + bijvoorbeeld als een gebruiker met een knop een actie in gang zet die langer kan duren, zoals een download.`, }, }, }, @@ -237,6 +257,7 @@ export const Toggle: Story = { ...ButtonTemplate, name: 'Toggle', args: { + ...ButtonTemplate.args, appearance: 'primary-action-button', pressed: true, }, @@ -249,10 +270,20 @@ export const Toggle: Story = { return ( - Toggle Button {args.pressed ? 'pressed' : 'not pressed'} + Button {args.pressed ? 'pressed' : 'not pressed'} ); }, + argTypes: { + pressed: { + control: 'boolean', + }, + children: { + table: { + disable: true, + }, + }, + }, parameters: { docs: { description: { @@ -290,3 +321,40 @@ export const ButtonWithIconAtPositionEnd: Story = { }, }, }; + +export const DesignTokens = createDesignTokensStory(meta); + +export const Visual = createVisualRegressionStory(() => ( + <> +

Light

+
Logius
+ + + + + + +
MijnAansluitingen
+ + + + + + +

Dark

+
Logius
+ + + + + + +
MijnAansluitingen
+ + + + + + + +)); diff --git a/packages/storybook/src/react-components/button/visual/States.tsx b/packages/storybook/src/react-components/button/visual/States.tsx new file mode 100644 index 000000000..0452c3c19 --- /dev/null +++ b/packages/storybook/src/react-components/button/visual/States.tsx @@ -0,0 +1,44 @@ +import { LuxButton } from '@lux-design-system/components-react'; +import { Fragment } from 'react'; + +const appearances = ['primary-action', 'secondary-action', 'subtle']; + +export const InteractiveStates = () => ( + <> + {appearances.map((appearance) => ( + + + Active Button ({appearance}) + + + Focus Button ({appearance}) + + + Hover Button ({appearance}) + + + ))} + +); + +export const PropertyStates = () => ( + <> + {appearances.map((appearance) => ( + + + Disabled Button ({appearance}) + + + Busy Button ({appearance}) + + + Pressed Button ({appearance}) + + + ))} + +); diff --git a/packages/storybook/src/react-components/button/visual/Variants.tsx b/packages/storybook/src/react-components/button/visual/Variants.tsx new file mode 100644 index 000000000..4133fe91d --- /dev/null +++ b/packages/storybook/src/react-components/button/visual/Variants.tsx @@ -0,0 +1,11 @@ +import { LuxButton } from '@lux-design-system/components-react'; + +export const Appearances = () => ( + <> + Primary Action Button + Secondary Action Button + Subtle Button + +); + +export const Sizes = () => Small Button; From 3adfa8dc6ac59b6d06b5882b438af96327f1b68a Mon Sep 17 00:00:00 2001 From: VladAfanasev Date: Wed, 20 Nov 2024 09:43:43 +0100 Subject: [PATCH 4/7] feat: added link component (#321) # Contents Link component toegevoegd op basis van Utrecht Link. Toevoeging is het icoon met de mogelijkheid om de positie te bepalen zoals bij Button. ## Checklist - [X] New features/components and bugfixes are covered by tests - [X] Changesets are created - [X] Definition of Done is checked --------- Co-authored-by: Vlad Afanasev --- .changeset/weak-weeks-rush.md | 5 + .lux.stylelintrc.json | 8 +- packages/components-react/src/index.ts | 1 + packages/components-react/src/link/Link.css | 20 ++ packages/components-react/src/link/Link.tsx | 64 ++++++ .../src/link/test/Link.spec.tsx | 140 ++++++++++++ .../src/react-components/link/link.mdx | 68 ++++++ .../react-components/link/link.stories.tsx | 206 ++++++++++++++++++ 8 files changed, 508 insertions(+), 4 deletions(-) create mode 100644 .changeset/weak-weeks-rush.md create mode 100644 packages/components-react/src/link/Link.css create mode 100644 packages/components-react/src/link/Link.tsx create mode 100644 packages/components-react/src/link/test/Link.spec.tsx create mode 100644 packages/storybook/src/react-components/link/link.mdx create mode 100644 packages/storybook/src/react-components/link/link.stories.tsx diff --git a/.changeset/weak-weeks-rush.md b/.changeset/weak-weeks-rush.md new file mode 100644 index 000000000..a2a09f017 --- /dev/null +++ b/.changeset/weak-weeks-rush.md @@ -0,0 +1,5 @@ +--- +"@lux-design-system/components-react": minor +--- + +Nieuw component: Link diff --git a/.lux.stylelintrc.json b/.lux.stylelintrc.json index 165d431ac..bd61539aa 100644 --- a/.lux.stylelintrc.json +++ b/.lux.stylelintrc.json @@ -12,10 +12,10 @@ } ], "order/properties-alphabetical-order": null, - "scss/dollar-variable-pattern": "^(lux)-[a-z0-9-]+$", - "scss/percent-placeholder-pattern": "^(lux)-[a-z0-9-]+$", + "scss/dollar-variable-pattern": "^(lux|utrecht)-[a-z0-9-]+$", + "scss/percent-placeholder-pattern": "^(lux|utrecht)-[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-]+$" + "selector-class-pattern": "^(lux|utrecht)-[a-z0-9_-]+|(force-state)--[a-z]+$", + "keyframes-name-pattern": "^(lux|utrecht)-[a-z0-9-]+$" } } diff --git a/packages/components-react/src/index.ts b/packages/components-react/src/index.ts index a53e4bd09..a81bd4057 100644 --- a/packages/components-react/src/index.ts +++ b/packages/components-react/src/index.ts @@ -22,6 +22,7 @@ export { LuxFormFieldErrorMessage, type LuxFormFieldErrorMessageProps, } from './form-field-error-message/FormFieldErrorMessage'; +export { LuxLink, type LuxLinkProps } from './link/Link'; 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'; diff --git a/packages/components-react/src/link/Link.css b/packages/components-react/src/link/Link.css new file mode 100644 index 000000000..b0311ff4b --- /dev/null +++ b/packages/components-react/src/link/Link.css @@ -0,0 +1,20 @@ +.utrecht-link--html-span:active, +.utrecht-link--html-a:any-link:active, +.utrecht-link--active, +.utrecht-link--visited { + --_utrecht-link-state-text-decoration-color: var(--utrecht-link-active-color); +} + +.lux-link { + display: inline flex; + align-items: baseline; + gap: var(--lux-link-column-gap); +} + +.lux-link-icon--start { + order: 0; +} + +.lux-link-icon--end { + order: 1; +} diff --git a/packages/components-react/src/link/Link.tsx b/packages/components-react/src/link/Link.tsx new file mode 100644 index 000000000..72862b348 --- /dev/null +++ b/packages/components-react/src/link/Link.tsx @@ -0,0 +1,64 @@ +import { + Link as UtrechtLink, + type LinkProps as UtrechtLinkProps, +} from '@utrecht/component-library-react/dist/css-module'; +import clsx from 'clsx'; +import React, { ReactElement } from 'react'; +import './Link.css'; + +type IconPosition = 'start' | 'end'; + +export type LuxLinkProps = UtrechtLinkProps & { + external?: boolean; + icon?: ReactElement | undefined; + iconPosition?: IconPosition; +}; + +const CLASSNAMES = { + link: 'lux-link', + text: 'lux-link__text', +}; + +const ICON_POSITIONS: { [key: string]: string } = { + start: 'lux-link-icon--start', + end: 'lux-link-icon--end', +}; + +export const LuxLink = (props: LuxLinkProps) => { + const { + external = false, + className, + children, + icon: iconNode, + iconPosition: providedIconPosition, + ...otherProps + } = props; + + // Set default icon position to 'start' if there's an icon but no position specified + const iconPosition = iconNode ? providedIconPosition || 'start' : undefined; + + const combinedClassName = clsx(CLASSNAMES.link, className); + + const positionedIcon = React.Children.map(iconNode, (iconElement) => { + if (!iconElement) { + return null; + } + + if (!React.isValidElement(iconElement)) { + return iconElement; + } + + return React.cloneElement(iconElement as ReactElement, { + className: clsx(iconElement?.props?.className, iconPosition && ICON_POSITIONS[iconPosition]), + }); + }); + + const externalProps = external ? { rel: 'external noopener noreferrer' } : {}; + + return ( + + {positionedIcon} + {children} + + ); +}; diff --git a/packages/components-react/src/link/test/Link.spec.tsx b/packages/components-react/src/link/test/Link.spec.tsx new file mode 100644 index 000000000..be8617b9f --- /dev/null +++ b/packages/components-react/src/link/test/Link.spec.tsx @@ -0,0 +1,140 @@ +import { describe, expect, it } from '@jest/globals'; +import { render, screen } from '@testing-library/react'; +import { LuxLink } from '../Link'; + +const ExampleIcon = ( + + + +); + +describe('Link', () => { + // Basic rendering + it('renders a basic link with correct attributes', () => { + render( + + Test Link + , + ); + + const link = screen.getByRole('link', { name: 'Test Link' }); + expect(link).toBeInTheDocument(); + expect(link).toHaveAttribute('href', '#'); + expect(link).toHaveClass('lux-link', 'custom-class'); + expect(link.querySelector('.lux-link__text')).toHaveTextContent('Test Link'); + }); + + // External link tests + describe('External link', () => { + it('renders external link with correct attributes', () => { + render( + + External Link + , + ); + + const link = screen.getByRole('link', { name: 'External Link' }); + expect(link).toHaveAttribute('rel', 'external noopener noreferrer'); + }); + + it('does not add external attributes when external prop is false', () => { + render(Regular Link); + + const link = screen.getByRole('link', { name: 'Regular Link' }); + expect(link).not.toHaveAttribute('rel'); + }); + }); + + // Icon tests + describe('Icon rendering', () => { + it('renders icon with default start position', () => { + render( + + Link with Icon + , + ); + + const link = screen.getByRole('link'); + const svg = link.querySelector('svg'); + expect(svg).toHaveClass('lux-link-icon--start'); + }); + + it('renders icon at end position', () => { + render( + + Link with End Icon + , + ); + + const link = screen.getByRole('link'); + const svg = link.querySelector('svg'); + expect(svg).toHaveClass('lux-link-icon--end'); + }); + + it('applies custom className to icon while preserving default classes', () => { + const iconWithClass = ( + + + + ); + + render( + + Link with Custom Icon Class + , + ); + + const svg = screen.getByRole('link').querySelector('svg'); + expect(svg).toHaveClass('custom-icon-class', 'lux-link-icon--start'); + }); + }); + + // Language attributes + it('renders link with correct language attributes', () => { + render( + + Nederlandse Link + , + ); + + const link = screen.getByRole('link', { name: 'Nederlandse Link' }); + expect(link).toHaveAttribute('hrefLang', 'nl'); + expect(link).toHaveAttribute('lang', 'nl'); + }); + + // Text wrapper test + it('wraps text content in span with correct class', () => { + render(Test Link); + + const textWrapper = screen.getByRole('link').querySelector('.lux-link__text'); + expect(textWrapper).toBeInTheDocument(); + expect(textWrapper).toHaveTextContent('Test Link'); + }); + + // Combined features test + it('renders correctly with all features combined', () => { + render( + + Complex Link + , + ); + + const link = screen.getByRole('link'); + const svg = link.querySelector('svg'); + const textWrapper = link.querySelector('.lux-link__text'); + + expect(link).toHaveAttribute('href', 'https://example.com'); + expect(link).toHaveAttribute('rel', 'external noopener noreferrer'); + expect(link).toHaveAttribute('hrefLang', 'en'); + expect(link).toHaveClass('lux-link', 'custom-class'); + expect(svg).toHaveClass('lux-link-icon--end'); + expect(textWrapper).toHaveTextContent('Complex Link'); + }); +}); diff --git a/packages/storybook/src/react-components/link/link.mdx b/packages/storybook/src/react-components/link/link.mdx new file mode 100644 index 000000000..c168d1669 --- /dev/null +++ b/packages/storybook/src/react-components/link/link.mdx @@ -0,0 +1,68 @@ +import { Canvas, Controls, Markdown, Meta } from "@storybook/blocks"; +import markdown from "@utrecht/link-css/README.md?raw"; +import * as LinkStories from "./link.stories.tsx"; +import { CitationDocumentation } from "../../utils/CitationDocumentation.tsx"; + + + +# Link + + + +{markdown} + +## Playground + +Experimenteer met de verschillende mogelijkheden van de Link component: + + + + +## Variants + +### Standaard Link + +Een basis link zonder extra toevoegingen. + + + +### External Link + +Een externe link opent in een nieuw tabblad en heeft de juiste security attributes. + + + +## States + +Links kunnen verschillende states hebben voor interactie: + +### Hover + + + +### Active + + + +### Focus + + + +## Link met Icoon + +Links kunnen worden verrijkt met een icoon voor extra visuele context. + +### Default Positie + + + +### Icoon aan het Begin + + + +### Icoon aan het Einde + + diff --git a/packages/storybook/src/react-components/link/link.stories.tsx b/packages/storybook/src/react-components/link/link.stories.tsx new file mode 100644 index 000000000..240f68036 --- /dev/null +++ b/packages/storybook/src/react-components/link/link.stories.tsx @@ -0,0 +1,206 @@ +import { LuxLink } 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/Link', + id: 'react-components-link', + component: LuxLink, + parameters: { + tokens, + tokensPrefix: 'utrecht-link', + }, + args: { + children: 'Link', + href: '#', + }, + argTypes: { + children: { + name: 'Label (children)', + }, + external: { + description: 'External link indicator', + control: 'boolean', + }, + placeholder: { + description: 'Shows link in placeholder/loading state', + control: 'boolean', + }, + href: { + description: 'URL', + control: 'text', + }, + 'aria-current': { + description: 'Current page indicator', + control: 'boolean', + }, + hrefLang: { + description: 'Language of the linked resource', + control: 'text', + }, + icon: { + description: 'Icon Node', + control: 'boolean', + table: { + type: { + summary: 'HTML Content', + detail: 'Use the boolean switch to show an Icon', + }, + }, + }, + iconPosition: { + description: 'Position of the icon relative to the text', + control: 'radio', + options: ['start', 'end'], + table: { + type: { summary: 'start | end' }, + defaultValue: { summary: 'start' }, + }, + }, + }, +} satisfies Meta; + +export default meta; + +//TODO replace icon in #308 +const ExampleIcon = ( + + + +); + +const LinkTemplate: Story = { + args: { + external: false, + placeholder: false, + icon: undefined, + iconPosition: undefined, + }, + render: (args) => ( + + {args.children} + + ), +}; + +export const Playground: Story = { + ...LinkTemplate, + name: 'Playground', + parameters: { + docs: { + sourceState: 'shown', + }, + }, + tags: ['!autodocs'], +}; + +export const Hover: Story = { + name: 'Hover', + parameters: { + pseudo: { hover: true }, + }, +}; + +export const Active: Story = { + name: 'Active', + parameters: { + pseudo: { active: true }, + }, +}; +export const Visisted: Story = { + name: 'Visited', + args: { + className: 'utrecht-link--visited', + }, + parameters: { + pseudo: { visited: true }, + }, +}; + +export const Focus: Story = { + name: 'Focus', + parameters: { + pseudo: { focus: true }, + }, +}; + +export const FocusVisible: Story = { + name: 'Focus Visible', + parameters: { + pseudo: { focusVisible: true }, + }, +}; + +export const Placeholder: Story = { + name: 'Placeholder', + args: { + placeholder: true, + }, + parameters: { + docs: { + description: { + story: 'Link in placeholder/loading state, useful for loading states or progressive enhancement.', + }, + }, + }, +}; +export const External: Story = { + name: 'External', + args: { + href: 'https://google.com', + external: true, + }, + parameters: { + docs: { + description: { + story: 'Een externe link opent in een nieuw tabblad en heeft de juiste security attributes.', + }, + }, + }, +}; + +export const LinkWithIcon: Story = { + name: 'Link with Icon', + args: { + icon: ExampleIcon, + }, + parameters: { + docs: { + description: { + story: 'Een link kan een icoon bevatten voor extra visuele context.', + }, + }, + }, +}; + +export const LinkWithIconStart: Story = { + name: 'Link with Icon at Start', + args: { + icon: ExampleIcon, + iconPosition: 'start', + }, + parameters: { + docs: { + description: { + story: 'Link met icoon aan het begin.', + }, + }, + }, +}; + +export const LinkWithIconEnd: Story = { + name: 'Link with Icon at End', + args: { + icon: ExampleIcon, + iconPosition: 'end', + }, + parameters: { + docs: { + description: { + story: 'Link met icoon aan het einde.', + }, + }, + }, +}; From a2ce8bc868e6f0bb5bab12591943aa9e850263e0 Mon Sep 17 00:00:00 2001 From: VladAfanasev Date: Wed, 20 Nov 2024 09:53:00 +0100 Subject: [PATCH 5/7] feat: added RadioButton and RadioGroup components. (#328) # Contents RadioButton and FormFieldRadioGroup components ## Checklist - [X] New features/components and bugfixes are covered by tests - [X] Changesets are created - [X] Definition of Done is checked --------- Co-authored-by: Vlad Afanasev Co-authored-by: Remy Parzinski Co-authored-by: Jaap-Hein Wester Co-authored-by: Jaap-Hein Wester --- .changeset/mean-books-sleep.md | 9 + .../FormFieldRadioGroup.css | 15 ++ .../FormFieldRadioGroup.tsx | 135 +++++++++++ .../test/FormFieldRadioGroup.spec.tsx | 143 ++++++++++++ .../FormFieldRadioOption.css | 4 + .../FormFieldRadioOption.tsx | 59 +++++ .../test/FormFieldRadioOption.spec.tsx | 78 +++++++ packages/components-react/src/index.ts | 5 + .../src/radio-button/RadioButton.tsx | 34 +++ .../radio-button/test/RadioButton.spec.tsx | 91 ++++++++ packages/storybook/package.json | 1 + .../form-field-radio-group.mdx | 59 +++++ .../form-field-radio-group.stories.tsx | 210 ++++++++++++++++++ .../form-field-radio-option.mdx | 37 +++ .../form-field-radio-option.stories.tsx | 152 +++++++++++++ pnpm-lock.yaml | 8 + 16 files changed, 1040 insertions(+) create mode 100644 .changeset/mean-books-sleep.md create mode 100644 packages/components-react/src/form-field-radio-group/FormFieldRadioGroup.css create mode 100644 packages/components-react/src/form-field-radio-group/FormFieldRadioGroup.tsx create mode 100644 packages/components-react/src/form-field-radio-group/test/FormFieldRadioGroup.spec.tsx create mode 100644 packages/components-react/src/form-field-radio-option/FormFieldRadioOption.css create mode 100644 packages/components-react/src/form-field-radio-option/FormFieldRadioOption.tsx create mode 100644 packages/components-react/src/form-field-radio-option/test/FormFieldRadioOption.spec.tsx create mode 100644 packages/components-react/src/radio-button/RadioButton.tsx create mode 100644 packages/components-react/src/radio-button/test/RadioButton.spec.tsx create mode 100644 packages/storybook/src/react-components/form-field-radio-group/form-field-radio-group.mdx create mode 100644 packages/storybook/src/react-components/form-field-radio-group/form-field-radio-group.stories.tsx create mode 100644 packages/storybook/src/react-components/form-field-radio-option/form-field-radio-option.mdx create mode 100644 packages/storybook/src/react-components/form-field-radio-option/form-field-radio-option.stories.tsx diff --git a/.changeset/mean-books-sleep.md b/.changeset/mean-books-sleep.md new file mode 100644 index 000000000..15d3302e4 --- /dev/null +++ b/.changeset/mean-books-sleep.md @@ -0,0 +1,9 @@ +--- +"@lux-design-system/components-react": major +--- + +In deze commit: + +- Nieuw component: Radio Button +- Nieuw component: Form Field Radio Option +- Nieuw component: Form Field Radio Group diff --git a/packages/components-react/src/form-field-radio-group/FormFieldRadioGroup.css b/packages/components-react/src/form-field-radio-group/FormFieldRadioGroup.css new file mode 100644 index 000000000..76eec7c69 --- /dev/null +++ b/packages/components-react/src/form-field-radio-group/FormFieldRadioGroup.css @@ -0,0 +1,15 @@ +.lux-radio-group { + display: flex; + row-gap: var(--lux-radio-group-row-gap); + flex-direction: column; +} + +.lux-radio-group__options { + display: flex; + row-gap: var(--lux-radio-group-row-gap); + flex-direction: column; +} + +.lux-radio-group__fieldset { + border: 0; +} diff --git a/packages/components-react/src/form-field-radio-group/FormFieldRadioGroup.tsx b/packages/components-react/src/form-field-radio-group/FormFieldRadioGroup.tsx new file mode 100644 index 000000000..0d90bfdc8 --- /dev/null +++ b/packages/components-react/src/form-field-radio-group/FormFieldRadioGroup.tsx @@ -0,0 +1,135 @@ +'use client'; +import React, { ForwardedRef, forwardRef } from 'react'; +import './FormFieldRadioGroup.css'; +import { LuxFormFieldRadioOption } from '../form-field-radio-option/FormFieldRadioOption'; + +interface RadioOption { + value: string; + label: string; + disabled?: boolean; + description?: React.ReactNode; +} + +export interface LuxFormFieldRadioGroupProps { + name: string; + label: string; + description?: string; + errorMessage?: string; + options: RadioOption[]; + value?: string; + invalid?: boolean; + required?: boolean; + className?: string; + // eslint-disable-next-line no-unused-vars + onChange?: (value: string) => void; +} + +const CLASSNAME: { [key: string]: string } = { + fieldset: 'lux-radio-group lux-radio-group__fieldset', + legend: 'utrecht-form-label', + description: 'utrecht-form-field-description', + options: 'lux-radio-group__options', + error: 'utrecht-form-field-error-message', +}; + +export const LuxFormFieldRadioGroup = forwardRef( + ( + { + name, + label, + description, + options, + value, + invalid, + required, + errorMessage = '', + className, + onChange, + }: LuxFormFieldRadioGroupProps, + ref: ForwardedRef, + ) => { + const uniqueId = React.useId(); + const descriptionId = description ? `${uniqueId}-description` : undefined; + const errorId = invalid ? `${uniqueId}-error` : undefined; + const legendId = `${uniqueId}-legend`; + + // Local state for uncontrolled mode (when no value prop is provided) + const [internalValue, setInternalValue] = React.useState(''); + + // Check if component is controlled by parent (through value prop) + const isControlled = value !== undefined; + + // Use parent value if controlled, otherwise use local state + const currentValue = isControlled ? value : internalValue; + + // Handle radio button selection + const handleChange = (newValue: string) => { + // Only update local state in uncontrolled mode + if (!isControlled) { + setInternalValue(newValue); + } + // Always notify parent of changes through onChange + onChange?.(newValue); + }; + + return ( +
+ + {label} + + + {description && ( +

+ {description} +

+ )} + {invalid && ( +

+ {errorMessage} +

+ )} + +
+ {options.map((option) => { + const { + value: optionValue, + label: optionLabel, + description: optionDescription, + disabled, + ...optionRestProps + } = option; + const optionId = `${uniqueId}-${optionValue}`; + + return ( + handleChange(e.target.value)} + className={className} + {...optionRestProps} + /> + ); + })} +
+
+ ); + }, +); + +LuxFormFieldRadioGroup.displayName = 'LuxFormFieldRadioGroup'; diff --git a/packages/components-react/src/form-field-radio-group/test/FormFieldRadioGroup.spec.tsx b/packages/components-react/src/form-field-radio-group/test/FormFieldRadioGroup.spec.tsx new file mode 100644 index 000000000..3ac77e6f9 --- /dev/null +++ b/packages/components-react/src/form-field-radio-group/test/FormFieldRadioGroup.spec.tsx @@ -0,0 +1,143 @@ +import { describe, expect, it, jest } from '@jest/globals'; +import { fireEvent, render, screen } from '@testing-library/react'; +import { LuxFormFieldRadioGroup } from '../FormFieldRadioGroup'; + +describe('FormFieldRadioGroup', () => { + const defaultProps = { + name: 'test-group', + label: 'Test Group', + options: [ + { value: 'option1', label: 'Option 1' }, + { value: 'option2', label: 'Option 2' }, + { value: 'option3', label: 'Option 3' }, + ], + }; + + it('renders radio group with options', () => { + render(); + + const radioGroup = screen.getByRole('radiogroup', { name: 'Test Group' }); + expect(radioGroup).toBeInTheDocument(); + + const options = screen.getAllByRole('radio'); + expect(options).toHaveLength(3); + }); + + it('renders radio group with custom className', () => { + render(); + + const radioGroup = screen.getByRole('radiogroup'); + + const elements = radioGroup.getElementsByClassName('custom-class'); + expect(elements.length).toBeGreaterThan(0); + }); + + it('renders disabled options', () => { + const propsWithDisabled = { + ...defaultProps, + options: [ + { value: 'option1', label: 'Option 1', disabled: true }, + { value: 'option2', label: 'Option 2' }, + ], + }; + + render(); + + const disabledOption = screen.getByRole('radio', { name: 'Option 1' }); + const enabledOption = screen.getByRole('radio', { name: 'Option 2' }); + + expect(disabledOption).toBeDisabled(); + expect(enabledOption).not.toBeDisabled(); + }); + + it('renders invalid state for all radio buttons', () => { + render(); + + const options = screen.getAllByRole('radio'); + options.forEach((option) => { + expect(option).toHaveAttribute('aria-invalid', 'true'); + }); + }); + + it('renders required state', () => { + render(); + + const radioGroup = screen.getByRole('radiogroup'); + expect(radioGroup).toHaveAttribute('aria-required', 'true'); + + const options = screen.getAllByRole('radio'); + options.forEach((option) => { + expect(option).toHaveAttribute('required'); + }); + }); + + it('renders with controlled value', () => { + render(); + + const selectedOption = screen.getByRole('radio', { name: 'Option 2' }); + expect(selectedOption).toBeChecked(); + }); + + it('handles uncontrolled state correctly', () => { + render(); + + const option1 = screen.getByRole('radio', { name: 'Option 1' }); + const option2 = screen.getByRole('radio', { name: 'Option 2' }); + + // Initially no option should be checked + expect(option1).not.toBeChecked(); + expect(option2).not.toBeChecked(); + + // Click first option + fireEvent.click(option1); + expect(option1).toBeChecked(); + expect(option2).not.toBeChecked(); + + // Click second option + fireEvent.click(option2); + expect(option1).not.toBeChecked(); + expect(option2).toBeChecked(); + }); + + it('calls onChange with selected value', () => { + const onChange = jest.fn(); + render(); + + const option = screen.getByRole('radio', { name: 'Option 1' }); + fireEvent.click(option); + + expect(onChange).toHaveBeenCalledWith('option1'); + }); + + it('generates unique ids for options', () => { + render(); + + const options = screen.getAllByRole('radio'); + + // Check that IDs are unique + const ids = options.map((option) => option.getAttribute('id')); + const uniqueIds = new Set(ids); + expect(uniqueIds.size).toBe(options.length); + + // Check that each radio has a corresponding label + options.forEach((option, index) => { + const optionId = option.getAttribute('id'); + const optionLabel = screen.getByText(`Option ${index + 1}`); + expect(optionLabel).toHaveAttribute('for', optionId); + }); + }); + + it('associates legend with FormFieldRadioGroup through aria-labelledby', () => { + render(); + + const radioGroup = screen.getByRole('radiogroup'); + const legend = screen.getByText(defaultProps.label); + + // Get the generated aria-labelledby value + const labelledById = radioGroup.getAttribute('aria-labelledby'); + + // Verify the relationship exists + expect(labelledById).toBeTruthy(); + expect(legend).toHaveAttribute('id', labelledById); + }); +}); diff --git a/packages/components-react/src/form-field-radio-option/FormFieldRadioOption.css b/packages/components-react/src/form-field-radio-option/FormFieldRadioOption.css new file mode 100644 index 000000000..7d2eb5f66 --- /dev/null +++ b/packages/components-react/src/form-field-radio-option/FormFieldRadioOption.css @@ -0,0 +1,4 @@ +.lux-radio-button__container { + display: inline flex; + align-items: start; +} diff --git a/packages/components-react/src/form-field-radio-option/FormFieldRadioOption.tsx b/packages/components-react/src/form-field-radio-option/FormFieldRadioOption.tsx new file mode 100644 index 000000000..f8cbeca99 --- /dev/null +++ b/packages/components-react/src/form-field-radio-option/FormFieldRadioOption.tsx @@ -0,0 +1,59 @@ +import React, { ForwardedRef, forwardRef, PropsWithChildren } from 'react'; +import { LuxFormFieldDescription } from '../form-field-description/FormFieldDescription'; +import { LuxFormFieldLabel } from '../form-field-label/FormFieldLabel'; +import { LuxRadioButton, type LuxRadioButtonProps } from '../radio-button/RadioButton'; +import './FormFieldRadioOption.css'; + +export type LuxFormFieldRadioOptionProps = LuxRadioButtonProps & { + label: string | React.ReactNode; + description?: React.ReactNode; + checked?: boolean; +}; + +const CLASSNAME = { + container: 'lux-radio-button__container', +}; + +export const LuxFormFieldRadioOption = forwardRef( + ( + { + disabled, + className, + invalid, + name, + label, + description, + id, + checked, + value, + ...restProps + }: PropsWithChildren, + ref: ForwardedRef, + ) => { + const radioId = id || React.useId(); + + return ( +
+ +
+ + {label} + + {description ? {description} : null} +
+
+ ); + }, +); + +LuxFormFieldRadioOption.displayName = 'LuxFormFieldRadioOption'; diff --git a/packages/components-react/src/form-field-radio-option/test/FormFieldRadioOption.spec.tsx b/packages/components-react/src/form-field-radio-option/test/FormFieldRadioOption.spec.tsx new file mode 100644 index 000000000..a7d2b5d1b --- /dev/null +++ b/packages/components-react/src/form-field-radio-option/test/FormFieldRadioOption.spec.tsx @@ -0,0 +1,78 @@ +import { describe, expect, it } from '@jest/globals'; +import { render, screen } from '@testing-library/react'; +import { LuxFormFieldRadioOption } from '../FormFieldRadioOption'; + +describe('FormFieldRadioOption', () => { + const defaultProps = { + name: 'test-radio', + value: 'option1', + label: 'Test Option', + }; + + it('renders a radio button with label', () => { + render(); + + const radio = screen.getByRole('radio', { + name: 'Test Option', + }); + expect(radio).toBeInTheDocument(); + expect(screen.getByLabelText('Test Option')).toBeInTheDocument(); + }); + + it('renders a disabled radio button', () => { + render(); + + const radio = screen.getByRole('radio'); + expect(radio).toBeDisabled(); + expect(radio).toHaveClass('utrecht-radio-button--disabled'); + }); + + it('renders an invalid radio button', () => { + render(); + + const radio = screen.getByRole('radio'); + expect(radio).toHaveAttribute('aria-invalid', 'true'); + }); + + it('renders a checked radio button', () => { + render(); + + const radio = screen.getByRole('radio'); + expect(radio).toBeChecked(); + }); + + it('uses custom id when provided', () => { + const customId = 'custom-radio-id'; + render(); + + const radio = screen.getByRole('radio'); + expect(radio).toHaveAttribute('id', customId); + }); + + it('generates unique id and maintains label association', () => { + render(); + + const radio = screen.getByRole('radio'); + const label = screen.getByText('Test Option'); + + expect(radio).toHaveAttribute('id'); + + const radioId = radio.getAttribute('id'); + expect(label).toHaveAttribute('for', radioId); + }); + + it('uses provided id when specified', () => { + const customId = 'custom-radio-id'; + render(); + + const radio = screen.getByRole('radio'); + expect(radio).toHaveAttribute('id', customId); + }); + + it('applies additional className when provided', () => { + render(); + + const radio = screen.getByRole('radio'); + expect(radio).toHaveClass('custom-class'); + }); +}); diff --git a/packages/components-react/src/index.ts b/packages/components-react/src/index.ts index a81bd4057..72b2abdb8 100644 --- a/packages/components-react/src/index.ts +++ b/packages/components-react/src/index.ts @@ -26,6 +26,11 @@ export { LuxLink, type LuxLinkProps } from './link/Link'; 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 { + LuxFormFieldRadioOption, + type LuxFormFieldRadioOptionProps, +} from './form-field-radio-option/FormFieldRadioOption'; +export { LuxFormFieldRadioGroup, type LuxFormFieldRadioGroupProps } from './form-field-radio-group/FormFieldRadioGroup'; export { LuxCheckbox, type LuxCheckboxProps } from './checkbox/Checkbox'; export { LuxPreHeading, type LuxPreHeadingProps } from './pre-heading/PreHeading'; export { LuxSection, type LuxSectionProps } from './section/Section'; diff --git a/packages/components-react/src/radio-button/RadioButton.tsx b/packages/components-react/src/radio-button/RadioButton.tsx new file mode 100644 index 000000000..3d62c014a --- /dev/null +++ b/packages/components-react/src/radio-button/RadioButton.tsx @@ -0,0 +1,34 @@ +import { + RadioButton as UtrechtRadioButton, + type RadioButtonProps as UtrechtRadioButtonProps, +} from '@utrecht/component-library-react/dist/css-module'; +import { ForwardedRef, forwardRef, PropsWithChildren } from 'react'; + +export type LuxRadioButtonProps = UtrechtRadioButtonProps & { + invalid?: boolean; + name: string; + checked?: boolean; +}; + +export const LuxRadioButton = forwardRef( + ( + { disabled, className, invalid, name, id, checked, value, ...restProps }: PropsWithChildren, + ref: ForwardedRef, + ) => { + return ( + + ); + }, +); + +LuxRadioButton.displayName = 'LuxRadioButton'; diff --git a/packages/components-react/src/radio-button/test/RadioButton.spec.tsx b/packages/components-react/src/radio-button/test/RadioButton.spec.tsx new file mode 100644 index 000000000..9948c8b83 --- /dev/null +++ b/packages/components-react/src/radio-button/test/RadioButton.spec.tsx @@ -0,0 +1,91 @@ +import { describe, expect, it, jest } from '@jest/globals'; +import { render, screen } from '@testing-library/react'; +import { LuxRadioButton } from '../RadioButton'; + +describe('LuxRadioButton', () => { + const defaultProps = { + name: 'test-radio', + value: 'option1', + }; + + it('renders a radio button with required props', () => { + render(); + + const radio = screen.getByRole('radio'); + expect(radio).toBeInTheDocument(); + expect(radio).toHaveAttribute('name', 'test-radio'); + expect(radio).toHaveAttribute('value', 'option1'); + }); + + it('renders a disabled radio button', () => { + render(); + + const radio = screen.getByRole('radio'); + expect(radio).toBeDisabled(); + }); + + it('renders an invalid radio button', () => { + render(); + + const radio = screen.getByRole('radio'); + expect(radio).toHaveAttribute('aria-invalid', 'true'); + }); + + it('does not set aria-invalid when invalid prop is false', () => { + render(); + + const radio = screen.getByRole('radio'); + expect(radio).not.toHaveAttribute('aria-invalid'); + }); + + it('renders a checked radio button', () => { + render(); + + const radio = screen.getByRole('radio'); + expect(radio).toBeChecked(); + }); + + it('handles unchecked state correctly', () => { + render(); + + const radio = screen.getByRole('radio'); + expect(radio).not.toBeChecked(); + }); + + it('applies custom id when provided', () => { + const customId = 'custom-radio-id'; + render(); + + const radio = screen.getByRole('radio'); + expect(radio).toHaveAttribute('id', customId); + }); + + it('applies additional className when provided', () => { + render(); + + const radio = screen.getByRole('radio'); + expect(radio).toHaveClass('custom-class'); + }); + + it('renders a required radio button', () => { + render(); + + const radio = screen.getByRole('radio'); + expect(radio).toHaveAttribute('required'); + }); + + it('forwards additional props to the underlying radio button', () => { + render(); + + const radio = screen.getByRole('radio'); + expect(radio).toHaveAttribute('data-testid', 'custom-test-id'); + }); + + it('forwards ref to the underlying radio button', () => { + const ref = jest.fn(); + render(); + + expect(ref).toHaveBeenCalled(); + expect(ref.mock.calls[0][0]).toBeInstanceOf(HTMLInputElement); + }); +}); diff --git a/packages/storybook/package.json b/packages/storybook/package.json index 92889296f..9d7915066 100644 --- a/packages/storybook/package.json +++ b/packages/storybook/package.json @@ -63,6 +63,7 @@ "@utrecht/heading-group-css": "1.2.0", "@utrecht/link-css": "1.1.0", "@utrecht/paragraph-css": "1.1.0", + "@utrecht/radio-button-css": "1.3.1", "@utrecht/textbox-css": "1.3.1", "@vitejs/plugin-react": "4.3.1", "chromatic": "11.7.0", diff --git a/packages/storybook/src/react-components/form-field-radio-group/form-field-radio-group.mdx b/packages/storybook/src/react-components/form-field-radio-group/form-field-radio-group.mdx new file mode 100644 index 000000000..8f11429e1 --- /dev/null +++ b/packages/storybook/src/react-components/form-field-radio-group/form-field-radio-group.mdx @@ -0,0 +1,59 @@ +import { Canvas, Controls, Meta } from "@storybook/blocks"; +import * as RadioGroupStories from "./form-field-radio-group.stories"; + + + +# Form Field Radio Group + +## Gebruik + +```tsx + +``` + +## Playground + +Experimenteer met de verschillende properties van de RadioGroup component. + + + + +## Voorbeelden + +### Default + +Standaard weergave van een radio group zonder voorgeselecteerde waarde. + + + +### Required + +Verplicht veld met een asterisk indicator. Gebruik dit wanneer een keuze verplicht is. + + + +### Invalid + +Radio group in foutmelding status. Wordt gebruikt om aan te geven dat er een ongeldige of ontbrekende selectie is. + + + +### Met uitgeschakelde optie + +Radio group waarbij één optie is uitgeschakeld. Gebruik dit voor opties die tijdelijk niet beschikbaar zijn. + + + +### Met lange labels + +Radio group met uitgebreide, beschrijvende labels. Geschikt voor het tonen van extra informatie bij elke optie. + + diff --git a/packages/storybook/src/react-components/form-field-radio-group/form-field-radio-group.stories.tsx b/packages/storybook/src/react-components/form-field-radio-group/form-field-radio-group.stories.tsx new file mode 100644 index 000000000..b8e2ea9d2 --- /dev/null +++ b/packages/storybook/src/react-components/form-field-radio-group/form-field-radio-group.stories.tsx @@ -0,0 +1,210 @@ +import { LuxFormFieldRadioGroup } from '@lux-design-system/components-react'; +import { Meta, StoryObj } from '@storybook/react'; +import { BADGES } from '../../../config/preview'; + +type Story = StoryObj; + +const meta = { + title: 'React Components/Form Field/Form Field Radio Group', + component: LuxFormFieldRadioGroup, + subcomponents: {}, + parameters: { + badges: [BADGES.WIP, BADGES.CANARY], + }, + args: { + name: 'contact-default', + label: 'Voorkeur contactmethode', + errorMessage: 'Selecteer een contactmethode.', + options: [ + { value: 'email', label: 'E-mail', description: 'Gebruik het e-mailadres dat bij ons bekend is.' }, + { + value: 'phone', + label: 'Telefoon', + description: ( + <> + Gebruik een geldig telefoonnummer. + + ), + }, + { value: 'mail', label: 'Post' }, + ], + }, + argTypes: { + label: { + description: 'Label for the radio group', + control: 'text', + table: { + type: { summary: 'string' }, + }, + }, + description: { + description: 'Help text for the radio group', + control: 'text', + table: { + type: { summary: 'string' }, + }, + }, + errorMessage: { + description: 'Error message to display when invalid', + control: 'text', + table: { + type: { summary: 'string' }, + }, + }, + name: { + description: 'Name attribute for the radio group (used for form submission and ID generation)', + control: 'text', + table: { + type: { summary: 'string' }, + category: 'HTML attribute', + }, + }, + options: { + description: 'Array of radio options with value and label properties', + control: 'object', + table: { + type: { + summary: 'Array<{ value: string; label: string; description?: React.ReactNode; disabled?: boolean; }>', + }, + }, + }, + invalid: { + description: 'Invalid state indicator', + control: 'boolean', + table: { + type: { summary: 'boolean' }, + }, + }, + required: { + description: 'Required field indicator', + control: 'boolean', + table: { + type: { summary: 'boolean' }, + category: 'HTML attribute', + }, + }, + value: { + description: 'Currently selected value (only needed for controlled components)', + control: 'text', + table: { + type: { summary: 'string' }, + }, + }, + className: { + table: { + disable: true, + }, + }, + onChange: { + table: { + disable: true, + }, + }, + }, +} satisfies Meta; + +export default meta; + +export const Playground: Story = { + args: { + ...meta.args, + name: 'contact-playground', + }, + parameters: { + docs: { + description: { + story: + 'Interactieve demo van de RadioGroup component. De radio buttons krijgen automatisch unieke IDs gebaseerd op de name en value combinatie.', + }, + sourceState: 'shown', + }, + }, + tags: ['!autodocs'], +}; + +export const Default: Story = { + args: { + ...meta.args, + name: 'contact-default', + description: 'Selecteer uw voorkeurswijze voor contact.', + }, + parameters: { + docs: { + description: { + story: 'Standaard weergave van een radio group met meerdere opties en een beschrijving.', + }, + }, + }, +}; + +export const Required: Story = { + args: { + ...meta.args, + name: 'contact-required', + required: true, + description: 'Dit is een verplicht veld.', + }, + parameters: { + docs: { + description: { + story: 'Verplicht veld met een asterisk indicator.', + }, + }, + }, +}; + +export const Invalid: Story = { + args: { + ...meta.args, + name: 'contact-invalid', + invalid: true, + description: 'Kies hoe u gecontacteerd wilt worden.', + }, + parameters: { + docs: { + description: { + story: 'Radio group in foutmelding status met foutbericht.', + }, + }, + }, +}; + +export const WithDisabledOption: Story = { + args: { + ...meta.args, + name: 'contact-disabled', + description: 'Telefonisch contact is momenteel niet beschikbaar.', + options: [ + { value: 'email', label: 'E-mail' }, + { value: 'phone', label: 'Telefoon', disabled: true }, + { value: 'mail', label: 'Post' }, + ], + }, + parameters: { + docs: { + description: { + story: 'Radio group met een uitgeschakelde optie.', + }, + }, + }, +}; + +export const WithLongLabels: Story = { + args: { + ...meta.args, + name: 'contact-long-labels', + description: 'Kies een contactmethode op basis van de gewenste reactietijd.', + options: [ + { value: 'email', label: 'Contact via e-mail (standaard reactietijd: 1 werkdag)' }, + { value: 'phone', label: 'Contact via telefoon (standaard reactietijd: direct)' }, + { value: 'mail', label: 'Contact via post (standaard reactietijd: 3-5 werkdagen)' }, + ], + }, + parameters: { + docs: { + description: { + story: 'Radio group met langere, beschrijvende labels.', + }, + }, + }, +}; diff --git a/packages/storybook/src/react-components/form-field-radio-option/form-field-radio-option.mdx b/packages/storybook/src/react-components/form-field-radio-option/form-field-radio-option.mdx new file mode 100644 index 000000000..81665e3b7 --- /dev/null +++ b/packages/storybook/src/react-components/form-field-radio-option/form-field-radio-option.mdx @@ -0,0 +1,37 @@ +import { Canvas, Controls, Meta } from "@storybook/blocks"; +import * as FormFieldRadioOptionStories from "./form-field-radio-option.stories.tsx"; + + + +# Form field Radio Option + +## Playground + + + + +## States + +## Default + + + +### Checked + + + +### Invalid + + + +### Disabled + + + +### Hover + + + +### Focus visible + + diff --git a/packages/storybook/src/react-components/form-field-radio-option/form-field-radio-option.stories.tsx b/packages/storybook/src/react-components/form-field-radio-option/form-field-radio-option.stories.tsx new file mode 100644 index 000000000..1551896cb --- /dev/null +++ b/packages/storybook/src/react-components/form-field-radio-option/form-field-radio-option.stories.tsx @@ -0,0 +1,152 @@ +import { LuxFormFieldRadioOption } 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'; + +type Story = StoryObj; + +const meta = { + title: 'React Components/Form Field/Form Field Radio Option', + id: 'react-components-radio-button', + component: LuxFormFieldRadioOption, + subcomponents: {}, + parameters: { + badges: [BADGES.WIP, BADGES.CANARY], + tokens, + tokensPrefix: 'utrecht-radio-button', + }, + argTypes: { + invalid: { + description: 'Invalid state indicator', + control: 'boolean', + table: { + type: { summary: 'boolean' }, + }, + }, + checked: { + description: 'Checked state', + control: 'boolean', + table: { + type: { summary: 'boolean' }, + category: 'HTML attribute', + }, + }, + description: { + description: 'Description for an option', + control: 'text', + table: { + type: { summary: 'text' }, + }, + }, + disabled: { + description: 'Disabled state', + control: 'boolean', + table: { + type: { summary: 'boolean' }, + category: 'HTML attribute', + }, + }, + // Hide other HTML attributes from controls + name: { + table: { + disable: true, + category: 'HTML attribute', + }, + }, + id: { + table: { + disable: true, + category: 'HTML attribute', + }, + }, + className: { + table: { + disable: true, + category: 'HTML attribute', + }, + }, + }, +} satisfies Meta; + +export default meta; + +const RadioButtonTemplate: Story = { + args: { + label: 'Option 1', + name: 'playground', + }, +}; + +export const Playground: Story = { + ...RadioButtonTemplate, + name: 'playground', + parameters: { + docs: { + sourceState: 'shown', + }, + }, + tags: ['!autodocs'], +}; + +export const Default: Story = { + ...RadioButtonTemplate, + args: { + ...RadioButtonTemplate.args, + name: 'default', + }, + name: 'default', +}; + +export const Checked: Story = { + ...RadioButtonTemplate, + args: { + ...RadioButtonTemplate.args, + name: 'checked', + checked: true, + }, + name: 'checked', +}; + +export const Invalid: Story = { + ...RadioButtonTemplate, + args: { + ...RadioButtonTemplate.args, + name: 'invalid', + invalid: true, + }, + name: 'invalid', +}; + +export const Disabled: Story = { + ...RadioButtonTemplate, + args: { + ...RadioButtonTemplate.args, + name: 'disabled', + disabled: true, + }, + name: 'disabled', +}; + +export const Hover: Story = { + ...RadioButtonTemplate, + args: { + ...RadioButtonTemplate.args, + name: 'hover', + }, + name: 'hover', + parameters: { + pseudo: { hover: true }, + }, +}; + +export const FocusVisible: Story = { + ...RadioButtonTemplate, + args: { + ...RadioButtonTemplate.args, + name: 'focus-visible', + }, + name: 'focus-visible', + parameters: { + pseudo: { focus: true, focusVisible: true }, + }, +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9040fdd8c..a76b6ef82 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -355,6 +355,9 @@ importers: '@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)) @@ -2841,6 +2844,9 @@ packages: '@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==} @@ -10519,6 +10525,8 @@ snapshots: '@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 From e094f1b17c60ca4add9823f3418ebe59757c5979 Mon Sep 17 00:00:00 2001 From: MMeijerink Date: Thu, 21 Nov 2024 15:11:50 +0100 Subject: [PATCH 6/7] alert icons added (#339) # Contents ## Checklist - [ ] New features/components and bugfixes are covered by tests - [ ] Changesets are created - [ ] Definition of Done is checked --- packages/components-react/src/alert/Alert.tsx | 81 +++++++++++++++++-- 1 file changed, 73 insertions(+), 8 deletions(-) diff --git a/packages/components-react/src/alert/Alert.tsx b/packages/components-react/src/alert/Alert.tsx index 125385553..0a81e6150 100644 --- a/packages/components-react/src/alert/Alert.tsx +++ b/packages/components-react/src/alert/Alert.tsx @@ -13,23 +13,88 @@ export interface LuxAlertProps extends Omit { //TODO replace icons in #308 const InfoIcon = () => ( - - + + + ); const SuccessIcon = () => ( - - + + + ); const WarningIcon = () => ( - - + + + ); const ErrorIcon = () => ( - - + + + ); From 3f11d695ce745fa796c65178847941cfe41198cf Mon Sep 17 00:00:00 2001 From: MMeijerink Date: Thu, 21 Nov 2024 15:12:10 +0100 Subject: [PATCH 7/7] remove align items from section (#340) # Contents ## Checklist - [ ] New features/components and bugfixes are covered by tests - [ ] Changesets are created - [ ] Definition of Done is checked --- packages/components-react/src/section/Section.css | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/components-react/src/section/Section.css b/packages/components-react/src/section/Section.css index 4ead2930f..e98dd91ad 100644 --- a/packages/components-react/src/section/Section.css +++ b/packages/components-react/src/section/Section.css @@ -3,7 +3,6 @@ row-gap: var(--lux-section-row-gap); flex-direction: column; justify-content: start; - align-items: center; border-radius: var(--lux-section-border-radius); background-color: var(--lux-section-background-color); padding-inline-start: var(--lux-section-padding-inline-start);