From 51977d04466430fd518094309b46aba01ed042fe Mon Sep 17 00:00:00 2001 From: Ihor Romanchuk Date: Fri, 3 Apr 2026 20:42:46 +0200 Subject: [PATCH 1/2] feat(design-system): add `DsSplitButton` component [AR-54024] --- .changeset/real-maps-strive.md | 5 + .../ds-split-button.module.scss | 107 +++++++++++++++++ .../ds-split-button.stories.module.scss | 5 + .../ds-split-button.stories.tsx | 110 ++++++++++++++++++ .../ds-split-button/ds-split-button.tsx | 59 ++++++++++ .../ds-split-button/ds-split-button.types.ts | 22 ++++ .../ds-split-button/ds-split-button.utils.ts | 6 + .../src/components/ds-split-button/index.ts | 2 + packages/design-system/src/index.ts | 1 + 9 files changed, 317 insertions(+) create mode 100644 .changeset/real-maps-strive.md create mode 100644 packages/design-system/src/components/ds-split-button/ds-split-button.module.scss create mode 100644 packages/design-system/src/components/ds-split-button/ds-split-button.stories.module.scss create mode 100644 packages/design-system/src/components/ds-split-button/ds-split-button.stories.tsx create mode 100644 packages/design-system/src/components/ds-split-button/ds-split-button.tsx create mode 100644 packages/design-system/src/components/ds-split-button/ds-split-button.types.ts create mode 100644 packages/design-system/src/components/ds-split-button/ds-split-button.utils.ts create mode 100644 packages/design-system/src/components/ds-split-button/index.ts diff --git a/.changeset/real-maps-strive.md b/.changeset/real-maps-strive.md new file mode 100644 index 000000000..e43c98d4b --- /dev/null +++ b/.changeset/real-maps-strive.md @@ -0,0 +1,5 @@ +--- +'@drivenets/design-system': minor +--- + +Add `DsSplitButton` component diff --git a/packages/design-system/src/components/ds-split-button/ds-split-button.module.scss b/packages/design-system/src/components/ds-split-button/ds-split-button.module.scss new file mode 100644 index 000000000..a9cf86af7 --- /dev/null +++ b/packages/design-system/src/components/ds-split-button/ds-split-button.module.scss @@ -0,0 +1,107 @@ +$highlighted-z-index: 1; +$divider-z-index: $highlighted-z-index + 1; +$divider-width: 1px; +$border-width: 1px; + +@mixin when-button-disabled { + .root:has(.actionButton:disabled) & { + @content; + } +} + +@mixin when-select-disabled { + .root:has(.select[data-disabled]) & { + @content; + } +} + +@mixin when-button-highlighted { + .root:has(.actionButton:not(:disabled):is(:hover, :focus-visible, :active)) & { + @content; + } +} + +@mixin when-select-highlighted { + .root:has( + .select:not([data-disabled]):is(:hover, :active, [data-state='open']), + .select:not([data-disabled]) :focus-visible + ) + & { + @content; + } +} + +.root { + display: inline-flex; +} + +.actionButton { + border: none !important; // TODO: remove once DsButtonV3 is used +} + +.root .actionButton:disabled .actionContent { + border-color: var(--color-border-disabled); // TODO: remove once DsButtonV3 is used +} + +.actionButton { + .actionContent { + height: 36px !important; // TODO: remove once DsButtonV3 is used + width: 36px !important; // TODO: remove once DsButtonV3 is used + border-top-right-radius: 0; + border-bottom-right-radius: 0; + + @include when-button-highlighted { + z-index: $highlighted-z-index; + } + } +} + +.select { + margin-left: -$divider-width; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + + // TODO: fix these styles inside the DsSelect component + @include when-select-disabled { + background-color: var(--background-background); + + & * { + color: var(--color-font-disabled) !important; + } + } +} + +.dividerAnchor { + position: relative; +} + +.dividerWrapper { + position: absolute; + top: $border-width; + bottom: $border-width; + left: -$divider-width; + z-index: $divider-z-index; + width: $divider-width; + padding: var(--spacing-3xs) 0; + background-color: var(--background-background); + + @include when-button-highlighted { + display: none; + } + @include when-select-highlighted { + display: none; + } +} + +.divider { + background-color: var(--color-border-secondary); + width: $divider-width; + height: 100%; + + @include when-button-disabled { + background-color: var(--color-border-disabled); + } + @include when-select-disabled { + background-color: var(--color-border-disabled); + } +} diff --git a/packages/design-system/src/components/ds-split-button/ds-split-button.stories.module.scss b/packages/design-system/src/components/ds-split-button/ds-split-button.stories.module.scss new file mode 100644 index 000000000..fbbbef45d --- /dev/null +++ b/packages/design-system/src/components/ds-split-button/ds-split-button.stories.module.scss @@ -0,0 +1,5 @@ +@use '../../styles/root_updated'; + +.spinner { + color: var(--background-background-primary); +} diff --git a/packages/design-system/src/components/ds-split-button/ds-split-button.stories.tsx b/packages/design-system/src/components/ds-split-button/ds-split-button.stories.tsx new file mode 100644 index 000000000..534533494 --- /dev/null +++ b/packages/design-system/src/components/ds-split-button/ds-split-button.stories.tsx @@ -0,0 +1,110 @@ +import { useState } from 'react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { fn } from 'storybook/test'; +import DsSplitButton from './ds-split-button'; +import { splitButtonSizes } from './ds-split-button.types'; +import { DsIcon } from '../ds-icon'; +import { DsSpinner } from '../ds-spinner'; +import styles from './ds-split-button.stories.module.scss'; +import type { DsSelectProps } from '../ds-select'; + +const refreshOptions = [ + { label: '30s', value: '30' }, + { label: '1m', value: '60' }, + { label: '5m', value: '300' }, + { label: '10m', value: '600' }, +]; + +const meta: Meta = { + title: 'Design System/SplitButton', + component: DsSplitButton, + parameters: { + layout: 'centered', + }, + args: { + size: 'medium', + disabled: false, + slotProps: { + button: { + children: , + 'aria-label': 'Refresh now', + }, + select: { + options: refreshOptions, + value: '30', + onValueChange: fn(), + multiple: false, + }, + }, + }, + argTypes: { + size: { control: 'radio', options: splitButtonSizes }, + className: { table: { disable: true } }, + style: { table: { disable: true } }, + ref: { table: { disable: true } }, + slotProps: { table: { disable: true } }, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + render: (args) => { + const [value, setValue] = useState('30'); + const [loading, setLoading] = useState(false); + + const handleAction = () => { + setLoading(true); + setTimeout(() => setLoading(false), 2000); + }; + + return ( + + ) : ( + + ), + onClick: handleAction, + disabled: loading, + 'aria-label': loading ? 'Refreshing' : 'Refresh now', + }, + select: { + ...args.slotProps.select, + value, + onValueChange: setValue, + } as DsSelectProps, + }} + /> + ); + }, +}; + +export const Loading: Story = { + args: { + slotProps: { + button: { + children: , + disabled: true, + 'aria-label': 'Refreshing', + }, + select: { + options: refreshOptions, + value: '30', + onValueChange: fn(), + }, + }, + }, +}; + +export const Disabled: Story = { + args: { + disabled: true, + }, +}; diff --git a/packages/design-system/src/components/ds-split-button/ds-split-button.tsx b/packages/design-system/src/components/ds-split-button/ds-split-button.tsx new file mode 100644 index 000000000..3065e6d71 --- /dev/null +++ b/packages/design-system/src/components/ds-split-button/ds-split-button.tsx @@ -0,0 +1,59 @@ +import classNames from 'classnames'; +import { DsButton } from '../ds-button'; +import { DsSelect } from '../ds-select'; +import styles from './ds-split-button.module.scss'; +import type { DsSplitButtonProps } from './ds-split-button.types'; +import { getSelectSize } from './ds-split-button.utils'; + +const DsSplitButton = ({ + ref, + className, + style, + size = 'medium', + disabled, + slotProps, +}: DsSplitButtonProps) => { + const { + className: buttonClassName, + contentClassName, + size: buttonSize, + disabled: buttonDisabled, + ...buttonRest + } = slotProps.button; + + const { + className: selectClassName, + size: selectSize, + disabled: selectDisabled, + ...selectRest + } = slotProps.select; + + return ( +
+ + +
+
+
+
+
+ + +
+ ); +}; + +export default DsSplitButton; diff --git a/packages/design-system/src/components/ds-split-button/ds-split-button.types.ts b/packages/design-system/src/components/ds-split-button/ds-split-button.types.ts new file mode 100644 index 000000000..9ed526348 --- /dev/null +++ b/packages/design-system/src/components/ds-split-button/ds-split-button.types.ts @@ -0,0 +1,22 @@ +import type { CSSProperties, Ref } from 'react'; +import type { DsButtonUnifiedProps } from '../ds-button'; +import type { DsSelectProps } from '../ds-select'; + +type ButtonV12Props = Extract; + +export const splitButtonSizes = ['medium', 'small'] as const; +export type SplitButtonSize = (typeof splitButtonSizes)[number]; + +export interface DsSplitButtonSlotProps { + button: Partial; + select: DsSelectProps; +} + +export interface DsSplitButtonProps { + ref?: Ref; + className?: string; + style?: CSSProperties; + size?: SplitButtonSize; + disabled?: boolean; + slotProps: DsSplitButtonSlotProps; +} diff --git a/packages/design-system/src/components/ds-split-button/ds-split-button.utils.ts b/packages/design-system/src/components/ds-split-button/ds-split-button.utils.ts new file mode 100644 index 000000000..53d9c9baa --- /dev/null +++ b/packages/design-system/src/components/ds-split-button/ds-split-button.utils.ts @@ -0,0 +1,6 @@ +import type { SelectSize } from '../ds-select'; +import type { SplitButtonSize } from './ds-split-button.types'; + +export const getSelectSize = (size: SplitButtonSize): SelectSize => { + return size === 'medium' ? 'default' : 'small'; +}; diff --git a/packages/design-system/src/components/ds-split-button/index.ts b/packages/design-system/src/components/ds-split-button/index.ts new file mode 100644 index 000000000..a4c0fd2af --- /dev/null +++ b/packages/design-system/src/components/ds-split-button/index.ts @@ -0,0 +1,2 @@ +export { default as DsSplitButton } from './ds-split-button'; +export type { DsSplitButtonProps, DsSplitButtonSlotProps } from './ds-split-button.types'; diff --git a/packages/design-system/src/index.ts b/packages/design-system/src/index.ts index ac8f389ec..d5aac1d31 100644 --- a/packages/design-system/src/index.ts +++ b/packages/design-system/src/index.ts @@ -42,6 +42,7 @@ export * from './components/ds-select'; export * from './components/ds-skeleton'; export * from './components/ds-smart-tabs'; export * from './components/ds-spinner'; +export * from './components/ds-split-button'; export * from './components/ds-status-badge'; export * from './components/ds-stepper'; export * from './components/ds-system-status'; From 6f82b6cd834ee2db6c21b44430e50e24b71ece27 Mon Sep 17 00:00:00 2001 From: Ihor Romanchuk Date: Fri, 3 Apr 2026 21:15:00 +0200 Subject: [PATCH 2/2] Update inline comment --- .../src/components/ds-split-button/ds-split-button.module.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/design-system/src/components/ds-split-button/ds-split-button.module.scss b/packages/design-system/src/components/ds-split-button/ds-split-button.module.scss index a9cf86af7..ec1da65a2 100644 --- a/packages/design-system/src/components/ds-split-button/ds-split-button.module.scss +++ b/packages/design-system/src/components/ds-split-button/ds-split-button.module.scss @@ -61,7 +61,7 @@ $border-width: 1px; border-top-left-radius: 0; border-bottom-left-radius: 0; - // TODO: fix these styles inside the DsSelect component + // TODO: remove once PR is merged https://github.com/drivenets/design-system/pull/346 @include when-select-disabled { background-color: var(--background-background);