UNPKG

5.8 kBPlain TextView Raw
1import * as React from "react";
2import { createComponent } from "reakit-system/createComponent";
3import { removeIndexFromArray } from "reakit-utils/removeIndexFromArray";
4import { createHook } from "reakit-system/createHook";
5import { useForkRef } from "reakit-utils/useForkRef";
6import { createEvent } from "reakit-utils/createEvent";
7import { warning } from "reakit-warning";
8import { useLiveRef } from "reakit-utils/useLiveRef";
9import {
10 ClickableOptions,
11 ClickableHTMLProps,
12 useClickable,
13} from "../Clickable/Clickable";
14import { CheckboxStateReturn } from "./CheckboxState";
15import { CHECKBOX_KEYS } from "./__keys";
16
17export type CheckboxOptions = ClickableOptions &
18 Pick<Partial<CheckboxStateReturn>, "state" | "setState"> & {
19 /**
20 * Checkbox's value is going to be used when multiple checkboxes share the
21 * same state. Checking a checkbox with value will add it to the state
22 * array.
23 */
24 value?: string | number;
25 /**
26 * Checkbox's checked state. If present, it's used instead of `state`.
27 */
28 checked?: boolean;
29 };
30
31export type CheckboxHTMLProps = ClickableHTMLProps &
32 React.InputHTMLAttributes<any> & {
33 value?: string | number;
34 };
35
36export type CheckboxProps = CheckboxOptions & CheckboxHTMLProps;
37
38function 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
49function 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
59function 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
82export 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 // If component is NOT rendered as a native input, it will not have
137 // the `checked` property. So we assign it for consistency.
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
193export const Checkbox = createComponent({
194 as: "input",
195 memo: true,
196 useHook: useCheckbox,
197});