diff --git a/.changeset/clever-gifts-crash.md b/.changeset/clever-gifts-crash.md new file mode 100644 index 000000000..5fc8722ac --- /dev/null +++ b/.changeset/clever-gifts-crash.md @@ -0,0 +1,5 @@ +--- +"@cube-dev/ui-kit": minor +--- + +New Switch sizes: `small` -> `medium` (and now default). new `small` size. diff --git a/src/components/fields/Switch/Switch.stories.tsx b/src/components/fields/Switch/Switch.stories.tsx index bee4e5d92..c282698d5 100644 --- a/src/components/fields/Switch/Switch.stories.tsx +++ b/src/components/fields/Switch/Switch.stories.tsx @@ -82,7 +82,7 @@ export default { /* Presentation */ size: { - options: ['small', 'large'], + options: ['small', 'medium', 'large'], control: { type: 'radio' }, description: 'Switch size', table: { @@ -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 = (props) => ( - console.log('change', isSelected)} - /> -); +const Template: StoryFn = (props) => ; export const Default = Template.bind({}); Default.args = { @@ -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', @@ -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 = (props) => ( +
+ + Small switch + + + Medium switch + + + Large switch + +
+); + +export const Sizes = SizesTemplate.bind({}); +Sizes.args = {}; + // Stories showing both selected and unselected states for visual testing const MultiStateTemplate: StoryFn = (props) => (
- console.log('unselected change', isSelected)} - > + {props.children} (unselected) - console.log('selected change', isSelected)} - > + {props.children} (selected)
diff --git a/src/components/fields/Switch/Switch.tsx b/src/components/fields/Switch/Switch.tsx index a5001c597..17803a2a3 100644 --- a/src/components/fields/Switch/Switch.tsx +++ b/src/components/fields/Switch/Switch.tsx @@ -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'; @@ -52,7 +53,8 @@ const SwitchElement = tasty({ fill: { '': '#white', checked: '#purple', - disabled: '#border', + disabled: '#border.5', + 'disabled & checked': '#border', }, color: { '': '#dark-03', @@ -60,16 +62,19 @@ const SwitchElement = tasty({ }, 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', @@ -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', @@ -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, ref) { @@ -148,10 +156,13 @@ function Switch(props: WithNullableSelected, 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); @@ -162,16 +173,7 @@ function Switch(props: WithNullableSelected, 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, @@ -189,12 +191,14 @@ function Switch(props: WithNullableSelected, ref) { qa={qa || 'SwitchWrapper'} mods={mods} data-size={size} + styles={styles} {...hoverProps} > , ref) { return wrapWithField(switchField, domRef, { ...props, + id, + labelProps: { + ...props.labelProps, + for: id, + }, children: null, labelStyles, inputStyles, - styles, + styles: fieldStyles, }); }