@furystack/shades-common-components
Version:
Common UI components for FuryStack Shades
173 lines • 7.37 kB
JavaScript
import { Shade, createComponent } from '@furystack/shades';
import { cssVariableTheme } from '../../services/css-variable-theme.js';
import { ThemeProviderService } from '../../services/theme-provider-service.js';
import { FormContextToken } from '../form.js';
export const Radio = Shade({
customElementName: 'shade-radio',
css: {
display: 'inline-flex',
fontFamily: cssVariableTheme.typography.fontFamily,
alignItems: 'center',
'& label': {
display: 'inline-flex',
alignItems: 'center',
gap: cssVariableTheme.spacing.sm,
cursor: 'pointer',
fontSize: cssVariableTheme.typography.fontSize.sm,
color: cssVariableTheme.text.primary,
userSelect: 'none',
webkitUserSelect: 'none',
},
'& .radio-control': {
position: 'relative',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
width: '20px',
height: '20px',
flexShrink: '0',
},
'& input[type="radio"]': {
appearance: 'none',
webkitAppearance: 'none',
width: '20px',
height: '20px',
margin: '0',
border: `2px solid ${cssVariableTheme.text.secondary}`,
borderRadius: cssVariableTheme.shape.borderRadius.full,
backgroundColor: 'transparent',
cursor: 'pointer',
transition: `all ${cssVariableTheme.transitions.duration.fast} ${cssVariableTheme.transitions.easing.default}`,
outline: 'none',
},
'& input[type="radio"]:hover:not(:disabled)': {
borderColor: 'var(--radio-color)',
},
'& input[type="radio"]:focus-visible': {
outline: 'none',
boxShadow: cssVariableTheme.action.focusRing,
},
'& input[type="radio"]:checked': {
borderColor: 'var(--radio-color)',
},
'& input[type="radio"]:checked::after': {
content: '""',
position: 'absolute',
left: '5px',
top: '5px',
width: '10px',
height: '10px',
borderRadius: cssVariableTheme.shape.borderRadius.full,
backgroundColor: 'var(--radio-color)',
pointerEvents: 'none',
},
'&[data-disabled] label': {
color: cssVariableTheme.text.disabled,
cursor: 'not-allowed',
},
'&[data-disabled] input[type="radio"]': {
opacity: cssVariableTheme.action.disabledOpacity,
cursor: 'not-allowed',
},
// Size: small
'&[data-size="small"] label': {
fontSize: cssVariableTheme.typography.fontSize.xs,
},
'&[data-size="small"] .radio-control': {
width: '16px',
height: '16px',
},
'&[data-size="small"] input[type="radio"]': {
width: '16px',
height: '16px',
},
'&[data-size="small"] input[type="radio"]:checked::after': {
left: '4px',
top: '4px',
width: '8px',
height: '8px',
},
// Size: large
'&[data-size="large"] label': {
fontSize: cssVariableTheme.typography.fontSize.md,
},
'&[data-size="large"] .radio-control': {
width: '24px',
height: '24px',
},
'&[data-size="large"] input[type="radio"]': {
width: '24px',
height: '24px',
},
'&[data-size="large"] input[type="radio"]:checked::after': {
left: '6px',
top: '6px',
width: '12px',
height: '12px',
},
},
render: ({ props, injector, useDisposable, useHostProps, useRef }) => {
const inputRef = useRef('formInput');
useDisposable('form-registration', () => {
const formService = injector.get(FormContextToken);
queueMicrotask(() => {
if (inputRef.current && formService) {
formService.inputs.add(inputRef.current);
}
// Read group-level overrides from parent RadioGroup
const group = inputRef.current?.closest('shade-radio-group');
if (group && inputRef.current) {
const groupName = group.getAttribute('data-group-name');
if (groupName)
inputRef.current.name = groupName;
const groupDisabled = group.hasAttribute('data-disabled');
if (groupDisabled)
inputRef.current.disabled = true;
const groupValue = group.getAttribute('data-group-value');
if (groupValue !== null) {
inputRef.current.checked = props.value === groupValue;
}
else {
const groupDefaultValue = group.getAttribute('data-group-default-value');
if (groupDefaultValue !== null && props.checked === undefined) {
inputRef.current.checked = props.value === groupDefaultValue;
}
}
}
});
return {
[Symbol.dispose]: () => {
if (inputRef.current && formService)
formService.inputs.delete(inputRef.current);
},
};
});
const themeProvider = injector.get(ThemeProviderService);
// Read group-level overrides from parent RadioGroup (inputRef is set after first mount)
const group = inputRef.current ? inputRef.current.closest('shade-radio-group') : null;
const groupName = group?.getAttribute('data-group-name');
const groupDisabled = group?.hasAttribute('data-disabled') ?? false;
const groupValue = group?.getAttribute('data-group-value');
const groupDefaultValue = group?.getAttribute('data-group-default-value');
const effectiveName = groupName ?? props.name;
const isDisabled = props.disabled || groupDisabled;
let isChecked = props.checked;
if (groupValue !== undefined && groupValue !== null) {
isChecked = props.value === groupValue;
}
else if (groupDefaultValue !== undefined && groupDefaultValue !== null && isChecked === undefined) {
isChecked = props.value === groupDefaultValue;
}
const color = themeProvider.theme.palette[props.color || 'primary'].main;
useHostProps({
'data-size': props.size && props.size !== 'medium' ? props.size : undefined,
'data-disabled': isDisabled ? '' : undefined,
style: { '--radio-color': color },
});
return (createComponent("label", { ...props.labelProps },
createComponent("span", { className: "radio-control" },
createComponent("input", { ref: inputRef, type: "radio", value: props.value, checked: isChecked, disabled: isDisabled, name: effectiveName })),
props.labelTitle ? createComponent("span", { className: "radio-label" }, props.labelTitle) : null));
},
});
//# sourceMappingURL=radio.js.map