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/4] 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 00000000..5518b9ac --- /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 889ea9fd..e78b32a6 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/4] 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 00000000..8fb8cd1e --- /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 00000000..117621ee --- /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 00000000..5469a5ac --- /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 00000000..75237c31 --- /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 590762ec..a53e4bd0 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 348d5822..92889296 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 00000000..9b5b39ec --- /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 00000000..96b65d12 --- /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 979f8a71..9040fdd8 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/4] 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 b3ee1f78..5eea05ed 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 24088da3..085a2a1a 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 d77052c7..e4f8e4c8 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 931bb674..cce9b6c7 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 00000000..0452c3c1 --- /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 00000000..4133fe91 --- /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/4] 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 00000000..a2a09f01 --- /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 165d431a..bd61539a 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 a53e4bd0..a81bd405 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 00000000..b0311ff4 --- /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 00000000..72862b34 --- /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 00000000..be8617b9 --- /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 00000000..c168d166 --- /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 00000000..240f6803 --- /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.', + }, + }, + }, +};