Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]
### Added
- #### Combo, Color picker, Date picker, Date range picker, Tooltip
- `scroll-strategy` attribute: `hide` (default) hides the popover while its anchor is scrolled fully out of view, `scroll` keeps it visible and anchored, `close` closes the component on any scroll. The date pickers ignore it in `dialog` mode. A tooltip with `scroll-strategy="close"` closes even when `sticky`.

### Changed
- #### Popover
- Popovers now position through native CSS anchor positioning in browsers that support it (Chrome/Edge 133+, Firefox 147+, Safari 26+). Other browsers keep the previous `@floating-ui/dom` behavior, and that module now loads only there.
- **BREAKING**: The `PopoverScrollStrategy` type is now `'scroll' | 'hide' | 'close'`, with `hide` as the default. The `block` value is removed; `block` or any unknown value behaves as `hide`.

## [7.3.0] - 2026-08-26
### Added
Expand Down
40 changes: 40 additions & 0 deletions src/components/color-picker/color-picker.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
simulateClick,
simulateInput,
simulateKeyboard,
simulateScroll,
} from '#internals/testing/simulate.spec.js';
import {
runValidationContainerTests,
Expand Down Expand Up @@ -308,6 +309,45 @@ describe('Color picker', () => {
});
});

describe('Scroll strategy', () => {
let container: HTMLDivElement;

async function openColorPicker() {
picker.open = true;
await elementUpdated(picker);
await nextFrame();
}

beforeEach(async () => {
container = await fixture(html`
<div style="height: 1200px">
<igc-color-picker></igc-color-picker>
</div>
`);
picker = container.querySelector(IgcColorPickerComponent.tagName)!;
});

it('`scroll` behavior', async () => {
picker.scrollStrategy = 'scroll';
await openColorPicker();
await simulateScroll(container, { top: 200 });

expect(picker.open).to.be.true;
});

it('`close` behavior', async () => {
const eventSpy = spy(picker, 'emitEvent');

picker.scrollStrategy = 'close';
await openColorPicker();
await simulateScroll(container, { top: 200 });

expect(picker.open).to.be.false;
expect(eventSpy.firstCall).calledWith('igcClosing');
expect(eventSpy.lastCall).calledWith('igcClosed');
});
});

