1 | import * as React from "react";
|
2 | import { createComponent } from "reakit-system/createComponent";
|
3 | import { createHook } from "reakit-system/createHook";
|
4 | import { useLiveRef } from "reakit-utils/useLiveRef";
|
5 | import { useForkRef } from "reakit-utils/useForkRef";
|
6 | import { createEvent } from "reakit-utils/createEvent";
|
7 | import { warning } from "reakit-warning/warning";
|
8 | import {
|
9 | CompositeItemOptions,
|
10 | CompositeItemHTMLProps,
|
11 | useCompositeItem,
|
12 | } from "../Composite/CompositeItem";
|
13 | import { RadioStateReturn } from "./RadioState";
|
14 | import { RADIO_KEYS } from "./__keys";
|
15 |
|
16 | export type RadioOptions = CompositeItemOptions &
|
17 | Pick<Partial<RadioStateReturn>, "state" | "setState"> & {
|
18 | |
19 |
|
20 |
|
21 | value: string | number;
|
22 | |
23 |
|
24 |
|
25 | checked?: boolean;
|
26 | |
27 |
|
28 |
|
29 | unstable_checkOnFocus?: boolean;
|
30 | };
|
31 |
|
32 | export type RadioHTMLProps = CompositeItemHTMLProps &
|
33 | React.InputHTMLAttributes<any>;
|
34 |
|
35 | export type RadioProps = RadioOptions & RadioHTMLProps;
|
36 |
|
37 | function 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 |
|
46 | function 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 |
|
58 | function 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 |
|
68 | export 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 |
|
161 | export const Radio = createComponent({
|
162 | as: "input",
|
163 | memo: true,
|
164 | useHook: useRadio,
|
165 | });
|