Skip to content

feat(popover): migrate to native CSS anchor positioning and add scroll straregies - #2355

Merged
rkaraivanov merged 10 commits into
masterfrom
rkaraivanov/popover-migration
Sep 21, 2026
Merged

rkaraivanov merged 10 commits into
masterfrom
rkaraivanov/popover-migration

Conversation

@rkaraivanov

@rkaraivanov rkaraivanov commented Aug 31, 2026 •

Copy link
Copy Markdown
Member

Description

Position igc-popover through native CSS anchor positioning in browsers that support it (Chrome/Edge 133+, Firefox 147+, Safari 26+). Other browsers keep the @floating-ui/dom behavior, and that module now loads on demand only there.

Popovers hide while their anchor is scrolled fully out of view. The new scroll-strategy attribute ('scroll' | 'hide' | 'close', default 'hide') controls this on igc-popover, dropdown, select, combo, color picker, date picker, date range picker and tooltip. With 'close' the popover emits the non-bubbling igcPopoverScrollClose event so the host that owns the open state can dismiss it. Each affected component gets an InScrollingPanel story showcasing the strategies.

INTERNAL BREAKING CHANGE: The shift, shift-padding and inline properties of igc-popover are removed - use flip to keep a popover in view. PopoverScrollStrategy drops 'block' and its default changes from 'scroll' to 'hide'; 'block' or any unknown value behaves as 'hide'.

Type of Change

  • Breaking change (fix or feature that causes existing functionality to change)
  • Documentation update
  • Refactoring (code improvements without functional changes)

Checklist

  • My code follows the project's coding standards
  • I have tested my changes locally
  • I have updated documentation if needed
  • Breaking changes are documented in the description

…l strategies

Position igc-popover through native CSS anchor positioning in browsers
that support it (Chrome/Edge 133+, Firefox 147+, Safari 26+). Other
browsers keep the @floating-ui/dom behavior, and that module now loads
on demand only there.

Popovers hide while their anchor is scrolled fully out of view. The new
scroll-strategy attribute ('scroll' | 'hide' | 'close', default 'hide')
controls this on igc-popover, dropdown, select, combo, color picker,
date picker, date range picker and tooltip. With 'close' the popover
emits the non-bubbling igcPopoverScrollClose event so the host that
owns the open state can dismiss it. Each affected component gets an
InScrollingPanel story showcasing the strategies.

BREAKING CHANGE: The shift, shift-padding and inline properties of
igc-popover are removed - use flip to keep a popover in view.
PopoverScrollStrategy drops 'block' and its default changes from
'scroll' to 'hide'; 'block' or any unknown value behaves as 'hide'.

Copilot AI 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.

🟡 Changes recommended

The updated popover test suite uses async describe callbacks (Mocha does not await suite definitions), which should be fixed to avoid unreliable or confusing test behavior.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR migrates igc-popover positioning to use native CSS anchor positioning when available, with an on-demand @floating-ui/dom fallback, and introduces a unified scroll-strategy API (scroll | hide | close) across multiple popover-based components and their Storybook stories/tests.

Changes:

  • Added a native anchor-positioning strategy for igc-popover with a floating-ui fallback that lazy-loads only when needed.
  • Introduced/updated scroll-strategy support (defaulting to hide) across popover consumers (select, dropdown, combo, date pickers, color picker, tooltip) and added “InScrollingPanel” stories.
  • Updated/expanded unit tests to cover strategy selection, anchor visibility behavior, and the new scroll close signaling/event flow.
