fix(webapp): enforce associated form labels (#4695)

## Summary

Finish associating dashboard form labels with their controls and enforce
`jsx-a11y/label-has-associated-control`.

Repeated data store dialogs use unique generated IDs, story controls and
notification filters have explicit associations, and display-only status
text no longer uses label elements.

Base: [#4694](https://github.com/triggerdotdev/trigger.dev/pull/4694)
This commit is contained in:
Chris Arderne
2026-08-19 16:35:44 +01:00
committed by GitHub
parent 3ffd123d27
commit 3a091eb764
5 changed files with 56 additions and 19 deletions
+1 -1
View File
@@ -78,7 +78,7 @@
"jsx-a11y/aria-role": "error",
"jsx-a11y/click-events-have-key-events": "off",
"jsx-a11y/control-has-associated-label": "off",
"jsx-a11y/label-has-associated-control": "off",
"jsx-a11y/label-has-associated-control": "error",
"jsx-a11y/no-autofocus": "off",
"jsx-a11y/no-noninteractive-element-interactions": "off",
"jsx-a11y/no-static-element-interactions": "off",
@@ -1034,9 +1034,9 @@ function PlaygroundSidebar({
{runFriendlyId && <SessionField label="Run ID" value={runFriendlyId} />}
<SessionField label="Messages" value={String(messageCount)} />
<div>
<label className="mb-0.5 block text-[10px] font-medium uppercase tracking-wider text-text-dimmed">
<span className="mb-0.5 block text-[10px] font-medium uppercase tracking-wider text-text-dimmed">
Status
</label>
</span>
<span className="flex items-center gap-1.5 text-xs">
<span
className={cn(
@@ -1353,9 +1353,9 @@ function safeParseJson(json: string): Record<string, unknown> {
function SessionField({ label, value }: { label: string; value: string }) {
return (
<div>
<label className="mb-0.5 block text-[10px] font-medium uppercase tracking-wider text-text-dimmed">
<span className="mb-0.5 block text-[10px] font-medium uppercase tracking-wider text-text-dimmed">
{label}
</label>
</span>
<code className="block truncate text-xs text-text-bright">{value}</code>
</div>
);
+26 -7
View File
@@ -1,4 +1,4 @@
import { useState } from "react";
import { useId, useState } from "react";
import { useFetcher } from "@remix-run/react";
import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/server-runtime";
import { redirect } from "@remix-run/server-runtime";
@@ -313,6 +313,7 @@ function DeleteButton({ name }: { name: string }) {
function EditButton({ name, organizationIds }: { name: string; organizationIds: string[] }) {
const [open, setOpen] = useState(false);
const fieldId = useId();
const fetcher = useFetcher<{ success?: boolean; error?: string }>();
const isSubmitting = fetcher.state !== "idle";
@@ -336,8 +337,11 @@ function EditButton({ name, organizationIds }: { name: string; organizationIds:
<input type="hidden" name="key" value={name} />
<div className="space-y-1.5">
<label className="text-xs font-medium text-text-dimmed">Key</label>
<label htmlFor={`${fieldId}-key`} className="text-xs font-medium text-text-dimmed">
Key
</label>
<Input
id={`${fieldId}-key`}
name="_key_display"
value={name}
readOnly
@@ -347,10 +351,14 @@ function EditButton({ name, organizationIds }: { name: string; organizationIds:
</div>
<div className="space-y-1.5">
<label className="text-xs font-medium text-text-dimmed">
<label
htmlFor={`${fieldId}-organizationIds`}
className="text-xs font-medium text-text-dimmed"
>
Organization IDs <span className="text-rose-400">*</span>
</label>
<Input
id={`${fieldId}-organizationIds`}
name="organizationIds"
defaultValue={organizationIds.join(", ")}
placeholder="clxxxxx, clyyyyy, clzzzzz"
@@ -394,6 +402,7 @@ function AddDataStoreDialog({
onOpenChange: (open: boolean) => void;
}) {
const fetcher = useFetcher<{ success?: boolean; error?: string }>();
const fieldId = useId();
const isSubmitting = fetcher.state !== "idle";
// Close dialog on success
@@ -412,10 +421,11 @@ function AddDataStoreDialog({
<input type="hidden" name="_action" value="add" />
<div className="space-y-1.5">
<label className="text-xs font-medium text-text-dimmed">
<label htmlFor={`${fieldId}-key`} className="text-xs font-medium text-text-dimmed">
Key <span className="text-rose-400">*</span>
</label>
<Input
id={`${fieldId}-key`}
name="key"
placeholder="e.g. hipaa-clickhouse-us-east"
variant="medium"
@@ -428,10 +438,11 @@ function AddDataStoreDialog({
</div>
<div className="space-y-1.5">
<label className="text-xs font-medium text-text-dimmed">
<label htmlFor={`${fieldId}-kind`} className="text-xs font-medium text-text-dimmed">
Kind <span className="text-rose-400">*</span>
</label>
<Input
id={`${fieldId}-kind`}
name="kind"
value="CLICKHOUSE"
readOnly
@@ -441,10 +452,14 @@ function AddDataStoreDialog({
</div>
<div className="space-y-1.5">
<label className="text-xs font-medium text-text-dimmed">
<label
htmlFor={`${fieldId}-organizationIds`}
className="text-xs font-medium text-text-dimmed"
>
Organization IDs <span className="text-rose-400">*</span>
</label>
<Input
id={`${fieldId}-organizationIds`}
name="organizationIds"
placeholder="clxxxxx, clyyyyy, clzzzzz"
variant="medium"
@@ -454,10 +469,14 @@ function AddDataStoreDialog({
</div>
<div className="space-y-1.5">
<label className="text-xs font-medium text-text-dimmed">
<label
htmlFor={`${fieldId}-connectionUrl`}
className="text-xs font-medium text-text-dimmed"
>
ClickHouse connection URL <span className="text-rose-400">*</span>
</label>
<Input
id={`${fieldId}-connectionUrl`}
name="connectionUrl"
type="password"
placeholder="https://user:password@host:8443"
@@ -434,8 +434,11 @@ export default function AdminNotificationsRoute() {
<Paragraph className="text-text-dimmed">
{total} notifications (page {page} of {pageCount || 1})
</Paragraph>
<label className="flex items-center gap-2 text-xs text-text-dimmed">
<Checkbox checked={hideInactive} onChange={toggleHideInactive} />
<label
htmlFor="hideInactive"
className="flex items-center gap-2 text-xs text-text-dimmed"
>
<Checkbox id="hideInactive" checked={hideInactive} onChange={toggleHideInactive} />
Hide inactive
</label>
</div>
@@ -16,8 +16,11 @@ export default function Story() {
<Header2>InputNumberStepper</Header2>
<Header3>Size: base (default)</Header3>
<div className="flex flex-col gap-2">
<label className="text-sm text-text-dimmed">Step: 75</label>
<label htmlFor="stepper-1" className="text-sm text-text-dimmed">
Step: 75
</label>
<InputNumberStepper
id="stepper-1"
value={value1}
onChange={(e) => setValue1(e.target.value === "" ? "" : Number(e.target.value))}
step={75}
@@ -25,8 +28,11 @@ export default function Story() {
</div>
<div className="flex flex-col gap-2">
<label className="text-sm text-text-dimmed">Step: 50, Min: 0, Max: 1000</label>
<label htmlFor="stepper-2" className="text-sm text-text-dimmed">
Step: 50, Min: 0, Max: 1000
</label>
<InputNumberStepper
id="stepper-2"
value={value2}
onChange={(e) => setValue2(e.target.value === "" ? "" : Number(e.target.value))}
step={50}
@@ -36,8 +42,11 @@ export default function Story() {
</div>
<div className="flex flex-col gap-2">
<label className="text-sm text-text-dimmed">Disabled state</label>
<label htmlFor="stepper-3" className="text-sm text-text-dimmed">
Disabled state
</label>
<InputNumberStepper
id="stepper-3"
value={value3}
onChange={(e) => setValue3(e.target.value === "" ? "" : Number(e.target.value))}
step={50}
@@ -49,8 +58,11 @@ export default function Story() {
<div className="flex flex-col gap-2">
<Header3>Size: large</Header3>
<div className="flex flex-col gap-2">
<label className="text-sm text-text-dimmed">Step: 50</label>
<label htmlFor="stepper-4" className="text-sm text-text-dimmed">
Step: 50
</label>
<InputNumberStepper
id="stepper-4"
value={value4}
onChange={(e) => setValue4(e.target.value === "" ? "" : Number(e.target.value))}
step={50}
@@ -59,8 +71,11 @@ export default function Story() {
</div>
<div className="flex flex-col gap-2">
<label className="text-sm text-text-dimmed">Step: 50, Disabled</label>
<label htmlFor="stepper-5" className="text-sm text-text-dimmed">
Step: 50, Disabled
</label>
<InputNumberStepper
id="stepper-5"
value={value5}
onChange={(e) => setValue5(e.target.value === "" ? "" : Number(e.target.value))}
step={50}