Skip to content

fix(directory): PrincipalPicker UI polish + Radix Dialog pointer-events bug#424

Open
larsgeorge-db wants to merge 1 commit into
feat/directory-phase4from
fix/principal-picker-pointer-events
Open

fix(directory): PrincipalPicker UI polish + Radix Dialog pointer-events bug#424
larsgeorge-db wants to merge 1 commit into
feat/directory-phase4from
fix/principal-picker-pointer-events

Conversation

@larsgeorge-db
Copy link
Copy Markdown
Collaborator

Stacks on #417 (Phase 4). Post-merge polish surfaced during manual QA against the file-backed directory provider. Merge after #417.

Root cause: Radix Dialog locks pointer-events: none on <body>

The picker's type-ahead Popover is portaled to <body> and therefore inherited that lock, making every result row unclickable inside any Dialog or Sheet host (Role form, Team form, Owner dialog, DC wizard, Workflow Designer sheet, Comment sidebar sheet, etc.). Clicks landed on the dialog's own help-text <p> instead.

Verified live via Playwright: elementsFromPoint(rowCenter) returned the dialog subtree, and ancestor walk on the popover row showed pointer-events: none propagating all the way from <body>:

BUTTON                     pointer-events: none
LI                         pointer-events: none
UL                         pointer-events: none
DIV (popover content)      pointer-events: none
DIV (fixed z-50 wrapper)   pointer-events: none
BODY                       pointer-events: none   ← Radix Dialog sets this

Fix: explicit style={{ pointerEvents: 'auto' }} on PrincipalPicker's PopoverContent. One-line change in the component covers all 18 call sites without touching any consumer.

Other UX issues fixed in the same pass

  • Browse sub-dialog always closes after a pick — even in multi-select. Browse is the discovery path; the inline type-ahead is the fast path for chaining adds. Previous behaviour added the badge but left the dialog open, which was confusing.
  • Browse sub-dialog resets query + filter on re-open, via useEffect on open. Several close paths (Close button, parent state change, single-select auto-close) bypassed the prior onOpenChange-based reset, so re-opening showed stale search text.
  • Inline row onMouseDown={preventDefault} keeps focus on the input so the popover's dismiss-on-blur can't race the click handler. Defensive even with the pointer-events fix in place.
  • SelectionBadges rendered below the input row (was inline). Pills can grow tall on multi-select; below-input layout matches Gmail-style chip lists and keeps the input width stable.
  • Dropped stale "(comma-separated)" suffix from the Assigned Directory Groups label. With the directory configured, badges are discrete — the hint was misleading. A self-rendered manual-mode hint inside the picker ("Press Enter, Tab, or comma to add an entry.") now shows only when the directory is unconfigured. i18n updated across en, de, es, fr, it, ja, nl.

Audit: all 18 call sites benefit

Because the fix lives inside PrincipalPicker, every consumer inherits it. Hosts surveyed:

Host Files #
Dialog role-form, tags-settings, assign-owner, team-form, mdm/create-review, dp/team-member, dc/team-member, data-asset-reviews/create-review-request, dc/wizard (×6) 14
Sheet workflows/workflow-designer (×2), comments/comment-sidebar 3
Page (no portal) views/entitlements 1

Sheet behaves identically to Dialog (both built on @radix-ui/react-dialog), so the same fix unbroke the workflow-designer Sheet pickers and the comment sidebar Sheet picker. The page-level entitlements call is unaffected — the fix is a no-op there.

Tests

principal-picker.test.tsx: 19 passed (was 13). New cases:

  • badge-below-input order
  • dialog reset-on-open
  • manual-mode hint visibility (configured vs unconfigured)
  • close-after-pick in multi-select
  • regression: row mousedown is default-prevented

Manual verification (Playwright-driven live browser)

  1. Settings → Roles → Edit Role (Admin)
  2. Type pro → popover opens with "Data Producers" row
  3. Click row → badge added (`["admins", "Data Producers"]`), input cleared ✓
  4. Click Browse directory → type con → click "Data Consumers" row → badge added, dialog closed ✓
  5. Reopen Browse after closing with con still in input → input is empty ✓
  6. Label reads Assigned Directory Groups (no "(comma-separated)") ✓
  7. Badges render below the input ✓

Test plan

  • Unit: `yarn vitest run src/components/common/principal-picker.test.tsx` → 19/19
  • Manual: inline pick + Browse pick verified in live browser on Role form
  • Spot-check workflow designer (Sheet host) and comment sidebar (Sheet host) — reviewer welcome to run the same scenario there

Caveat (out of scope)

The underlying Radix pattern affects any portaled interactive content (custom Popover, DropdownMenu, Combobox/cmdk) rendered inside our Dialogs/Sheets. This PR fixes the directory lookup path. A broader codebase sweep for the same pattern is a follow-up.

…ts bug

Stacks on Phase 4 (#417). Post-merge polish surfaced during manual QA
against the file-backed directory provider.

Pointer-events (root cause):
- Radix Dialog locks ``pointer-events: none`` on <body> while open.
  The picker's type-ahead Popover is portaled to <body> and therefore
  inherited the lock, making every result row unclickable inside any
  Dialog or Sheet host (Role form, Team form, Owner dialog, DC wizard,
  workflow designer Sheet, comment sidebar Sheet, etc.). Clicks landed
  on the dialog's own help-text <p> instead. Verified end-to-end via
  Playwright (elementsFromPoint returned the dialog subtree).
- Fix: explicit ``style={{ pointerEvents: 'auto' }}`` on the picker's
  PopoverContent so the portaled popover subtree overrides the body
  lock. One-line change inside PrincipalPicker covers all 18 call
  sites without touching any consumer.

Browse sub-dialog UX:
- Always close after a pick, even in multi-select. Browse is the
  discovery path; the inline type-ahead is the fast path for adding
  several entries in succession. Avoids the previous confusing
  "added but the dialog stayed open" behaviour.
- Reset query + filter when the dialog re-opens (via useEffect on
  ``open``), not on the close path. Several close paths (Close
  button, parent state change, single-select auto-close) bypassed
  the prior onOpenChange-based reset, so re-opening showed stale
  search text.

Inline type-ahead focus:
- onMouseDown={preventDefault} on PrincipalRow keeps focus on the
  input so the popover's dismiss-on-blur doesn't race the click
  handler. Defensive even with the pointer-events fix in place.

Layout + labels:
- SelectionBadges now render below the input row (was inline). Pills
  can grow tall on multi-select; below-input layout matches Gmail-
  style chip lists and keeps the input width stable.
- Drop the stale "(comma-separated)" suffix from the Assigned
  Directory Groups label. With the directory configured the badges
  are discrete, not comma-separated, so the hint was misleading.
  Self-rendered manual-mode hint inside the picker shows
  "Press Enter, Tab, or comma to add an entry." only when the
  directory is unconfigured. i18n updated across en, de, es, fr,
  it, ja, nl.

Tests:
- principal-picker.test.tsx: 19 passed (was 13). New cases cover the
  badge-below-input order, dialog reset-on-open, manual-mode hint
  visibility, close-after-pick in multi-select, and a regression
  test asserting row mousedown is default-prevented.

Verified live via Playwright: inline pick (Edit Role -> "pro" ->
Data Producers added as badge, input cleared); Browse dialog pick
(search "con" -> Data Consumers added, dialog closed); reset-on-
reopen confirmed empty input after closing with text and reopening.
@larsgeorge-db larsgeorge-db requested a review from a team as a code owner May 22, 2026 11:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant