Skip to content

Support indeterminate state for Checkbox#512

Open
msmx-mnakagawa wants to merge 6 commits intomasterfrom
support-indeterminate-checkbox
Open

Support indeterminate state for Checkbox#512
msmx-mnakagawa wants to merge 6 commits intomasterfrom
support-indeterminate-checkbox

Conversation

@msmx-mnakagawa
Copy link
Collaborator

@msmx-mnakagawa msmx-mnakagawa commented Mar 5, 2026

Summary

Add indeterminate?: boolean prop to Checkbox, following the DOM HTMLInputElement.indeterminate property pattern.

Closes #511

Why onChange interception is needed

The DOM indeterminate property has no corresponding HTML attribute, so React cannot manage it declaratively. We set it imperatively via useEffect([indeterminate]) on mount and when the prop changes.

However, the browser resets indeterminate to false on every click. Since the prop value doesn't change (it's still true), useEffect does not re-run and the visual state is lost.

To solve this, Checkbox intercepts onChange and re-applies indeterminate after each click. This dual mechanism ensures:

  1. useEffect handles initial mount and prop changes
  2. handleChange handles the browser's click-triggered reset

Test plan

  • Verify Default story still works (boolean true/false)
  • Verify Indeterminate story renders the indeterminate dash mark for both indeterminate only and checked + indeterminate
  • Verify clicking an indeterminate checkbox preserves the indeterminate visual state
  • Verify controlled usage with external useState works correctly
  • Verify ControlledWithKnobs story still works
  • npm run type-check passes
  • npm run lint passes with 0 errors

🤖 Generated with Claude Code

msmx-mnakagawa and others added 2 commits March 5, 2026 12:59
Extend the `checked` prop to accept string literals (`'checked'`,
`'unchecked'`, `'indeterminate'`) in addition to `boolean`. This lets
consumers control all three checkbox states — checked, unchecked, and
indeterminate — through a single prop, using either boolean or string
form.

The HTML `indeterminate` property is DOM-only (no corresponding HTML
attribute), so it must be set imperatively via a ref. To do this without
breaking the existing `inputRef` prop, the component now uses a merged
ref (`internalRef` + `mergedRef`):

- `internalRef` holds the DOM node for the `useEffect` that toggles
  `indeterminate`.
- `mergedRef` forwards the same node to the consumer-supplied `inputRef`,
  whether it is a `RefObject` or a callback ref.

A simpler approach (e.g. reading `inputRef.current` directly) would fail
when `inputRef` is omitted or is a callback ref. Dropping `inputRef`
support would break backward compatibility.

Backward compatibility: existing `checked={true}` / `checked={false}`
usage continues to work unchanged. The `CheckboxProps` type uses
`Omit<InputHTMLAttributes, 'checked'>` to avoid a union conflict with
the native `checked?: boolean` from `InputHTMLAttributes`.

Ref: #511

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@msmx-mnakagawa msmx-mnakagawa changed the title (Checkbox): support indeterminate state Support indeterminate state for Checkbox Mar 5, 2026
@reg-suit
Copy link

reg-suit bot commented Mar 5, 2026

reg-suit detected visual differences.

Check this report, and review them.

🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵

What do the circles mean? The number of circles represent the number of changed images.
🔴 : Changed items, ⚪ : New items, ⚫ : Deleted items, and 🔵 Passed items

How can I change the check status? If reviewers approve this PR, the reg context status will be green automatically.

@msmx-mnakagawa msmx-mnakagawa requested a review from stomita March 5, 2026 04:10
@msmx-mnakagawa msmx-mnakagawa marked this pull request as draft March 6, 2026 08:05
@msmx-mnakagawa msmx-mnakagawa removed the request for review from stomita March 6, 2026 08:05
msmx-mnakagawa and others added 4 commits March 6, 2026 18:39
Replace the `CheckedState` string literal approach
(`checked: boolean | 'checked' | 'unchecked' | 'indeterminate'`)
with a separate `indeterminate?: boolean` prop, following the
DOM `HTMLInputElement.indeterminate` property pattern.

When both `checked` and `indeterminate` are true, `indeterminate`
takes visual precedence (matching native browser behavior).
The `checked` prop type remains `boolean` — no breaking change.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Cover both combinations: `indeterminate` without `checked`, and
`indeterminate` with `checked` (indeterminate takes visual precedence).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The browser resets the DOM `indeterminate` property to `false` on
every click. Since the `indeterminate` prop doesn't change,
`useEffect([indeterminate])` does not re-run, leaving the DOM
out of sync.

Intercept `onChange` to re-apply the `indeterminate` DOM property
after the consumer's handler runs. The `useEffect` is kept for
initial mount and prop-change synchronization.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@msmx-mnakagawa msmx-mnakagawa self-assigned this Mar 6, 2026
@msmx-mnakagawa msmx-mnakagawa marked this pull request as ready for review March 6, 2026 10:24
@msmx-mnakagawa msmx-mnakagawa requested a review from stomita March 6, 2026 10:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support indeterminate state for Checkbox

1 participant