diff --git a/packages/ui/src/components/sections/openchamber/PasskeySettings.tsx b/packages/ui/src/components/sections/openchamber/PasskeySettings.tsx index 5dcbc15c..450410d6 100644 --- a/packages/ui/src/components/sections/openchamber/PasskeySettings.tsx +++ b/packages/ui/src/components/sections/openchamber/PasskeySettings.tsx @@ -219,7 +219,7 @@ export const PasskeySettings: React.FC = () => { {passkeys.map((passkey) => ( {passkey.label}} + label={{passkey.label}} alignEnd={false} controlClassName="justify-between sm:flex-1" > diff --git a/packages/ui/src/components/sections/shared/SettingsSection.tsx b/packages/ui/src/components/sections/shared/SettingsSection.tsx index 7672b8a2..5cc520f9 100644 --- a/packages/ui/src/components/sections/shared/SettingsSection.tsx +++ b/packages/ui/src/components/sections/shared/SettingsSection.tsx @@ -310,8 +310,8 @@ export const SettingsFieldRow: React.FC = ({ )} >
-
-
{label}
+
+
{label}
{info != null ? {info} : null}
{description != null ? ( diff --git a/packages/ui/src/components/sections/shared/fieldrow.repro.test.tsx b/packages/ui/src/components/sections/shared/fieldrow.repro.test.tsx new file mode 100644 index 00000000..a13820db --- /dev/null +++ b/packages/ui/src/components/sections/shared/fieldrow.repro.test.tsx @@ -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`). + {longLabel}} + alignEnd={false} + controlClassName="justify-between sm:flex-1" + > + Added Aug 27, 2026 + + , + ); + }); + + // 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); + }); +});