fix(hosts): keep compact row actions clear of host labels

Give compact connected-host rows separate text and action widths so long names and producer labels cannot compete with primary controls.
This commit is contained in:
Mohamed Boudra
2026-10-21 10:33:12 +02:00
parent 7b4bc87f9a
commit 8b658bdb1e
4 changed files with 63 additions and 3 deletions
@@ -1,5 +1,8 @@
import { test } from "../support/fixtures";
import {
expectConnectedHostControlsFit,
closeConnectedHostDetails,
toggleConnectedHost,
openRestartableConnectedHosts,
beginConnectedHostDraft,
reconnectManagingHost,
@@ -64,3 +67,33 @@ test("connection loss retains the unsaved name and credential input", async ({ p
await host.close();
}
});
import { startIsolatedHostDaemon } from "../support/helpers/isolated-host-daemon";
test.describe("compact connected host controls", () => {
test.use({ viewport: { width: 390, height: 844 } });
test("long host names keep both actions visible with details open and closed", async ({
page,
}, testInfo) => {
const destination = await startIsolatedHostDaemon("srv_connected_layout");
const label = "Production build host — development-infrastructure-europe-west";
try {
await openCompactConnectedHosts(page);
await addConnectedHost(page, `127.0.0.1:${destination.port}`, label);
await expectConnectedHostControlsFit(page, label, true);
await inspectConnectedHost(page, label);
await expectConnectedHostControlsFit(page, label, true);
await closeConnectedHostDetails(page);
await toggleConnectedHost(page, true);
await expectConnectedHostControlsFit(page, label, false);
await inspectConnectedHost(page, label);
await expectConnectedHostControlsFit(page, label, false);
await page.screenshot({ path: testInfo.outputPath("connected-host-controls.png") });
await closeConnectedHostDetails(page);
await toggleConnectedHost(page, false);
await expectConnectedHostControlsFit(page, label, true);
} finally {
await destination.close();
}
});
});
@@ -115,3 +115,18 @@ export async function editConnectedHost(page: Page) {
page.getByRole("textbox", { name: "Replacement connection", exact: true }),
).toHaveValue("");
}
export async function expectConnectedHostControlsFit(page: Page, label: string, enabled: boolean) {
const details = page.getByRole("button", { name: `Details for ${label}`, exact: true });
const toggle = page.getByRole("button", { name: enabled ? "Disable" : "Enable", exact: true });
await details.scrollIntoViewIfNeeded();
await expect(details).toBeInViewport({ ratio: 1 });
await expect(toggle).toBeInViewport({ ratio: 1 });
}
export async function closeConnectedHostDetails(page: Page) {
await page.getByRole("button", { name: "Cancel", exact: true }).click();
}
export async function toggleConnectedHost(page: Page, enabled: boolean) {
await page.getByRole("button", { name: enabled ? "Disable" : "Enable", exact: true }).click();
await expect(page.getByText(enabled ? /disabled · Disabled/ : /online · Enabled/)).toBeVisible();
}
+13 -3
View File
@@ -31,6 +31,7 @@ export { SettingsCollapsibleRow } from "./collapsible-row";
interface AppSettingsRowProps extends Omit<SettingsRowProps, "hint"> {
hint?: ReactNode;
labelAccessory?: ReactNode;
stacked?: boolean;
}
export function SettingsCard({ children, testID }: { children: ReactNode; testID?: string }) {
@@ -55,12 +56,16 @@ export function SettingsRow({
error,
children,
testID,
stacked = false,
}: AppSettingsRowProps) {
const compact = useIsCompactFormFactor();
const rowStyle = useMemo(() => [settingsStyles.row, compact && styles.compactRow], [compact]);
const rowStyle = useMemo(
() => [settingsStyles.row, compact && styles.compactRow, stacked && styles.stackedRow],
[compact, stacked],
);
return (
<View style={rowStyle} testID={testID}>
<View style={styles.label}>
<View style={[styles.label, stacked && styles.stackedLabel]}>
{labelAccessory ? (
<View style={styles.labelRow}>
<Text style={[settingsStyles.rowTitle, styles.accessoryLabel]}>{label}</Text>
@@ -76,7 +81,9 @@ export function SettingsRow({
</Text>
) : null}
</View>
{children ? <View style={styles.control}>{children}</View> : null}
{children ? (
<View style={[styles.control, stacked && styles.stackedControl]}>{children}</View>
) : null}
</View>
);
}
@@ -193,6 +200,9 @@ export function SettingsAction({ actionLabel, onPress, disabled, ...row }: Setti
}
const styles = StyleSheet.create((theme) => ({
stackedRow: { flexDirection: "column", alignItems: "stretch", gap: theme.spacing[3] },
stackedLabel: { flexBasis: "auto", marginRight: 0, minWidth: 0 },
stackedControl: { width: "100%" },
compactRow: { flexWrap: "wrap", gap: theme.spacing[3] },
label: { flexGrow: 1, flexShrink: 1, flexBasis: 160, marginRight: theme.spacing[3] },
labelRow: {
@@ -146,10 +146,12 @@ function HostRow({
canManage: boolean;
pending: boolean;
}) {
const compact = useIsCompactFormFactor();
const edit = useCallback(() => model.edit(host), [host, model]);
const toggle = useCallback(() => void model.setEnabled(host), [host, model]);
return (
<SettingsRow
stacked={compact}
label={host.label ?? host.serverId ?? host.registrationId}
hint={`${host.status} · ${host.enabled ? "Enabled" : "Disabled"} · ${sourceLabel(host.source.id)}`}
>