describe('API', () => {
beforeEach(async () => {
picker = await createDefaultColorPicker();
Expand Down
27 changes: 25 additions & 2 deletions src/components/color-picker/color-picker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,11 @@ import IgcInputComponent from '../input/input.js';
import IgcPopoverComponent from '../popover/popover.js';
import type IgcSelectItemComponent from '../select/select-item.js';
import IgcSelectComponent from '../select/select.js';
import type { ColorFormat, ColorPickerMode } from '../types.js';
import type {
ColorFormat,
ColorPickerMode,
PopoverScrollStrategy,
} from '../types.js';
import IgcValidationContainerComponent from '../validation-container/validation-container.js';
import IgcVisuallyHiddenComponent from '../visually-hidden/visually-hidden.js';
import { isValidColor, normalizeColor } from './common.js';
Expand Down Expand Up @@ -288,6 +292,20 @@ export default class IgcColorPickerComponent extends FormAssociatedRequiredMixin
@property({ reflect: true })
public mode: ColorPickerMode = 'default';

/**
* Sets the behavior of the component when the parent container scrolls.
*
* If the value is `hide`, the component hides while the anchor is fully out
* of view. `hide` is the default value.
*
* If the value is `scroll`, the component stays visible and anchored.
*
* If the value is `close`, the component closes on each scroll.
* @attr scroll-strategy
*/
@property({ attribute: 'scroll-strategy' })
public scrollStrategy: PopoverScrollStrategy = 'hide';

/**
* Pre-defined color strings. The component renders them as clickable
* swatches below the picker controls. A click on a swatch commits its color
Expand Down Expand Up @@ -1014,7 +1032,12 @@ export default class IgcColorPickerComponent extends FormAssociatedRequiredMixin
protected override render(): TemplateResult {
return html`
<div part="color-picker">
<igc-popover ?open=${this.open} shift flip>
<igc-popover
?open=${this.open}
flip
.scrollStrategy=${this.scrollStrategy}
@igcPopoverScrollClose=${this._handleClosing}
>
${this._renderAnchor()}${this._renderPicker()}
</igc-popover>
${
Expand Down
40 changes: 40 additions & 0 deletions src/components/combo/combo.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
simulateClick,
simulateKeyboard,
simulatePointerDown,
simulateScroll,
} from '#internals/testing/simulate.spec.js';
import {
runValidationContainerTests,
Expand Down Expand Up @@ -1616,6 +1617,45 @@ describe('Combo', () => {
});
});

describe('Scroll strategy', () => {
let container: HTMLDivElement;

beforeEach(async () => {
container = await fixture(html`
<div style="height: 1200px">
<igc-combo
.data=${cities}
value-key="id"
display-key="name"
></igc-combo>
</div>
`);
combo = container.querySelector<IgcComboComponent<City>>(
IgcComboComponent.tagName
)!;
});

it('`scroll` behavior', async () => {
combo.scrollStrategy = 'scroll';
await openComboPopover(combo);
await simulateScroll(container, { top: 200 });

expect(combo.open).to.be.true;
});

it('`close` behavior', async () => {
const eventSpy = spy(combo, 'emitEvent');

combo.scrollStrategy = 'close';
await openComboPopover(combo);
await simulateScroll(container, { top: 200 });

expect(combo.open).to.be.false;
expect(eventSpy.firstCall).calledWith('igcClosing');
expect(eventSpy.lastCall).calledWith('igcClosed');
});
});

describe('ARIA', () => {
beforeEach(async () => {
combo = await fixture<IgcComboComponent<City>>(
Expand Down
23 changes: 22 additions & 1 deletion src/components/combo/combo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { addThemingController } from '#theming/theming-controller.js';
import IgcIconComponent from '../icon/icon.js';
import IgcInputComponent from '../input/input.js';
import IgcPopoverComponent from '../popover/popover.js';
import type { PopoverScrollStrategy } from '../types.js';
import IgcValidationContainerComponent from '../validation-container/validation-container.js';
import type { VirtualScrollItemContext } from '../virtualization/types.js';
import IgcVirtualScrollComponent from '../virtualization/virtualization.js';
Expand Down Expand Up @@ -361,6 +362,20 @@ export default class IgcComboComponent<
return super.locale;
}

/**
* Sets the behavior of the component when the parent container scrolls.
*
* If the value is `hide`, the component hides while the anchor is fully out
* of view. `hide` is the default value.
*
* If the value is `scroll`, the component stays visible and anchored.
*
* If the value is `close`, the component closes on each scroll.
* @attr scroll-strategy
*/
@property({ attribute: 'scroll-strategy' })
public scrollStrategy: PopoverScrollStrategy = 'hide';

/**
* The label of the control.
* @attr label
Expand Down Expand Up @@ -1317,7 +1332,13 @@ export default class IgcComboComponent<

protected override render() {
return html`
<igc-popover ?open=${this.open} flip shift same-width>
<igc-popover
?open=${this.open}
flip
same-width
.scrollStrategy=${this.scrollStrategy}
@igcPopoverScrollClose=${this._handleClosing}
>
${this._renderMainInput()} ${this._renderList()}
</igc-popover>
${this._renderHelperText()}
Expand Down
26 changes: 25 additions & 1 deletion src/components/date-picker/date-picker.base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import type {
ContentOrientation,
DateRangeValue,
PickerMode,
PopoverScrollStrategy,
} from '../types.js';
import IgcValidationContainerComponent from '../validation-container/validation-container.js';

Expand Down Expand Up @@ -278,6 +279,23 @@ export abstract class IgcDatePickerBaseComponent<
@property()
public mode: PickerMode = 'dropdown';

/**
* Sets the behavior of the component when the parent container scrolls.
*
* If the value is `hide`, the component hides while the anchor is fully out
* of view. `hide` is the default value.
*
* If the value is `scroll`, the component stays visible and anchored.
*
* If the value is `close`, the component closes on each scroll.
*
* In the `dialog` mode the picker ignores this property, because a scroll
* does not move a modal dialog.
* @attr scroll-strategy
*/
@property({ attribute: 'scroll-strategy' })
public scrollStrategy: PopoverScrollStrategy = 'hide';

/**
* Makes the control a readonly field.
* @attr readonly
Expand Down Expand Up @@ -790,7 +808,13 @@ export abstract class IgcDatePickerBaseComponent<

return this._isDropDown
? html`
<igc-popover ?open=${this.open} anchor=${id} flip shift>
<igc-popover
?open=${this.open}
anchor=${id}
flip
.scrollStrategy=${this.scrollStrategy}
@igcPopoverScrollClose=${this._handleClosing}
>
<igc-focus-trap ?disabled=${isDisabled}>
${this._renderPickerContent(id)}
</igc-focus-trap>
Expand Down
46 changes: 46 additions & 0 deletions src/components/date-picker/date-picker.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
simulateClick,
simulateInput,
simulateKeyboard,
simulateScroll,
} from '#internals/testing/simulate.spec.js';
import IgcCalendarComponent from '../calendar/calendar.js';
import { DateRangeType } from '../calendar/types.js';
Expand Down Expand Up @@ -644,6 +645,51 @@ describe('Date picker', () => {
});
});

describe('Scroll strategy', () => {
let container: HTMLDivElement;

beforeEach(async () => {
container = await fixture(html`
<div style="height: 1200px">
<igc-date-picker></igc-date-picker>
</div>
`);
picker = container.querySelector(IgcDatePickerComponent.tagName)!;
});

it('`scroll` behavior', async () => {
picker.scrollStrategy = 'scroll';
await picker.show();
await simulateScroll(container, { top: 200 });

expect(picker.open).to.be.true;
});

it('`close` behavior', async () => {
const eventSpy = spy(picker, 'emitEvent');

picker.scrollStrategy = 'close';
await picker.show();
await simulateScroll(container, { top: 200 });

expect(picker.open).to.be.false;
expect(eventSpy.firstCall).calledWith('igcClosing');
expect(eventSpy.lastCall).calledWith('igcClosed');
});

it('`close` is ignored in dialog mode', async () => {
const eventSpy = spy(picker, 'emitEvent');

picker.mode = 'dialog';
picker.scrollStrategy = 'close';
await picker.show();
await simulateScroll(container, { top: 200 });

expect(picker.open).to.be.true;
expect(eventSpy).not.to.be.called;
});
});

describe('Methods', () => {
let input: HTMLInputElement;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -239,12 +239,13 @@ describe('DateRangeMaskParser', () => {
const newRange = parser.spinDateRangePart(monthPart, 1, null, true);

const today = CalendarDay.today.native;
// Spinning clamps the day to the target month's length (Aug 31 -> Sep 30),
// mirrored here by CalendarDay's month-rollover clamp.
const spun = CalendarDay.today.add('month', 1).native;
expect(newRange.start).to.not.be.null;
expect(newRange.start!.getFullYear()).to.equal(today.getFullYear());
expect(newRange.start!.getMonth()).to.equal(
CalendarDay.today.add('month', 1).native.getMonth()
);
expect(newRange.start!.getDate()).to.equal(today.getDate());
expect(newRange.start!.getMonth()).to.equal(spun.getMonth());
expect(newRange.start!.getDate()).to.equal(spun.getDate());
expect(newRange.end).to.be.null;
});
});
Expand Down
23 changes: 23 additions & 0 deletions src/components/date-range-picker/date-range-picker-single.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
simulateClick,
simulateInput,
simulateKeyboard,
simulateScroll,
} from '#internals/testing/simulate.spec.js';
import IgcCalendarComponent from '../calendar/calendar.js';
import type IgcDialogComponent from '../dialog/dialog.js';
Expand Down Expand Up @@ -420,6 +421,28 @@ describe('Date range picker - single input', () => {
expect(input.value).to.equal('01/15/2025 - 01/15/2026');
});
});
describe('Scroll strategy', () => {
// Inherited from the picker base class - one end-to-end smoke test.
it('`close` behavior', async () => {
const container = await fixture<HTMLDivElement>(html`
<div style="height: 1200px">
<igc-date-range-picker
scroll-strategy="close"
></igc-date-range-picker>
</div>
`);
picker = container.querySelector(IgcDateRangePickerComponent.tagName)!;
const eventSpy = spy(picker, 'emitEvent');

await picker.show();
await simulateScroll(container, { top: 200 });

expect(picker.open).to.be.false;
expect(eventSpy.firstCall).calledWith('igcClosing');
expect(eventSpy.lastCall).calledWith('igcClosed');
});
});

describe('Methods', () => {
it('should clear the input on invoking clear()', async () => {
const eventSpy = spy(picker, 'emitEvent');
Expand Down
9 changes: 1 addition & 8 deletions src/components/dropdown/dropdown.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ describe('Dropdown', () => {
});

it('`scroll` behavior', async () => {
dropDown.scrollStrategy = 'scroll';
await openDropdown();
await simulateScroll(container, { top: 200 });

Expand All @@ -154,14 +155,6 @@ describe('Dropdown', () => {
expect(eventSpy.firstCall).calledWith('igcClosing');
expect(eventSpy.lastCall).calledWith('igcClosed');
});

it('`block behavior`', async () => {
dropDown.scrollStrategy = 'block';
await openDropdown();
await simulateScroll(container, { top: 200 });

expect(dropDown.open).to.be.true;
});
});

describe('Detached (non-slotted anchor)', () => {
Expand Down
Loading