Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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