UNPKG

4.77 kBPlain TextView Raw
1import * as React from "react";
2import { createComponent } from "reakit-system/createComponent";
3import { createHook } from "reakit-system/createHook";
4import { useLiveRef } from "reakit-utils/useLiveRef";
5import { useForkRef } from "reakit-utils/useForkRef";
6import { createEvent } from "reakit-utils/createEvent";
7import { warning } from "reakit-warning/warning";
8import {
9 CompositeItemOptions,
10 CompositeItemHTMLProps,
11 useCompositeItem,
12} from "../Composite/CompositeItem";
13import { RadioStateReturn } from "./RadioState";
14import { RADIO_KEYS } from "./__keys";
15
16export type RadioOptions = CompositeItemOptions &
17 Pick<Partial<RadioStateReturn>, "state" | "setState"> & {
18 /**
19 * Same as the `value` attribute.
20 */
21 value: string | number;
22 /**
23 * Same as the `checked` attribute.
24 */
25 checked?: boolean;
26 /**
27 * @private
28 */
29 unstable_checkOnFocus?: boolean;
30 };
31
32export type RadioHTMLProps = CompositeItemHTMLProps &
33 React.InputHTMLAttributes<any>;
34
35export type RadioProps = RadioOptions & RadioHTMLProps;
36
37function getChecked(options: RadioOptions) {
38 if (typeof options.checked !== "undefined") {
39 return options.checked;
40 }
41 return (
42 typeof options.value !== "undefined" && options.state === options.value
43 );
44}
45
46function useInitialChecked(options: RadioOptions) {
47 const [initialChecked] = React.useState(() => getChecked(options));
48 const [initialCurrentId] = React.useState(options.currentId);
49 const { id, setCurrentId } = options;
50
51 React.useEffect(() => {
52 if (initialChecked && id && initialCurrentId !== id) {
53 setCurrentId?.(id);
54 }
55 }, [initialChecked, id, setCurrentId, initialCurrentId]);
56}
57
58function fireChange(element: HTMLElement, onChange?: React.ChangeEventHandler) {
59 const event = createEvent(element, "change");
60 Object.defineProperties(event, {
61 type: { value: "change" },
62 target: { value: element },
63 currentTarget: { value: element },
64 });
65 onChange?.(event as any);
66}
67
68export const useRadio = createHook<RadioOptions, RadioHTMLProps>({
69 name: "Radio",
70 compose: useCompositeItem,
71 keys: RADIO_KEYS,
72
73 useOptions(
74 { unstable_clickOnEnter = false, unstable_checkOnFocus = true, ...options },
75 { value, checked }
76 ) {
77 return {
78 checked,
79 unstable_clickOnEnter,
80 unstable_checkOnFocus,
81 ...options,
82 value: options.value ?? value,
83 };
84 },
85
86 useProps(
87 options,
88 { ref: htmlRef, onChange: htmlOnChange, onClick: htmlOnClick, ...htmlProps }
89 ) {
90 const ref = React.useRef<HTMLInputElement>(null);
91 const [isNativeRadio, setIsNativeRadio] = React.useState(true);
92 const checked = getChecked(options);
93 const isCurrentItemRef = useLiveRef(options.currentId === options.id);
94 const onChangeRef = useLiveRef(htmlOnChange);
95 const onClickRef = useLiveRef(htmlOnClick);
96
97 useInitialChecked(options);
98
99 React.useEffect(() => {
100 const element = ref.current;
101 if (!element) {
102 warning(
103 true,
104 "Can't determine whether the element is a native radio because `ref` wasn't passed to the component",
105 "See https://reakit.io/docs/radio"
106 );
107 return;
108 }
109 if (element.tagName !== "INPUT" || element.type !== "radio") {
110 setIsNativeRadio(false);
111 }
112 }, []);
113
114 const onChange = React.useCallback(
115 (event: React.ChangeEvent) => {
116 onChangeRef.current?.(event);
117 if (event.defaultPrevented) return;
118 if (options.disabled) return;
119 options.setState?.(options.value);
120 },
121 [options.disabled, options.setState, options.value]
122 );
123
124 const onClick = React.useCallback(
125 (event: React.MouseEvent<HTMLInputElement, MouseEvent>) => {
126 onClickRef.current?.(event);
127 if (event.defaultPrevented) return;
128 if (isNativeRadio) return;
129 fireChange(event.currentTarget, onChange);
130 },
131 [onChange, isNativeRadio]
132 );
133
134 React.useEffect(() => {
135 const element = ref.current;
136 if (!element) return;
137 if (
138 options.unstable_moves &&
139 isCurrentItemRef.current &&
140 options.unstable_checkOnFocus
141 ) {
142 fireChange(element, onChange);
143 }
144 }, [options.unstable_moves, options.unstable_checkOnFocus, onChange]);
145
146 return {
147 ref: useForkRef(ref, htmlRef),
148 role: !isNativeRadio ? "radio" : undefined,
149 type: isNativeRadio ? "radio" : undefined,
150 value: isNativeRadio ? options.value : undefined,
151 name: isNativeRadio ? options.baseId : undefined,
152 "aria-checked": checked,
153 checked,
154 onChange,
155 onClick,
156 ...htmlProps,
157 };
158 },
159});
160
161export const Radio = createComponent({
162 as: "input",
163 memo: true,
164 useHook: useRadio,
165});