fix(vendors): keep trades live region out of Stack spacing (SH-249)

The always-mounted aria-live feedback region inherited MUI Stack spacing,
adding a permanent gap under the trades input and shifting the approved
Vendor edit drawer layout. vendor-visual-regression caught it.

Move the region outside the spaced Stack and collapse it while silent, so
the live region stays in the DOM for announcements without claiming layout.
Restores the caption to MUI Typography; that swap was cosmetic and out of
scope for SH-249.
This commit is contained in:
Codex Review Integration 2026-08-18 14:27:42 -03:00
parent df00e56d6e
commit 63e37dd0bb

View file

@ -4,10 +4,53 @@ import AddIcon from "@mui/icons-material/Add";
import ArrowDownwardIcon from "@mui/icons-material/ArrowDownward";
import ArrowUpwardIcon from "@mui/icons-material/ArrowUpward";
import CloseIcon from "@mui/icons-material/Close";
import { Autocomplete, Box, Button, Chip, FormLabel, Stack, TextField } from "@mui/material";
import {
Autocomplete,
Box,
Button,
Chip,
FormLabel,
Stack,
TextField,
Typography,
} from "@mui/material";
import { Text } from "@/components/ui/text";
import type { VendorCompanyRosterFormValues } from "@/domain/vendors/schemas/vendor-roster-schema";
/**
* `Text variant="feedback"` stays mounted when silent so the aria-live region can announce later.
* An empty paragraph still claims a line box, which would add a permanent gap under the trades
* input and shift the approved drawer layout. Collapse the box while keeping the node in the DOM
* (never `display: none`, which would stop announcements).
*/
function collapsedWhenSilent(visible: boolean) {
return visible ? undefined : { height: 0, margin: 0, overflow: "hidden" as const };
}
function TradeFeedbackRegions({
listUnavailable,
message,
}: {
listUnavailable: boolean;
message: string | null;
}) {
return (
<>
<Text
variant="feedback"
tone="warning"
when={listUnavailable}
sx={collapsedWhenSilent(listUnavailable)}
>
Trade list is unavailable right now — you can still type a trade manually.
</Text>
<Text variant="feedback" when={Boolean(message)} sx={collapsedWhenSilent(Boolean(message))}>
{message}
</Text>
</>
);
}
function splitTrades(value: string | undefined): string[] {
return (value ?? "")
.split(",")
@ -52,9 +95,9 @@ export function VendorTradeSpecialtiesField({
return (
<Box>
<FormLabel component="legend">Trade Specialties</FormLabel>
<Text variant="caption" tone="muted">
<Typography variant="caption" sx={{ color: "text.secondary" }}>
First trade is primary. Reorder with the arrows.
</Text>
</Typography>
<Controller
control={control}
name={`technicians.${index}.tradeSpecialties`}
@ -150,12 +193,6 @@ export function VendorTradeSpecialtiesField({
<AddIcon />
</Button>
</Stack>
<Text variant="feedback" tone="warning" when={tradeOptions.length === 0}>
Trade list is unavailable right now — you can still type a trade manually.
</Text>
<Text variant="feedback" when={Boolean(feedbackMessage)}>
{feedbackMessage}
</Text>
{trades.length > 1 && (
<Stack direction="row" spacing={1} className="flex-wrap">
{trades.map((trade, tradeIndex) => (
@ -186,6 +223,9 @@ export function VendorTradeSpecialtiesField({
);
}}
/>
{/* Outside the spaced Stack: an always-mounted live region would otherwise inherit Stack
spacing and add a permanent gap under the trades input, shifting the approved layout. */}
<TradeFeedbackRegions listUnavailable={tradeOptions.length === 0} message={feedbackMessage} />
</Box>
);
}