Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
"prepublishOnly": "npm run check && npm run typecheck && npm run pack:check && npm run types:check",
"types:check": "attw --pack . --profile esm-only --no-summary",
"typecheck": "tsc --noEmit",
"test": "npm run build && node --test tests/*.test.mjs"
"test": "npm run build && node --test test/*.test.mjs tests/*.test.mjs"
},
"devDependencies": {
"@arethetypeswrong/cli": "^0.18.3",
Expand Down
87 changes: 50 additions & 37 deletions src/admin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -280,12 +280,17 @@ function renderSubField(

return (
<div key={field.key} style={fieldStyle}>
{type === "boolean" ? null : <span style={labelStyle}>{field.label}</span>}
{type === "boolean" || type === "select" ? null : (
<label htmlFor={id} style={labelStyle}>
{field.label}
</label>
)}
{type === "textarea" ? (
<Textarea {...commonProps} aria-label={field.label} className="min-h-24 w-full" rows={4} />
<Textarea {...commonProps} className="min-h-24 w-full" rows={4} />
) : type === "boolean" ? (
<label style={checkboxRowStyle}>
<label htmlFor={id} style={checkboxRowStyle}>
<input
id={id}
type="checkbox"
checked={Boolean(value)}
name={field.key}
Expand All @@ -297,7 +302,7 @@ function renderSubField(
</label>
) : type === "select" ? (
<Select
aria-label={field.label}
label={field.label}

@augmentcode augmentcode Bot Jun 17, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

src/admin.tsx:285 — This swaps aria-label for Kumo Select’s label prop; can we confirm label actually creates an accessible name association for the underlying control (otherwise this could be an a11y regression)? Other locations where this applies: src/admin.tsx:471.

Severity: medium

Other Locations
  • src/admin.tsx:471

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

className="w-full"
items={[
{ value: "", label: "Select..." },
Expand All @@ -312,7 +317,6 @@ function renderSubField(
) : (
<Input
{...commonProps}
aria-label={field.label}
className="w-full"
type={
type === "number" || type === "integer" ? "number" : type === "url" ? "url" : "text"
Expand Down Expand Up @@ -483,9 +487,8 @@ export function LinkField({
return (
<div id={id} tabIndex={-1} style={wrapperStyle}>
<div style={fieldStyle}>
<span style={labelStyle}>Type</span>
<Select
aria-label="Link type"
label="Type"
className="w-full"
items={[
{ value: "url", label: "URL" },
Expand All @@ -499,27 +502,30 @@ export function LinkField({
/>
</div>
<div style={fieldStyle}>
<span style={labelStyle}>Value</span>
<label htmlFor={`${id}-value`} style={labelStyle}>
Value
</label>
<Input
id={`${id}-value`}
aria-label="Link value"
className="w-full"
value={data.value ?? ""}
onChange={(event) => update({ value: event.currentTarget.value })}
/>
</div>
<div style={fieldStyle}>
<span style={labelStyle}>Text</span>
<label htmlFor={`${id}-text`} style={labelStyle}>
Text
</label>
<Input
id={`${id}-text`}
aria-label="Link text"
className="w-full"
value={data.text ?? ""}
onChange={(event) => update({ text: event.currentTarget.value })}
/>
</div>
<label style={checkboxRowStyle}>
<label htmlFor={`${id}-target`} style={checkboxRowStyle}>
<input
id={`${id}-target`}
type="checkbox"
checked={data.target === "_blank"}
onChange={(event: ChangeEvent<HTMLInputElement>) =>
Expand All @@ -535,10 +541,12 @@ export function LinkField({
export function ChoicesField({
value,
onChange,
label,
id = "fields-choices",
options,
}: FieldWidgetProps<ChoicesOptions>) {
const choicesList = choices(options?.choices ?? options?.options);
const legend = label ?? "Choices";
const multiple = Boolean(options?.multiple);
const horizontal = options?.orientation === "horizontal";
const selected = multiple
Expand All @@ -556,7 +564,7 @@ export function ChoicesField({
if (horizontal) {
return (
<fieldset id={id} style={fieldsetStyle}>
<legend style={legendStyle}>Choices</legend>
<legend style={legendStyle}>{legend}</legend>
<div style={horizontalChoiceGridStyle(options?.columns, choicesList.length)}>
{choicesList.map((choice) => {
const checked = selected.has(choice.value);
Expand Down Expand Up @@ -607,37 +615,42 @@ export function ChoicesField({

if (multiple) {
return (
<div id={id} tabIndex={-1} style={fieldStyle}>
<span style={labelStyle}>Choices</span>
{choicesList.map((choice) => (
<label key={choice.value} style={checkboxChoiceRowStyle}>
<input
type="checkbox"
checked={selected.has(choice.value)}
value={choice.value}
style={choiceControlStyle}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
const nextSelected = new Set(selected);
if (event.currentTarget.checked) {
nextSelected.add(choice.value);
} else {
nextSelected.delete(choice.value);
}
onChange([...nextSelected]);
}}
/>
{choice.icon ? renderChoiceCardLabel(choice) : (choice.label ?? choice.value)}
</label>
))}
<fieldset id={id} style={fieldsetStyle}>

@augmentcode augmentcode Bot Jun 17, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

src/admin.tsx:598 — In the multiple branch, the wrapper changed from a focusable <div id={id} tabIndex={-1}> to a <fieldset id={id}>, which is not programmatically focusable by default. If any error/scroll-to-field code calls .focus() on the widget root by id, this could break that behavior.

Severity: medium

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

<legend style={legendStyle}>{legend}</legend>
{choicesList.map((choice) => {
const inputId = choiceInputId(id, choice.value);

@augmentcode augmentcode Bot Jun 17, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

src/admin.tsx:601choiceInputId() sanitizes values, which can cause distinct choice.values to collapse to the same id (e.g., "a b" vs "a-b"), breaking labelinput association and creating duplicate IDs. Consider ensuring IDs are guaranteed-unique even after normalization.

Severity: medium

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.


return (
<label key={choice.value} htmlFor={inputId} style={checkboxChoiceRowStyle}>
<input
id={inputId}
type="checkbox"
checked={selected.has(choice.value)}
value={choice.value}
style={choiceControlStyle}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
const nextSelected = new Set(selected);
if (event.currentTarget.checked) {
nextSelected.add(choice.value);
} else {
nextSelected.delete(choice.value);
}
onChange([...nextSelected]);
}}
/>
{choice.icon ? renderChoiceCardLabel(choice) : (choice.label ?? choice.value)}
</label>
);
})}
{options?.helpText ? <small style={helpTextStyle}>{options.helpText}</small> : null}
</div>
</fieldset>
);
}

return (
<div id={id} tabIndex={-1}>
<Radio.Group
legend="Choices"
legend={legend}
appearance="card"
value={typeof value === "string" ? value : ""}
onValueChange={(nextValue) => onChange(nextValue)}
Expand Down
24 changes: 24 additions & 0 deletions test/semantics.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { readFile } from "node:fs/promises";
import test from "node:test";
import assert from "node:assert/strict";

const source = await readFile(new URL("../src/admin.tsx", import.meta.url), "utf8");

test("text-like subfields render visible labels connected to inputs", () => {
assert.match(source, /<label htmlFor=\{id\} style=\{labelStyle\}>/);
assert.match(source, /<Textarea \{\.\.\.commonProps\}/);
assert.match(source, /<Input\s+\{\.\.\.commonProps\}/);
});

test("link inputs and checkboxes use explicit label associations", () => {
assert.match(source, /<label htmlFor=\{`\$\{id\}-value`\} style=\{labelStyle\}>/);
assert.match(source, /<label htmlFor=\{`\$\{id\}-text`\} style=\{labelStyle\}>/);
assert.match(source, /<label htmlFor=\{`\$\{id\}-target`\} style=\{checkboxRowStyle\}>/);
});

test("choice collections expose semantic groups and labelled controls", () => {
assert.match(source, /<fieldset id=\{id\} style=\{fieldsetStyle\}>/);
assert.match(source, /<legend style=\{legendStyle\}>\{legend\}<\/legend>/);
assert.match(source, /htmlFor=\{inputId\}/);
assert.match(source, /id=\{inputId\}/);
});