Files
ADS3-Design-System/src/components/ui/Checkbox/Checkbox.tsx
Richie c00335ef84 Refactor Button to semantic tokens and intent prop, fix error focus rings
Replace palette token references (blue-01, red-02, grey-01) with dedicated
button domain tokens. Rename `color` prop to `intent` with semantic values
(default, danger, subtle, neutral). Add loading state with spinner and
aria-busy. Fix Checkbox and Radio error states leaking teal focus ring.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-21 14:19:36 +10:00

145 lines
5.0 KiB
TypeScript

import { forwardRef, useId, useRef, useEffect, type InputHTMLAttributes } from 'react'
import { cn } from '@/lib/utils'
export interface CheckboxProps
extends Omit<InputHTMLAttributes<HTMLInputElement>, 'type' | 'size'> {
label?: string
description?: string
error?: string
indeterminate?: boolean
}
export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
(
{
label,
description,
error,
indeterminate = false,
disabled,
className,
id: idProp,
...props
},
forwardedRef,
) => {
const autoId = useId()
const id = idProp ?? autoId
const descriptionId = `${id}-description`
const errorId = `${id}-error`
const internalRef = useRef<HTMLInputElement>(null)
const hasError = !!error
useEffect(() => {
const el = internalRef.current
if (el) el.indeterminate = indeterminate
}, [indeterminate])
const describedBy =
[description ? descriptionId : undefined, hasError ? errorId : undefined]
.filter(Boolean)
.join(' ') || undefined
return (
<div className={cn('flex gap-2', className)}>
<div className="flex h-6 items-center">
<input
ref={(node) => {
(internalRef as React.MutableRefObject<HTMLInputElement | null>).current = node
if (typeof forwardedRef === 'function') forwardedRef(node)
else if (forwardedRef) forwardedRef.current = node
}}
type="checkbox"
id={id}
disabled={disabled}
aria-invalid={hasError || undefined}
aria-describedby={describedBy}
className={cn(
'peer size-5 cursor-pointer appearance-none rounded-[3px] border-2 border-control-border bg-control-bg transition-colors',
'hover:border-control-border-hover',
'checked:border-control-checked checked:bg-control-checked',
'checked:hover:border-control-checked-hover checked:hover:bg-control-checked-hover',
'indeterminate:border-control-checked indeterminate:bg-control-checked',
'indeterminate:hover:border-control-checked-hover indeterminate:hover:bg-control-checked-hover',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-control-focus-ring focus-visible:ring-offset-1',
'active:scale-95',
'disabled:pointer-events-none disabled:opacity-50',
hasError &&
'border-control-error hover:border-control-error checked:border-control-error checked:bg-control-error checked:hover:border-control-error checked:hover:bg-control-error focus-visible:ring-red-03',
)}
{...props}
/>
<svg
className="pointer-events-none absolute size-5 text-white opacity-0 peer-checked:opacity-100 peer-indeterminate:hidden"
viewBox="0 0 20 20"
fill="none"
stroke="currentColor"
strokeWidth={3}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<polyline points="4.5 10.5 8 14 15.5 6.5" />
</svg>
<svg
className="pointer-events-none absolute size-5 text-white opacity-0 peer-indeterminate:opacity-100"
viewBox="0 0 20 20"
fill="none"
stroke="currentColor"
strokeWidth={3}
strokeLinecap="round"
aria-hidden="true"
>
<line x1="5" y1="10" x2="15" y2="10" />
</svg>
</div>
{(label || description || hasError) && (
<div className="flex flex-col gap-0.5 pt-px">
{label && (
<label
htmlFor={id}
className={cn(
'cursor-pointer text-body font-normal text-grey-01',
disabled && 'pointer-events-none opacity-50',
)}
>
{label}
</label>
)}
{description && (
<p
id={descriptionId}
className={cn('text-small text-text', disabled && 'opacity-50')}
>
{description}
</p>
)}
{hasError && (
<div id={errorId} className="flex items-center gap-1 text-small text-control-error">
<svg
className="size-4 shrink-0"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<circle cx="12" cy="12" r="10" />
<line x1="12" y1="8" x2="12" y2="12" />
<line x1="12" y1="16" x2="12.01" y2="16" />
</svg>
<p>{error}</p>
</div>
)}
</div>
)}
</div>
)
},
)
Checkbox.displayName = 'Checkbox'