Skip to content

Commit a4bdfa4

Browse files
authored
feat(overlay) use popover in overlay service (#16835)
1 parent 8bc18f9 commit a4bdfa4

12 files changed

Lines changed: 467 additions & 131 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ All notable changes for each version of this project will be documented in this
4747
```
4848
- `IgxNavigationDrawer` - Integrated HTML Popover API to place overlay elements when not pinned in the top layer, eliminating z-index stacking issues.
4949

50+
- `IgxOverlayService`
51+
- Integrated HTML Popover API into the overlay service for improved z-index management and layering control.
52+
- The overlay service now uses the Popover API to place overlay elements in the top layer, eliminating z-index stacking issues.
53+
- Improved positioning accuracy for container-based overlays with fixed container bounds.
54+
5055
### General
5156

5257
- `IgxGrid`, `IgxTreeGrid`, `IgxHierarchicalGrid`, `IgxPivotGrid`

projects/igniteui-angular/combo/src/combo/combo.component.spec.ts

Lines changed: 36 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1016,13 +1016,14 @@ describe('igxCombo', () => {
10161016
fixture.detectChanges();
10171017
verifyDropdownItemHeight();
10181018
}));
1019-
it('should render grouped items properly', (done) => {
1019+
it('should render grouped items properly', async () => {
10201020
let dropdownContainer;
10211021
let dropdownItems;
1022-
let scrollIndex = 0;
10231022
const headers: Array<string> = Array.from(new Set(combo.data.map(item => item.region)));
10241023
combo.toggle();
1024+
await wait();
10251025
fixture.detectChanges();
1026+
10261027
const checkGroupedItemsClass = () => {
10271028
fixture.detectChanges();
10281029
dropdownContainer = fixture.debugElement.query(By.css(`.${CSS_CLASS_CONTAINER}`)).nativeElement;
@@ -1033,18 +1034,18 @@ describe('igxCombo', () => {
10331034
const expectedClass: string = headers.includes(itemText) ? CSS_CLASS_HEADERITEM : CSS_CLASS_DROPDOWNLISTITEM;
10341035
expect(itemElement.classList.contains(expectedClass)).toBeTruthy();
10351036
});
1036-
scrollIndex += 10;
1037-
if (scrollIndex < combo.data.length) {
1038-
combo.virtualScrollContainer.scrollTo(scrollIndex);
1039-
combo.virtualScrollContainer.chunkLoad.pipe(take(1)).subscribe(async () => {
1040-
await wait(30);
1041-
checkGroupedItemsClass();
1042-
});
1043-
} else {
1044-
done();
1045-
}
10461037
};
1038+
1039+
// Check initial state
10471040
checkGroupedItemsClass();
1041+
1042+
// Scroll through the list in chunks and verify items
1043+
for (let scrollIndex = 10; scrollIndex < combo.data.length; scrollIndex += 10) {
1044+
combo.virtualScrollContainer.scrollTo(scrollIndex);
1045+
await firstValueFrom(combo.virtualScrollContainer.chunkLoad);
1046+
await wait(30);
1047+
checkGroupedItemsClass();
1048+
}
10481049
});
10491050
it('should render selected items properly', () => {
10501051
combo.toggle();
@@ -1195,8 +1196,8 @@ describe('igxCombo', () => {
11951196
const comboWrapper = fixture.debugElement.query(By.css(CSS_CLASS_COMBO)).nativeElement;
11961197
let containerElementWidth = containerElement.getBoundingClientRect().width;
11971198
let wrapperWidth = comboWrapper.getBoundingClientRect().width;
1198-
expect(containerElementWidth).toEqual(containerWidth);
1199-
expect(containerElementWidth).toEqual(wrapperWidth);
1199+
expect(containerElementWidth).toBeCloseTo(containerWidth, 1);
1200+
expect(containerElementWidth).toBeCloseTo(wrapperWidth, 1);
12001201

12011202
combo.toggle();
12021203
tick();
@@ -1764,6 +1765,7 @@ describe('igxCombo', () => {
17641765
it('should properly navigate using HOME/END key', (async () => {
17651766
let firstVisibleItem: Element;
17661767
combo.toggle();
1768+
await wait();
17671769
fixture.detectChanges();
17681770
const dropdownContent = fixture.debugElement.query(By.css(`.${CSS_CLASS_CONTENT}`));
17691771
const scrollbar = fixture.debugElement.query(By.css(`.${CSS_CLASS_SCROLLBAR_VERTICAL}`)).nativeElement as HTMLElement;
@@ -1773,7 +1775,7 @@ describe('igxCombo', () => {
17731775
await firstValueFrom(combo.virtualScrollContainer.chunkLoad);
17741776
fixture.detectChanges();
17751777
// Content was scrolled to bottom
1776-
expect(scrollbar.scrollHeight - scrollbar.scrollTop).toEqual(scrollbar.clientHeight);
1778+
expect(scrollbar.scrollHeight - scrollbar.scrollTop - scrollbar.clientHeight).toBeLessThan(1);
17771779

17781780
// Scroll to top
17791781
UIInteractions.triggerEventHandlerKeyDown('Home', dropdownContent);
@@ -1782,7 +1784,7 @@ describe('igxCombo', () => {
17821784
const dropdownContainer: HTMLElement = fixture.debugElement.query(By.css(`.${CSS_CLASS_CONTAINER}`)).nativeElement;
17831785
firstVisibleItem = dropdownContainer.querySelector(`.${CSS_CLASS_DROPDOWNLISTITEM}` + ':first-child');
17841786
// Container is scrolled to top
1785-
expect(scrollbar.scrollTop).toEqual(32);
1787+
expect(scrollbar.scrollTop - 32).toBeLessThan(1);
17861788

17871789
// First item is focused
17881790
expect(firstVisibleItem.classList.contains(CSS_CLASS_FOCUSED)).toBeTruthy();
@@ -1791,7 +1793,7 @@ describe('igxCombo', () => {
17911793
firstVisibleItem = dropdownContainer.querySelector(`.${CSS_CLASS_DROPDOWNLISTITEM}` + ':first-child');
17921794

17931795
// Scroll has not change
1794-
expect(scrollbar.scrollTop).toEqual(32);
1796+
expect(scrollbar.scrollTop - 32).toBeLessThan(1);
17951797
// First item is no longer focused
17961798
expect(firstVisibleItem.classList.contains(CSS_CLASS_FOCUSED)).toBeFalsy();
17971799
UIInteractions.triggerEventHandlerKeyDown('Home', dropdownContent);
@@ -1895,6 +1897,7 @@ describe('igxCombo', () => {
18951897
input = fixture.debugElement.query(By.css(`.${CSS_CLASS_INPUTGROUP}`));
18961898
let firstVisibleItem: Element;
18971899
combo.toggle();
1900+
await wait();
18981901
fixture.detectChanges();
18991902
const dropdownContent = fixture.debugElement.query(By.css(`.${CSS_CLASS_CONTENT}`));
19001903
const scrollbar = fixture.debugElement.query(By.css(`.${CSS_CLASS_SCROLLBAR_VERTICAL}`))
@@ -1905,7 +1908,7 @@ describe('igxCombo', () => {
19051908
await firstValueFrom(combo.virtualScrollContainer.chunkLoad);
19061909
fixture.detectChanges();
19071910
// Content was scrolled to bottom
1908-
expect(scrollbar.scrollHeight - scrollbar.scrollTop).toEqual(scrollbar.clientHeight);
1911+
expect(scrollbar.scrollHeight - scrollbar.scrollTop - scrollbar.clientHeight).toBeLessThan(1);
19091912

19101913
// Scroll to top
19111914
UIInteractions.triggerEventHandlerKeyDown('Home', dropdownContent);
@@ -1983,8 +1986,9 @@ describe('igxCombo', () => {
19831986
vContainerScrollHeight = virtDir.getScroll().scrollHeight;
19841987
expect(virtDir.getScroll().scrollTop).toEqual(vContainerScrollHeight / 2);
19851988
});
1986-
it('should display vertical scrollbar properly', () => {
1989+
it('should display vertical scrollbar properly', async () => {
19871990
combo.toggle();
1991+
await wait();
19881992
fixture.detectChanges();
19891993
const scrollbarContainer = fixture.debugElement
19901994
.query(By.css(`.${CSS_CLASS_SCROLLBAR_VERTICAL}`))
@@ -1995,12 +1999,14 @@ describe('igxCombo', () => {
19951999
combo.data = [{ field: 'Mid-Atlantic', region: 'New Jersey' }, { field: 'Mid-Atlantic', region: 'New York' }];
19962000
fixture.detectChanges();
19972001
combo.toggle();
2002+
await wait();
19982003
fixture.detectChanges();
19992004
hasScrollbar = scrollbarContainer.scrollHeight > scrollbarContainer.clientHeight;
20002005
expect(hasScrollbar).toBeFalsy();
20012006
});
20022007
it('should preserve selection on scrolling', async () => {
20032008
combo.toggle();
2009+
await wait();
20042010
fixture.detectChanges();
20052011
const scrollbar = fixture.debugElement.query(By.css(`.${CSS_CLASS_SCROLLBAR_VERTICAL}`)).nativeElement as HTMLElement;
20062012
expect(scrollbar.scrollTop).toEqual(0);
@@ -2019,7 +2025,7 @@ describe('igxCombo', () => {
20192025
await firstValueFrom(combo.virtualScrollContainer.chunkLoad);
20202026
fixture.detectChanges();
20212027
// Content was scrolled to bottom
2022-
expect(scrollbar.scrollHeight - scrollbar.scrollTop).toEqual(scrollbar.clientHeight);
2028+
expect(scrollbar.scrollHeight - scrollbar.scrollTop - scrollbar.clientHeight).toBeLessThan(1);
20232029

20242030
combo.virtualScrollContainer.scrollTo(4);
20252031
await firstValueFrom(combo.virtualScrollContainer.chunkLoad);
@@ -2577,7 +2583,7 @@ describe('igxCombo', () => {
25772583
fixture.detectChanges();
25782584
expect(combo.dropdown.headers[0].element.nativeElement.innerText).toEqual('New England')
25792585
});
2580-
it('should sort groups with diacritics correctly', () => {
2586+
it('should sort groups with diacritics correctly', async() => {
25812587
combo.data = [
25822588
{ field: "Alaska", region: "Méxícó" },
25832589
{ field: "California", region: "Méxícó" },
@@ -2589,6 +2595,7 @@ describe('igxCombo', () => {
25892595
];
25902596
combo.groupSortingDirection = SortingDirection.Asc;
25912597
combo.toggle();
2598+
await wait();
25922599
fixture.detectChanges();
25932600
let headers = combo.dropdown.headers.map(header => header.element.nativeElement.innerText);
25942601
expect(headers).toEqual(['Ángel', 'Boris', 'México']);
@@ -2745,9 +2752,11 @@ describe('igxCombo', () => {
27452752
combo.filterFunction = comboIgnoreDiacriticsFilter;
27462753
combo.displayKey = null;
27472754
combo.valueKey = null;
2755+
combo.groupKey = null;
27482756
combo.filteringOptions = { caseSensitive: false, filteringKey: undefined };
27492757
combo.data = ['José', 'Óscar', 'Ángel', 'Germán', 'Niño', 'México', 'Méxícó', 'Mexico', 'Köln', 'München'];
27502758
combo.toggle();
2759+
tick();
27512760
fixture.detectChanges();
27522761

27532762
const searchInput = fixture.debugElement.query(By.css(`input[name="searchInput"]`));
@@ -2762,8 +2771,8 @@ describe('igxCombo', () => {
27622771

27632772
verifyFilteredItems('jose', 1);
27642773
verifyFilteredItems('mexico', 3);
2765-
verifyFilteredItems('o', 6);
2766-
verifyFilteredItems('é', 6);
2774+
verifyFilteredItems('o', 7);
2775+
verifyFilteredItems('é', 7);
27672776
}));
27682777

27692778
it('should filter the dropdown items when typing in the search input', fakeAsync(() => {
@@ -2779,6 +2788,7 @@ describe('igxCombo', () => {
27792788
};
27802789

27812790
combo.toggle();
2791+
tick();
27822792
fixture.detectChanges();
27832793
const searchInput = fixture.debugElement.query(By.css('input[name=\'searchInput\']'));
27842794
const verifyFilteredItems = (inputValue: string, expectedItemsNumber) => {
@@ -2842,10 +2852,11 @@ describe('igxCombo', () => {
28422852
verifyOnSearchInputEventIsFired('Miss');
28432853
verifyOnSearchInputEventIsFired('Misso');
28442854
});
2845-
it('should restore the initial combo dropdown list after clearing the search input', () => {
2855+
it('should restore the initial combo dropdown list after clearing the search input', fakeAsync(() => {
28462856
let dropdownList;
28472857
let dropdownItems;
28482858
combo.toggle();
2859+
tick();
28492860
fixture.detectChanges();
28502861
const searchInput = fixture.debugElement.query(By.css(CSS_CLASS_SEARCHINPUT));
28512862

@@ -2864,7 +2875,7 @@ describe('igxCombo', () => {
28642875
verifyFilteredItems('Mi', 3, 5);
28652876
verifyFilteredItems('M', 4, 15);
28662877
combo.filteredData.forEach((item) => expect(combo.data).toContain(item));
2867-
});
2878+
}));
28682879
it('should clear the search input and close the dropdown list on pressing ESC key', fakeAsync(() => {
28692880
combo.toggle();
28702881
fixture.detectChanges();

projects/igniteui-angular/core/src/core/styles/components/overlay/_overlay-theme.scss

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,23 @@
2020
background: transparent;
2121
transition: background .25s $in-out-quad;
2222
pointer-events: none;
23-
z-index: 10005;
2423
box-sizing: content-box;
24+
25+
// Override browser's default popover styles to maintain our custom positioning
26+
&[popover] {
27+
// Reset popover defaults to use our positioning with !important to override UA styles
28+
position: fixed !important;
29+
margin: 0 !important;
30+
border: 0 !important;
31+
padding: 0 !important;
32+
width: auto;
33+
height: auto;
34+
overflow: visible !important;
35+
36+
&:not(:popover-open) {
37+
display: initial !important;
38+
}
39+
}
2540
}
2641

2742
%overlay-wrapper--modal {
@@ -79,6 +94,5 @@
7994
pointer-events: none;
8095
overflow: hidden;
8196
appearance: none;
82-
z-index: -1;
8397
}
8498
}

projects/igniteui-angular/core/src/services/overlay/README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,14 @@
33
The overlay service allows users to show components on overlay div above all other elements in the page.
44
A walk through of how to get started can be found [here](https://www.infragistics.com/products/ignite-ui-angular/angular/components/overlay-main)
55

6+
## Key Features
7+
8+
- **Popover API Integration**: Uses the HTML Popover API for improved z-index management and automatic top-layer placement
9+
- **Flexible Positioning**: Multiple position strategies (global, connected, auto, elastic, container)
10+
- **Scroll Strategies**: Handle scroll behavior with NoOp, Block, Close, and Absolute strategies
11+
- **Modal Support**: Optional modal backdrop with configurable close-on-outside-click behavior
12+
- **Animation Support**: Built-in support for open/close animations
13+
614
## Usage
715

816
### With igxToggleDirective

0 commit comments

Comments
 (0)