From 8e7571404a8f987885f63602a15643f9fae6abdf Mon Sep 17 00:00:00 2001 From: Micky Date: Tue, 24 Mar 2026 11:38:56 +0100 Subject: [PATCH 01/10] feat(design-system)!: add DsButton v1.3 [AR-52648] image --- .changeset/tidy-singers-travel.md | 5 + .../src/components/ds-button/index.ts | 1 + .../__tests__/ds-button-v3.browser.test.tsx | 187 +++++++++ .../ds-button-v3/ds-button-v3.module.scss | 394 ++++++++++++++++++ .../ds-button-v3.stories.module.scss | 83 ++++ .../ds-button-v3/ds-button-v3.stories.tsx | 225 ++++++++++ .../versions/ds-button-v3/ds-button-v3.tsx | 66 +++ .../ds-button-v3/ds-button-v3.types.ts | 58 +++ .../ds-button/versions/ds-button-v3/index.ts | 2 + 9 files changed, 1021 insertions(+) create mode 100644 .changeset/tidy-singers-travel.md create mode 100644 packages/design-system/src/components/ds-button/versions/ds-button-v3/__tests__/ds-button-v3.browser.test.tsx create mode 100644 packages/design-system/src/components/ds-button/versions/ds-button-v3/ds-button-v3.module.scss create mode 100644 packages/design-system/src/components/ds-button/versions/ds-button-v3/ds-button-v3.stories.module.scss create mode 100644 packages/design-system/src/components/ds-button/versions/ds-button-v3/ds-button-v3.stories.tsx create mode 100644 packages/design-system/src/components/ds-button/versions/ds-button-v3/ds-button-v3.tsx create mode 100644 packages/design-system/src/components/ds-button/versions/ds-button-v3/ds-button-v3.types.ts create mode 100644 packages/design-system/src/components/ds-button/versions/ds-button-v3/index.ts diff --git a/.changeset/tidy-singers-travel.md b/.changeset/tidy-singers-travel.md new file mode 100644 index 000000000..af95ea575 --- /dev/null +++ b/.changeset/tidy-singers-travel.md @@ -0,0 +1,5 @@ +--- +'@drivenets/design-system': minor +--- + +Add the latest version of the `DsButton` component diff --git a/packages/design-system/src/components/ds-button/index.ts b/packages/design-system/src/components/ds-button/index.ts index 26a914360..88dd3ec10 100644 --- a/packages/design-system/src/components/ds-button/index.ts +++ b/packages/design-system/src/components/ds-button/index.ts @@ -1,2 +1,3 @@ export { default as DsButton } from './ds-button'; export * from './ds-button.types'; +export * from './versions/ds-button-v3'; diff --git a/packages/design-system/src/components/ds-button/versions/ds-button-v3/__tests__/ds-button-v3.browser.test.tsx b/packages/design-system/src/components/ds-button/versions/ds-button-v3/__tests__/ds-button-v3.browser.test.tsx new file mode 100644 index 000000000..742e2992b --- /dev/null +++ b/packages/design-system/src/components/ds-button/versions/ds-button-v3/__tests__/ds-button-v3.browser.test.tsx @@ -0,0 +1,187 @@ +import { createRef } from 'react'; +import { describe, expect, it, vi } from 'vitest'; +import { page } from 'vitest/browser'; +import { DsButtonV3 } from '..'; +import styles from '../ds-button-v3.module.scss'; + +describe('DsButtonV3', () => { + it('calls onClick when clicked', async () => { + const onClick = vi.fn(); + + await page.render(Click me); + + await page.getByRole('button', { name: 'Click me' }).click(); + + expect(onClick).toHaveBeenCalled(); + }); + + it('does not call onClick when disabled', async () => { + const onClick = vi.fn(); + + await page.render( + + Click me + , + ); + + const button = page.getByRole('button', { name: 'Click me', disabled: true }); + + await button.click({ force: true }); + + expect(onClick).not.toHaveBeenCalled(); + await expect.element(button).toBeDisabled(); + }); + + it('applies selected state', async () => { + await page.render(Label); + + const button = page.getByRole('button', { name: 'Label' }); + + await expect.element(button).toHaveAttribute('data-selected', 'true'); + }); + + it('sets data-color for negative palette', async () => { + await page.render(Delete); + + const button = page.getByRole('button', { name: 'Delete' }); + + await expect.element(button).toHaveAttribute('data-color', 'negative'); + }); + + it('sets data-on-dark when onDark prop is true', async () => { + await page.render(Label); + + const button = page.getByRole('button', { name: 'Label' }); + + await expect.element(button).toHaveAttribute('data-on-dark', 'true'); + }); + + it('applies iconOnly layout when icon is set without children', async () => { + await page.render(); + + const button = page.getByRole('button', { name: 'Confirm' }); + + await expect.element(button).toHaveClass(styles.iconOnly); + }); + + it('does not apply iconOnly layout when icon is set with children', async () => { + await page.render(Save); + + const button = page.getByRole('button', { name: 'Save' }); + + await expect.element(button).not.toHaveClass(styles.iconOnly); + }); + + it('renders native submit button type', async () => { + await page.render(Send); + + const button = page.getByRole('button', { name: 'Send' }); + + await expect.element(button).toHaveAttribute('type', 'submit'); + }); + + it('merges className', async () => { + await page.render(X); + + await expect.element(page.getByRole('button', { name: 'X' })).toHaveClass('extra'); + }); + + it('sets aria-busy and loading class when loading', async () => { + await page.render(Save); + + const button = page.getByRole('button', { name: 'Save' }); + + await expect.element(button).toHaveAttribute('aria-busy', 'true'); + await expect.element(button).toHaveClass(styles.loading); + }); + + it('renders spinner instead of icon when loading', async () => { + await page.render(); + + const button = page.getByRole('button', { name: 'Saving' }); + + await expect.element(button).toHaveAttribute('aria-busy', 'true'); + + const el = button.element(); + expect(el.querySelector('svg')).toBeTruthy(); + expect(el.querySelector('[aria-hidden]')).toBeNull(); + }); + + it('does not call onClick when loading', async () => { + const onClick = vi.fn(); + + await page.render( + + Save + , + ); + + await page.getByRole('button', { name: 'Save' }).click({ force: true }); + + expect(onClick).not.toHaveBeenCalled(); + }); + + it('sets data-variant for each variant', async () => { + for (const variant of ['primary', 'secondary', 'tertiary'] as const) { + await page.render( + + Label + , + ); + + await expect + .element(page.getByRole('button', { name: variant })) + .toHaveAttribute('data-variant', variant); + } + }); + + it('applies correct size class for each size', async () => { + const sizeClassMap = { + large: styles.sizeLarge, + medium: styles.sizeMedium, + small: styles.sizeSmall, + tiny: styles.sizeTiny, + } as const; + + for (const [size, cls] of Object.entries(sizeClassMap) as [keyof typeof sizeClassMap, string][]) { + await page.render( + + {size !== 'tiny' ? 'Label' : undefined} + , + ); + + await expect.element(page.getByRole('button', { name: size })).toHaveClass(cls); + } + }); + + it('applies default props when none are specified', async () => { + await page.render(Default); + + const button = page.getByRole('button', { name: 'Default' }); + + await expect.element(button).toHaveAttribute('data-color', 'default'); + await expect.element(button).toHaveAttribute('data-variant', 'primary'); + await expect.element(button).toHaveClass(styles.sizeMedium); + }); + + it('forwards ref to the button element', async () => { + const ref = createRef(); + + await page.render(Ref); + + expect(ref.current).toBeInstanceOf(HTMLButtonElement); + expect(ref.current?.textContent).toBe('Ref'); + }); + + it('spreads rest props onto the button element', async () => { + await page.render( + + Rest + , + ); + + const button = page.getByRole('button', { name: 'Spread' }); + + await expect.element(button).toHaveAttribute('data-testid', 'my-btn'); + }); +}); diff --git a/packages/design-system/src/components/ds-button/versions/ds-button-v3/ds-button-v3.module.scss b/packages/design-system/src/components/ds-button/versions/ds-button-v3/ds-button-v3.module.scss new file mode 100644 index 000000000..d29f25f6c --- /dev/null +++ b/packages/design-system/src/components/ds-button/versions/ds-button-v3/ds-button-v3.module.scss @@ -0,0 +1,394 @@ +@use '../../../../styles/root_updated'; + +$height-large: 40px; +$height-medium: 36px; +$height-small: 28px; +$border-radius: 4px; +$focus-ring-width: 2px; +$focus-ring-offset: 1px; +// it looks a bit better with 0.3 than 0.2 +$transition-duration-default: 0.3s; +$transition-duration-quick: 0.15s; + +@mixin focus-ring($outer-color) { + outline: $focus-ring-width solid $outer-color; + outline-offset: $focus-ring-offset; +} + +.root { + box-sizing: border-box; + display: inline-flex; + align-items: center; + justify-content: center; + gap: var(--3xs); + margin: 0; + border: 1px solid transparent; + border-radius: $border-radius; + background: transparent; + font-family: var(--font-family-base); + font-weight: var(--font-weight-medium); + text-align: center; + cursor: pointer; + transition: + background-color $transition-duration-default, + border-color $transition-duration-quick, + color $transition-duration-default, + outline-color $transition-duration-quick; + + &:disabled { + cursor: not-allowed; + } + + &.loading { + pointer-events: none; + } + + &:focus { + outline: none; + } + + &:focus-visible { + outline-style: solid; + } + + &.sizeLarge { + padding: var(--3xs) var(--sm); + min-height: $height-large; + font-size: var(--font-size-md); + line-height: var(--line-height-md); + } + + &.sizeMedium { + padding: var(--3xs) var(--sm); + min-height: $height-medium; + font-size: var(--font-size-sm); + line-height: var(--line-height-sm); + } + + &.sizeSmall { + padding: var(--3xs) var(--sm); + min-height: $height-small; + font-size: var(--font-size-xs); + line-height: var(--line-height-xs); + } + + &.sizeTiny { + min-height: var(--icon-width-tiny); + padding: 0 var(--3xs); + font-size: var(--font-size-xs); + line-height: var(--line-height-xs); + } + + &.iconOnly.sizeLarge { + padding: var(--3xs); + min-width: $height-large; + } + + &.iconOnly.sizeMedium { + padding: var(--3xs); + min-width: $height-medium; + } + + &.iconOnly.sizeSmall { + padding: var(--3xs); + min-width: $height-small; + } + + &.iconOnly.sizeTiny { + padding: 0; + min-width: var(--icon-width-tiny); + } +} + +.root[data-color='default'][data-variant='primary'] { + background-color: var(--background-background-primary); + color: var(--font-font-on-action); + border-color: transparent; + + &:hover:not(:disabled):not(:active):not([data-selected='true']) { + background-color: var(--background-background-primary-hover); + } + + &:active:not(:disabled) { + background-color: var(--background-background-primary-selected); + } + + &:focus-visible { + @include focus-ring(var(--border-border-action-primary)); + + background-color: var(--background-background-primary-hover); + border-color: var(--border-border-inverse); + } + + &[data-selected='true'] { + background-color: var(--background-background-primary-selected); + } + + &:disabled { + background-color: var(--background-background-disable); + color: var(--font-font-on-action); + } +} + +.root[data-color='default'][data-variant='secondary'] { + background-color: var(--background-background-action-secondary); + color: var(--font-font-main); + border-color: var(--border-border); + + &:hover:not(:disabled):not(:active):not([data-selected='true']) { + background-color: var(--background-background-secondary-hover); + color: var(--font-font-action-secondary); + border-color: var(--border-border-secondary-hover); + } + + &:active:not(:disabled) { + background-color: var(--background-background-action-secondary-hover); + color: var(--font-font-action-secondary); + border-color: var(--border-border-secondary-hover); + } + + &:focus-visible { + @include focus-ring(var(--border-border-action-primary-hover)); + + background-color: var(--background-background-secondary-hover); + border-color: var(--border-border-inverse); + color: var(--font-font-action-secondary); + } + + &[data-selected='true'] { + background-color: var(--background-background-action-secondary-hover); + color: var(--font-font-action-secondary); + border-color: var(--border-border-secondary-hover); + } + + &:disabled { + background-color: var(--background-background-action-secondary); + color: var(--font-font-disabled); + border-color: var(--border-border); + } +} + +.root[data-color='default'][data-variant='tertiary'] { + background-color: transparent; + color: var(--font-font-main); + border-color: transparent; + + &:hover:not(:disabled):not(:active):not([data-selected='true']) { + background-color: var(--background-background-secondary-hover); + color: var(--font-font-action-secondary); + } + + &:active:not(:disabled) { + background-color: var(--background-background-action-secondary-hover); + color: var(--font-font-action-secondary); + } + + &:focus-visible { + @include focus-ring(var(--border-border-action-primary-hover)); + + background-color: var(--background-background-secondary-hover); + border-color: var(--border-border-inverse); + color: var(--font-font-action-secondary); + } + + &[data-selected='true'] { + background-color: var(--background-background-action-secondary-hover); + color: var(--font-font-action-secondary); + } + + &:disabled { + background-color: transparent; + color: var(--font-font-disabled); + } +} + +.root[data-color='negative'][data-variant='primary'] { + background-color: var(--background-background-negative); + color: var(--font-font-on-action); + border-color: transparent; + + &:hover:not(:disabled):not(:active):not([data-selected='true']) { + background-color: var(--background-background-negative-hover); + } + + &:active:not(:disabled) { + background-color: var(--background-background-negative-selected); + } + + &:focus-visible { + @include focus-ring(var(--border-border-negative)); + + background-color: var(--background-background-negative-hover); + border-color: var(--border-border-inverse); + } + + &[data-selected='true'] { + background-color: var(--background-background-negative-selected); + } + + &:disabled { + background-color: var(--background-background-disable); + color: var(--font-font-on-action); + } +} + +.root[data-color='negative'][data-variant='secondary'] { + background-color: var(--background-background-action-secondary); + color: var(--font-font-negative); + border-color: var(--border-border-negative); + + &:hover:not(:disabled):not(:active):not([data-selected='true']) { + background-color: var(--background-background-negative-secondary-hover); + border-color: var(--border-border-negative-hover); + } + + &:active:not(:disabled) { + background-color: var(--background-background-negative-secondary-selected); + border-color: var(--border-border-negative-hover); + } + + &:focus-visible { + @include focus-ring(var(--border-border-negative)); + + background-color: var(--background-background-negative-secondary-hover); + border-color: var(--border-border-inverse); + } + + &[data-selected='true'] { + background-color: var(--background-background-negative-secondary-selected); + border-color: var(--border-border-negative-hover); + } + + &:disabled { + background-color: var(--background-background-action-secondary); + color: var(--font-font-disabled); + border-color: var(--border-border); + } +} + +.root[data-color='negative'][data-variant='tertiary'] { + background-color: transparent; + color: var(--font-font-negative); + border-color: transparent; + + &:hover:not(:disabled):not(:active):not([data-selected='true']) { + background-color: var(--background-background-negative-secondary-hover); + } + + &:active:not(:disabled) { + background-color: var(--background-background-negative-secondary-selected); + } + + &:focus-visible { + @include focus-ring(var(--border-border-negative)); + + background-color: var(--background-background-negative-secondary-hover); + border-color: var(--border-border-inverse); + } + + &[data-selected='true'] { + background-color: var(--background-background-negative-secondary-selected); + } + + &:disabled { + background-color: transparent; + color: var(--font-font-disabled); + } +} + +.root[data-on-dark][data-variant='primary'] { + background-color: var(--background-background-ondark-primary); + color: var(--font-font-on-action); + border-color: transparent; + + &:hover:not(:disabled):not(:active):not([data-selected='true']) { + background-color: var(--background-background-ondark-primary-hover); + } + + &:active:not(:disabled) { + background-color: var(--background-background-ondark-primary-selected); + } + + &:focus-visible { + @include focus-ring(var(--outline-outline-inverse)); + + background-color: var(--background-background-ondark-primary-hover); + border-color: var(--border-border-action-secondary); + } + + &[data-selected='true'] { + background-color: var(--background-background-ondark-primary-selected); + } + + &:disabled { + background-color: var(--background-background-ondark-disabled); + color: var(--font-font-disabled); + } +} + +.root[data-on-dark][data-variant='secondary'] { + background-color: transparent; + color: var(--font-font-on-action); + border-color: var(--border-border-ondark-secondary); + + &:hover:not(:disabled):not(:active):not([data-selected='true']) { + background-color: var(--background-background-ondark-secondary-hover); + color: var(--font-font-on-action); + } + + &:active:not(:disabled) { + background-color: var(--background-background-ondark-secondary-selected); + color: var(--font-font-on-action); + } + + &:focus-visible { + @include focus-ring(var(--outline-outline-inverse)); + + background-color: var(--background-background-ondark-secondary-hover); + border-color: var(--border-border-action-secondary); + color: var(--font-font-on-action); + } + + &[data-selected='true'] { + background-color: var(--background-background-ondark-secondary-selected); + } + + &:disabled { + background-color: transparent; + color: var(--font-font-ondark-disabled); + border-color: var(--background-background-ondark-disabled); + } +} + +.root[data-on-dark][data-variant='tertiary'] { + background-color: transparent; + color: var(--font-font-on-action); + border-color: transparent; + + &:hover:not(:disabled):not(:active):not([data-selected='true']) { + background-color: var(--background-background-ondark-secondary-hover); + color: var(--font-font-on-action); + } + + &:active:not(:disabled) { + background-color: var(--background-background-ondark-secondary-selected); + color: var(--font-font-on-action); + } + + &:focus-visible { + @include focus-ring(var(--outline-outline-inverse)); + + background-color: var(--background-background-ondark-secondary-hover); + border-color: var(--border-border-action-secondary); + color: var(--font-font-on-action); + } + + &[data-selected='true'] { + background-color: var(--background-background-ondark-secondary-selected); + } + + &:disabled { + background-color: transparent; + color: var(--font-font-ondark-disabled); + } +} diff --git a/packages/design-system/src/components/ds-button/versions/ds-button-v3/ds-button-v3.stories.module.scss b/packages/design-system/src/components/ds-button/versions/ds-button-v3/ds-button-v3.stories.module.scss new file mode 100644 index 000000000..2752d06cc --- /dev/null +++ b/packages/design-system/src/components/ds-button/versions/ds-button-v3/ds-button-v3.stories.module.scss @@ -0,0 +1,83 @@ +@use '../../../../styles/root_updated'; + +.matrix { + display: flex; + flex-direction: column; + gap: var(--lg); + padding: var(--2xl); +} + +.section { + display: flex; + flex-direction: column; + gap: var(--sm); +} + +.sectionTitle { + margin: 0 0 var(--xs); + font-family: var(--font-family-base); + font-size: var(--font-size-xs); + font-weight: var(--font-weight-semi-bold); + color: var(--font-font-secondary); + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.columnHeaders { + display: flex; + align-items: center; + gap: var(--sm); + padding-left: 120px; +} + +.columnHeader { + flex: 1; + font-family: var(--font-family-base); + font-size: var(--font-size-xs); + font-weight: var(--font-weight-medium); + color: var(--font-font-secondary); + text-align: center; +} + +.row { + display: flex; + align-items: center; + gap: var(--sm); +} + +.rowLabel { + width: 120px; + flex-shrink: 0; + font-family: var(--font-family-base); + font-size: var(--font-size-sm); + font-weight: var(--font-weight-medium); + color: var(--font-font-secondary); +} + +.cell { + flex: 1; + display: flex; + justify-content: center; +} + +.onDark { + padding: var(--2xl); + background-color: var(--darks-500); + border-radius: 4px; +} + +.onDarkLabel { + color: var(--secondary-050); +} + +.onDarkSectionTitle { + color: var(--secondary-300); +} + +.sectionTitleSpaced { + margin-top: var(--lg); +} + +.onDarkColumnHeader { + color: var(--secondary-300); +} diff --git a/packages/design-system/src/components/ds-button/versions/ds-button-v3/ds-button-v3.stories.tsx b/packages/design-system/src/components/ds-button/versions/ds-button-v3/ds-button-v3.stories.tsx new file mode 100644 index 000000000..e4ff7c31d --- /dev/null +++ b/packages/design-system/src/components/ds-button/versions/ds-button-v3/ds-button-v3.stories.tsx @@ -0,0 +1,225 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; +import classNames from 'classnames'; +import { fn } from 'storybook/test'; +import DsButtonV3 from './ds-button-v3'; +import { buttonV3Colors, buttonV3Sizes, buttonV3Variants } from './ds-button-v3.types'; +import storyStyles from './ds-button-v3.stories.module.scss'; + +const meta: Meta = { + title: 'Design System/ButtonV3', + component: DsButtonV3, + parameters: { + layout: 'centered', + }, + argTypes: { + color: { control: 'select', options: buttonV3Colors }, + variant: { control: 'select', options: buttonV3Variants }, + size: { control: 'select', options: buttonV3Sizes }, + onDark: { control: 'boolean' }, + loading: { control: 'boolean' }, + className: { table: { disable: true } }, + style: { table: { disable: true } }, + ref: { table: { disable: true } }, + }, + args: { onClick: fn() }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + color: 'default', + variant: 'primary', + size: 'medium', + icon: 'check_circle', + children: 'Button', + }, +}; + +const matrixRows = [ + ...buttonV3Variants.map((v) => ({ label: v, loading: false })), + { label: 'loading', loading: true }, +]; + +const defaultIconMatrixRows = [ + { label: 'check circle', icon: 'check_circle', variant: 'primary', color: 'default', loading: false }, + { label: 'info', icon: 'info', variant: 'secondary', color: 'default', loading: false }, + { label: 'delete', icon: 'delete', variant: 'tertiary', color: 'negative', loading: false }, + { label: 'loading', icon: 'check_circle', variant: 'primary', color: 'default', loading: true }, +] as const; + +const onDarkIconMatrixRows = [ + { + label: 'arrow down', + icon: 'keyboard_arrow_down', + variant: 'primary', + color: 'default', + loading: false, + }, + { label: 'home', icon: 'home', variant: 'secondary', color: 'default', loading: false }, + { label: 'info', icon: 'info', variant: 'tertiary', color: 'default', loading: false }, + { label: 'loading', icon: 'info', variant: 'primary', color: 'default', loading: true }, +] as const; + +const MatrixGrid = ({ onDark = false, color }: { onDark?: boolean; color?: 'default' | 'negative' }) => ( +
+
+ {buttonV3Sizes.map((size) => ( + + {size.charAt(0).toUpperCase() + size.slice(1)} + + ))} +
+ + {matrixRows.map(({ label, loading }) => ( +
+ + {label.charAt(0).toUpperCase() + label.slice(1)} + + + {buttonV3Sizes.map((size) => ( +
+ + {size !== 'tiny' ? 'Button' : undefined} + +
+ ))} +
+ ))} +
+); + +const IconMatrixGrid = ({ + rows, + onDark = false, +}: { + rows: ReadonlyArray<{ + label: string; + icon: 'check_circle' | 'info' | 'delete' | 'keyboard_arrow_down' | 'home'; + variant: (typeof buttonV3Variants)[number]; + color: (typeof buttonV3Colors)[number]; + loading: boolean; + }>; + onDark?: boolean; +}) => ( +
+
+ {buttonV3Sizes.map((size) => ( + + {size.charAt(0).toUpperCase() + size.slice(1)} + + ))} +
+ + {rows.map(({ label, icon, loading, variant, color }) => ( +
+ + {label.charAt(0).toUpperCase() + label.slice(1)} + + + {buttonV3Sizes.map((size) => { + const ariaLabel = `${label} ${size}`; + + return ( +
+ +
+ ); + })} +
+ ))} +
+); + +export const MatrixDefault: Story = { + parameters: { layout: 'fullscreen' }, + render: () => ( +
+

Default

+ +
+ ), +}; + +export const MatrixNegative: Story = { + parameters: { layout: 'fullscreen' }, + render: () => ( +
+

Negative

+ +
+ ), +}; + +export const MatrixOnDark: Story = { + parameters: { layout: 'fullscreen' }, + render: () => ( +
+
+

+ On Dark — Default +

+ +
+
+ ), +}; + +export const MatrixIcons: Story = { + parameters: { layout: 'fullscreen' }, + render: () => ( +
+

Icons — Default

+ + +
+

+ Icons — On Dark +

+ +
+
+ ), +}; diff --git a/packages/design-system/src/components/ds-button/versions/ds-button-v3/ds-button-v3.tsx b/packages/design-system/src/components/ds-button/versions/ds-button-v3/ds-button-v3.tsx new file mode 100644 index 000000000..2ede80620 --- /dev/null +++ b/packages/design-system/src/components/ds-button/versions/ds-button-v3/ds-button-v3.tsx @@ -0,0 +1,66 @@ +import classNames from 'classnames'; +import { DsIcon, type IconSize } from '../../../ds-icon'; +import { DsSpinner } from '../../../ds-spinner'; +import styles from './ds-button-v3.module.scss'; +import type { ButtonV3Size, DsButtonV3Props } from './ds-button-v3.types'; + +const sizeClassMap = { + large: styles.sizeLarge, + medium: styles.sizeMedium, + small: styles.sizeSmall, + tiny: styles.sizeTiny, +} as const; + +const iconSizeMap: Record = Object.freeze({ + large: 'small', + medium: 'tiny', + small: 'tiny', + tiny: 'tiny', +}); + +const DsButtonV3 = ({ + ref, + className, + style, + children, + icon, + disabled, + loading = false, + color = 'default', + onDark = false, + variant = 'primary', + size = 'medium', + selected = false, + type = 'button', + ...rest +}: DsButtonV3Props) => { + const isIconOnly = icon !== undefined && !children; + + return ( + + ); +}; + +export default DsButtonV3; diff --git a/packages/design-system/src/components/ds-button/versions/ds-button-v3/ds-button-v3.types.ts b/packages/design-system/src/components/ds-button/versions/ds-button-v3/ds-button-v3.types.ts new file mode 100644 index 000000000..d4d1d6819 --- /dev/null +++ b/packages/design-system/src/components/ds-button/versions/ds-button-v3/ds-button-v3.types.ts @@ -0,0 +1,58 @@ +import type { ButtonHTMLAttributes, CSSProperties, ReactNode, Ref } from 'react'; +import type { IconType } from '../../../ds-icon'; + +export const buttonV3Variants = ['primary', 'secondary', 'tertiary'] as const; +export type ButtonV3Variant = (typeof buttonV3Variants)[number]; + +export const buttonV3Colors = ['default', 'negative'] as const; +export type ButtonV3Color = (typeof buttonV3Colors)[number]; + +export const buttonV3Sizes = ['large', 'medium', 'small', 'tiny'] as const; +export type ButtonV3Size = (typeof buttonV3Sizes)[number]; + +export interface DsButtonV3Props extends ButtonHTMLAttributes { + ref?: Ref; + className?: string; + style?: CSSProperties; + children?: ReactNode; + + /** + * - `default` — standard light-UI palette + * - `negative` — destructive / danger palette (red tones) + * @default 'default' + */ + color?: ButtonV3Color; + + /** + * Render the button for a dark-background surface (Figma **Type** onDark). + * Applies the on-dark token set regardless of `color`. + * @default false + */ + onDark?: boolean; + + /** + * @default 'primary' + */ + variant?: ButtonV3Variant; + + /** + * @default 'medium' + */ + size?: ButtonV3Size; + + /** + * If true the 'pressed' styles are applied + */ + selected?: boolean; + + /** + * Leading icon. When set without children, renders as icon-only (square) layout. + */ + icon?: IconType; + + /** + * Shows a spinner as the leading element and disables interaction. + * @default false + */ + loading?: boolean; +} diff --git a/packages/design-system/src/components/ds-button/versions/ds-button-v3/index.ts b/packages/design-system/src/components/ds-button/versions/ds-button-v3/index.ts new file mode 100644 index 000000000..76dd2de13 --- /dev/null +++ b/packages/design-system/src/components/ds-button/versions/ds-button-v3/index.ts @@ -0,0 +1,2 @@ +export { default as DsButtonV3 } from './ds-button-v3'; +export * from './ds-button-v3.types'; From f5216299ba2aa381ee52330028b5e63b4b0037ec Mon Sep 17 00:00:00 2001 From: Micky Date: Fri, 27 Mar 2026 11:06:18 +0100 Subject: [PATCH 02/10] Remove 'selected' prop comment, not needed --- .../ds-button/versions/ds-button-v3/ds-button-v3.types.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/design-system/src/components/ds-button/versions/ds-button-v3/ds-button-v3.types.ts b/packages/design-system/src/components/ds-button/versions/ds-button-v3/ds-button-v3.types.ts index d4d1d6819..85d1c2373 100644 --- a/packages/design-system/src/components/ds-button/versions/ds-button-v3/ds-button-v3.types.ts +++ b/packages/design-system/src/components/ds-button/versions/ds-button-v3/ds-button-v3.types.ts @@ -40,9 +40,6 @@ export interface DsButtonV3Props extends ButtonHTMLAttributes */ size?: ButtonV3Size; - /** - * If true the 'pressed' styles are applied - */ selected?: boolean; /** From 91fe3bc9867d5b800782252130ea4cf98a633314 Mon Sep 17 00:00:00 2001 From: Michal Murawski Date: Fri, 27 Mar 2026 12:50:19 +0100 Subject: [PATCH 03/10] CR changes v2 --- .../__tests__/ds-button-v3.browser.test.tsx | 44 +++++++++++++++++++ .../ds-button-v3/ds-button-v3.module.scss | 21 ++++----- .../versions/ds-button-v3/ds-button-v3.tsx | 7 +-- .../ds-button-v3/ds-button-v3.types.ts | 8 ++-- 4 files changed, 63 insertions(+), 17 deletions(-) diff --git a/packages/design-system/src/components/ds-button/versions/ds-button-v3/__tests__/ds-button-v3.browser.test.tsx b/packages/design-system/src/components/ds-button/versions/ds-button-v3/__tests__/ds-button-v3.browser.test.tsx index 742e2992b..17d41dde4 100644 --- a/packages/design-system/src/components/ds-button/versions/ds-button-v3/__tests__/ds-button-v3.browser.test.tsx +++ b/packages/design-system/src/components/ds-button/versions/ds-button-v3/__tests__/ds-button-v3.browser.test.tsx @@ -173,6 +173,50 @@ describe('DsButtonV3', () => { expect(ref.current?.textContent).toBe('Ref'); }); + it('is disabled and keeps normal colors when loading', async () => { + const onClick = vi.fn(); + + await page.render( + + Saving + , + ); + + const button = page.getByRole('button', { name: 'Saving', disabled: true }); + + await expect.element(button).toBeDisabled(); + await expect.element(button).toHaveClass(styles.loading); + await button.click({ force: true }); + expect(onClick).not.toHaveBeenCalled(); + }); + + it('selected + disabled does not remove selected styling', async () => { + await page.render( + + Toggle + , + ); + + const button = page.getByRole('button', { name: 'Toggle', disabled: true }); + + await expect.element(button).toBeDisabled(); + await expect.element(button).toHaveAttribute('aria-pressed', 'true'); + await expect.element(button).toHaveAttribute('data-selected', 'true'); + }); + + it('applies onDark + negative color together', async () => { + await page.render( + + Remove + , + ); + + const button = page.getByRole('button', { name: 'Remove' }); + + await expect.element(button).toHaveAttribute('data-on-dark', 'true'); + await expect.element(button).toHaveAttribute('data-color', 'negative'); + }); + it('spreads rest props onto the button element', async () => { await page.render( diff --git a/packages/design-system/src/components/ds-button/versions/ds-button-v3/ds-button-v3.module.scss b/packages/design-system/src/components/ds-button/versions/ds-button-v3/ds-button-v3.module.scss index d29f25f6c..36ba73201 100644 --- a/packages/design-system/src/components/ds-button/versions/ds-button-v3/ds-button-v3.module.scss +++ b/packages/design-system/src/components/ds-button/versions/ds-button-v3/ds-button-v3.module.scss @@ -35,11 +35,12 @@ $transition-duration-quick: 0.15s; color $transition-duration-default, outline-color $transition-duration-quick; - &:disabled { + &:disabled:not(.loading) { cursor: not-allowed; } &.loading { + cursor: default; pointer-events: none; } @@ -124,7 +125,7 @@ $transition-duration-quick: 0.15s; background-color: var(--background-background-primary-selected); } - &:disabled { + &:disabled:not(.loading) { background-color: var(--background-background-disable); color: var(--font-font-on-action); } @@ -161,7 +162,7 @@ $transition-duration-quick: 0.15s; border-color: var(--border-border-secondary-hover); } - &:disabled { + &:disabled:not(.loading) { background-color: var(--background-background-action-secondary); color: var(--font-font-disabled); border-color: var(--border-border); @@ -196,7 +197,7 @@ $transition-duration-quick: 0.15s; color: var(--font-font-action-secondary); } - &:disabled { + &:disabled:not(.loading) { background-color: transparent; color: var(--font-font-disabled); } @@ -226,7 +227,7 @@ $transition-duration-quick: 0.15s; background-color: var(--background-background-negative-selected); } - &:disabled { + &:disabled:not(.loading) { background-color: var(--background-background-disable); color: var(--font-font-on-action); } @@ -259,7 +260,7 @@ $transition-duration-quick: 0.15s; border-color: var(--border-border-negative-hover); } - &:disabled { + &:disabled:not(.loading) { background-color: var(--background-background-action-secondary); color: var(--font-font-disabled); border-color: var(--border-border); @@ -290,7 +291,7 @@ $transition-duration-quick: 0.15s; background-color: var(--background-background-negative-secondary-selected); } - &:disabled { + &:disabled:not(.loading) { background-color: transparent; color: var(--font-font-disabled); } @@ -320,7 +321,7 @@ $transition-duration-quick: 0.15s; background-color: var(--background-background-ondark-primary-selected); } - &:disabled { + &:disabled:not(.loading) { background-color: var(--background-background-ondark-disabled); color: var(--font-font-disabled); } @@ -353,7 +354,7 @@ $transition-duration-quick: 0.15s; background-color: var(--background-background-ondark-secondary-selected); } - &:disabled { + &:disabled:not(.loading) { background-color: transparent; color: var(--font-font-ondark-disabled); border-color: var(--background-background-ondark-disabled); @@ -387,7 +388,7 @@ $transition-duration-quick: 0.15s; background-color: var(--background-background-ondark-secondary-selected); } - &:disabled { + &:disabled:not(.loading) { background-color: transparent; color: var(--font-font-ondark-disabled); } diff --git a/packages/design-system/src/components/ds-button/versions/ds-button-v3/ds-button-v3.tsx b/packages/design-system/src/components/ds-button/versions/ds-button-v3/ds-button-v3.tsx index 2ede80620..6e3beeebb 100644 --- a/packages/design-system/src/components/ds-button/versions/ds-button-v3/ds-button-v3.tsx +++ b/packages/design-system/src/components/ds-button/versions/ds-button-v3/ds-button-v3.tsx @@ -4,12 +4,12 @@ import { DsSpinner } from '../../../ds-spinner'; import styles from './ds-button-v3.module.scss'; import type { ButtonV3Size, DsButtonV3Props } from './ds-button-v3.types'; -const sizeClassMap = { +const sizeClassMap = Object.freeze({ large: styles.sizeLarge, medium: styles.sizeMedium, small: styles.sizeSmall, tiny: styles.sizeTiny, -} as const; +}); const iconSizeMap: Record = Object.freeze({ large: 'small', @@ -42,8 +42,9 @@ const DsButtonV3 = ({ // Dynamic by nature of this component // eslint-disable-next-line react/button-has-type type={type} - disabled={disabled} + disabled={disabled || loading} aria-busy={loading || undefined} + aria-pressed={selected} className={classNames( styles.root, sizeClassMap[size], diff --git a/packages/design-system/src/components/ds-button/versions/ds-button-v3/ds-button-v3.types.ts b/packages/design-system/src/components/ds-button/versions/ds-button-v3/ds-button-v3.types.ts index 85d1c2373..05a83c6dc 100644 --- a/packages/design-system/src/components/ds-button/versions/ds-button-v3/ds-button-v3.types.ts +++ b/packages/design-system/src/components/ds-button/versions/ds-button-v3/ds-button-v3.types.ts @@ -1,4 +1,4 @@ -import type { ButtonHTMLAttributes, CSSProperties, ReactNode, Ref } from 'react'; +import type { ButtonHTMLAttributes, Ref } from 'react'; import type { IconType } from '../../../ds-icon'; export const buttonV3Variants = ['primary', 'secondary', 'tertiary'] as const; @@ -12,9 +12,6 @@ export type ButtonV3Size = (typeof buttonV3Sizes)[number]; export interface DsButtonV3Props extends ButtonHTMLAttributes { ref?: Ref; - className?: string; - style?: CSSProperties; - children?: ReactNode; /** * - `default` — standard light-UI palette @@ -40,6 +37,9 @@ export interface DsButtonV3Props extends ButtonHTMLAttributes */ size?: ButtonV3Size; + /** + * @default false + */ selected?: boolean; /** From 3a8edcc963edb261b72cca7537c9a1d0460a7018 Mon Sep 17 00:00:00 2001 From: Michal Murawski Date: Fri, 27 Mar 2026 13:18:00 +0100 Subject: [PATCH 04/10] CR changes v3 --- .../__tests__/ds-button-v3.browser.test.tsx | 18 ++++++++++++++++-- .../ds-button-v3/ds-button-v3.stories.tsx | 1 + .../versions/ds-button-v3/ds-button-v3.tsx | 2 +- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/packages/design-system/src/components/ds-button/versions/ds-button-v3/__tests__/ds-button-v3.browser.test.tsx b/packages/design-system/src/components/ds-button/versions/ds-button-v3/__tests__/ds-button-v3.browser.test.tsx index 17d41dde4..7fd13c558 100644 --- a/packages/design-system/src/components/ds-button/versions/ds-button-v3/__tests__/ds-button-v3.browser.test.tsx +++ b/packages/design-system/src/components/ds-button/versions/ds-button-v3/__tests__/ds-button-v3.browser.test.tsx @@ -173,11 +173,11 @@ describe('DsButtonV3', () => { expect(ref.current?.textContent).toBe('Ref'); }); - it('is disabled and keeps normal colors when loading', async () => { + it('loading without disabled keeps normal colors', async () => { const onClick = vi.fn(); await page.render( - + Saving , ); @@ -190,6 +190,20 @@ describe('DsButtonV3', () => { expect(onClick).not.toHaveBeenCalled(); }); + it('loading + disabled shows spinner with disabled styling', async () => { + await page.render( + + Saving + , + ); + + const button = page.getByRole('button', { name: 'Saving', disabled: true }); + + await expect.element(button).toBeDisabled(); + await expect.element(button).not.toHaveClass(styles.loading); + await expect.element(button).toHaveAttribute('aria-busy', 'true'); + }); + it('selected + disabled does not remove selected styling', async () => { await page.render( diff --git a/packages/design-system/src/components/ds-button/versions/ds-button-v3/ds-button-v3.stories.tsx b/packages/design-system/src/components/ds-button/versions/ds-button-v3/ds-button-v3.stories.tsx index e4ff7c31d..4d2bd7936 100644 --- a/packages/design-system/src/components/ds-button/versions/ds-button-v3/ds-button-v3.stories.tsx +++ b/packages/design-system/src/components/ds-button/versions/ds-button-v3/ds-button-v3.stories.tsx @@ -17,6 +17,7 @@ const meta: Meta = { size: { control: 'select', options: buttonV3Sizes }, onDark: { control: 'boolean' }, loading: { control: 'boolean' }, + disabled: { control: 'boolean' }, className: { table: { disable: true } }, style: { table: { disable: true } }, ref: { table: { disable: true } }, diff --git a/packages/design-system/src/components/ds-button/versions/ds-button-v3/ds-button-v3.tsx b/packages/design-system/src/components/ds-button/versions/ds-button-v3/ds-button-v3.tsx index 6e3beeebb..91976b352 100644 --- a/packages/design-system/src/components/ds-button/versions/ds-button-v3/ds-button-v3.tsx +++ b/packages/design-system/src/components/ds-button/versions/ds-button-v3/ds-button-v3.tsx @@ -48,7 +48,7 @@ const DsButtonV3 = ({ className={classNames( styles.root, sizeClassMap[size], - { [styles.iconOnly]: isIconOnly, [styles.loading]: loading }, + { [styles.iconOnly]: isIconOnly, [styles.loading]: loading && !disabled }, className, )} style={style} From 1f4e77b124b0b7e52bcd19b30d3e4d08cbde2a4c Mon Sep 17 00:00:00 2001 From: Michal Murawski Date: Mon, 30 Mar 2026 10:18:38 +0200 Subject: [PATCH 05/10] improve browser tests performance --- .../ds-key-value-pair.browser.test.tsx | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/packages/design-system/src/components/ds-key-value-pair/__tests__/ds-key-value-pair.browser.test.tsx b/packages/design-system/src/components/ds-key-value-pair/__tests__/ds-key-value-pair.browser.test.tsx index 8658f6c75..c44dbe115 100644 --- a/packages/design-system/src/components/ds-key-value-pair/__tests__/ds-key-value-pair.browser.test.tsx +++ b/packages/design-system/src/components/ds-key-value-pair/__tests__/ds-key-value-pair.browser.test.tsx @@ -17,6 +17,15 @@ const MANUFACTURER_OPTIONS: DsSelectOption[] = [ { label: 'Nokia', value: 'nokia' }, ]; +/** + * Focuses the nearest `[data-editable]` container ancestor of the given text. + * Direct DOM focus bypasses the CSS `:hover` swap that causes Playwright's + * click/tab to race with the display toggle between value and editor slot. + */ +const focusEditable = (text: string) => { + (page.getByText(text).element().closest('[data-editable]') as HTMLElement).focus(); +}; + describe('DsKeyValuePair', () => { it('should render read-only vertical layout', async () => { await page.render( @@ -67,7 +76,7 @@ describe('DsKeyValuePair', () => { await expect.element(page.getByText('99887766')).toBeVisible(); - await userEvent.tab(); + focusEditable('99887766'); await expect.element(page.getByRole('textbox')).toBeVisible(); }); @@ -98,13 +107,11 @@ describe('DsKeyValuePair', () => { await expect.element(page.getByText('99887766')).toBeVisible(); - await userEvent.tab(); + focusEditable('99887766'); const input = page.getByRole('textbox'); await expect.element(input).toBeVisible(); - await userEvent.tab(); - await input.clear(); await input.fill('NEW SERIAL'); await expect.element(input).toHaveValue('NEW SERIAL'); @@ -130,7 +137,7 @@ describe('DsKeyValuePair', () => { await expect.element(page.getByText('Initial')).toBeVisible(); - await userEvent.tab(); + focusEditable('Initial'); const input = page.getByRole('textbox'); await input.fill('Updated'); await userEvent.tab(); @@ -165,7 +172,7 @@ describe('DsKeyValuePair', () => { await expect.element(page.getByText('Editable value')).toBeVisible(); await expect.element(page.getByText('info').first()).toBeVisible(); - await userEvent.tab(); + focusEditable('Editable value'); await expect.element(page.getByRole('textbox')).toBeVisible(); }); From 25367480327564e49071bf1777fc6d5f8a685237 Mon Sep 17 00:00:00 2001 From: Michal Murawski Date: Mon, 30 Mar 2026 12:49:44 +0200 Subject: [PATCH 06/10] export buttonv3 --- packages/design-system/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/design-system/src/index.ts b/packages/design-system/src/index.ts index ac8f389ec..eff7072e0 100644 --- a/packages/design-system/src/index.ts +++ b/packages/design-system/src/index.ts @@ -4,6 +4,7 @@ export * from './components/ds-avatar'; export * from './components/ds-avatar-group'; export * from './components/ds-breadcrumb'; export * from './components/ds-button'; +export * from './components/ds-button/versions/ds-button-v3/index.ts'; export * from './components/ds-card'; export * from './components/ds-checkbox'; export * from './components/ds-chip'; From 05ec2afa882c0da28579e0b8a724b5ccf3deb6fd Mon Sep 17 00:00:00 2001 From: Michal Murawski Date: Mon, 30 Mar 2026 12:56:57 +0200 Subject: [PATCH 07/10] move buttonv3 to root --- .changeset/tidy-singers-travel.md | 2 +- .../ds-button-v3/__tests__/ds-button-v3.browser.test.tsx | 2 +- .../versions => }/ds-button-v3/ds-button-v3.module.scss | 2 +- .../ds-button-v3/ds-button-v3.stories.module.scss | 2 +- .../versions => }/ds-button-v3/ds-button-v3.stories.tsx | 4 ++-- .../{ds-button/versions => }/ds-button-v3/ds-button-v3.tsx | 6 +++--- .../versions => }/ds-button-v3/ds-button-v3.types.ts | 2 +- packages/design-system/src/components/ds-button-v3/index.ts | 2 ++ packages/design-system/src/components/ds-button/index.ts | 1 - .../src/components/ds-button/versions/ds-button-v3/index.ts | 2 -- packages/design-system/src/index.ts | 2 +- 11 files changed, 13 insertions(+), 14 deletions(-) rename packages/design-system/src/components/{ds-button/versions => }/ds-button-v3/__tests__/ds-button-v3.browser.test.tsx (99%) rename packages/design-system/src/components/{ds-button/versions => }/ds-button-v3/ds-button-v3.module.scss (99%) rename packages/design-system/src/components/{ds-button/versions => }/ds-button-v3/ds-button-v3.stories.module.scss (97%) rename packages/design-system/src/components/{ds-button/versions => }/ds-button-v3/ds-button-v3.stories.tsx (98%) rename packages/design-system/src/components/{ds-button/versions => }/ds-button-v3/ds-button-v3.tsx (92%) rename packages/design-system/src/components/{ds-button/versions => }/ds-button-v3/ds-button-v3.types.ts (96%) create mode 100644 packages/design-system/src/components/ds-button-v3/index.ts delete mode 100644 packages/design-system/src/components/ds-button/versions/ds-button-v3/index.ts diff --git a/.changeset/tidy-singers-travel.md b/.changeset/tidy-singers-travel.md index af95ea575..c3d1d1353 100644 --- a/.changeset/tidy-singers-travel.md +++ b/.changeset/tidy-singers-travel.md @@ -2,4 +2,4 @@ '@drivenets/design-system': minor --- -Add the latest version of the `DsButton` component +Add the latest version of the `DsButtonV3` component diff --git a/packages/design-system/src/components/ds-button/versions/ds-button-v3/__tests__/ds-button-v3.browser.test.tsx b/packages/design-system/src/components/ds-button-v3/__tests__/ds-button-v3.browser.test.tsx similarity index 99% rename from packages/design-system/src/components/ds-button/versions/ds-button-v3/__tests__/ds-button-v3.browser.test.tsx rename to packages/design-system/src/components/ds-button-v3/__tests__/ds-button-v3.browser.test.tsx index 7fd13c558..742c2deaf 100644 --- a/packages/design-system/src/components/ds-button/versions/ds-button-v3/__tests__/ds-button-v3.browser.test.tsx +++ b/packages/design-system/src/components/ds-button-v3/__tests__/ds-button-v3.browser.test.tsx @@ -1,7 +1,7 @@ import { createRef } from 'react'; import { describe, expect, it, vi } from 'vitest'; import { page } from 'vitest/browser'; -import { DsButtonV3 } from '..'; +import { DsButtonV3 } from '../index.ts'; import styles from '../ds-button-v3.module.scss'; describe('DsButtonV3', () => { diff --git a/packages/design-system/src/components/ds-button/versions/ds-button-v3/ds-button-v3.module.scss b/packages/design-system/src/components/ds-button-v3/ds-button-v3.module.scss similarity index 99% rename from packages/design-system/src/components/ds-button/versions/ds-button-v3/ds-button-v3.module.scss rename to packages/design-system/src/components/ds-button-v3/ds-button-v3.module.scss index 36ba73201..762996546 100644 --- a/packages/design-system/src/components/ds-button/versions/ds-button-v3/ds-button-v3.module.scss +++ b/packages/design-system/src/components/ds-button-v3/ds-button-v3.module.scss @@ -1,4 +1,4 @@ -@use '../../../../styles/root_updated'; +@use '../../styles/root_updated'; $height-large: 40px; $height-medium: 36px; diff --git a/packages/design-system/src/components/ds-button/versions/ds-button-v3/ds-button-v3.stories.module.scss b/packages/design-system/src/components/ds-button-v3/ds-button-v3.stories.module.scss similarity index 97% rename from packages/design-system/src/components/ds-button/versions/ds-button-v3/ds-button-v3.stories.module.scss rename to packages/design-system/src/components/ds-button-v3/ds-button-v3.stories.module.scss index 2752d06cc..a853f039c 100644 --- a/packages/design-system/src/components/ds-button/versions/ds-button-v3/ds-button-v3.stories.module.scss +++ b/packages/design-system/src/components/ds-button-v3/ds-button-v3.stories.module.scss @@ -1,4 +1,4 @@ -@use '../../../../styles/root_updated'; +@use '../../styles/root_updated'; .matrix { display: flex; diff --git a/packages/design-system/src/components/ds-button/versions/ds-button-v3/ds-button-v3.stories.tsx b/packages/design-system/src/components/ds-button-v3/ds-button-v3.stories.tsx similarity index 98% rename from packages/design-system/src/components/ds-button/versions/ds-button-v3/ds-button-v3.stories.tsx rename to packages/design-system/src/components/ds-button-v3/ds-button-v3.stories.tsx index 4d2bd7936..7946e21cc 100644 --- a/packages/design-system/src/components/ds-button/versions/ds-button-v3/ds-button-v3.stories.tsx +++ b/packages/design-system/src/components/ds-button-v3/ds-button-v3.stories.tsx @@ -1,8 +1,8 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; import classNames from 'classnames'; import { fn } from 'storybook/test'; -import DsButtonV3 from './ds-button-v3'; -import { buttonV3Colors, buttonV3Sizes, buttonV3Variants } from './ds-button-v3.types'; +import DsButtonV3 from './ds-button-v3.tsx'; +import { buttonV3Colors, buttonV3Sizes, buttonV3Variants } from './ds-button-v3.types.ts'; import storyStyles from './ds-button-v3.stories.module.scss'; const meta: Meta = { diff --git a/packages/design-system/src/components/ds-button/versions/ds-button-v3/ds-button-v3.tsx b/packages/design-system/src/components/ds-button-v3/ds-button-v3.tsx similarity index 92% rename from packages/design-system/src/components/ds-button/versions/ds-button-v3/ds-button-v3.tsx rename to packages/design-system/src/components/ds-button-v3/ds-button-v3.tsx index 91976b352..e05391200 100644 --- a/packages/design-system/src/components/ds-button/versions/ds-button-v3/ds-button-v3.tsx +++ b/packages/design-system/src/components/ds-button-v3/ds-button-v3.tsx @@ -1,8 +1,8 @@ import classNames from 'classnames'; -import { DsIcon, type IconSize } from '../../../ds-icon'; -import { DsSpinner } from '../../../ds-spinner'; +import { DsIcon, type IconSize } from '../ds-icon'; +import { DsSpinner } from '../ds-spinner'; import styles from './ds-button-v3.module.scss'; -import type { ButtonV3Size, DsButtonV3Props } from './ds-button-v3.types'; +import type { ButtonV3Size, DsButtonV3Props } from './ds-button-v3.types.ts'; const sizeClassMap = Object.freeze({ large: styles.sizeLarge, diff --git a/packages/design-system/src/components/ds-button/versions/ds-button-v3/ds-button-v3.types.ts b/packages/design-system/src/components/ds-button-v3/ds-button-v3.types.ts similarity index 96% rename from packages/design-system/src/components/ds-button/versions/ds-button-v3/ds-button-v3.types.ts rename to packages/design-system/src/components/ds-button-v3/ds-button-v3.types.ts index 05a83c6dc..ad81bda20 100644 --- a/packages/design-system/src/components/ds-button/versions/ds-button-v3/ds-button-v3.types.ts +++ b/packages/design-system/src/components/ds-button-v3/ds-button-v3.types.ts @@ -1,5 +1,5 @@ import type { ButtonHTMLAttributes, Ref } from 'react'; -import type { IconType } from '../../../ds-icon'; +import type { IconType } from '../ds-icon'; export const buttonV3Variants = ['primary', 'secondary', 'tertiary'] as const; export type ButtonV3Variant = (typeof buttonV3Variants)[number]; diff --git a/packages/design-system/src/components/ds-button-v3/index.ts b/packages/design-system/src/components/ds-button-v3/index.ts new file mode 100644 index 000000000..50f2f2bab --- /dev/null +++ b/packages/design-system/src/components/ds-button-v3/index.ts @@ -0,0 +1,2 @@ +export { default as DsButtonV3 } from './ds-button-v3.tsx'; +export * from './ds-button-v3.types.ts'; diff --git a/packages/design-system/src/components/ds-button/index.ts b/packages/design-system/src/components/ds-button/index.ts index 88dd3ec10..26a914360 100644 --- a/packages/design-system/src/components/ds-button/index.ts +++ b/packages/design-system/src/components/ds-button/index.ts @@ -1,3 +1,2 @@ export { default as DsButton } from './ds-button'; export * from './ds-button.types'; -export * from './versions/ds-button-v3'; diff --git a/packages/design-system/src/components/ds-button/versions/ds-button-v3/index.ts b/packages/design-system/src/components/ds-button/versions/ds-button-v3/index.ts deleted file mode 100644 index 76dd2de13..000000000 --- a/packages/design-system/src/components/ds-button/versions/ds-button-v3/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default as DsButtonV3 } from './ds-button-v3'; -export * from './ds-button-v3.types'; diff --git a/packages/design-system/src/index.ts b/packages/design-system/src/index.ts index eff7072e0..b25f51f82 100644 --- a/packages/design-system/src/index.ts +++ b/packages/design-system/src/index.ts @@ -4,7 +4,7 @@ export * from './components/ds-avatar'; export * from './components/ds-avatar-group'; export * from './components/ds-breadcrumb'; export * from './components/ds-button'; -export * from './components/ds-button/versions/ds-button-v3/index.ts'; +export * from './components/ds-button-v3'; export * from './components/ds-card'; export * from './components/ds-checkbox'; export * from './components/ds-chip'; From e450815241e7dac2c5637c14c14cb75900af0a5d Mon Sep 17 00:00:00 2001 From: Michal Murawski Date: Tue, 31 Mar 2026 12:53:44 +0200 Subject: [PATCH 08/10] CR changes --- .changeset/tidy-singers-travel.md | 2 +- .../__tests__/ds-button-v3.browser.test.tsx | 39 +----- .../ds-button-v3/ds-button-v3.module.scss | 6 +- .../ds-button-v3.stories.module.scss | 2 + .../ds-button-v3/ds-button-v3.stories.tsx | 121 +++++++++--------- .../components/ds-button-v3/ds-button-v3.tsx | 5 +- .../ds-button-v3/ds-button-v3.types.ts | 10 +- 7 files changed, 76 insertions(+), 109 deletions(-) diff --git a/.changeset/tidy-singers-travel.md b/.changeset/tidy-singers-travel.md index c3d1d1353..67a88a855 100644 --- a/.changeset/tidy-singers-travel.md +++ b/.changeset/tidy-singers-travel.md @@ -2,4 +2,4 @@ '@drivenets/design-system': minor --- -Add the latest version of the `DsButtonV3` component +Add the `DsButtonV3` component diff --git a/packages/design-system/src/components/ds-button-v3/__tests__/ds-button-v3.browser.test.tsx b/packages/design-system/src/components/ds-button-v3/__tests__/ds-button-v3.browser.test.tsx index 742c2deaf..5d1d7c68a 100644 --- a/packages/design-system/src/components/ds-button-v3/__tests__/ds-button-v3.browser.test.tsx +++ b/packages/design-system/src/components/ds-button-v3/__tests__/ds-button-v3.browser.test.tsx @@ -48,12 +48,12 @@ describe('DsButtonV3', () => { await expect.element(button).toHaveAttribute('data-color', 'negative'); }); - it('sets data-on-dark when onDark prop is true', async () => { - await page.render(Label); + it('sets ondark color palette', async () => { + await page.render(Label); const button = page.getByRole('button', { name: 'Label' }); - await expect.element(button).toHaveAttribute('data-on-dark', 'true'); + await expect.element(button).toHaveAttribute('data-color', 'ondark'); }); it('applies iconOnly layout when icon is set without children', async () => { @@ -135,25 +135,6 @@ describe('DsButtonV3', () => { } }); - it('applies correct size class for each size', async () => { - const sizeClassMap = { - large: styles.sizeLarge, - medium: styles.sizeMedium, - small: styles.sizeSmall, - tiny: styles.sizeTiny, - } as const; - - for (const [size, cls] of Object.entries(sizeClassMap) as [keyof typeof sizeClassMap, string][]) { - await page.render( - - {size !== 'tiny' ? 'Label' : undefined} - , - ); - - await expect.element(page.getByRole('button', { name: size })).toHaveClass(cls); - } - }); - it('applies default props when none are specified', async () => { await page.render(Default); @@ -161,7 +142,6 @@ describe('DsButtonV3', () => { await expect.element(button).toHaveAttribute('data-color', 'default'); await expect.element(button).toHaveAttribute('data-variant', 'primary'); - await expect.element(button).toHaveClass(styles.sizeMedium); }); it('forwards ref to the button element', async () => { @@ -218,19 +198,6 @@ describe('DsButtonV3', () => { await expect.element(button).toHaveAttribute('data-selected', 'true'); }); - it('applies onDark + negative color together', async () => { - await page.render( - - Remove - , - ); - - const button = page.getByRole('button', { name: 'Remove' }); - - await expect.element(button).toHaveAttribute('data-on-dark', 'true'); - await expect.element(button).toHaveAttribute('data-color', 'negative'); - }); - it('spreads rest props onto the button element', async () => { await page.render( diff --git a/packages/design-system/src/components/ds-button-v3/ds-button-v3.module.scss b/packages/design-system/src/components/ds-button-v3/ds-button-v3.module.scss index 762996546..558e89a3a 100644 --- a/packages/design-system/src/components/ds-button-v3/ds-button-v3.module.scss +++ b/packages/design-system/src/components/ds-button-v3/ds-button-v3.module.scss @@ -297,7 +297,7 @@ $transition-duration-quick: 0.15s; } } -.root[data-on-dark][data-variant='primary'] { +.root[data-color='ondark'][data-variant='primary'] { background-color: var(--background-background-ondark-primary); color: var(--font-font-on-action); border-color: transparent; @@ -327,7 +327,7 @@ $transition-duration-quick: 0.15s; } } -.root[data-on-dark][data-variant='secondary'] { +.root[data-color='ondark'][data-variant='secondary'] { background-color: transparent; color: var(--font-font-on-action); border-color: var(--border-border-ondark-secondary); @@ -361,7 +361,7 @@ $transition-duration-quick: 0.15s; } } -.root[data-on-dark][data-variant='tertiary'] { +.root[data-color='ondark'][data-variant='tertiary'] { background-color: transparent; color: var(--font-font-on-action); border-color: transparent; diff --git a/packages/design-system/src/components/ds-button-v3/ds-button-v3.stories.module.scss b/packages/design-system/src/components/ds-button-v3/ds-button-v3.stories.module.scss index a853f039c..9bf55707c 100644 --- a/packages/design-system/src/components/ds-button-v3/ds-button-v3.stories.module.scss +++ b/packages/design-system/src/components/ds-button-v3/ds-button-v3.stories.module.scss @@ -37,6 +37,7 @@ font-weight: var(--font-weight-medium); color: var(--font-font-secondary); text-align: center; + text-transform: capitalize; } .row { @@ -52,6 +53,7 @@ font-size: var(--font-size-sm); font-weight: var(--font-weight-medium); color: var(--font-font-secondary); + text-transform: capitalize; } .cell { diff --git a/packages/design-system/src/components/ds-button-v3/ds-button-v3.stories.tsx b/packages/design-system/src/components/ds-button-v3/ds-button-v3.stories.tsx index 7946e21cc..0b0ab3f87 100644 --- a/packages/design-system/src/components/ds-button-v3/ds-button-v3.stories.tsx +++ b/packages/design-system/src/components/ds-button-v3/ds-button-v3.stories.tsx @@ -2,7 +2,13 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; import classNames from 'classnames'; import { fn } from 'storybook/test'; import DsButtonV3 from './ds-button-v3.tsx'; -import { buttonV3Colors, buttonV3Sizes, buttonV3Variants } from './ds-button-v3.types.ts'; +import { + type ButtonV3Color, + buttonV3Colors, + buttonV3Sizes, + type ButtonV3Variant, + buttonV3Variants, +} from './ds-button-v3.types.ts'; import storyStyles from './ds-button-v3.stories.module.scss'; const meta: Meta = { @@ -15,7 +21,6 @@ const meta: Meta = { color: { control: 'select', options: buttonV3Colors }, variant: { control: 'select', options: buttonV3Variants }, size: { control: 'select', options: buttonV3Sizes }, - onDark: { control: 'boolean' }, loading: { control: 'boolean' }, disabled: { control: 'boolean' }, className: { table: { disable: true } }, @@ -55,71 +60,74 @@ const onDarkIconMatrixRows = [ label: 'arrow down', icon: 'keyboard_arrow_down', variant: 'primary', - color: 'default', + color: 'ondark', loading: false, }, - { label: 'home', icon: 'home', variant: 'secondary', color: 'default', loading: false }, - { label: 'info', icon: 'info', variant: 'tertiary', color: 'default', loading: false }, - { label: 'loading', icon: 'info', variant: 'primary', color: 'default', loading: true }, + { label: 'home', icon: 'home', variant: 'secondary', color: 'ondark', loading: false }, + { label: 'info', icon: 'info', variant: 'tertiary', color: 'ondark', loading: false }, + { label: 'loading', icon: 'info', variant: 'primary', color: 'ondark', loading: true }, ] as const; -const MatrixGrid = ({ onDark = false, color }: { onDark?: boolean; color?: 'default' | 'negative' }) => ( -
-
- {buttonV3Sizes.map((size) => ( - - {size.charAt(0).toUpperCase() + size.slice(1)} - - ))} -
- - {matrixRows.map(({ label, loading }) => ( -
- - {label.charAt(0).toUpperCase() + label.slice(1)} - +const MatrixGrid = ({ color }: { color?: ButtonV3Color }) => { + const isOnDark = color === 'ondark'; + return ( +
+
{buttonV3Sizes.map((size) => ( -
- - {size !== 'tiny' ? 'Button' : undefined} - -
+ + {size} + ))}
- ))} -
-); + + {matrixRows.map(({ label, loading }) => ( +
+ + {label} + + + {buttonV3Sizes.map((size) => ( +
+ + {size !== 'tiny' ? 'Button' : undefined} + +
+ ))} +
+ ))} +
+ ); +}; const IconMatrixGrid = ({ rows, - onDark = false, + isOnDark = false, }: { rows: ReadonlyArray<{ label: string; icon: 'check_circle' | 'info' | 'delete' | 'keyboard_arrow_down' | 'home'; - variant: (typeof buttonV3Variants)[number]; - color: (typeof buttonV3Colors)[number]; + variant: ButtonV3Variant; + color: ButtonV3Color; loading: boolean; }>; - onDark?: boolean; + isOnDark?: boolean; }) => (
@@ -127,10 +135,10 @@ const IconMatrixGrid = ({ - {size.charAt(0).toUpperCase() + size.slice(1)} + {size} ))}
@@ -139,10 +147,10 @@ const IconMatrixGrid = ({
- {label.charAt(0).toUpperCase() + label.slice(1)} + {label} {buttonV3Sizes.map((size) => { @@ -154,7 +162,6 @@ const IconMatrixGrid = ({ color={color} variant={variant} size={size} - onDark={onDark} icon={icon} loading={loading} aria-label={ariaLabel} @@ -196,7 +203,7 @@ export const MatrixOnDark: Story = {

On Dark — Default

- +
), @@ -219,7 +226,7 @@ export const MatrixIcons: Story = { > Icons — On Dark

- +
), diff --git a/packages/design-system/src/components/ds-button-v3/ds-button-v3.tsx b/packages/design-system/src/components/ds-button-v3/ds-button-v3.tsx index e05391200..14877eefe 100644 --- a/packages/design-system/src/components/ds-button-v3/ds-button-v3.tsx +++ b/packages/design-system/src/components/ds-button-v3/ds-button-v3.tsx @@ -27,7 +27,6 @@ const DsButtonV3 = ({ disabled, loading = false, color = 'default', - onDark = false, variant = 'primary', size = 'medium', selected = false, @@ -39,8 +38,7 @@ const DsButtonV3 = ({ return (