Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions .changeset/clever-gifts-crash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cube-dev/ui-kit": minor
---

New Switch sizes: `small` -> `medium` (and now default). new `small` size.
53 changes: 27 additions & 26 deletions src/components/fields/Switch/Switch.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export default {

/* Presentation */
size: {
options: ['small', 'large'],
options: ['small', 'medium', 'large'],
control: { type: 'radio' },
description: 'Switch size',
table: {
Expand All @@ -92,29 +92,21 @@ export default {

/* Events */
onChange: {
action: 'change',
description: 'Callback fired when the switch value changes',
control: { type: null },
},
onFocus: {
action: 'focus',
description: 'Callback fired when the switch receives focus',
control: { type: null },
},
onBlur: {
action: 'blur',
description: 'Callback fired when the switch loses focus',
control: { type: null },
},
},
};

const Template: StoryFn<CubeSwitchProps> = (props) => (
<Switch
{...props}
onChange={(isSelected) => console.log('change', isSelected)}
/>
);
const Template: StoryFn<CubeSwitchProps> = (props) => <Switch {...props} />;

export const Default = Template.bind({});
Default.args = {
Expand All @@ -127,12 +119,6 @@ WithDefaultSelected.args = {
defaultSelected: true,
};

export const Small = Template.bind({});
Small.args = {
children: 'Small switch',
size: 'small',
};

export const Invalid = Template.bind({});
Invalid.args = {
children: 'Required switch',
Expand All @@ -152,21 +138,36 @@ Loading.args = {
isLoading: true,
};

export const WithLabel = Template.bind({});
WithLabel.args = {
label: 'Toggle feature',
};

// Stories showing all sizes for visual comparison
const SizesTemplate: StoryFn<CubeSwitchProps> = (props) => (
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
<Switch {...props} size="small">
Small switch
</Switch>
<Switch {...props} size="medium">
Medium switch
</Switch>
<Switch {...props} size="large">
Large switch
</Switch>
</div>
);

export const Sizes = SizesTemplate.bind({});
Sizes.args = {};

// Stories showing both selected and unselected states for visual testing
const MultiStateTemplate: StoryFn<CubeSwitchProps> = (props) => (
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
<Switch
{...props}
isSelected={false}
onChange={(isSelected) => console.log('unselected change', isSelected)}
>
<Switch {...props} isSelected={false}>
{props.children} (unselected)
</Switch>
<Switch
{...props}
isSelected={true}
onChange={(isSelected) => console.log('selected change', isSelected)}
>
<Switch {...props} isSelected={true}>
{props.children} (selected)
</Switch>
</div>
Expand Down
69 changes: 39 additions & 30 deletions src/components/fields/Switch/Switch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
castNullableIsSelected,
WithNullableSelected,
} from '../../../utils/react/nullableValue';
import { useId } from '../../../utils/react/useId';
import { Text } from '../../content/Text';
import { useFieldProps, useFormProps, wrapWithField } from '../../form';
import { HiddenInput } from '../../HiddenInput';
Expand Down Expand Up @@ -52,24 +53,28 @@ const SwitchElement = tasty({
fill: {
'': '#white',
checked: '#purple',
disabled: '#border',
disabled: '#border.5',
'disabled & checked': '#border',
},
color: {
'': '#dark-03',
checked: '#white',
},
border: {
'': '#dark-05',
checked: '#purple',
disabled: '#dark-05',
checked: '#purple-text',
disabled: '#dark-05.5',
invalid: '#danger',
},
width: {
'': '5.25x 5.25x',
'[data-size="small"]': '4x 4x',
'': '5x 5x',
'[data-size="medium"]': '4x 4x',
'[data-size="small"]': '3x 3x',
},
height: {
'': '3x 3x',
'[data-size="small"]': '2.5x 2.5x',
'[data-size="medium"]': '2.5x 2.5x',
'[data-size="small"]': '2x 2x',
},
outline: {
'': '#purple-text.0',
Expand All @@ -78,35 +83,37 @@ const SwitchElement = tasty({
outlineOffset: 1,
transition: 'theme',
cursor: 'pointer',
shadow: {
'': '0 0 0 0 #clear',
invalid: '0 0 0 1bw #white, 0 0 0 1ow #danger',
},

Thumb: {
position: 'absolute',
width: {
'': '2x 2x',
'[data-size="small"]': '1.5x 1.5x',
'[data-size="medium"]': '1.5x 1.5x',
'[data-size="small"]': '1.25x 1.25x',
},
height: {
'': '2x 2x',
'[data-size="small"]': '1.5x 1.5x',
'[data-size="medium"]': '1.5x 1.5x',
'[data-size="small"]': '1.25x 1.25x',
},
radius: 'round',
fill: {
'': 'currentColor',
disabled: '#white.7',
disabled: '#current.5',
'disabled & checked': '#white.8',
},
top: {
'': '.375x',
'[data-size="small"]': '.375x',
'[data-size="medium"]': '.375x',
'[data-size="small"]': '.25x',
},
left: {
'': '.375x',
'[data-size="small"]': '.375x',
checked: '2.5x',
'checked & [data-size="small"]': '1.75x',
'[data-size="medium"]': '.375x',
'[data-size="small"]': '.25x',
checked: '2.25x',
'checked & [data-size="medium"]': '1.75x',
'checked & [data-size="small"]': '1.25x',
},
transition: 'left, theme',
cursor: 'pointer',
Expand All @@ -121,8 +128,9 @@ export interface CubeSwitchProps
FieldBaseProps,
AriaSwitchProps {
inputStyles?: Styles;
fieldStyles?: Styles;
isLoading?: boolean;
size?: 'large' | 'small';
size?: 'large' | 'medium' | 'small';
}

function Switch(props: WithNullableSelected<CubeSwitchProps>, ref) {
Expand All @@ -148,10 +156,13 @@ function Switch(props: WithNullableSelected<CubeSwitchProps>, ref) {
isLoading,
labelPosition,
inputStyles,
fieldStyles,
validationState,
size = 'large',
size = 'medium',
} = props;

const id = useId(props.id);

let styles = extractStyles(props, OUTER_STYLES);

inputStyles = extractStyles(props, BLOCK_STYLES, inputStyles);
Expand All @@ -162,16 +173,7 @@ function Switch(props: WithNullableSelected<CubeSwitchProps>, ref) {
let inputRef = useRef(null);
let domRef = useFocusableRef(ref, inputRef);

let { inputProps } = useSwitch(
{
...props,
...(typeof label === 'string' && label.trim()
? { 'aria-label': label }
: {}),
},
useToggleState(props),
inputRef,
);
let { inputProps } = useSwitch(props, useToggleState(props), inputRef);

const mods = {
checked: inputProps.checked,
Expand All @@ -189,12 +191,14 @@ function Switch(props: WithNullableSelected<CubeSwitchProps>, ref) {
qa={qa || 'SwitchWrapper'}
mods={mods}
data-size={size}
styles={styles}
{...hoverProps}
>
<HiddenInput
data-qa="HiddenInput"
{...mergeProps(inputProps, focusProps)}
ref={inputRef}
id={id}
/>
<SwitchElement
qa="Switch"
Expand All @@ -216,10 +220,15 @@ function Switch(props: WithNullableSelected<CubeSwitchProps>, ref) {

return wrapWithField(switchField, domRef, {
...props,
id,
labelProps: {
...props.labelProps,
for: id,
},
children: null,
labelStyles,
inputStyles,
styles,
styles: fieldStyles,
});
}

Expand Down
Loading