addons/components/Checkbox.tsxblame
View source
b69ab311/**
b69ab312 * Copyright (c) Meta Platforms, Inc. and affiliates.
b69ab313 *
b69ab314 * This source code is licensed under the MIT license found in the
b69ab315 * LICENSE file in the root directory of this source tree.
b69ab316 */
b69ab317
b69ab318import type react from 'react';
b69ab319import type {ReactProps} from './utils';
b69ab3110
b69ab3111import * as stylex from '@stylexjs/stylex';
b69ab3112import {useEffect, useId, useRef} from 'react';
b69ab3113import {layout} from './theme/layout';
b69ab3114import {spacing} from './theme/tokens.stylex';
b69ab3115
b69ab3116const cssVarFocusWithinBorder = '--checkbox-focus-within-color';
b69ab3117const styles = stylex.create({
b69ab3118 label: {
b69ab3119 [cssVarFocusWithinBorder]: {
b69ab3120 default: 'var(--checkbox-border)',
b69ab3121 ':focus-within': 'var(--focus-border)',
b69ab3122 },
b69ab3123 cursor: 'pointer',
b69ab3124 alignItems: 'center',
b69ab3125 position: 'relative',
b69ab3126 outline: 'none',
b69ab3127 userSelect: 'none',
b69ab3128 },
b69ab3129 input: {
b69ab3130 opacity: 0,
b69ab3131 outline: 'none',
b69ab3132 appearance: 'none',
b69ab3133 position: 'absolute',
b69ab3134 },
b69ab3135 disabled: {
b69ab3136 opacity: 0.5,
b69ab3137 cursor: 'not-allowed',
b69ab3138 },
b69ab3139 withChildren: {
b69ab3140 marginRight: spacing.pad,
b69ab3141 },
b69ab3142 checkmark: {
b69ab3143 background: 'var(--checkbox-background)',
b69ab3144 borderRadius: '3px',
b69ab3145 width: '16px',
b69ab3146 height: '16px',
b69ab3147 border: '1px solid var(--checkbox-border)',
b69ab3148 borderColor: `var(${cssVarFocusWithinBorder})`,
b69ab3149 display: 'inline-block',
b69ab3150 color: 'var(--checkbox-foreground)',
b69ab3151 transition: '60ms transform ease-in-out',
b69ab3152 },
b69ab3153});
b69ab3154
b69ab3155function Checkmark({checked}: {checked: boolean}) {
b69ab3156 return (
b69ab3157 <svg
b69ab3158 width="16"
b69ab3159 height="16"
b69ab3160 viewBox="0 0 16 16"
b69ab3161 xmlns="http://www.w3.org/2000/svg"
b69ab3162 fill={checked ? 'currentColor' : 'transparent'}
b69ab3163 {...stylex.props(styles.checkmark)}>
b69ab3164 <path
b69ab3165 fillRule="evenodd"
b69ab3166 clipRule="evenodd"
b69ab3167 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>
b69ab3168 </svg>
b69ab3169 );
b69ab3170}
b69ab3171
b69ab3172function Indeterminate() {
b69ab3173 return (
b69ab3174 <svg
b69ab3175 width="16"
b69ab3176 height="16"
b69ab3177 viewBox="0 0 16 16"
b69ab3178 xmlns="http://www.w3.org/2000/svg"
b69ab3179 fill={'currentColor'}
b69ab3180 {...stylex.props(styles.checkmark)}>
b69ab3181 <rect x="4" y="4" height="8" width="8" rx="2" />
b69ab3182 </svg>
b69ab3183 );
b69ab3184}
b69ab3185
b69ab3186export function Checkbox({
b69ab3187 children,
b69ab3188 checked,
b69ab3189 onChange,
b69ab3190 disabled,
b69ab3191 indeterminate,
b69ab3192 xstyle,
b69ab3193 ...rest
b69ab3194}: {
b69ab3195 children?: react.ReactNode;
b69ab3196 checked: boolean;
b69ab3197 /** "indeterminate" state is neither true nor false, and renders as a box instead of a checkmark.
b69ab3198 * Usually represents partial selection of children. */
b69ab3199 indeterminate?: boolean;
b69ab31100 disabled?: boolean;
b69ab31101 onChange?: (checked: boolean) => unknown;
b69ab31102 xstyle?: stylex.StyleXStyles;
b69ab31103} & Omit<ReactProps<HTMLInputElement>, 'onChange'>) {
b69ab31104 const id = useId();
b69ab31105 const inputRef = useRef<HTMLInputElement>(null);
b69ab31106 // Indeterminate cannot be set in HTML, use an effect to synchronize
b69ab31107 useEffect(() => {
b69ab31108 if (inputRef.current) {
b69ab31109 inputRef.current.indeterminate = indeterminate === true;
b69ab31110 }
b69ab31111 }, [indeterminate]);
b69ab31112 return (
b69ab31113 <label
b69ab31114 htmlFor={id}
b69ab31115 {...stylex.props(
b69ab31116 layout.flexRow,
b69ab31117 styles.label,
b69ab31118 children != null && styles.withChildren,
b69ab31119 disabled && styles.disabled,
b69ab31120 xstyle,
b69ab31121 )}>
b69ab31122 <input
b69ab31123 ref={inputRef}
b69ab31124 type="checkbox"
b69ab31125 id={id}
b69ab31126 checked={checked}
b69ab31127 onChange={e => !disabled && onChange?.(e.target.checked)}
b69ab31128 disabled={disabled}
b69ab31129 {...stylex.props(styles.input)}
b69ab31130 {...rest}
b69ab31131 />
b69ab31132 {indeterminate === true ? <Indeterminate /> : <Checkmark checked={checked} />}
b69ab31133 {children}
b69ab31134 </label>
b69ab31135 );
b69ab31136}