UNPKG

@reusable-ui/radio

Version:

A UI for the user to select single option.

193 lines (192 loc) 7.88 kB
// 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 };