Skip to content

Commit f804500

Browse files
committed
Support an indeterminate state on Checkbox
1 parent 6fe8d9e commit f804500

File tree

7 files changed

+120
-13
lines changed

7 files changed

+120
-13
lines changed

packages/ui/src/components/checkbox/checkbox.module.css

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,20 +36,22 @@
3636
.checkbox:not(.disabled) .input:focus ~ .box {
3737
border-color: var(--figma-color-border-selected);
3838
}
39-
.checkbox:not(.disabled) .input:checked ~ .box {
39+
.checkbox:not(.disabled) .input:checked ~ .box,
40+
.checkbox:not(.disabled) .input:indeterminate ~ .box {
4041
border-color: var(--figma-color-border-brand-strong);
4142
background-color: var(--figma-color-bg-brand);
4243
}
4344

4445
.disabled .input ~ .box {
4546
background-color: transparent;
4647
}
47-
.disabled .input:checked ~ .box {
48+
.disabled .input:checked ~ .box,
49+
.disabled .input:indeterminate ~ .box {
4850
border-color: transparent;
4951
background-color: var(--figma-color-bg-disabled);
5052
}
5153

52-
.checkIcon {
54+
.icon {
5355
position: absolute;
5456
top: calc(-1 * var(--border-width-1));
5557
left: calc(-1 * var(--border-width-1));

packages/ui/src/components/checkbox/checkbox.tsx

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1-
import { ComponentChildren, h } from 'preact'
2-
import { useCallback } from 'preact/hooks'
1+
import { MIXED_BOOLEAN } from '@create-figma-plugin/utilities'
2+
import { ComponentChildren, h, RefObject } from 'preact'
3+
import { useCallback, useEffect, useRef } from 'preact/hooks'
34

45
import { IconCheck16 } from '../../icons/icon-16/icon-check-16.js'
6+
import { IconMixed16 } from '../../icons/icon-16/icon-mixed-16.js'
57
import { Event, EventHandler } from '../../types/event-handler.js'
68
import { FocusableComponentProps } from '../../types/focusable-component-props.js'
79
import { createClassName } from '../../utilities/create-class-name.js'
810
import { createComponent } from '../../utilities/create-component.js'
11+
import { getCurrentFromRef } from '../../utilities/get-current-from-ref.js'
912
import { noop } from '../../utilities/no-op.js'
1013
import styles from './checkbox.module.css'
1114

@@ -15,7 +18,7 @@ export interface CheckboxProps
1518
disabled?: boolean
1619
onChange?: EventHandler.onChange<HTMLInputElement>
1720
onValueChange?: EventHandler.onValueChange<boolean>
18-
value: boolean
21+
value: typeof MIXED_BOOLEAN | boolean
1922
}
2023

2124
export const Checkbox = createComponent<HTMLInputElement, CheckboxProps>(
@@ -32,6 +35,8 @@ export const Checkbox = createComponent<HTMLInputElement, CheckboxProps>(
3235
},
3336
ref
3437
) {
38+
const inputElementRef: RefObject<HTMLInputElement> = useRef(null)
39+
3540
const handleChange = useCallback(
3641
function (event: Event.onChange<HTMLInputElement>) {
3742
onChange(event)
@@ -54,16 +59,40 @@ export const Checkbox = createComponent<HTMLInputElement, CheckboxProps>(
5459
[onKeyDown, propagateEscapeKeyDown]
5560
)
5661

62+
useEffect(
63+
function () {
64+
const inputElement = getCurrentFromRef(inputElementRef)
65+
inputElement.indeterminate = value === MIXED_BOOLEAN ? true : false
66+
},
67+
[value]
68+
)
69+
70+
const refCallback = useCallback(
71+
function (inputElement: null | HTMLInputElement) {
72+
inputElementRef.current = inputElement
73+
if (ref === null) {
74+
return
75+
}
76+
if (typeof ref === 'function') {
77+
ref(inputElement)
78+
return
79+
}
80+
ref.current = inputElement
81+
},
82+
[ref]
83+
)
84+
5785
return (
5886
<label
5987
class={createClassName([
6088
styles.checkbox,
61-
disabled === true ? styles.disabled : null
89+
disabled === true ? styles.disabled : null,
90+
value === MIXED_BOOLEAN ? styles.mixed : null
6291
])}
6392
>
6493
<input
6594
{...rest}
66-
ref={ref}
95+
ref={refCallback}
6796
checked={value === true}
6897
class={styles.input}
6998
disabled={disabled === true}
@@ -73,8 +102,12 @@ export const Checkbox = createComponent<HTMLInputElement, CheckboxProps>(
73102
type="checkbox"
74103
/>
75104
<div class={styles.box}>
76-
{value === true ? (
77-
<div class={styles.checkIcon}>
105+
{value === MIXED_BOOLEAN ? (
106+
<div class={styles.icon}>
107+
<IconMixed16 />
108+
</div>
109+
) : value === true ? (
110+
<div class={styles.icon}>
78111
<IconCheck16 />
79112
</div>
80113
) : null}

packages/ui/src/components/checkbox/stories/checkbox-selected.stories.tsx renamed to packages/ui/src/components/checkbox/stories/checkbox-checked.stories.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { Checkbox } from '../checkbox.js'
88

99
export default {
1010
tags: ['2'],
11-
title: 'Components/Checkbox/Selected'
11+
title: 'Components/Checkbox/Checked'
1212
}
1313

1414
export const Passive = function () {
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/* eslint-disable no-console */
2+
import { MIXED_BOOLEAN } from '@create-figma-plugin/utilities'
3+
import { h, JSX } from 'preact'
4+
import { useState } from 'preact/hooks'
5+
6+
import { useInitialFocus } from '../../../hooks/use-initial-focus/use-initial-focus.js'
7+
import { Text } from '../../text/text.js'
8+
import { Checkbox } from '../checkbox.js'
9+
10+
export default {
11+
tags: ['3'],
12+
title: 'Components/Checkbox/Mixed'
13+
}
14+
15+
export const Passive = function () {
16+
const [value, setValue] = useState<boolean | typeof MIXED_BOOLEAN>(
17+
MIXED_BOOLEAN
18+
)
19+
function handleChange(event: JSX.TargetedEvent<HTMLInputElement>) {
20+
const newValue = event.currentTarget.checked
21+
console.log(newValue)
22+
setValue(newValue)
23+
}
24+
return (
25+
<Checkbox onChange={handleChange} value={value}>
26+
<Text>Text</Text>
27+
</Checkbox>
28+
)
29+
}
30+
31+
export const Focused = function () {
32+
const [value, setValue] = useState<boolean | typeof MIXED_BOOLEAN>(
33+
MIXED_BOOLEAN
34+
)
35+
function handleChange(event: JSX.TargetedEvent<HTMLInputElement>) {
36+
const newValue = event.currentTarget.checked
37+
console.log(newValue)
38+
setValue(newValue)
39+
}
40+
return (
41+
<Checkbox {...useInitialFocus()} onChange={handleChange} value={value}>
42+
<Text>Text</Text>
43+
</Checkbox>
44+
)
45+
}
46+
47+
export const Disabled = function () {
48+
function handleChange() {
49+
throw new Error('This function should not be called')
50+
}
51+
return (
52+
<Checkbox disabled onChange={handleChange} value={MIXED_BOOLEAN}>
53+
<Text>Text</Text>
54+
</Checkbox>
55+
)
56+
}
57+
58+
export const OnValueChange = function () {
59+
const [value, setValue] = useState<boolean | typeof MIXED_BOOLEAN>(
60+
MIXED_BOOLEAN
61+
)
62+
function handleValueChange(newValue: boolean) {
63+
console.log(newValue)
64+
setValue(newValue)
65+
}
66+
return (
67+
<Checkbox onValueChange={handleValueChange} value={value}>
68+
<Text>Text</Text>
69+
</Checkbox>
70+
)
71+
}

packages/ui/src/components/checkbox/stories/checkbox-unselected.stories.tsx renamed to packages/ui/src/components/checkbox/stories/checkbox-unchecked.stories.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { Checkbox } from '../checkbox.js'
88

99
export default {
1010
tags: ['1'],
11-
title: 'Components/Checkbox/Unselected'
11+
title: 'Components/Checkbox/Unchecked'
1212
}
1313

1414
export const Passive = function () {

packages/utilities/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export { createImageElementFromBlobAsync } from './image/create-image-element-fr
1212
export { createImageElementFromBytesAsync } from './image/create-image-element-from-bytes-async.js'
1313
export { createImagePaint } from './image/create-image-paint.js'
1414
export { readBytesFromCanvasElementAsync } from './image/read-bytes-from-canvas-element-async.js'
15-
export { MIXED_NUMBER, MIXED_STRING } from './mixed-values.js'
15+
export { MIXED_BOOLEAN, MIXED_NUMBER, MIXED_STRING } from './mixed-values.js'
1616
export {
1717
getDocumentUseCount,
1818
incrementDocumentUseCount,
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1+
export const MIXED_BOOLEAN = null
12
export const MIXED_NUMBER = 999999999999999
23
export const MIXED_STRING = '999999999999999'

0 commit comments

Comments
 (0)