3.4 KB137 lines
Blame
1/**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8import type react from 'react';
9import type {ReactProps} from './utils';
10
11import * as stylex from '@stylexjs/stylex';
12import {useEffect, useId, useRef} from 'react';
13import {layout} from './theme/layout';
14import {spacing} from './theme/tokens.stylex';
15
16const cssVarFocusWithinBorder = '--checkbox-focus-within-color';
17const styles = stylex.create({
18 label: {
19 [cssVarFocusWithinBorder]: {
20 default: 'var(--checkbox-border)',
21 ':focus-within': 'var(--focus-border)',
22 },
23 cursor: 'pointer',
24 alignItems: 'center',
25 position: 'relative',
26 outline: 'none',
27 userSelect: 'none',
28 },
29 input: {
30 opacity: 0,
31 outline: 'none',
32 appearance: 'none',
33 position: 'absolute',
34 },
35 disabled: {
36 opacity: 0.5,
37 cursor: 'not-allowed',
38 },
39 withChildren: {
40 marginRight: spacing.pad,
41 },
42 checkmark: {
43 background: 'var(--checkbox-background)',
44 borderRadius: '3px',
45 width: '16px',
46 height: '16px',
47 border: '1px solid var(--checkbox-border)',
48 borderColor: `var(${cssVarFocusWithinBorder})`,
49 display: 'inline-block',
50 color: 'var(--checkbox-foreground)',
51 transition: '60ms transform ease-in-out',
52 },
53});
54
55function Checkmark({checked}: {checked: boolean}) {
56 return (
57 <svg
58 width="16"
59 height="16"
60 viewBox="0 0 16 16"
61 xmlns="http://www.w3.org/2000/svg"
62 fill={checked ? 'currentColor' : 'transparent'}
63 {...stylex.props(styles.checkmark)}>
64 <path
65 fillRule="evenodd"
66 clipRule="evenodd"
67 d="M14.431 3.323l-8.47 10-.79-.036-3.35-4.77.818-.574 2.978 4.24 8.051-9.506.764.646z"></path>
68 </svg>
69 );
70}
71
72function Indeterminate() {
73 return (
74 <svg
75 width="16"
76 height="16"
77 viewBox="0 0 16 16"
78 xmlns="http://www.w3.org/2000/svg"
79 fill={'currentColor'}
80 {...stylex.props(styles.checkmark)}>
81 <rect x="4" y="4" height="8" width="8" rx="2" />
82 </svg>
83 );
84}
85
86export function Checkbox({
87 children,
88 checked,
89 onChange,
90 disabled,
91 indeterminate,
92 xstyle,
93 ...rest
94}: {
95 children?: react.ReactNode;
96 checked: boolean;
97 /** "indeterminate" state is neither true nor false, and renders as a box instead of a checkmark.
98 * Usually represents partial selection of children. */
99 indeterminate?: boolean;
100 disabled?: boolean;
101 onChange?: (checked: boolean) => unknown;
102 xstyle?: stylex.StyleXStyles;
103} & Omit<ReactProps<HTMLInputElement>, 'onChange'>) {
104 const id = useId();
105 const inputRef = useRef<HTMLInputElement>(null);
106 // Indeterminate cannot be set in HTML, use an effect to synchronize
107 useEffect(() => {
108 if (inputRef.current) {
109 inputRef.current.indeterminate = indeterminate === true;
110 }
111 }, [indeterminate]);
112 return (
113 <label
114 htmlFor={id}
115 {...stylex.props(
116 layout.flexRow,
117 styles.label,
118 children != null && styles.withChildren,
119 disabled && styles.disabled,
120 xstyle,
121 )}>
122 <input
123 ref={inputRef}
124 type="checkbox"
125 id={id}
126 checked={checked}
127 onChange={e => !disabled && onChange?.(e.target.checked)}
128 disabled={disabled}
129 {...stylex.props(styles.input)}
130 {...rest}
131 />
132 {indeterminate === true ? <Indeterminate /> : <Checkmark checked={checked} />}
133 {children}
134 </label>
135 );
136}
137