bug(persons): person selection dropdown is visually clipped / cut off #343
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Bug
The person dropdown (PersonTypeahead / PersonMultiSelect) is clipped by a parent container — list items below the fold are hidden and unreachable.
Steps to reproduce
Expected behavior
The dropdown overflows its container (via
overflow: visible, absolute/fixed positioning, or a portal) so all options are visible and scrollable.Acceptance criteria
👨💻 Markus Keller — Application Architect
Observations
The root cause is a split-brain positioning strategy:
PersonTypeaheadusesposition: absoluteon adiv.relativewrapper (CSS stacking context inside the card), whilePersonMultiSelecthas already solved this correctly — it usesfixedpositioning computed viagetBoundingClientRect(). The problem is thatPersonTypeahead's dropdown sits inside the.rounded-sm.border.border-line.bg-surface.p-6card container inWhoWhenSection.svelte, and anything that establishes a stacking context (even justshadow-smcombined withz-on a sibling) can clip it.The fix is a pure CSS/positioning concern, not an architectural issue. No new abstraction, no portal library. The
PersonMultiSelectapproach (fixed+getBoundingClientRect) already exists in the codebase and works —PersonTypeaheadjust needs to adopt the same pattern.The
ConversationFilterBar.sveltedefensively patches this withrelative z-30on each column wrapper andrelative z-10on the lower grid — a workaround, not a fix. This kind of scattered compensation code is the signal that the component itself needs to be corrected.Recommendations
PersonTypeaheadto use the same fixed-position approach asPersonMultiSelect: bind a ref to the input element, callgetBoundingClientRect()on open, and setstyle="position:fixed;top:…;left:…;width:…"on the dropdowndiv. Addsvelte:windowlisteners for scroll and resize (same asPersonMultiSelect).PersonTypeaheadis fixed, remove thez-30/z-10z-index workarounds fromConversationFilterBar.svelte— they become dead code.TagInput, etc.)" is important:TagInputusesposition: absolutewithin arelativewrapper inside the chip row's innerdiv.relative.min-w-[120px]. That innerrelativeis correctly scoped below any card-level clipping context, soTagInputis likely unaffected — but verify visually.💻 Felix Brandt — Fullstack Developer
Observations
PersonTypeahead(line 144–167): The dropdown usesclass="absolute top-full left-0 z-50 …". The outer wrapper<div class="relative">is the positioning ancestor. When any ancestor element in the DOM tree creates a new stacking context (viaoverflow,transform,will-change, or even combinedposition+z-index), theabsolutedropdown is clipped to that ancestor's bounds. This is the confirmed root cause.PersonMultiSelect(lines 21–25 + 107–111): Already solved the same problem correctly. It computesdropdownStyleviagetBoundingClientRect()and usesposition:fixed, escaping any CSS stacking context. It also registerssvelte:window onscrollandonresizehandlers to keep the position current.Inconsistency: Two components in the same component library use different positioning strategies for the same visual pattern. The
PersonMultiSelectpattern is correct — apply it toPersonTypeahead.ConversationFilterBar.svelte: Therelative z-30defensive wrapper on eachPersonTypeaheadcolumn is a symptom of the bug, not a fix. It partially works for the filter bar's own stacking context but fails in other parent containers.Code smell:
PersonMultiSelectuses bareletstate (let results,let showDropdown,let loading,let debounceTimer) alongside Svelte 5 runes. The debounce timer is a rawsetTimeoutwith manual cleanup — no$effectcleanup. This is a minor technical debt but separate from this bug.Recommendations
let inputEl: HTMLInputElementbinding andlet dropdownStyle = $state('')toPersonTypeahead(it doesn't have them yet).updateDropdownPosition()fromPersonMultiSelectand replicate inPersonTypeahead— or better, extract it to a shared utility functioncomputeFixedDropdownStyle(el: HTMLElement): stringin$lib/utils/dom.ts.absolute top-full left-0classes on the dropdowndivwithstyle={dropdownStyle}and remove the positioning classes entirely.<svelte:window onscroll={updateDropdownPosition} onresize={updateDropdownPosition} />toPersonTypeahead.relative z-30workarounds inConversationFilterBar.svelte.🔒 Nora "NullX" Steiner — Security Engineer
Observations
fixed-position approach (copyingPersonMultiSelect) renders the dropdown in a layer that escapes the card boundary — no DOM mutation, no innerHTML, no eval. ThegetBoundingClientRect()values are read-only geometry — no XSS vector.PersonTypeaheadalready usesrole="button"andtabindex="0"on each dropdown item. After the positioning fix, verify that keyboard Tab navigation andEnterselection still work correctly (they should be unaffected by the CSS change).fetchcalls insidePersonTypeaheadgo to/api/persons?q=…with user-typed input passed as a URL-encoded query param — this is correct. The backend is responsible for parameterized query handling.role="button"but are<div>elements. This is not a security issue, but it is an accessibility gap (see UX persona).Recommendations
PersonMultiSelectpattern to copy is already safe.clickOutsideaction still dismisses the dropdown correctly — afixed-positioned dropdown that interceptspointerdownevents improperly could break the dismiss behavior, but the existingclickOutsideaction checksnode.contains(event.target)which works regardless of CSS positioning.fixedback toabsolute, the clip bug returns silently. Consider a comment in the component explaining whyfixedpositioning is intentional.🧪 Sara Holt — QA Engineer
Observations
PersonTypeaheadis used in six files:SearchFilterBar,PersonMergePanel,CorrespondenzHero,CorrespondenzPersonBar,ConversationFilterBar, andWhoWhenSection(document edit/new). The fix must be verified at each call site, at minimum at the two most critical ones (document edit form and the conversation filter bar).PersonMultiSelectalready usesfixedpositioning — so that component is not affected and does not need regression tests for this specific bug. But it does lack keyboard interaction tests.Recommendations
/documents/{id}/edit), click the sender typeahead, type a known name, and assert that the first suggestionlielement isvisible(not occluded by a sibling). Usepage.locator('.person-dropdown-item').first().isVisible()or equivalent.clickOutsidedismiss behavior after the fix: clicking outside the dropdown while it is infixedposition should still close it. This is a behavior edge case thatfixed-positioned elements sometimes break.Open Decisions
person-typeahead.spec.ts? This is a workflow question for the contributor.🎨 Leonie Voss — UX Design Lead
Observations
The clipping bug means that on document edit, the person picker's dropdown list is unreachable for users who cannot mouse outside the card boundary. For the 60+ transcribers using a laptop or tablet, this is a Critical blocker — they cannot select a sender without the dropdown being visible.
The dropdown items in
PersonTypeaheaduserole="button"on<div>elements. The correct ARIA pattern for a combobox dropdown isrole="listbox"on the container androle="option"on each item.TagInputalready usesrole="listbox"androle="option"—PersonTypeaheadshould be aligned to the same pattern.PersonMultiSelectalso uses<div role="button">— both should be corrected.There is no keyboard navigation (ArrowDown/ArrowUp) in
PersonTypeahead.TagInputhas full arrow-key navigation implemented. After fixing the visual bug, the keyboard experience remains incomplete. For the transcriber audience (often 60+, possibly using keyboard navigation), this is a High accessibility gap.The
PersonMultiSelectremove button hasaria-label={m.comp_multiselect_remove()}— good. But the remove buttonclass="ml-0.5 text-ink/50 hover:text-red-500 focus:outline-none"removes the focus ring entirely (focus:outline-nonewith no replacement). This fails WCAG 2.2 Focus Appearance.Touch target on each dropdown item:
py-2gives ~8px top+bottom padding ontext-sm(~20px line height) = ~36px total. WCAG 2.2 requires 24×24px minimum target area; 44px is the recommended threshold. At 36px this passes the minimum but misses the preferred threshold. For the 60+ audience, recommendpy-3(48px total).Recommendations
PersonMultiSelect'sfixed+getBoundingClientRect()pattern.PersonTypeaheaddropdown markup fromdiv[role="button"]to aul[role="listbox"]+li[role="option"]structure, matchingTagInput. Addaria-expandedon the input andaria-activedescendantpointing to the active option.PersonTypeahead, matchingTagInput's existinghandleKeydownpattern.focus:outline-noneon the remove button inPersonMultiSelectwithfocus-visible:ring-2 focus-visible:ring-focus-ring. Same fix needed on the same button inTagInput.py-3in bothPersonTypeaheadandPersonMultiSelectfor 48px touch targets.Open Decisions
PersonTypeaheadinput does not announce the dropdown open/close state to screen readers. Should this issue's scope include addingaria-expandedandaria-controls/aria-ownsfor a full combobox ARIA pattern, or is that a separate accessibility issue?⚙️ Tobias Wendt — DevOps & Platform
Observations
PersonTypeahead.svelteand optionallyPersonMultiSelect.sveltealignment). The SvelteKit build pipeline handles this without changes.page.setViewportSize({ width: 768, height: 1024 })in the test.Recommendations
npm run test:e2eas part of the merge check — if it does not, add it so the new regression tests actually gate the PR.npm run generate:apiis not needed.📋 Elicit — Requirements Engineer
Observations
Issue quality: The issue is clear, reproducible, and P1-high / bug-labeled — appropriate for the severity. The acceptance criteria cover the visible fix and the no-regression requirement. Good structure.
AC gap — ARIA/keyboard: The acceptance criteria only specify "all list items are visible and scrollable." There is no criterion for keyboard accessibility (ArrowDown/ArrowUp navigation, Enter to select, Escape to close). Given that
PersonTypeaheadlacks these capabilities entirely (whileTagInputhas them), and the audience includes 60+ users who may rely on keyboard navigation, this is a missing acceptance criterion for a component that is also a de-facto form control.AC gap — tablet definition: "Tablet (768–1023px)" is specified but the tested entry point is not — the issue says "document edit or new document" but does not specify that the filter bar (Briefwechsel), the merge panel, and the search bar must also pass. Either scope the fix to all usages or explicitly call out which pages are in scope.
Scope creep risk: The UX reviewer identified 5 additional improvements (ARIA roles, keyboard nav, focus rings, touch targets). These are real gaps but are NOT in scope for this bug fix. They should be filed as separate issues to prevent this fix from growing into a full component overhaul.
Recommendations
enhancement(persons): PersonTypeahead keyboard navigation and ARIA combobox patternwith the ARIA and arrow-key requirements, linking it as "relates to #343."enhancement(a11y): remove button focus ring missing in PersonMultiSelect and TagInputfor the focus ring gap.Open Decisions
PersonTypeaheadbe fixed in this PR (since the dropdown is already being reworked) or deferred to the follow-up issue? The cost is low if the developer is already touching the dropdown markup.🗳️ Decision Queue
Consolidated from persona reviews. One decision per contributor needed.
Test Organization
From Sara (QA): Should the Playwright regression tests for dropdown visibility live inside an existing E2E spec file, or in a dedicated
person-typeahead.spec.ts?Context: There are six usage sites for
PersonTypeahead. A dedicated spec file keeps person-component coverage together and makes it easy to add keyboard/ARIA tests later. An existing file avoids file proliferation. Either works; the choice only matters for maintainability.Scope: Keyboard Navigation in This PR
From Leonie (UX) and Elicit (RE): Should ArrowDown/ArrowUp keyboard navigation and the
role="listbox"/role="option"ARIA correction be included in this PR, or deferred to a follow-up issue?Context: The developer is already touching the dropdown markup for the positioning fix. Adding keyboard navigation now has low marginal cost. However, it adds scope and test surface to what is filed as a P1 bug fix.
TagInputalready has the reference implementation (handleKeydownwith ArrowDown/Up/Enter/Backspace), so the pattern is available to copy.Full Combobox ARIA Pattern
From Leonie (UX): Should this PR add
aria-expanded,aria-controls, andaria-activedescendantto complete a proper WCAG combobox pattern, or is that a separate issue?Context: Without
aria-expanded, screen readers don't announce when the suggestion list opens. This is a High accessibility gap for the target audience. But it is a more involved change (requires IDs on the listbox element, dynamicaria-activedescendantupdates on keyboard navigation) and may belong in the follow-up ARIA issue.Chose whatever test pattern is more maintainable
Keyboard navigation is fixed here
Full ARIA pattern
Implementation complete — branch
feat/issue-343-person-dropdown-clippingWhat was done
Root cause fixed —
PersonTypeaheadnow uses fixed positioningThe dropdown was using
position: absoluteinside a.relativewrapper, which clipped whenever any ancestor established a new stacking context (viaoverflow,transform,shadow-sm+z-index, etc.). Adopted the same pattern already used byPersonMultiSelect:bind:this={inputEl}to the text inputupdateDropdownPosition()computingposition:fixed;top:…;left:…;width:…viagetBoundingClientRect()<svelte:window onscroll={...} onresize={...} />to keep position currentclass="absolute top-full left-0 z-50 ..."withstyle={dropdownStyle}on the dropdown containerFull ARIA combobox pattern added
role="combobox",aria-expanded,aria-haspopup="listbox",aria-controls,aria-activedescendantrole="listbox"(was a barediv)role="option"witharia-selected(wasrole="button")Keyboard navigation added (matching
TagInput's existing pattern)ArrowDown/ArrowUp— cycle through options with wrap-aroundEnter— select the highlighted optionEscape— close without selectingDead code removed from
ConversationFilterBar.svelteThe
relative z-30wrappers around eachPersonTypeaheadcolumn and therelative z-10on the lower grid were workarounds for this bug. Both removed.Commits
69b940c2fix(persons): fix PersonTypeahead dropdown clipping with fixed positioningTests
29 unit tests — all green (
PersonTypeahead.svelte.spec.ts):role="listbox",role="option",aria-expanded,aria-controls,aria-haspopuparia-activedescendantE2E regression tests added (
frontend/e2e/person-typeahead.spec.ts):aria-expandedreflects state