Address PR review on data request pages

Require a verified viewer email before creating a rights request and
validate the free-text fields with the same SafeText bounds the console
uses, so this public portal mutation stays safe and bounded.

Move myRightsRequests onto the base Query, drop the now-dead count
loaders, and order the RECTIFICATION enum value before PORTABILITY so
the Postgres sort order matches RightsRequestTypes().

Harden the v2 kit primitives: SegmentedControl keeps equal-width cards
(auto-fill), preserves its selection when the active card is toggled,
and forwards an accessible name; Field associates its label and error
by id/aria instead of wrapping the control in a label. Give the type
group an accessible name, require the name field for non-complaint
types, use a timezone-stable reference year, drop the underreporting
header count, and neutralize the response-deadline copy.

Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
Émile Ré
2026-07-20 09:47:55 +02:00
parent 6623cbc6f2
commit 622f1ba67d
16 changed files with 156 additions and 146 deletions

View File

@@ -19,40 +19,66 @@
// SOFTWARE.
import { ToggleGroup as BaseToggleGroup } from "@base-ui/react/toggle-group";
import type { ReactNode } from "react";
import { type ReactNode, useState } from "react";
import { segmentedControl } from "./variants";
export type SegmentedControlProps = {
// Single selected value (controlled).
value?: string;
"value"?: string;
// Single selected value (uncontrolled).
defaultValue?: string;
"defaultValue"?: string;
// Fired with the newly selected value. Never fired with an empty selection,
// so a value always stays selected (clicking the active item is a no-op).
onValueChange?: (value: string) => void;
disabled?: boolean;
className?: string;
children?: ReactNode;
"onValueChange"?: (value: string) => void;
"disabled"?: boolean;
"className"?: string;
// Accessible name for the group (or reference a visible label via
// `aria-labelledby`), since the control has no intrinsic label.
"aria-label"?: string;
"aria-labelledby"?: string;
"children"?: ReactNode;
};
// Single-select pill group. Wraps Base UI's array-based ToggleGroup with a
// friendlier single-value API.
// friendlier single-value API. Selection is tracked internally (seeded from
// `value`/`defaultValue`) so toggling the active item off — which Base UI
// reports as an empty group value — never clears the selection.
export function SegmentedControl(props: SegmentedControlProps) {
const { value, defaultValue, onValueChange, disabled, className, children } = props;
const {
value,
defaultValue,
onValueChange,
disabled,
className,
children,
"aria-label": ariaLabel,
"aria-labelledby": ariaLabelledby,
} = props;
const { root } = segmentedControl();
const isControlled = value !== undefined;
const [internalValue, setInternalValue] = useState(defaultValue);
const currentValue = isControlled ? value : internalValue;
return (
<BaseToggleGroup
className={root({ className })}
disabled={disabled}
value={value != null ? [value] : undefined}
defaultValue={defaultValue != null ? [defaultValue] : undefined}
aria-label={ariaLabel}
aria-labelledby={ariaLabelledby}
value={currentValue != null ? [currentValue] : []}
onValueChange={(groupValue) => {
const next = groupValue[0];
if (next != null) {
onValueChange?.(next);
// Ignore the empty value emitted when the active item is toggled off,
// preserving the single-selection contract.
if (next == null) {
return;
}
if (!isControlled) {
setInternalValue(next);
}
onValueChange?.(next);
}}
>
{children}

View File

@@ -28,7 +28,7 @@ import { tv } from "tailwind-variants/lite";
// (the pressed state only darkens the border, so selection never shifts layout).
export const segmentedControl = tv({
slots: {
root: "grid grid-cols-[repeat(auto-fit,minmax(9rem,1fr))] gap-1",
root: "grid grid-cols-[repeat(auto-fill,minmax(9rem,1fr))] gap-1",
item: [
"min-w-0 cursor-pointer select-none rounded-3 border border-sand-a6 bg-sand-1 px-4 py-3.5",
"text-center text-2 font-medium text-sand-12 outline-none transition-colors",

View File

@@ -18,32 +18,60 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import type { ReactNode } from "react";
import { cloneElement, isValidElement, type ReactElement, type ReactNode, useId } from "react";
import { field } from "./variants";
export type FieldProps = {
// Text shown above the control. The control is nested inside the <label> so
// the association is implicit (no htmlFor / id threading required).
// Text shown above the control, associated with it via `htmlFor`/`id`.
label?: ReactNode;
// Validation / server error shown below the control.
// Validation / server error shown below the control and linked to it via
// `aria-describedby` so assistive technology announces it.
error?: ReactNode;
className?: string;
// A single form control (TextField, Textarea, …). It receives an injected
// `id`, plus `aria-describedby`/`aria-invalid` when an error is present.
children: ReactNode;
};
// Vertical label + control + error grouping for form dialogs.
// Vertical label + control + error grouping for form dialogs. Unlike a raw
// <label> wrapper, the label associates with the control by id, so controls
// whose root is a <div> (and multi-element controls) remain valid and clicks
// never activate an unintended descendant.
export function Field(props: FieldProps) {
const { label, error, className, children } = props;
const { root, label: labelSlot, labelText, error: errorSlot } = field();
const { root, labelText, error: errorSlot } = field();
const generatedId = useId();
const errorId = useId();
const child = isValidElement(children)
? (children as ReactElement<Record<string, unknown>>)
: null;
const existingId = typeof child?.props.id === "string" ? child.props.id : undefined;
const controlId = existingId ?? generatedId;
const control = child
? cloneElement(child, {
"id": controlId,
"aria-describedby": error != null ? errorId : undefined,
"aria-invalid": error != null ? true : undefined,
})
: children;
return (
<div className={root({ className })}>
<label className={labelSlot()}>
{label != null && <span className={labelText()}>{label}</span>}
{children}
</label>
{error != null && <p className={errorSlot()}>{error}</p>}
{label != null && (
<label htmlFor={controlId} className={labelText()}>
{label}
</label>
)}
{control}
{error != null && (
<p id={errorId} className={errorSlot()}>
{error}
</p>
)}
</div>
);
}

View File

@@ -81,7 +81,6 @@ export const textArea = tv({
export const field = tv({
slots: {
root: "flex flex-col gap-1.5",
label: "flex flex-col gap-1.5",
labelText: "text-2 font-medium text-sand-12",
error: "text-1 text-red-a11",
},