fix(ui): let SettingsFieldRow labels truncate on long content (#3196)

* test: reproduce long passkey label overlay (issue #3181)

* fix(ui): let SettingsFieldRow labels truncate on long content

---------

Co-authored-by: Repro Agent <repro-agent@example.com>
This commit is contained in:
Gautam0507
2026-08-28 23:02:50 +03:00
committed by GitHub
co-authored by Repro Agent
parent 339c88783c
commit 8fee25c97b
3 changed files with 74 additions and 3 deletions
@@ -219,7 +219,7 @@ export const PasskeySettings: React.FC = () => {
{passkeys.map((passkey) => (
<SettingsFieldRow
key={passkey.id}
label={<span className="truncate">{passkey.label}</span>}
label={<span className="truncate" title={passkey.label}>{passkey.label}</span>}
alignEnd={false}
controlClassName="justify-between sm:flex-1"
>
@@ -310,8 +310,8 @@ export const SettingsFieldRow: React.FC<SettingsFieldRowProps> = ({
)}
>
<div className="min-w-0 @xl:w-56 @xl:shrink-0">
<div className="flex items-center gap-1.5">
<div className={SETTINGS_FIELD_LABEL_CLASS}>{label}</div>
<div className="flex min-w-0 items-center gap-1.5">
<div className={cn('min-w-0 truncate', SETTINGS_FIELD_LABEL_CLASS)}>{label}</div>
{info != null ? <SettingsInfoHint>{info}</SettingsInfoHint> : null}
</div>
{description != null ? (
@@ -0,0 +1,71 @@
import { afterEach, describe, expect, test } from 'bun:test';
import { Window } from 'happy-dom';
import React, { act } from 'react';
import { createRoot, type Root } from 'react-dom/client';
import { SettingsFieldRow } from '@/components/sections/shared/SettingsSection';
// Browser-generated passkeys (e.g. Edge on Windows) use the full user-agent
// string as the device label, so it is very long.
const longLabel =
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36 Edg/125.0.0.0';
let windowInstance: Window;
let host: HTMLDivElement;
let root: Root;
afterEach(() => {
root?.unmount();
});
describe('PasskeySettings long device-name label (issue #3181)', () => {
test('label truncation requires min-w-0 on every flex ancestor', async () => {
windowInstance = new Window({ width: 1000, height: 800 });
Object.assign(globalThis, {
window: windowInstance,
document: windowInstance.document,
HTMLElement: windowInstance.HTMLElement,
Element: windowInstance.Element,
Node: windowInstance.Node,
IS_REACT_ACT_ENVIRONMENT: true,
});
host = document.createElement('div');
document.body.appendChild(host);
root = createRoot(host);
await act(async () => {
root.render(
// Mirrors the PasskeySettings row (label = passkey.label with `truncate`).
<SettingsFieldRow
label={<span className="truncate">{longLabel}</span>}
alignEnd={false}
controlClassName="justify-between sm:flex-1"
>
<span className="typography-meta text-muted-foreground truncate">Added Aug 27, 2026</span>
<button>Delete</button>
</SettingsFieldRow>,
);
});
// For `truncate` (overflow:hidden + text-overflow:ellipsis + white-space:nowrap)
// to constrain a flex child, every flex ancestor between the row and the
// truncated element must carry min-w-0. Without it, the flex item's default
// min-width:auto stops it shrinking, and the long text overflows the 224px
// label column, overlaying the date and delete button in the control column.
const row = host.firstElementChild as HTMLElement;
const labelColumn = row?.firstElementChild as HTMLElement;
const innerFlex = labelColumn?.firstElementChild as HTMLElement;
const labelDiv = innerFlex?.firstElementChild as HTMLElement;
const labelSpan = labelDiv?.firstElementChild as HTMLElement;
const flexAncestors = [labelColumn, innerFlex, labelDiv].filter(Boolean);
const missingMinW0 = flexAncestors.filter(
(el) => !String(el.className).split(/\s+/).includes('min-w-0'),
);
expect(labelSpan?.className.split(/\s+/)).toContain('truncate');
// Repro: two intermediate flex containers lack min-w-0, so the ellipsis is
// inert and the long label overflows its column.
expect(missingMinW0.length).toBe(0);
});
});