Skip to content

Commit 6b94080

Browse files
committed
feat(Radio): tabs
1 parent e88acca commit 6b94080

File tree

7 files changed

+495
-194
lines changed

7 files changed

+495
-194
lines changed

.changeset/fresh-olives-march.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@cube-dev/ui-kit": minor
3+
---
4+
5+
Introduces a brand new Radio.Tabs component as a replacement for RadioGroup with isSolid flag.

src/components/HiddenInput.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export const HiddenInput = tasty({
2020
cursor: {
2121
'': 'default',
2222
button: 'pointer',
23+
disabled: 'not-allowed',
2324
},
2425
},
2526
});

src/components/fields/RadioGroup/Radio.tsx

Lines changed: 128 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@ import {
1010
filterBaseProps,
1111
OUTER_STYLES,
1212
OuterStyleProps,
13-
Styles,
1413
tasty,
1514
} from '../../../tasty';
1615
import { mergeProps } from '../../../utils/react';
1716
import { useFocus } from '../../../utils/react/interactions';
17+
import { CubeItemBaseProps, ItemBase } from '../../content/ItemBase/ItemBase';
1818
import { INLINE_LABEL_STYLES, useFieldProps, useFormProps } from '../../form';
1919
import { HiddenInput } from '../../HiddenInput';
2020

@@ -49,64 +49,6 @@ const RadioWrapperElement = tasty({
4949
checked: 1,
5050
},
5151
flexGrow: 1,
52-
53-
Input: {
54-
radius: {
55-
'': 'round',
56-
button: true,
57-
'button & solid': 0,
58-
'button & solid & :first-child': '1r 0 0 1r',
59-
'button & solid & :last-child': '0 1r 1r 0',
60-
},
61-
margin: {
62-
'': 'initial',
63-
'button & solid': '-1bw right',
64-
'button & solid & :last-child': 0,
65-
},
66-
},
67-
},
68-
});
69-
70-
const RadioButtonElement = tasty({
71-
styles: {
72-
display: 'grid',
73-
flow: 'column',
74-
placeItems: 'center',
75-
gap: '.75x',
76-
fill: {
77-
'': '#white',
78-
hovered: '#purple-text.04',
79-
checked: '#white',
80-
disabled: '#light',
81-
},
82-
color: {
83-
'': '#dark.85',
84-
invalid: '#danger-text',
85-
checked: '#purple-text',
86-
disabled: '#dark-04',
87-
'disabled & checked': '#dark-02',
88-
},
89-
preset: 't3m',
90-
border: {
91-
'': '#dark-05',
92-
checked: '#purple',
93-
'invalid & checked': '#danger-text',
94-
disabled: '#dark-05',
95-
'disabled & checked': '#dark-03',
96-
},
97-
padding: '(.75x - 1bw) (1.25x - 1bw)',
98-
cursor: 'pointer',
99-
opacity: {
100-
'': 1,
101-
disabled: 0.5,
102-
},
103-
outline: {
104-
'': '#purple-text.0',
105-
focused: '1bw #purple-text',
106-
},
107-
outlineOffset: 1,
108-
transition: 'theme',
109-
whiteSpace: 'nowrap',
11052
},
11153
});
11254

