@nex-ui/react
Version:
🎉 A beautiful, modern, and reliable React component library.
205 lines (202 loc) • 6.22 kB
JavaScript
import { jsx } from 'react/jsx-runtime';
import { useState, useMemo } from 'react';
import { useFocusRing, useEvent } from '@nex-ui/hooks';
import { isFunction } from '@nex-ui/utils';
import { defineRecipe } from '@nex-ui/system';
import { useSlot } from '../utils/useSlot.mjs';
const getRole = (type)=>{
switch(type){
case 'checkbox':
return 'checkbox';
case 'radio':
return 'radio';
case 'text':
return 'textbox';
default:
return undefined;
}
};
const useAriaProps = (props)=>{
const { disabled, invalid, checked, required, readOnly, role, type, as, tabIndex, 'aria-disabled': ariaDisabled, 'aria-checked': ariaChecked, 'aria-required': ariaRequired, 'aria-readonly': ariaReadOnly } = props;
return useMemo(()=>{
if (isFunction(as)) {
return {};
}
let ariaProps = {
tabIndex: disabled ? -1 : tabIndex,
'aria-invalid': invalid || undefined
};
if (as === 'input') {
ariaProps = {
...ariaProps,
role
};
} else {
ariaProps = {
...ariaProps,
role: role ?? getRole(type),
'aria-disabled': ariaDisabled ?? disabled,
'aria-checked': ariaChecked ?? checked,
'aria-required': ariaRequired ?? required,
'aria-readonly': ariaReadOnly ?? readOnly
};
}
return ariaProps;
}, [
as,
disabled,
tabIndex,
invalid,
type,
checked,
required,
readOnly,
role,
ariaDisabled,
ariaChecked,
ariaRequired,
ariaReadOnly
]);
};
const isCheckableControl = (element)=>{
const role = element.getAttribute('role');
const type = element.getAttribute('type');
const { tagName } = element;
if (role) {
return [
'switch',
'radio',
'checkbox'
].includes(role);
}
if (tagName === 'INPUT' && type) {
return [
'radio',
'checkbox'
].includes(type);
}
};
/**
* InputBase is a lower-level construct that is leveraged by the following components:
*
* - Switch
* - Input
* - Checkbox
* - Radio
*/ const recipe = defineRecipe({
base: {
m: '0',
p: '0',
border: 'none',
background: 'none',
outline: 'none',
appearance: 'none',
WebkitAppearance: 'none',
MozAppearance: 'none',
borderRadius: '0',
boxShadow: 'none',
boxSizing: 'border-box',
'&[type="search"]': {
'::-webkit-search-decoration': {
WebkitAppearance: 'none'
},
'::-webkit-search-cancel-button': {
WebkitAppearance: 'none'
}
},
'&:is(:-webkit-autofill, :autofill)': {
bg: 'transparent !important',
transition: 'background-color 50000s ease-in-out 0s'
}
}
});
const style = recipe();
const InputBase = (props)=>{
const { defaultChecked, disabled, autoFocus, onCheckedChange, onClick, onKeyUp, onChange, checked: checkProp, type = 'text', as = 'input', tabIndex = 0, ...remainingProps } = props;
const controlled = 'checked' in props;
const [internalChecked, setInternalChecked] = useState(defaultChecked ?? false);
const currentChecked = controlled ? checkProp : internalChecked;
const { focusVisible, focusProps } = useFocusRing({
autoFocus,
input: as === 'input'
});
const toggleCheckableState = useEvent((element)=>{
const role = element.getAttribute('role');
const ariaChecked = element.getAttribute('aria-checked');
const newChecked = role === 'radio' ? true : !(ariaChecked === 'true');
onCheckedChange?.(newChecked);
if (!controlled) {
setInternalChecked(newChecked);
}
});
const handleClick = useEvent((event)=>{
if (disabled) {
event.preventDefault();
event.stopPropagation();
return;
}
const { currentTarget } = event;
if (currentTarget.tagName !== 'INPUT' && currentTarget === event.target && isCheckableControl(currentTarget)) {
toggleCheckableState(currentTarget);
}
onClick?.(event);
});
const handleKeyUp = useEvent((event)=>{
if (disabled) {
event.preventDefault();
event.stopPropagation();
return;
}
const { currentTarget } = event;
// Keyboard accessibility for non interactive elements
if (focusVisible && event.key === ' ' && currentTarget.tagName !== 'INPUT' && currentTarget === event.target && isCheckableControl(currentTarget)) {
toggleCheckableState(currentTarget);
}
onKeyUp?.(event);
});
const handleChange = useEvent((event)=>{
if (disabled) {
event.preventDefault();
event.stopPropagation();
return;
}
if (event.currentTarget.tagName === 'INPUT' && isCheckableControl(event.currentTarget)) {
onCheckedChange?.(event.currentTarget.checked);
}
onChange?.(event);
});
const ariaProps = useAriaProps({
...props,
as,
type,
tabIndex,
checked: currentChecked
});
const [InputRoot, getInputRootProps] = useSlot({
style,
elementType: 'input',
a11y: {
...ariaProps,
onKeyUp: handleKeyUp,
onClick: handleClick,
onChange: handleChange
},
externalForwardedProps: remainingProps,
additionalProps: {
as,
type,
disabled,
autoFocus,
checked: currentChecked,
...focusProps
},
dataAttrs: {
focusVisible: focusVisible || undefined
}
});
return /*#__PURE__*/ jsx(InputRoot, {
...getInputRootProps()
});
};
InputBase.displayName = 'InputBase';
export { InputBase };