From 87ce0a4c84b549467a4f3ca40739886d74c79702 Mon Sep 17 00:00:00 2001 From: myelinated-wackerow <263208946+myelinated-wackerow@users.noreply.github.com> Date: Fri, 8 May 2026 09:58:29 -0700 Subject: [PATCH] a11y: ProductTable filter sidebar semantics Wraps shared Filter groups in
via shadcn FieldSet with sr-only legends, and replaces the SwitchFilterInput sibling-

label with a real

{filter.title}

- {filter.items.map((item, itemIndex) => { - return ( -
- {item.input( - filterIndex, - itemIndex, - item.inputState, - handleChange - )} - {item.inputState === true && item.options.length ? ( -
- {item.options.map((option, optionIndex) => { - return ( - - {option.input( - filterIndex, - itemIndex, - optionIndex, - option.inputState, - handleChange - )} - - ) - })} -
- ) : null} -
- ) - })} +
+ {filter.title} + {filter.items.map((item, itemIndex) => { + return ( +
+ {item.input( + filterIndex, + itemIndex, + item.inputState, + handleChange + )} + {item.inputState === true && item.options.length ? ( +
+ {item.optionsLegend && ( + + {item.optionsLegend} + + )} + {item.options.map((option, optionIndex) => { + return ( + + {option.input( + filterIndex, + itemIndex, + optionIndex, + option.inputState, + handleChange + )} + + ) + })} +
+ ) : null} +
+ ) + })} +
) diff --git a/src/components/ProductTable/FilterInputs/SwitchFilterInput.tsx b/src/components/ProductTable/FilterInputs/SwitchFilterInput.tsx index 55305775463..60939e75f0c 100644 --- a/src/components/ProductTable/FilterInputs/SwitchFilterInput.tsx +++ b/src/components/ProductTable/FilterInputs/SwitchFilterInput.tsx @@ -1,8 +1,11 @@ -import { ReactElement } from "react" +"use client" + +import { ReactElement, useId } from "react" import type { LucideIcon } from "lucide-react" import { FilterInputState } from "@/lib/types" +import { Field, FieldDescription, FieldLabel } from "@/components/ui/field" import Switch from "@/components/ui/switch" interface SwitchFilterInputProps { @@ -28,26 +31,40 @@ const SwitchFilterInput = ({ inputState, updateFilterState, }: SwitchFilterInputProps) => { + const id = useId() + const descriptionId = description ? `${id}-description` : undefined return ( - <> -
-
-
+ +
+ + {Icon && ( )} -
-

{label}

-
+ + {label} + { updateFilterState(filterIndex, itemIndex, e as boolean) }} />
-

{description}

- + {description && ( + + {description} + + )} + ) } diff --git a/src/components/ui/field.tsx b/src/components/ui/field.tsx new file mode 100644 index 00000000000..64921f8f070 --- /dev/null +++ b/src/components/ui/field.tsx @@ -0,0 +1,243 @@ +import { useMemo } from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { Label } from "@/components/ui/label" +import { Separator } from "@/components/ui/separator" + +import { cn } from "@/lib/utils/cn" + +function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) { + return ( +
[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3", + className + )} + {...props} + /> + ) +} + +function FieldLegend({ + className, + variant = "legend", + ...props +}: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) { + return ( + + ) +} + +function FieldGroup({ className, ...props }: React.ComponentProps<"div">) { + return ( +
[data-slot=field-group]]:gap-4", + className + )} + {...props} + /> + ) +} + +const fieldVariants = cva( + "group/field data-[invalid=true]:text-error flex w-full gap-3", + { + variants: { + orientation: { + vertical: ["flex-col [&>*]:w-full [&>.sr-only]:w-auto"], + horizontal: [ + "flex-row items-center", + "[&>[data-slot=field-label]]:flex-auto", + "has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px has-[>[data-slot=field-content]]:items-start", + ], + responsive: [ + "@md/field-group:flex-row @md/field-group:items-center @md/field-group:[&>*]:w-auto flex-col [&>*]:w-full [&>.sr-only]:w-auto", + "@md/field-group:[&>[data-slot=field-label]]:flex-auto", + "@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px", + ], + }, + }, + defaultVariants: { + orientation: "vertical", + }, + } +) + +function Field({ + className, + orientation = "vertical", + ...props +}: React.ComponentProps<"div"> & VariantProps) { + return ( +
+ ) +} + +function FieldContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function FieldLabel({ + className, + ...props +}: React.ComponentProps) { + return ( +