Skip to content

Commit 3d156df

Browse files
authored
fix(webapp): use native checkbox label semantics (#4697)
## Summary Use native label and checkbox behavior for `CheckboxWithLabel` and enforce `jsx-a11y/no-noninteractive-element-interactions`. The component no longer simulates checkbox activation with click handlers on non-interactive wrappers. Native change events now drive the controlled checked state. Base: [#4696](#4696)
1 parent 6461411 commit 3d156df

2 files changed

Lines changed: 25 additions & 17 deletions

File tree

.oxlintrc.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@
8686
],
8787
"jsx-a11y/label-has-associated-control": "error",
8888
"jsx-a11y/no-autofocus": "off",
89-
"jsx-a11y/no-noninteractive-element-interactions": "off",
89+
"jsx-a11y/no-noninteractive-element-interactions": "error",
9090
"jsx-a11y/no-static-element-interactions": "off",
9191
"jsx-a11y/prefer-tag-over-role": "off",
9292
"jsx-a11y/anchor-ambiguous-text": "error",

apps/webapp/app/components/primitives/Checkbox.tsx

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,12 @@ export const CheckboxWithLabel = React.forwardRef<HTMLInputElement, CheckboxProp
8686
) => {
8787
const [isChecked, setIsChecked] = useState<boolean>(defaultChecked ?? false);
8888
const [isDisabled, setIsDisabled] = useState<boolean>(disabled ?? false);
89+
const generatedId = React.useId();
90+
const inputId = id ?? generatedId;
91+
const labelId = `${inputId}-label`;
92+
const descriptionId = `${inputId}-description`;
93+
const ariaLabelledBy =
94+
props["aria-label"] || props["aria-labelledby"] ? props["aria-labelledby"] : labelId;
8995

9096
const buttonClassName = variants[variant].button;
9197
const labelClassName = variants[variant].label;
@@ -109,7 +115,7 @@ export const CheckboxWithLabel = React.forwardRef<HTMLInputElement, CheckboxProp
109115
}, [defaultChecked]);
110116

111117
return (
112-
<div
118+
<label
113119
className={cn(
114120
"group flex items-start gap-x-2 transition ",
115121
props.readOnly || disabled ? "cursor-default" : "cursor-pointer",
@@ -118,22 +124,21 @@ export const CheckboxWithLabel = React.forwardRef<HTMLInputElement, CheckboxProp
118124
(isDisabled || props.readOnly) && isDisabledClassName,
119125
className
120126
)}
121-
onClick={(e) => {
122-
//returning false is not setting the state to false, it stops the event from bubbling up
123-
if (isDisabled || props.readOnly === true) return false;
124-
setIsChecked((c) => !c);
125-
}}
126127
>
127128
<input
128129
{...props}
129130
name={name}
130131
type="checkbox"
131132
value={value}
132133
checked={isChecked}
134+
aria-labelledby={ariaLabelledBy}
135+
aria-describedby={
136+
props["aria-describedby"] ??
137+
(variant === "description" && description ? descriptionId : undefined)
138+
}
133139
onChange={(e) => {
134-
//returning false is not setting the state to false, it stops the event from bubbling up
135-
if (isDisabled || props.readOnly === true) return false;
136-
setIsChecked(!isChecked);
140+
if (isDisabled || props.readOnly === true) return;
141+
setIsChecked(e.target.checked);
137142
}}
138143
disabled={isDisabled}
139144
className={cn(
@@ -145,22 +150,21 @@ export const CheckboxWithLabel = React.forwardRef<HTMLInputElement, CheckboxProp
145150
(isDisabled || props.readOnly) &&
146151
"bg-background-raised! checked:bg-background-raised! checked:group-hover:bg-background-raised! group-hover:bg-background-raised!"
147152
)}
148-
id={id}
153+
id={inputId}
149154
ref={ref}
150155
/>
151156
<div>
152157
<div className="flex items-center gap-x-2">
153-
<label
154-
htmlFor={id}
158+
<span
159+
id={labelId}
155160
className={cn(
156161
props.readOnly || disabled ? "cursor-default" : "cursor-pointer",
157162
labelClassName,
158163
externalLabelClassName
159164
)}
160-
onClick={(e) => e.preventDefault()}
161165
>
162166
{label}
163-
</label>
167+
</span>
164168
{badges && (
165169
<span className="-mr-2 flex gap-x-1.5">
166170
{badges.map((badge) => (
@@ -170,12 +174,16 @@ export const CheckboxWithLabel = React.forwardRef<HTMLInputElement, CheckboxProp
170174
)}
171175
</div>
172176
{variant === "description" && (
173-
<Paragraph variant="small" className={cn("mt-0.5", descriptionClassName)}>
177+
<Paragraph
178+
id={descriptionId}
179+
variant="small"
180+
className={cn("mt-0.5", descriptionClassName)}
181+
>
174182
{description}
175183
</Paragraph>
176184
)}
177185
</div>
178-
</div>
186+
</label>
179187
);
180188
}
181189
);

0 commit comments

Comments
 (0)