@nex-ui/react
Version:
🎉 A beautiful, modern, and reliable React component library.
205 lines (202 loc) • 6.67 kB
JavaScript
"use client";
import { jsxs, jsx } from 'react/jsx-runtime';
import { useRef, useId, useMemo } from 'react';
import { useControlledState, useEvent } from '@nex-ui/hooks';
import { isString } from '@nex-ui/utils';
import { RadioGroupProvider } from './RadioGroupContext.mjs';
import { useDefaultProps } from '../utils/useDefaultProps.mjs';
import { useStyles } from '../utils/useStyles.mjs';
import { useSlotClasses } from '../utils/useSlotClasses.mjs';
import { useSlot } from '../utils/useSlot.mjs';
import { radioGroupRecipe } from '../../theme/recipes/radioGroup.mjs';
const slots = [
'root',
'label',
'wrapper'
];
const useSlotAriaProps = (ownerState)=>{
const id = useId();
const { slotProps, label, role = 'radiogroup', 'aria-labelledby': labelledBy } = ownerState;
const stringLabel = isString(label);
const labelId = slotProps?.label?.id || (stringLabel ? id : undefined);
return useMemo(()=>({
root: {
role,
'aria-labelledby': labelledBy ?? labelId
},
label: {
id: labelId
}
}), [
role,
labelledBy,
labelId
]);
};
const RadioGroup = (inProps)=>{
const props = useDefaultProps({
name: 'RadioGroup',
props: inProps
});
const rootRef = useRef(null);
// Generate a unique name for the radio group to ensure that Tab moves to the correct position.
const defaultName = useId();
// The DOM API casts value to a string, so need to record the original value.
const radioGroupStateRef = useRef([]);
radioGroupStateRef.current = [];
const { color, disabled, defaultValue, onValueChange, children, label, slotProps, role, size, classNames, value: valueProp, name = defaultName, orientation = 'horizontal', ...remainingProps } = props;
const [value, setValue] = useControlledState(valueProp, defaultValue, onValueChange);
const ownerState = {
...props,
role,
orientation,
value,
name
};
const styles = useStyles({
name: 'RadioGroup',
recipe: radioGroupRecipe,
ownerState
});
const slotClasses = useSlotClasses({
name: 'RadioGroup',
slots,
classNames
});
const slotAriaProps = useSlotAriaProps(ownerState);
const handleKeyDown = useEvent((e)=>{
switch(e.key){
case 'ArrowUp':
case 'ArrowLeft':
case 'ArrowDown':
case 'ArrowRight':
e.stopPropagation();
e.preventDefault();
break;
}
});
const handleKeyUp = useEvent((e)=>{
let nextDirection = null;
switch(e.key){
case 'ArrowUp':
case 'ArrowLeft':
nextDirection = 'prev';
break;
case 'ArrowDown':
case 'ArrowRight':
nextDirection = 'next';
break;
}
if (!nextDirection) return;
e.preventDefault();
e.stopPropagation();
const rootElement = rootRef.current;
// istanbul ignore if
if (!rootElement) return;
const radioNodes = Array.from(rootElement.querySelectorAll("input[type='radio'], [role='radio']"));
let nextIndex = radioNodes.findIndex((node)=>node === e.target);
let nextElement = null;
while(!nextElement){
if (nextDirection === 'next') {
nextIndex = (nextIndex + 1) % radioNodes.length;
} else {
nextIndex = (nextIndex - 1 + radioNodes.length) % radioNodes.length;
}
const element = radioNodes[nextIndex];
if (element === e.target) {
return;
}
const currentRadioState = radioGroupStateRef.current[nextIndex];
if (currentRadioState && !currentRadioState.disabled) {
nextElement = element;
}
}
const { value } = radioGroupStateRef.current[nextIndex];
if (value !== undefined) {
nextElement.focus();
setValue(value);
}
});
const [RadioGroupRoot, getRadioGroupRootProps] = useSlot({
elementType: 'div',
classNames: slotClasses.root,
externalForwardedProps: remainingProps,
style: styles.root,
additionalProps: {
ref: rootRef,
onKeyUp: handleKeyUp,
onKeyDown: handleKeyDown
},
a11y: slotAriaProps.root,
dataAttrs: {
orientation
}
});
const [RadioGroupLabel, getRadioGroupLabelProps] = useSlot({
elementType: 'h3',
style: styles.label,
classNames: slotClasses.label,
externalSlotProps: slotProps?.label,
a11y: slotAriaProps.label
});
const [RadioGroupWrapper, getRadioGroupWrapperProps] = useSlot({
elementType: 'div',
style: styles.wrapper,
classNames: slotClasses.wrapper,
externalSlotProps: slotProps?.wrapper
});
const ctx = useMemo(()=>{
const isChecked = (v)=>{
if (v === undefined) {
return false;
}
return v === value;
};
const isTabbable = (v)=>{
if (value === undefined) {
const first = radioGroupStateRef.current.find((state)=>state.value !== undefined && !state.disabled);
return v === first?.value;
}
return isChecked(v);
};
const setGroupState = (state)=>{
if (radioGroupStateRef.current.some((s)=>s === state)) return;
radioGroupStateRef.current.push(state);
};
return {
size,
color,
disabled,
name,
setValue,
isChecked,
isTabbable,
setGroupState
};
}, [
setValue,
size,
color,
disabled,
name,
value
]);
return /*#__PURE__*/ jsxs(RadioGroupRoot, {
...getRadioGroupRootProps(),
children: [
label ? /*#__PURE__*/ jsx(RadioGroupLabel, {
...getRadioGroupLabelProps(),
children: label
}) : null,
/*#__PURE__*/ jsx(RadioGroupProvider, {
value: ctx,
children: /*#__PURE__*/ jsx(RadioGroupWrapper, {
...getRadioGroupWrapperProps(),
children: children
})
})
]
});
};
RadioGroup.displayName = 'RadioGroup';
export { RadioGroup };