@@ -161,12 +103,26 @@ const RadioLabelElement = tasty({
161103
export interface CubeRadioProps
162104
extends BaseProps,
163105
AriaRadioProps,
164-
FieldBaseProps,
106+
Omit<FieldBaseProps, 'tooltip'>,
165107
OuterStyleProps {
166-
inputStyles?: Styles;
167108
/* The visual type of the radio button */
168109
type?: 'button' | 'radio';
110+
buttonType?: Exclude<CubeItemBaseProps['type'], 'secondary'>;
169111
value?: string;
112+
/* Whether the radio is invalid */
113+
isInvalid?: boolean;
114+
/* Size of the button (for button type only) */
115+
size?: CubeItemBaseProps['size'];
116+
/* Icon to display (for button type only) */
117+
icon?: CubeItemBaseProps['icon'];
118+
/* Icon to display on the right (for button type only) */
119+
rightIcon?: CubeItemBaseProps['rightIcon'];
120+
/* Description text (for button type only) */
121+
description?: CubeItemBaseProps['description'];
122+
/* Tooltip configuration (for button type only) */
123+
tooltip?: CubeItemBaseProps['tooltip'];
124+
/* Keyboard shortcut (for button type only) */
125+
hotkeys?: CubeItemBaseProps['hotkeys'];
170126
}
171127

172128
function Radio(props: CubeRadioProps, ref) {
@@ -177,27 +133,29 @@ function Radio(props: CubeRadioProps, ref) {
177133
let {
178134
qa,
179135
isDisabled,
180-
validationState,
136+
isInvalid,
181137
children,
182138
label,
183139
autoFocus,
184140
labelStyles,
185141
labelProps,
186-
inputStyles,
187-
type = 'radio',
142+
type,
143+
buttonType,
144+
size,
145+
icon,
146+
rightIcon,
147+
description,
148+
tooltip,
149+
hotkeys,
188150
'aria-label': ariaLabel,
189151
form,
190152
...otherProps
191153
} = props;
192154

193-
let isButton = type === 'button';
194-
195155
label = label || children;
196156

197157
let styles = extractStyles(otherProps, OUTER_STYLES);
198158

199-
const RadioElement = isButton ? RadioButtonElement : RadioNormalElement;
200-
201159
labelStyles = {
202160
...INLINE_LABEL_STYLES,
203161
...labelStyles,
@@ -207,14 +165,58 @@ function Radio(props: CubeRadioProps, ref) {
207165

208166
let state = radioGroupProps && radioGroupProps.state;
209167
let name = radioGroupProps && radioGroupProps.name;
210-
let isSolid = (radioGroupProps && radioGroupProps.isSolid) || false;
168+
let contextSize = radioGroupProps?.size;
169+
let contextButtonType = radioGroupProps?.buttonType;
170+
let contextType = radioGroupProps?.type;
171+
let contextIsDisabled = radioGroupProps?.isDisabled;
211172

212173
if (!state) {
213174
throw new Error('CubeUI: The Radio button is used outside the RadioGroup.');
214175
}
215176

216-
let { isFocused, focusProps } = useFocus({ isDisabled }, true);
217-
let { hoverProps, isHovered } = useHover({ isDisabled });
177+
// Determine effective type from props or context
178+
let effectiveType = type ?? contextType ?? 'radio';
179+
let isButton = effectiveType === 'button' || effectiveType === 'tabs';
180+
181+
// Determine effective size with priority: prop > context > default
182+
let effectiveSize = size ?? contextSize ?? 'medium';
183+
184+
// Apply size mapping for tabs mode button radios
185+
if (effectiveType === 'tabs' && isButton) {
186+
if (effectiveSize === 'small' || effectiveSize === 'medium') {
187+
effectiveSize = 'xsmall';
188+
} else if (effectiveSize === 'large') {
189+
effectiveSize = 'medium';
190+
} else if (effectiveSize === 'xlarge') {
191+
effectiveSize = 'large';
192+
}
193+
// 'xsmall' stays 'xsmall', 'inline' stays 'inline'
194+
}
195+
196+
// Determine effective button type
197+
// In tabs mode, always use 'neutral' and ignore buttonType prop
198+
let effectiveButtonType;
199+
if (effectiveType === 'tabs') {
200+
effectiveButtonType = 'neutral'; // Force neutral for tabs, ignore buttonType
201+
} else {
202+
const baseButtonType = buttonType ?? contextButtonType ?? 'outline';
203+
// When buttonType is 'primary', use 'secondary' for non-selected and 'primary' for selected
204+
if (baseButtonType === 'primary') {
205+
effectiveButtonType =
206+
state.selectedValue === props.value ? 'primary' : 'secondary';
207+
} else {
208+
effectiveButtonType = baseButtonType;
209+
}
210+
}
211+
212+
// Use context isDisabled if prop isDisabled is not explicitly set
213+
let effectiveIsDisabled = isDisabled ?? contextIsDisabled ?? false;
214+
215+
let { isFocused, focusProps } = useFocus(
216+
{ isDisabled: effectiveIsDisabled },
217+
true,
218+
);
219+
let { hoverProps, isHovered } = useHover({ isDisabled: effectiveIsDisabled });
218220

219221
let inputRef = useRef(null);
220222
let domRef = useFocusableRef(ref, inputRef);
@@ -227,7 +229,7 @@ function Radio(props: CubeRadioProps, ref) {
227229
{
228230
name,
229231
...props,
230-
isDisabled,
232+
isDisabled: effectiveIsDisabled,
231233
},
232234
state,
233235
inputRef,
@@ -236,25 +238,66 @@ function Radio(props: CubeRadioProps, ref) {
236238
const mods = useMemo(
237239
() => ({
238240
checked: isRadioSelected,
239-
invalid: validationState === 'invalid',
240-
valid: validationState === 'valid',
241+
invalid: !!isInvalid,
241242
disabled: isRadioDisabled,
242243
hovered: isHovered,
243244
button: isButton,
244245
focused: isFocused,
245-
solid: isSolid,
246+
tabs: effectiveType === 'tabs',
246247
}),
247248
[
248249
isRadioSelected,
249-
validationState,
250+
isInvalid,
250251
isRadioDisabled,
251252
isHovered,
252253
isButton,
253254
isFocused,
254-
isSolid,
255+
effectiveType,
255256
],
256257
);
257258

259+
// Render button type using ItemBase
260+
if (isButton) {
261+
return (
262+
<ItemBase
263+
ref={domRef}
264+
as="label"
265+
qa={qa || 'Radio'}
266+
type={effectiveButtonType}
267+
theme={isInvalid ? 'danger' : 'default'}
268+
size={effectiveSize}
269+
icon={icon}
270+
rightIcon={rightIcon}
271+
description={description}
272+
tooltip={tooltip}
273+
hotkeys={hotkeys}
274+
isSelected={isRadioSelected}
275+
isDisabled={isRadioDisabled}
276+
mods={mods}
277+
preset="t3m"
278+
styles={{
279+
preset: 't3m',
280+
lineHeight: '1fs',
281+
...(isRadioSelected && effectiveType === 'tabs'
282+
? { fill: '#white', shadow: '0 0 .5x #shadow' }
283+
: {}),
284+
...styles,
285+
}}
286+
{...mergeProps(hoverProps, focusProps)}
287+
>
288+
<HiddenInput
289+
data-qa={qa || 'Radio'}
290+
aria-label={ariaLabel}
291+
{...inputProps}
292+
ref={inputRef}
293+
mods={{ button: isButton, disabled: isRadioDisabled }}
294+
/>
295+
{label}
296+
</ItemBase>
297+
);
298+
}
299+
300+
// Render classic radio type
258301
return (
259302
<RadioWrapperElement
260303
styles={styles}
@@ -270,15 +313,10 @@ function Radio(props: CubeRadioProps, ref) {
270313
ref={inputRef}
271314
mods={{ button: isButton }}
272315
/>
273-
<RadioElement
274-
data-element="Input"
275-
mods={mods}
276-
data-type={type}
277-
styles={inputStyles}
278-
>
279-
{!isButton ? RadioCircleElement : children}
280-
</RadioElement>
281-
{label && !isButton && (
316+
<RadioNormalElement data-element="Input" mods={mods} data-type={type}>
317+
{RadioCircleElement}
318+
</RadioNormalElement>
319+
{label && (
282320
<RadioLabelElement
283321
mods={mods}
284322
styles={labelStyles}
@@ -308,20 +346,20 @@ const _Radio = forwardRef(Radio);
308346
*/
309347
const _RadioButton = forwardRef(RadioButton);
310348

311-
const ButtonGroup = tasty(RadioGroup, {
312-
isSolid: true,
349+
const Tabs = tasty(RadioGroup, {
350+
type: 'tabs',
313351
});
314352

315353
const __Radio = Object.assign(
316354
_Radio as typeof _Radio & {
317355
Group: typeof RadioGroup;
318356
Button: typeof _RadioButton;
319-
ButtonGroup: typeof ButtonGroup;
357+
Tabs: typeof Tabs;
320358
},
321359
{
322360
Group: RadioGroup,
323361
Button: _RadioButton,
324-
ButtonGroup,
362+
Tabs,
325363
},
326364
);
327365

0 commit comments

Comments
 (0)