@reusable-ui/radio
Version:
A UI for the user to select single option.
193 lines (192 loc) • 7.88 kB
JavaScript
// react:
import {
// react:
default as React,
// hooks:
useRef, useEffect, useState, } from 'react';
// cssfn:
import {
// checks if a certain css feature is supported by the running browser:
supportsHasPseudoClass, } from '@cssfn/core'; // writes css in javascript
import {
// style sheets:
dynamicStyleSheet, } from '@cssfn/cssfn-react'; // writes css in react hook
// reusable-ui core:
import {
// react helper hooks:
useEvent, useMergeEvents, useMergeRefs, useUncontrollableActivatable, } from '@reusable-ui/core'; // a set of reusable-ui packages which are responsible for building any component
// reusable-ui components:
import { Check, } from '@reusable-ui/check'; // a base component
// internals:
import { broadcastClearEvent, } from './utilities.js'; // local utilities
// styles:
export const useRadioStyleSheet = dynamicStyleSheet(() => import(/* webpackPrefetch: true */ './styles/styles.js'), {
id: 'f4fvh7cm5b', // a unique salt for SSR support, ensures the server-side & client-side have the same generated class names
lazyCsr: supportsHasPseudoClass(), // dealing with browsers that don't support the :has() selector
});
const Radio = (props) => {
// props:
const {
// refs:
elmRef,
// values:
defaultChecked: fallbackDefaultActive, // take, to be aliased to `defaultActive`
checked: fallbackActive, // take, to be aliased to `active`
// states:
defaultActive = fallbackDefaultActive, // take, to be handled by `useUncontrollableActivatable` // if the `defaultActive` is not provided, fallback to `defaultChecked`
active = fallbackActive, // take, to be handled by `useUncontrollableActivatable` // if the `active` is not provided, fallback to `checked`
inheritActive, // take, to be handled by `useUncontrollableActivatable`
onActiveChange, // take, to be handled by `useUncontrollableActivatable`
// handlers:
onClick, onKeyUp, onChange,
// other props:
...restRadioProps } = props;
// styles:
const styles = useRadioStyleSheet();
// refs:
const inputRefInternal = useRef(null);
const mergedInputRef = useMergeRefs(
// preserves the original `elmRef` from `props`:
elmRef, inputRefInternal);
// states:
const [isActive, setActive] = useUncontrollableActivatable({
enabled: props.enabled,
inheritEnabled: props.inheritEnabled,
readOnly: props.readOnly,
inheritReadOnly: props.inheritReadOnly,
defaultActive,
active,
inheritActive,
onActiveChange,
}, /*changeEventTarget :*/ inputRefInternal);
const [activeBuddyRadio, setActiveBuddyRadio] = useState(null);
const setActiveRadio = useEvent((activeOrOtherRadio) => {
if (activeOrOtherRadio === true) {
setActive(true);
setActiveBuddyRadio(null);
}
else {
setActive(false);
setActiveBuddyRadio(activeOrOtherRadio);
} // if
});
const collectiveRadioIsActive = (isActive ? true : !!activeBuddyRadio?.parentElement /* the buddyRadio still exists in DOM */);
// handlers:
const handleClickInternal = useEvent((event) => {
// conditions:
if (event.defaultPrevented)
return; // the event was already handled by user => nothing to do
// actions:
setActiveRadio(true); // handle click as selecting [active]
event.preventDefault(); // handled
});
const handleClick = useMergeEvents(
// preserves the original `onClick` from `props`:
onClick,
// actions:
handleClickInternal);
const handleKeyUpInternal = useEvent((event) => {
// conditions:
if (event.defaultPrevented)
return; // the event was already handled by user => nothing to do
// actions:
if ((event.key === ' ') || (event.code === 'Space')) {
setActiveRadio(true); // handle click as selecting [active]
event.preventDefault(); // handled
} // if
});
const handleKeyUp = useMergeEvents(
// preserves the original `onKeyUp` from `props`:
onKeyUp,
// actions:
handleKeyUpInternal);
const handleChangeInternal = useEvent((event) => {
// conditions:
if (event.defaultPrevented)
return; // the event was already handled by user => nothing to do
const currentRadioElm = event.target;
const name = currentRadioElm.name;
if (!name)
return; // the <Radio> must have a name
const isChecked = currentRadioElm.checked;
if (!isChecked)
return; // the <Radio> is checked not cleared
// actions:
broadcastClearEvent(currentRadioElm); // notify other budy <Radio>s that this <Radio> is selected
event.preventDefault(); // handled
});
const handleChange = useMergeEvents(
// preserves the original `onChange` from `props`:
onChange,
// actions:
handleChangeInternal);
// effects:
// listens to the `clear` event to un-select this <Radio> when another <Radio> is selected or no longer selected:
useEffect(() => {
// conditions:
const currentRadioElm = inputRefInternal.current;
if (!currentRadioElm)
return; // radio was unmounted => nothing to do
// handlers:
const handleClear = (event) => {
setActiveRadio(event.detail.selected); // handle clear as de-selecting [active]
};
// setups:
currentRadioElm.addEventListener('clear', handleClear); // when another <Radio> is selected, this <Radio> should be un-selected
// cleanups:
return () => {
currentRadioElm.removeEventListener('clear', handleClear);
};
}, []); // runs once on startup
// when the <Radio> is unmounted and it's currently selected, it should notify other budy <Radio>s to update their `validityState`:
const isActiveRef = useRef(isActive);
isActiveRef.current = isActive; // sync
useEffect(() => {
// conditions:
const currentRadioElm = inputRefInternal.current; // take a snapshot of the current radio element before unmounting later
if (!currentRadioElm)
return; // radio was unmounted => nothing to do
// setups:
// no need setup, just cleanup
// cleanups:
return () => {
// conditions:
if (!isActiveRef.current)
return; // get the most recent of the `isActive` state before unmounting, if it's not selected now => nothing to do
// actions:
broadcastClearEvent(currentRadioElm, null); // notify other budy <Radio>s that this <Radio> is unselected because it was unmounted
};
}, []); // runs once on startup
// default props:
const {
// semantics:
tag = 'span', semanticTag = '', // no corresponding semantic tag => defaults to <div> (overwritten to <span>)
semanticRole = 'radio', // uses [role="radio"] as the default semantic role
// classes:
mainClass = styles.main,
// values:
notifyValueChange = collectiveRadioIsActive,
// formats:
type = 'radio',
// other props:
...restCheckProps } = restRadioProps;
// jsx:
return (React.createElement(Check, { ...restCheckProps,
// refs:
elmRef: mergedInputRef,
// semantics:
tag: tag, semanticTag: semanticTag, semanticRole: semanticRole,
// classes:
mainClass: mainClass,
// values:
notifyValueChange: notifyValueChange,
// formats:
type: type,
// states:
active: isActive,
// handlers:
onClick: handleClick, onKeyUp: handleKeyUp, onChange: handleChange }));
};
export { Radio, // named export for readibility
Radio as default, // default export to support React.lazy
};