Skip to content

feat(app-ui): unified overlay layering, focus and scroll lock; public Portal and hooks - #874

Merged
cakeinpanic merged 2 commits into
mainfrom
fix/app-ui-review-3-overlays
Oct 30, 2026
Merged

cakeinpanic merged 2 commits into
mainfrom
fix/app-ui-review-3-overlays

Conversation

@exromany

@exromany exromany commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

Overlays (Modal, Popover, Tooltip, TokenSelector) now share one layer stack for Escape, outside click, focus and scroll lock, and the hooks are public.

What / why

  • Public hooks (A-17, O-12): Portal, useEscape, useFocusTrap, useOutsideClick and useBodyScrollLock are exported with { enabled, … } options. Why: consumer drawers and sheets couldn't join the stacks, and the flag sat in a different position in each hook.
  • Escape (O-1, O-17): one Escape closes only the innermost overlay, and Escape during IME composition is ignored. Why: Tooltip and TokenSelector listened outside the stack, so one Escape also closed the enclosing Modal.
  • Focus trap (X-2, O-4): follows the browser tab order. It skips tabindex=-1, display:none and inert subtrees, and a radio group is one stop. Why: Tab could leave the Modal.
  • Modal:
    • autoFocus children keep focus, focus returns to the opener on close, and there is a new initialFocus (O-2).
    • Scrim clicks go through the outside-click stack, so a nested Popover closes first (O-3).
    • New open prop and ModalCard export (O-10).
    • The title gets role=heading, and the subtitle is wired as aria-describedby (O-22).
    • Close stays pinned on full-screen mobile (X-6).
    • Text stays readable in nested theme scopes (X-1).
    • Opt-in inertOthers, with data-lido-overlay-keep for toast roots (O-5).
  • Popover:
    • Caps itself to the viewport and repositions when the panel or alignRef resizes (O-7, O-14).
    • Hover popovers no longer steal focus (O-8).
    • Wires aria-haspopup, aria-expanded and aria-controls on the anchor, and warns in dev when the dialog is unnamed (O-9).
    • ref, dataTestId, and any px offset (O-19).
  • Tooltip (O-6, F-11): portaled inside a dark ThemeProvider, so it looks the same in any scope. The bubble is opaque with a shadow (Figma), and it describes the focused child.
  • TokenSelector (O-13): the list renders through Popover, so it is no longer clipped by a Modal card's overflow: clip (supply/borrow flows). The dialog and listbox are named by listLabel.
  • Touch (O-16): outside dismiss fires on tap, so starting a scroll no longer closes overlays.
  • Scroll lock (O-15): locks <html> when it is the scroller and exposes --lido-app-ui-scrollbar-width. Why: the old body-only lock detached sticky headers.
  • Portal (O-20): leaves no hidden marker <span> in consumer DOM.
  • Pagination: Escape no longer commits the draft page, Enter fires onPageChange once, and there is a new labels.pageSize.

Watch for

  • TokenSelector is the riskiest change. The panel is portaled to <body>, and aria-haspopup is now dialog. e2e that relies on DOM shape (within(root)) breaks, although dataTestId.panel still resolves.
  • Tests doing fireEvent.mouseDown(overlay) + click no longer close a Modal. Use pointerDown or userEvent.click.
  • Code reading body.style.overflow may miss the scroll lock.
  • feat/app-ui-sidebar calls the old hook signatures and won't compile after the merge.

Screenshots (Storybook)

O-3: A Popover open inside a Modal. An outside press or Escape closes only the popover.

after

O-5: With inertOthers the page is inert; the toast host marked data-lido-overlay-keep stays live.

after

F-11: Dark theme: the bubble is opaque with a light border and shadow, so it separates from the page.

Before

before

After

after

O-8: Keyboard focus on the trigger now opens the preview. Before, only mouse hover did.

Before

before

After

after

X-1: Nested DarkThemeProvider on a light page: card text keeps the dark-scope colour.

after

X-6: At 375px, after scrolling, the body rows slide under the pinned title and Close.

after

O-13: Same look when open, but the list is now a portaled, focus-trapped dialog, so a Modal card cannot clip it.

Before

before

After

after

O-21, B-1: Nested modals: Escape closes only the inner one, and scroll stays locked until the last closes.

after

Stack 4/7.

@exromany
exromany requested a review from a team as a code owner October 25, 2026 18:50
@exromany
exromany added this pull request to stack #879 October 25, 2026 19:57
@preview-stands

preview-stands Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

Preview stand status

Deploying...

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved focus-trapping and inert-state defects can make dialogs unusable or leave background controls interactive, alongside popover clipping, scroll restoration, and portal theme-scope issues.

6 open issues
What changed in this PR

This PR unifies overlay behavior in lido-app-ui and exposes reusable primitives so consumer-built overlays can participate in the same dismissal, focus, and scroll-lock handling.

Changes:

  • Publishes Portal and overlay hooks; adds Modal visibility, initial-focus, inert-background, and bare-card APIs.
  • Updates Popover positioning and accessibility, moves TokenSelector into a portal, and gives Tooltip a consistent dark theme.
  • Fixes Pagination draft handling and expands interaction tests and Storybook examples.
File Description
packages/​lido-app-ui/​src/​tooltip/​Tooltip.tsx Uses shared Portal and Escape handling; describes focused children.
packages/​lido-app-ui/​src/​tooltip/​Tooltip.test.tsx Tests visibility, theming, and accessible descriptions.
packages/​lido-app-ui/​src/​tooltip/​Tooltip.stories.tsx Removes redundant documentation metadata.
packages/​lido-app-ui/​src/​tooltip/​Tooltip.module.css Applies opaque themed colors and shadow.
packages/​lido-app-ui/​src/​token-selector/​TokenSelector.tsx Renders options through a named Popover.
packages/​lido-app-ui/​src/​token-selector/​TokenSelector.test.tsx Tests portal rendering, selection, dismissal, and naming.
packages/​lido-app-ui/​src/​token-selector/​TokenSelector.stories.tsx Simplifies story metadata.
packages/​lido-app-ui/​src/​token-selector/​TokenSelector.module.css Adapts dropdown styling for Popover.
packages/​lido-app-ui/​src/​smoke.test.tsx Checks ModalCard and public overlay exports.
packages/​lido-app-ui/​src/​popover/​Popover.tsx Adds sizing constraints, resize observation, anchor attributes, and refs.
packages/​lido-app-ui/​src/​popover/​Popover.test.tsx Expands positioning, focus, accessibility, and theme tests.
packages/​lido-app-ui/​src/​popover/​Popover.stories.tsx Adds hover-pinning, tall-content, and dark-theme examples.
packages/​lido-app-ui/​src/​popover/​Popover.module.css Uses spacing tokens and explicit typography.
packages/​lido-app-ui/​src/​popover/​index.ts Exports the test-ID type.
packages/​lido-app-ui/​src/​pagination/​Pagination.tsx Separates draft cancellation from commit and labels page size.
packages/​lido-app-ui/​src/​pagination/​Pagination.test.tsx Tests single commits, cancellation, and localized naming.
packages/​lido-app-ui/​src/​pagination/​Pagination.stories.tsx Removes redundant documentation metadata.
packages/​lido-app-ui/​src/​modal/​Modal.tsx Adds controlled visibility, focus options, inert handling, and ModalCard.
packages/​lido-app-ui/​src/​modal/​Modal.test.tsx Covers nested overlays, focus restoration, inert state, and dismissal.
packages/​lido-app-ui/​src/​modal/​Modal.stories.tsx Demonstrates mobile, nested, themed, and inert-background behavior.
packages/​lido-app-ui/​src/​modal/​Modal.module.css Pins the mobile header and preserves themed text.
packages/​lido-app-ui/​src/​modal/​index.ts Exports ModalCard and its props.
packages/​lido-app-ui/​src/​index.ts Publishes overlay primitives and types.
packages/​lido-app-ui/​src/​hooks/​useOutsideClick.ts Defers touch dismissal until pointer release.
packages/​lido-app-ui/​src/​hooks/​useHideOthers.ts Adds inert-background management.
packages/​lido-app-ui/​src/​hooks/​useFocusTrap.ts Updates tabbable filtering, radio handling, and initial focus.
packages/​lido-app-ui/​src/​hooks/​useEscape.ts Standardizes options and ignores composition events.
packages/​lido-app-ui/​src/​hooks/​useEdgeFade.ts Adds horizontal overflow indicators and navigation support.
packages/​lido-app-ui/​src/​hooks/​useEdgeFade.test.tsx Tests fade state, wheel handling, and keyboard focus.
packages/​lido-app-ui/​src/​hooks/​useBodyScrollLock.ts Selects the page scroller and exposes scrollbar compensation.
packages/​lido-app-ui/​src/​hooks/​Portal.tsx Removes the marker after resolving the portal target.
packages/​lido-app-ui/​src/​hooks/​index.ts Exports additional internal hooks.
packages/​lido-app-ui/​src/​hooks/​hooks.test.tsx Expands overlay-hook and Portal coverage.
packages/​lido-app-ui/​src/​hooks/​Hooks.stories.tsx Documents public hook signatures and composition.

Review effort: Balanced

@cakeinpanic cakeinpanic left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please resolve copilot comments

@exromany
exromany force-pushed the fix/app-ui-review-3-overlays branch from e3bc15a to 56560e8 Compare October 28, 2026 17:29
@exromany
exromany requested a review from cakeinpanic October 28, 2026 17:30
@exromany
exromany force-pushed the fix/app-ui-review-3-overlays branch from 56560e8 to aced85c Compare October 28, 2026 20:27
@exromany
exromany force-pushed the fix/app-ui-review-3-overlays branch from aced85c to f02c5c4 Compare October 29, 2026 14:55
@cakeinpanic
cakeinpanic force-pushed the fix/app-ui-review-3-overlays branch from f02c5c4 to 6d98d81 Compare October 30, 2026 10:40
Base automatically changed from fix/app-ui-review-2-layout to main October 30, 2026 10:42
…Portal and overlay hooks

- Portal and overlay hooks (useEscape, useOutsideClick, useFocusTrap, useBodyScrollLock, useEdgeFade) exported
- Modal: focus trap, scroll lock, inert siblings, ModalCard
- Popover and Tooltip: shared z-index layers, escape/outside-click handling, a11y labelling
- TokenSelector and Pagination a11y and behaviour fixes
Portal keeps theme scope on container→undefined; scroll lock restores overflow axes; radio groups count as one tab stop; stacked inertOthers modals coordinate inert; popover maxHeight follows available room.
@cakeinpanic
cakeinpanic force-pushed the fix/app-ui-review-3-overlays branch from 6d98d81 to 3c8f46a Compare October 30, 2026 10:42
@cakeinpanic
cakeinpanic merged commit 3cf71c8 into main Oct 30, 2026
8 checks passed
@cakeinpanic
cakeinpanic deleted the fix/app-ui-review-3-overlays branch October 30, 2026 10:43
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.

3 participants