File summaries
File Description
stories/tooltip.stories.ts Adds scrollStrategy control and an InScrollingPanel story demonstrating strategies.
stories/select.stories.ts Updates scrollStrategy options/default and adds InScrollingPanel story.
stories/dropdown.stories.ts Updates scrollStrategy options/default and revises story description to match new behavior.
stories/date-range-picker.stories.ts Adds scrollStrategy control/default and InScrollingPanel story; notes dialog-mode behavior.
stories/date-picker.stories.ts Adds scrollStrategy control/default and InScrollingPanel story; notes dialog-mode behavior.
stories/combo.stories.ts Adds scrollStrategy control/default and InScrollingPanel story.
stories/color-picker.stories.ts Adds scrollStrategy control/default and InScrollingPanel story.
src/internals/controllers/root-scroll.ts Removes the old root scroll controller (superseded by popover behavior).
src/components/types.ts Updates PopoverScrollStrategy type to `'scroll'
src/components/tooltip/tooltip.ts Adds scrollStrategy to tooltip and wires popover close-on-scroll event handling.
src/components/tooltip/tooltip.spec.ts Adds scroll-strategy tests for tooltip behavior, including sticky close behavior.
src/components/select/select.ts Removes root scroll controller usage; forwards scrollStrategy to popover and closes on scroll-close event.
src/components/select/select.spec.ts Updates scroll-strategy tests (removes old block coverage).
src/components/popover/themes/light/popover.base.scss Adds native anchor-positioning CSS rules keyed off data-anchored/placement/strategy attributes.
src/components/popover/position/types.ts Introduces shared positioning types and feature detection + test forcing hooks.
src/components/popover/position/native.ts Implements the native CSS anchor positioning strategy (including arrow updates).
src/components/popover/position/floating.ts Implements floating-ui strategy with dynamic import and scroll-strategy hiding middleware.
src/components/popover/position/arrow.ts Extracts shared arrow styling logic used by both strategies.
src/components/popover/popover.ts Refactors popover to strategy-based positioning, adds scrollStrategy, emits igcPopoverScrollClose on scroll when configured.
src/components/popover/popover.spec.ts Adds native/fallback strategy suites and new behavioral tests (placement matrix, visibility, scroll-close).
src/components/dropdown/dropdown.ts Removes root scroll controller usage; forwards scrollStrategy to popover and closes on scroll-close event.
src/components/dropdown/dropdown.spec.ts Updates scroll-strategy tests (removes old block coverage).
src/components/date-range-picker/date-range-picker-single.spec.ts Adds end-to-end smoke test for close scroll strategy on date range picker.
src/components/date-range-picker/date-range-mask-parser.spec.ts Stabilizes month spin assertions by mirroring the clamp behavior.
src/components/date-picker/date-picker.spec.ts Adds scroll-strategy tests, including dialog-mode ignore behavior.
src/components/date-picker/date-picker.base.ts Adds scrollStrategy to picker base and forwards it to popover + closes on scroll-close event.
src/components/combo/combo.ts Adds scrollStrategy to combo and forwards it to popover + closes on scroll-close event.
src/components/combo/combo.spec.ts Adds scroll-strategy tests for combo.
src/components/color-picker/color-picker.ts Adds scrollStrategy to color picker and forwards it to popover + closes on scroll-close event.
src/components/color-picker/color-picker.spec.ts Adds scroll-strategy tests for color picker.
CHANGELOG.md Documents the new scroll-strategy attribute and popover positioning change (including breaking changes).
Review details

Suppressed comments (1)

src/components/popover/popover.spec.ts:195

  • The enclosing suite is defined as describe('Non-slotted anchor element', async () => …) (line 189). Mocha suite definitions should be synchronous; describe(async () => …) is not awaited by Mocha. Remove async from that describe callback.
        const root = await fixture<HTMLElement>(createNonSlottedPopover(true));
  • Files reviewed: 31/31 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/components/popover/popover.spec.ts Outdated

Copilot AI 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.

🟡 Changes recommended

Address native capability gating and scroll-listener lifecycle issues before approval.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (5)

src/components/popover/popover.spec.ts:884

  • This gate checks only the CSS syntax support. As documented in position/native.ts, Chromium 125–132 passes these CSS.supports checks but ignores showPopover({ source }); the forced native suite will still run there and its placement assertions fail instead of exercising the fallback. The native suite needs to be gated by the implicit-anchor capability as well, or must not force native when that capability is absent.
    const describeMode =
      mode === 'native' && !SUPPORTS_ANCHOR_POSITIONING
        ? describe.skip
        : describe;

src/components/popover/popover.ts:308

  • The new public igcPopoverScrollClose event is dispatched directly, so IgcPopoverComponent has no event map and TypeScript consumers get only the generic Event listener overload. Public component events are consistently declared in an event map and exposed through EventEmitterMixin (for example src/components/tooltip/tooltip.ts:31-68); add the typed event API while preserving the required non-bubbling initialization.
  private readonly _handleRootScroll = (): void => {
    this.dispatchEvent(new CustomEvent('igcPopoverScrollClose'));

src/components/popover/popover.ts:249

  • When the anchor is removed, this path hides the native/floating container but leaves the document scroll listener registered for scrollStrategy="close" because _syncScrollStrategy(false) is not called here. The now-closed popover can continue dispatching close requests on every scroll until it is explicitly closed or disconnected. Remove the listener when hiding after anchor removal.
    this._setPopoverState(false);

src/components/popover/popover.ts:308

  • The new public igcPopoverScrollClose event is dispatched directly from a plain LitElement, so the popover has no typed event map or typed addEventListener surface. Other event-emitting components define an Igc*ComponentEventMap and use EventEmitterMixin (for example src/components/tooltip/tooltip.ts:31-36); please expose this event through the same API while preserving its non-bubbling/non-composed behavior.
    this.dispatchEvent(new CustomEvent('igcPopoverScrollClose'));

src/components/popover/position/types.ts:19

  • SUPPORTS_ANCHOR_POSITIONING gates the native path without checking position-visibility: anchors-visible, although that declaration provides the documented default hide/close behavior. The CSS below explicitly notes that Safari 26.0/26.1 ignore it, so those browsers select the native strategy and leave the popover visible after its anchor is fully clipped. Include this capability in the support gate (or route those browsers to the floating fallback).
  CSS.supports('position-area: top span-right');
  • Files reviewed: 31/31 changed files
  • Comments generated: 3
  • Review effort level: Lite

Comment thread src/components/popover/popover.ts
Comment thread CHANGELOG.md Outdated
Comment thread CHANGELOG.md Outdated
`_syncScrollStrategy` took the requested `open` state, so the document
scroll listener stayed installed when no anchor resolved and after
`_handleAnchorRemoved` had hidden the container. The popover then
emitted `igcPopoverScrollClose` on every scroll while it showed nothing,
which contradicts the documented open-only contract.

The listener now follows the container through
`isPopoverOpen(this._container)`, and `_setPopoverState` - renamed to
`_syncContainerState` - drives it. Both paths that hide the container
run through that method, so the listener cannot outlive the container. A
strongly hidden popover still matches `:popover-open`, so the `close`
strategy keeps emitting while the anchor is out of view. Adds specs for
the unresolved anchor and for an anchor that leaves the DOM.

The rest of the module gets a cleanup pass:

- the native strategy keeps one `MutationObserver` instead of a new
  observer and closure for each attach
- the three arrow listener methods collapse into `_syncArrowListeners`
- the implicit anchor probe sets `cssText` instead of twelve style
  properties
- `_createMiddleware` pushes its entries instead of filtering a
  null-padded array, and `attach` resets the container styles in one
  call
- `toggleEventListener` in `internals/utils/events.ts` replaces the
  three add and remove ternaries, and `SCROLL_LISTENER_OPTIONS` replaces
  the duplicate capture and passive literals

Copilot AI 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.

Copilot review overview

🟡 Changes recommended

Moderate issues remain in native capability-test coverage, shadow-DOM scroll handling, and CSS-only RTL consistency.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 Medium severity

Open (1)
Resolved since last review (3)

Comment thread src/components/popover/popover.spec.ts Outdated

Copilot AI 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.

Copilot review overview

🔵 Needs a closer look

Four moderate positioning and browser-compatibility issues remain unresolved.

Review effort: Lite
Findings: None

Resolved since last review (1)

Both strategies duplicated the attach and detach scaffolding, the
container state and the arrow math. A `PopoverPositionStrategy` base
class now holds them, and each strategy implements only what differs.
The component no longer shows and hides the container itself, because
the native strategy must re-bind the implicit anchor in `showPopover`.

`getUpdateComplete` now awaits the strategy. The fallback strategy loads
`@floating-ui/dom` on demand and positions asynchronously, so before
this change `await popover.updateComplete` could measure a container
that has no position yet.

The two `CSS.supports` calls at module evaluation become one lazy probe
that runs on the first use, and the `position-area` rules collapse into
`@each` loops. The compiled CSS does not change.

Five components repeated the same property and the same documentation.
`IgcBaseComboBoxComponent` now declares it, and the manifest still
reports `scroll-strategy` on each of them.

`toggleEventListener` was declared two times in the same module, and
`slot.ts` carried a duplicated `@internal` tag. The tooltip service and
the color picker now call `toggleEventListener` and `isKey` instead of
their own equivalents.
@rkaraivanov
rkaraivanov merged commit 86008a9 into master Sep 21, 2026
7 checks passed
@rkaraivanov
rkaraivanov deleted the rkaraivanov/popover-migration branch September 21, 2026 13:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants