1 | import * as React from "react";
|
2 | import { createComponent } from "reakit-system/createComponent";
|
3 | import { removeIndexFromArray } from "reakit-utils/removeIndexFromArray";
|
4 | import { createHook } from "reakit-system/createHook";
|
5 | import { useForkRef } from "reakit-utils/useForkRef";
|
6 | import { createEvent } from "reakit-utils/createEvent";
|
7 | import { warning } from "reakit-warning";
|
8 | import { useLiveRef } from "reakit-utils/useLiveRef";
|
9 | import {
|
10 | ClickableOptions,
|
11 | ClickableHTMLProps,
|
12 | useClickable,
|
13 | } from "../Clickable/Clickable";
|
14 | import { CheckboxStateReturn } from "./CheckboxState";
|
15 | import { CHECKBOX_KEYS } from "./__keys";
|
16 |
|
17 | export type CheckboxOptions = ClickableOptions &
|
18 | Pick<Partial<CheckboxStateReturn>, "state" | "setState"> & {
|
19 | |
20 |
|
21 |
|
22 |
|
23 |
|
24 | value?: string | number;
|
25 | |
26 |
|
27 |
|
28 | checked?: boolean;
|
29 | };
|
30 |
|
31 | export type CheckboxHTMLProps = ClickableHTMLProps &
|
32 | React.InputHTMLAttributes<any> & {
|
33 | value?: string | number;
|
34 | };
|
35 |
|
36 | export type CheckboxProps = CheckboxOptions & CheckboxHTMLProps;
|
37 |
|
38 | function getChecked(options: CheckboxOptions) {
|
39 | if (typeof options.checked !== "undefined") {
|
40 | return options.checked;
|
41 | }
|
42 | if (typeof options.value === "undefined") {
|
43 | return !!options.state;
|
44 | }
|
45 | const state = Array.isArray(options.state) ? options.state : [];
|
46 | return state.indexOf(options.value) !== -1;
|
47 | }
|
48 |
|
49 | function fireChange(element: HTMLElement, onChange?: React.ChangeEventHandler) {
|
50 | const event = createEvent(element, "change");
|
51 | Object.defineProperties(event, {
|
52 | type: { value: "change" },
|
53 | target: { value: element },
|
54 | currentTarget: { value: element },
|
55 | });
|
56 | onChange?.(event as any);
|
57 | }
|
58 |
|
59 | function useIndeterminateState(
|
60 | ref: React.RefObject<HTMLInputElement>,
|
61 | options: CheckboxOptions
|
62 | ) {
|
63 | React.useEffect(() => {
|
64 | const element = ref.current;
|
65 | if (!element) {
|
66 | warning(
|
67 | options.state === "indeterminate",
|
68 | "Can't set indeterminate state because `ref` wasn't passed to component.",
|
69 | "See https://reakit.io/docs/checkbox/#indeterminate-state"
|
70 | );
|
71 | return;
|
72 | }
|
73 |
|
74 | if (options.state === "indeterminate") {
|
75 | element.indeterminate = true;
|
76 | } else if (element.indeterminate) {
|
77 | element.indeterminate = false;
|
78 | }
|
79 | }, [options.state, ref]);
|
80 | }
|
81 |
|
82 | export const useCheckbox = createHook<CheckboxOptions, CheckboxHTMLProps>({
|
83 | name: "Checkbox",
|
84 | compose: useClickable,
|
85 | keys: CHECKBOX_KEYS,
|
86 |
|
87 | useOptions(
|
88 | { unstable_clickOnEnter = false, ...options },
|
89 | { value, checked }
|
90 | ) {
|
91 | return {
|
92 | unstable_clickOnEnter,
|
93 | value,
|
94 | checked: getChecked({ checked, ...options }),
|
95 | ...options,
|
96 | };
|
97 | },
|
98 |
|
99 | useProps(
|
100 | options,
|
101 | { ref: htmlRef, onChange: htmlOnChange, onClick: htmlOnClick, ...htmlProps }
|
102 | ) {
|
103 | const ref = React.useRef<HTMLInputElement>(null);
|
104 | const [isNativeCheckbox, setIsNativeCheckbox] = React.useState(true);
|
105 | const onChangeRef = useLiveRef(htmlOnChange);
|
106 | const onClickRef = useLiveRef(htmlOnClick);
|
107 |
|
108 | React.useEffect(() => {
|
109 | const element = ref.current;
|
110 | if (!element) {
|
111 | warning(
|
112 | true,
|
113 | "Can't determine whether the element is a native checkbox because `ref` wasn't passed to the component",
|
114 | "See https://reakit.io/docs/checkbox"
|
115 | );
|
116 | return;
|
117 | }
|
118 | if (element.tagName !== "INPUT" || element.type !== "checkbox") {
|
119 | setIsNativeCheckbox(false);
|
120 | }
|
121 | }, []);
|
122 |
|
123 | useIndeterminateState(ref, options);
|
124 |
|
125 | const onChange = React.useCallback(
|
126 | (event: React.ChangeEvent<HTMLInputElement>) => {
|
127 | const element = event.currentTarget;
|
128 |
|
129 | if (options.disabled) {
|
130 | event.stopPropagation();
|
131 | event.preventDefault();
|
132 | return;
|
133 | }
|
134 |
|
135 | if (onChangeRef.current) {
|
136 |
|
137 |
|
138 | if (!isNativeCheckbox) {
|
139 | element.checked = !element.checked;
|
140 | }
|
141 | onChangeRef.current(event);
|
142 | }
|
143 |
|
144 | if (!options.setState) return;
|
145 |
|
146 | if (typeof options.value === "undefined") {
|
147 | options.setState(!options.checked);
|
148 | } else {
|
149 | const state = Array.isArray(options.state) ? options.state : [];
|
150 | const index = state.indexOf(options.value);
|
151 | if (index === -1) {
|
152 | options.setState([...state, options.value]);
|
153 | } else {
|
154 | options.setState(removeIndexFromArray(state, index));
|
155 | }
|
156 | }
|
157 | },
|
158 | [
|
159 | options.disabled,
|
160 | isNativeCheckbox,
|
161 | options.setState,
|
162 | options.value,
|
163 | options.checked,
|
164 | options.state,
|
165 | ]
|
166 | );
|
167 |
|
168 | const onClick = React.useCallback(
|
169 | (event: React.MouseEvent<HTMLElement, MouseEvent>) => {
|
170 | onClickRef.current?.(event);
|
171 | if (event.defaultPrevented) return;
|
172 | if (isNativeCheckbox) return;
|
173 | fireChange(event.currentTarget, onChange);
|
174 | },
|
175 | [isNativeCheckbox, onChange]
|
176 | );
|
177 |
|
178 | return {
|
179 | ref: useForkRef(ref, htmlRef),
|
180 | role: !isNativeCheckbox ? "checkbox" : undefined,
|
181 | type: isNativeCheckbox ? "checkbox" : undefined,
|
182 | value: isNativeCheckbox ? options.value : undefined,
|
183 | checked: options.checked,
|
184 | "aria-checked":
|
185 | options.state === "indeterminate" ? "mixed" : options.checked,
|
186 | onChange,
|
187 | onClick,
|
188 | ...htmlProps,
|
189 | };
|
190 | },
|
191 | });
|
192 |
|
193 | export const Checkbox = createComponent({
|
194 | as: "input",
|
195 | memo: true,
|
196 | useHook: useCheckbox,
|
197 | });
|