feat(popover): migrate to native CSS anchor positioning and add scroll straregies - #2355
Conversation
…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'.
There was a problem hiding this comment.
🟡 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-popoverwith a floating-ui fallback that lazy-loads only when needed. - Introduced/updated
scroll-strategysupport (defaulting tohide) 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. Removeasyncfrom thatdescribecallback.
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.
…niteUI/igniteui-webcomponents into rkaraivanov/popover-migration
There was a problem hiding this comment.
🟡 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 theseCSS.supportschecks but ignoresshowPopover({ 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
igcPopoverScrollCloseevent is dispatched directly, soIgcPopoverComponenthas no event map and TypeScript consumers get only the genericEventlistener overload. Public component events are consistently declared in an event map and exposed throughEventEmitterMixin(for examplesrc/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
scrolllistener registered forscrollStrategy="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
igcPopoverScrollCloseevent is dispatched directly from a plainLitElement, so the popover has no typed event map or typedaddEventListenersurface. Other event-emitting components define anIgc*ComponentEventMapand useEventEmitterMixin(for examplesrc/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_POSITIONINGgates the native path without checkingposition-visibility: anchors-visible, although that declaration provides the documented defaulthide/closebehavior. 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
`_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
There was a problem hiding this comment.
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
Open (1)
Resolved since last review (3)
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.


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
Checklist