Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
4dc93c6
chore(SplitButton): 🤖 extend stories with types, disabled states and …
punkbit Apr 14, 2026
8fc9e1f
test(SplitButton): 💍 generated baseline snapshots for visual regression
punkbit Apr 14, 2026
8aaf5f5
test(SplitButton): 💍 fix snapshot tests to capture entire component w…
punkbit Apr 14, 2026
c791422
test(SplitButton): 💍 regenerate all 12 snapshots capturing full compo…
punkbit Apr 14, 2026
7a1b6e9
test(SplitButton): 💍 add aria-disabled assertions to all disabled tes…
punkbit Apr 14, 2026
c39c77d
test(SplitButton): 💍 add aria-disabled to PrimaryButton and accessibi…
punkbit Apr 14, 2026
2267a0a
fix(SplitButton): ♿ add aria-disabled to SecondaryButton per CONVENTI…
punkbit Apr 14, 2026
fa5f07c
test(SplitButton): 💍 assert aria-disabled on dropdown trigger in disa…
punkbit Apr 14, 2026
1bb14c2
test(SplitButton): 💍 add hover and focus tests for secondary variant …
punkbit Apr 14, 2026
91a6898
fix(SplitButton): 🐛 add aria-disabled to SecondaryButton per CONVENTI…
punkbit Apr 14, 2026
2c523f2
test(SplitButton): 💍 fix fragile keyboard navigation test to use dire…
punkbit Apr 14, 2026
a734c23
test(SplitButton): 💍 add aria-disabled assertions on dropdown trigger…
punkbit Apr 14, 2026
47421a1
chore: 🤖 add changeset for SplitButton CSS module migration
punkbit Apr 14, 2026
8263743
style: 💄 add CSS module for SplitButton with BEM naming
punkbit Apr 14, 2026
45b44cd
refactor: 💡 migrate SplitButton from styled-components to CSS modules
punkbit Apr 14, 2026
e524d85
fix: 🐛 increase specificity for SplitButton border-radius reset
punkbit Apr 14, 2026
bdc19e8
fix: 🐛 increase specificity for all SplitButton variant classes to re…
punkbit Apr 14, 2026
9b72595
style: 💄 fix CSS property order and selector specificity lint errors
punkbit Apr 14, 2026
9a2dd13
refactor(SplitButton): 💡 remove styled-components, use inline style f…
punkbit Apr 14, 2026
e30953a
style(SplitButton): 💄 change :focus to :focus-visible per project con…
punkbit Apr 14, 2026
ee032ea
fix(SplitButton): 🐛 add aria-disabled to secondary button per convent…
punkbit Apr 14, 2026
ff06190
style(SplitButton): 💄 update to proper BEM element naming with __
punkbit Apr 14, 2026
4dc5648
fix(SplitButton): 🐛 fix CSS class names to match JS references with p…
punkbit Apr 14, 2026
298604d
fix(SplitButton): 🐛 add data-testid to wrapper div for testability
punkbit Apr 14, 2026
c34a509
refactor(SplitButton): 💡 remove unused button-data div and CSS class
punkbit Apr 14, 2026
88ece39
fix(SplitButton): 🐛 add aria-disabled to BaseButton to fix failing tests
punkbit Apr 14, 2026
abcdc64
fix(SplitButton): 🐛 forward className to root element instead of Base…
punkbit Apr 14, 2026
99ea3d3
fix(SplitButton): 🐛 align disabled selectors to use [data-disabled] c…
punkbit Apr 14, 2026
84c9b02
test(SplitButton): 💍 add FillWidth story for visual regression coverage
punkbit Apr 14, 2026
a38c3d0
fix(SplitButton): 🐛 increase CSS specificity to override BaseButton b…
punkbit Apr 14, 2026
a9c1826
fix(SplitButton): 🐛 add explicit outline on focus-visible states per …
punkbit Apr 14, 2026
5a506d0
fix(SplitButton): 🐛 move data-testid to inner button for aria-disable…
punkbit Apr 14, 2026
e63836a
fix(SplitButton): 🐛 add role=group and aria-label to wrapper div for …
punkbit Apr 14, 2026
df479f5
refactor(SplitButton): 💡 remove redundant cn() call from secondary bu…
punkbit Apr 14, 2026
c49fa43
docs(SplitButton): 📝 add comment explaining specificity bump for Base…
punkbit Apr 14, 2026
3bb3a50
chore: 🤖 add .storybook to tsconfig include to fix Vite docgen warning
punkbit Apr 14, 2026
6b527c7
fix(SplitButton): 🐛 make group aria-label dynamic based on children text
punkbit Apr 14, 2026
e0af869
fix(SplitButton): 🐛 add explicit disabled prop to inner dropdown button
punkbit Apr 14, 2026
cc9fb4a
fix(SplitButton): 🐛 replace overflow:hidden with clip-path to preserv…
punkbit Apr 14, 2026
d1787c7
chore: 🤖 add tsconfigPath to Storybook react-docgen-typescript config
punkbit Apr 14, 2026
a3ef5db
style: 💄 revert clip-path back to overflow:hidden
punkbit Apr 14, 2026
b2a669c
style: 💄 fix CSS property order lint errors
punkbit Apr 14, 2026
6ef0b7c
refactor(SplitButton): 💡 wrap component with React.forwardRef for pro…
punkbit Apr 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .changeset/migrate-splitbutton-css-modules.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
'@clickhouse/click-ui': patch
---

🔄 **SplitButton Component Migration to CSS Modules**

The `SplitButton` component has been migrated from Styled Components to CSS Modules (Web Standards + BEM class name convention).

**Changes:**
- Migrated from `styled-components` to CSS Modules (`SplitButton.module.css`)
- Using BEM naming convention (`.split-button`, `.primary-button`, `.secondary-button`, etc.)
- Maintained full API compatibility - no breaking changes
- Both `primary` and `secondary` variants preserved
- Interactive states (hover, focus, disabled) maintained with CSS pseudo-classes

**Visual Testing:**
This migration is protected by visual regression tests covering all button types and states in both light and dark themes.
29 changes: 16 additions & 13 deletions .storybook/main.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,27 @@
import type { StorybookConfig } from "@storybook/react-vite";
import type { StorybookConfig } from '@storybook/react-vite';
const config: StorybookConfig = {
core: {
disableTelemetry: true
disableTelemetry: true,
},
stories: [
"./Introduction.mdx",
"../src/**/*.stories.@(ts|tsx)",
],
stories: ['./Introduction.mdx', '../src/**/*.stories.@(ts|tsx)'],

addons: ["@storybook/addon-links", //"@storybook/addon-interactions",
"storybook-addon-pseudo-states", "@storybook/addon-a11y", "@storybook/addon-docs"],
addons: [
'@storybook/addon-links', //"@storybook/addon-interactions",
'storybook-addon-pseudo-states',
'@storybook/addon-a11y',
'@storybook/addon-docs',
],

framework: {
name: "@storybook/react-vite",
name: '@storybook/react-vite',
options: {},
},

staticDirs: ["../public"],
staticDirs: ['../public'],
typescript: {
reactDocgen: "react-docgen-typescript",
reactDocgen: 'react-docgen-typescript',
reactDocgenTypescriptOptions: {
tsconfigPath: '../tsconfig.json',
compilerOptions: {
allowSyntheticDefaultImports: false,
esModuleInterop: false,
Expand All @@ -32,8 +34,9 @@ const config: StorybookConfig = {
},

async viteFinal(config, { configType }) {
config.plugins = (config.plugins || []).filter((plugin) => {
const pluginName = plugin && typeof plugin === 'object' && 'name' in plugin ? plugin.name : null;
config.plugins = (config.plugins || []).filter(plugin => {
const pluginName =
plugin && typeof plugin === 'object' && 'name' in plugin ? plugin.name : null;
return pluginName !== 'css-external';
});
config.plugins.push({
Expand Down
174 changes: 174 additions & 0 deletions src/components/SplitButton/SplitButton.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
.split-button {
display: inline-flex;
overflow: hidden;
align-items: center;
border: 1px solid transparent;
border-radius: var(--click-button-radii-all);
user-select: none;
}

.split-button_fill-width {
width: 100%;
}

.split-button_primary {
border-color: var(--click-button-split-primary-stroke-default);
}

.split-button_primary:hover:not([data-disabled]) {
border-color: var(--click-button-split-primary-stroke-hover);
}

.split-button_primary:focus-within:not([data-disabled]) {
border-color: var(--click-button-split-primary-stroke-active);
}

.split-button_primary[data-disabled] {
border-color: var(--click-button-split-primary-stroke-disabled);
cursor: not-allowed;
}

.split-button_secondary {
border-color: var(--click-button-split-secondary-stroke-default);
}

.split-button_secondary:hover:not([data-disabled]) {
border-color: var(--click-button-split-secondary-stroke-hover);
}

.split-button_secondary:focus-within:not([data-disabled]) {
border-color: var(--click-button-split-secondary-stroke-active);
}

.split-button_secondary[data-disabled] {
border-color: var(--click-button-split-secondary-stroke-disabled);
cursor: not-allowed;
}

/* Override BaseButton's styled-component border-radius until BaseButton is migrated to CSS modules */
.split-button .split-button__primary-button {
display: flex;
padding: var(--click-button-split-space-y) var(--click-button-split-space-x);
justify-content: center;
align-items: center;
align-self: stretch;
border: none;
border-radius: 0;
background: transparent;
color: inherit;
font: var(--click-button-split-typography-label-default);
cursor: pointer;
}

.split-button .split-button__primary-button_fill-width {
width: 100%;
}

.split-button .split-button__primary-button_primary {
background: var(--click-button-split-primary-background-main-default);
color: var(--click-button-split-primary-text-default);
}

.split-button .split-button__primary-button_primary[data-disabled] {
background: var(--click-button-split-primary-background-main-disabled);
color: var(--click-button-split-primary-text-disabled);
font: var(--click-button-split-typography-label-disabled);
cursor: not-allowed;
}

.split-button .split-button__primary-button_primary:hover:not([data-disabled]) {
background: var(--click-button-split-primary-background-main-hover);
color: var(--click-button-split-primary-text-hover);
font: var(--click-button-split-typography-label-hover);
}

.split-button .split-button__primary-button_primary:focus-visible:not([data-disabled]) {
background: var(--click-button-split-primary-background-main-active);
color: var(--click-button-split-primary-text-active);
font: var(--click-button-split-typography-label-active);
outline: 2px solid var(--click-button-split-primary-stroke-active);
outline-offset: 2px;
}

.split-button .split-button__primary-button_secondary {
background: var(--click-button-split-secondary-background-main-default);
color: var(--click-button-split-secondary-text-default);
}

.split-button .split-button__primary-button_secondary[data-disabled] {
background: var(--click-button-split-secondary-background-main-disabled);
color: var(--click-button-split-secondary-text-disabled);
font: var(--click-button-split-typography-label-disabled);
cursor: not-allowed;
}

.split-button .split-button__primary-button_secondary:hover:not([data-disabled]) {
background: var(--click-button-split-secondary-background-main-hover);
color: var(--click-button-split-secondary-text-hover);
font: var(--click-button-split-typography-label-hover);
}

.split-button .split-button__primary-button_secondary:focus-visible:not([data-disabled]) {
background: var(--click-button-split-secondary-background-main-active);
color: var(--click-button-split-secondary-text-active);
font: var(--click-button-split-typography-label-active);
outline: 2px solid var(--click-button-split-secondary-stroke-active);
outline-offset: 2px;
}

.split-button .split-button__secondary-button {
display: flex;
padding: var(--click-button-split-icon-space-y) var(--click-button-split-icon-space-x);
align-self: stretch;
border: none;
border-radius: 0;
background: transparent;
color: inherit;
cursor: pointer;
}

.split-button .split-button__secondary-button_primary {
background: var(--click-button-split-primary-background-action-default);
color: var(--click-button-split-primary-text-default);
}

.split-button .split-button__secondary-button_primary:hover:not([data-disabled]) {
background: var(--click-button-split-primary-background-action-hover);
color: var(--click-button-split-primary-text-hover);
}

.split-button .split-button__secondary-button_primary:focus-visible:not([data-disabled]) {
background: var(--click-button-split-primary-background-action-active);
color: var(--click-button-split-primary-text-active);
outline: 2px solid var(--click-button-split-primary-stroke-active);
outline-offset: 2px;
}

.split-button .split-button__secondary-button_primary[data-disabled] {
background: var(--click-button-split-primary-background-action-disabled);
color: var(--click-button-split-primary-text-disabled);
cursor: not-allowed;
}

.split-button .split-button__secondary-button_secondary {
background: var(--click-button-split-secondary-background-action-default);
color: var(--click-button-split-secondary-text-default);
}

.split-button .split-button__secondary-button_secondary:hover:not([data-disabled]) {
background: var(--click-button-split-secondary-background-action-hover);
color: var(--click-button-split-secondary-text-hover);
}

.split-button .split-button__secondary-button_secondary:focus-visible:not([data-disabled]) {
background: var(--click-button-split-secondary-background-action-active);
color: var(--click-button-split-secondary-text-active);
outline: 2px solid var(--click-button-split-secondary-stroke-active);
outline-offset: 2px;
}

.split-button .split-button__secondary-button_secondary[data-disabled] {
background: var(--click-button-split-secondary-background-action-disabled);
color: var(--click-button-split-secondary-text-disabled);
cursor: not-allowed;
}
59 changes: 59 additions & 0 deletions src/components/SplitButton/SplitButton.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,62 @@ export const Playground: Story = {
menu: menuItems,
},
};

// Button Types
export const Primary: Story = {
args: {
type: 'primary',
children: 'Primary Split Button',
menu: menuItems,
},
};

export const Secondary: Story = {
args: {
type: 'secondary',
children: 'Secondary Split Button',
menu: menuItems,
},
};

// Disabled States
export const PrimaryDisabled: Story = {
args: {
type: 'primary',
children: 'Disabled Primary',
menu: menuItems,
disabled: true,
},
};

export const SecondaryDisabled: Story = {
args: {
type: 'secondary',
children: 'Disabled Secondary',
menu: menuItems,
disabled: true,
},
};

// Interactive
export const Interactive: Story = {
args: {
type: 'primary',
children: 'Interactive Split Button',
menu: menuItems,
onClick: () => console.log('clicked'),
},
};

// Layout Variants
export const FillWidth: Story = {
args: {
type: 'primary',
children: 'Full Width Split Button',
menu: menuItems,
fillWidth: true,
},
parameters: {
layout: 'padded',
},
};
Loading
Loading