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:
+1
-1
@@ -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",
|
||||
|
||||
+4
-4
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user