Skip to content

Commit 2d06faf

Browse files
CopilotkdinevdamyanpetevCopilotChronosSF
authored
refactor(select): switch default to AutoPositionStrategy; export IgxSelectOverlapPositionStrategy as opt-in (#17344)
* feat(select): default to AutoPositionStrategy; add IgxSelectOverlapPositionStrategy opt-in * refactor(select): move changelog entry to General; add migration note for default strategy change * fix(select): insert HTML comment above igx-select in migration instead of logger.warn * refactor(select): address PR review feedback (AutoPositionStrategy defaults, changelog order, migration note, package-lock) * fix(select): use combo's target assign and move to the correct hook --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Konstantin Dinev <kdinev@infragistics.com> Co-authored-by: Damyan Petev <damyanpetev@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: Stamen Stoychev <chronos.stz@gmail.com>
1 parent 698e5ae commit 2d06faf

9 files changed

Lines changed: 170 additions & 29 deletions

File tree

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,17 @@ All notable changes for each version of this project will be documented in this
6565

6666
- Added a dedicated `excel-filtering-theme()` for styling the `Excel Style Filtering`. Use it instead of the excel-filtering color properties from `grid-theme()`.
6767

68+
### General
69+
70+
- `IgxSelectComponent`
71+
- The default positioning strategy has changed from the internal overlap strategy to `AutoPositionStrategy`. The dropdown now opens below (or above, if there is not enough space) the input element, consistent with other connected components.
72+
- Added `IgxSelectOverlapPositionStrategy` - a new publicly exported strategy that preserves the previous behavior of aligning the selected item's text over the input text. To opt into the previous overlap behavior:
73+
```ts
74+
this.select.overlaySettings = {
75+
positionStrategy: new IgxSelectOverlapPositionStrategy(this.select)
76+
};
77+
```
78+
6879
## 21.2.0
6980

7081
### New Features

projects/igniteui-angular/migrations/update-22_0_0/index.spec.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ import { setupTestTree } from '../common/setup.spec';
55

66
const version = '22.0.0';
77

8+
const SELECT_POSITION_NOTE =
9+
`<!-- IgxSelect: default positioning changed to AutoPositionStrategy (below/above input).\n` +
10+
` To preserve overlap behavior: this.select.overlaySettings = { positionStrategy: new IgxSelectOverlapPositionStrategy(this.select) }; -->\n`;
11+
812
describe(`Update to ${version}`, () => {
913
let appTree: UnitTestTree;
1014
const schematicRunner = new SchematicTestRunner('ig-migrate', path.join(__dirname, '../migration-collection.json'));
@@ -27,7 +31,7 @@ describe(`Update to ${version}`, () => {
2731
.toEqual(`<igx-input-group type="line"><input igxInput></igx-input-group>`);
2832
});
2933

30-
it('should add type="line" to igx-select without explicit type', async () => {
34+
it('should add type="line" and a positioning note to igx-select without explicit type', async () => {
3135
appTree.create(
3236
`/testSrc/appPrefix/component/test.component.html`,
3337
`<igx-select><igx-select-item>Option</igx-select-item></igx-select>`
@@ -36,7 +40,19 @@ describe(`Update to ${version}`, () => {
3640
const tree = await schematicRunner.runSchematic(migrationName, {}, appTree);
3741

3842
expect(tree.readContent('/testSrc/appPrefix/component/test.component.html'))
39-
.toEqual(`<igx-select type="line"><igx-select-item>Option</igx-select-item></igx-select>`);
43+
.toEqual(`${SELECT_POSITION_NOTE}<igx-select type="line"><igx-select-item>Option</igx-select-item></igx-select>`);
44+
});
45+
46+
it('should add a positioning note to igx-select that already has an explicit type', async () => {
47+
appTree.create(
48+
`/testSrc/appPrefix/component/test.component.html`,
49+
`<igx-select type="border"><igx-select-item>Option</igx-select-item></igx-select>`
50+
);
51+
52+
const tree = await schematicRunner.runSchematic(migrationName, {}, appTree);
53+
54+
expect(tree.readContent('/testSrc/appPrefix/component/test.component.html'))
55+
.toEqual(`${SELECT_POSITION_NOTE}<igx-select type="border"><igx-select-item>Option</igx-select-item></igx-select>`);
4056
});
4157

4258
it('should add type="line" to igx-date-picker without explicit type', async () => {
@@ -111,7 +127,7 @@ describe(`Update to ${version}`, () => {
111127

112128
expect(tree.readContent('/testSrc/appPrefix/component/test.component.html'))
113129
.toEqual(`<igx-input-group type="line"><input igxInput></igx-input-group>
114-
<igx-select type="line"></igx-select>
130+
${SELECT_POSITION_NOTE}<igx-select type="line"></igx-select>
115131
<igx-input-group type="border"><input igxInput></igx-input-group>`);
116132
});
117133
});

projects/igniteui-angular/migrations/update-22_0_0/index.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,31 @@ export default (): Rule => async (host: Tree, context: SchematicContext) => {
7171
}
7272
}
7373

74+
applyChanges();
75+
changes.clear();
76+
77+
// IgxSelect: default positioning strategy changed to AutoPositionStrategy.
78+
// Insert a comment above each <igx-select> to inform the developer.
79+
const SELECT_NOTE =
80+
`<!-- IgxSelect: default positioning changed to AutoPositionStrategy (below/above input).\n` +
81+
` To preserve overlap behavior: this.select.overlaySettings = { positionStrategy: new IgxSelectOverlapPositionStrategy(this.select) }; -->\n`;
82+
83+
for (const path of update.templateFiles) {
84+
const content = host.read(path)!.toString();
85+
const root = parseFile(parser, host, path);
86+
const nodes = findElementNodes(root, 'igx-select');
87+
88+
for (const node of nodes) {
89+
if (!(node instanceof Element)) continue;
90+
91+
const { startTag, file } = getSourceOffset(node);
92+
const noteStart = Math.max(0, startTag.start - SELECT_NOTE.length);
93+
if (content.slice(noteStart, startTag.start) === SELECT_NOTE) continue;
94+
95+
addChange(file.url, new FileChange(startTag.start, SELECT_NOTE));
96+
}
97+
}
98+
7499
applyChanges();
75100
update.applyChanges();
76101
};

projects/igniteui-angular/select/src/select/public_api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { IgxSelectComponent, IgxSelectFooterDirective, IgxSelectHeaderDirective,
66
export * from './select-group.component';
77
export * from './select-item.component';
88
export * from './select.component';
9+
export * from './select-overlap-positioning-strategy';
910

1011
/* NOTE: Select directives collection for ease-of-use import in standalone components scenario */
1112
export const IGX_SELECT_DIRECTIVES = [

projects/igniteui-angular/select/src/select/select-positioning-strategy.ts renamed to projects/igniteui-angular/select/src/select/select-overlap-positioning-strategy.ts

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import { VerticalAlignment, HorizontalAlignment, PositionSettings, ConnectedFit, Point, Size, BaseFitPositionStrategy, Util } from 'igniteui-angular/core';
22
import { IPositionStrategy } from 'igniteui-angular/core';
33

4-
import { IgxSelectBase } from './select.common';
5-
import { PlatformUtil } from 'igniteui-angular/core';
6-
import { Optional } from '@angular/core';
4+
import type { IgxSelectComponent } from './select.component';
75
import { fadeIn, fadeOut } from 'igniteui-angular/animations';
86

9-
/** @hidden @internal */
10-
export class SelectPositioningStrategy extends BaseFitPositionStrategy implements IPositionStrategy {
7+
/**
8+
* Positions the select dropdown so that the active item's text overlaps
9+
* the value displayed in the select input box.
10+
*/
11+
export class IgxSelectOverlapPositionStrategy extends BaseFitPositionStrategy implements IPositionStrategy {
1112
private _selectDefaultSettings = {
1213
horizontalDirection: HorizontalAlignment.Right,
1314
verticalDirection: VerticalAlignment.Bottom,
@@ -22,7 +23,10 @@ export class SelectPositioningStrategy extends BaseFitPositionStrategy implement
2223
private global_xOffset = 0;
2324
private global_styles: SelectStyles = {};
2425

25-
constructor(public select: IgxSelectBase, settings?: PositionSettings, @Optional() protected platform?: PlatformUtil) {
26+
/** @hidden @internal */
27+
public isItemOverlapPositioning = true;
28+
29+
constructor(private select: IgxSelectComponent, settings?: PositionSettings) {
2630
super();
2731
this.settings = Object.assign({}, this._selectDefaultSettings, settings);
2832
}
@@ -87,7 +91,7 @@ export class SelectPositioningStrategy extends BaseFitPositionStrategy implement
8791
/**
8892
* Obtain the selected item if there is such one or otherwise use the first one
8993
*/
90-
public getInteractionItemElement(): HTMLElement {
94+
private getInteractionItemElement(): HTMLElement {
9195
let itemElement;
9296
if (this.select.selectedItem) {
9397
itemElement = this.select.selectedItem.element.nativeElement;
@@ -103,7 +107,7 @@ export class SelectPositioningStrategy extends BaseFitPositionStrategy implement
103107
*
104108
* @param selectFit selectFit to use for computation.
105109
*/
106-
protected fitInViewport(_contentElement: HTMLElement, selectFit: SelectFit) {
110+
protected override fitInViewport(_contentElement: HTMLElement, selectFit: SelectFit) {
107111
const footer = selectFit.scrollContainerRect.bottom - selectFit.contentElementRect.bottom;
108112
const header = selectFit.scrollContainerRect.top - selectFit.contentElementRect.top;
109113
const lastItemFitSize = selectFit.targetRect.bottom + selectFit.styles.itemTextToInputTextDiff - footer;
@@ -212,7 +216,7 @@ export class SelectPositioningStrategy extends BaseFitPositionStrategy implement
212216
}
213217

214218
/** @hidden */
215-
export interface SelectFit extends ConnectedFit {
219+
interface SelectFit extends ConnectedFit {
216220
itemElement?: HTMLElement;
217221
scrollContainer: HTMLElement;
218222
scrollContainerRect: ClientRect;
@@ -222,7 +226,7 @@ export interface SelectFit extends ConnectedFit {
222226
}
223227

224228
/** @hidden */
225-
export interface SelectStyles {
229+
interface SelectStyles {
226230
itemTextPadding?: number;
227231
itemTextIndent?: number;
228232
itemTextToInputTextDiff?: number;

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

Lines changed: 68 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,14 @@ import { IGX_DROPDOWN_BASE, IgxDropDownItemComponent, ISelectionEventArgs } from
99
import { IgxHintDirective, IgxInputState, IgxLabelDirective, IgxPrefixDirective, IgxSuffixDirective } from '../../../input-group/src/public_api';
1010
import { IgxSelectComponent, IgxSelectFooterDirective, IgxSelectHeaderDirective } from './select.component';
1111
import { IgxSelectItemComponent } from './select-item.component';
12-
import { HorizontalAlignment, VerticalAlignment, ConnectedPositioningStrategy, AbsoluteScrollStrategy, IgxSelectionAPIService } from 'igniteui-angular/core';
12+
import { HorizontalAlignment, VerticalAlignment, ConnectedPositioningStrategy, AbsoluteScrollStrategy, AutoPositionStrategy, IgxSelectionAPIService } from 'igniteui-angular/core';
1313
import { UIInteractions } from '../../../test-utils/ui-interactions.spec';
1414
import { IgxButtonDirective } from '../../../directives/src/directives/button/button.directive';
1515
import { IgxIconComponent } from 'igniteui-angular/icon';
1616
import { IgxSelectGroupComponent } from './select-group.component';
1717
import { IgxDropDownItemBaseDirective } from '../../../drop-down/src/drop-down/drop-down-item.base';
1818
import { addScrollDivToElement } from 'igniteui-angular/core/src/services/overlay/overlay.spec';
19+
import { IgxSelectOverlapPositionStrategy } from './select-overlap-positioning-strategy';
1920

2021
const CSS_CLASS_INPUT_GROUP = 'igx-input-group';
2122
const CSS_CLASS_INPUT = 'igx-input-group__input';
@@ -191,6 +192,39 @@ describe('igxSelect', () => {
191192
expect(select.disabled).toBeTruthy();
192193
});
193194

195+
it('should use AutoPositionStrategy as the default position strategy', () => {
196+
// The public overlaySettings input is undefined by default
197+
expect(select.overlaySettings).toBeUndefined();
198+
// The internal _overlayDefaults should use AutoPositionStrategy
199+
expect((select as any)._overlayDefaults.positionStrategy).toBeInstanceOf(AutoPositionStrategy);
200+
// The merged settings should target the input-group bundle element, not the raw input
201+
const merged = (select as any).getMergedOverlaySettings();
202+
const bundleElement = select.inputGroup.element.nativeElement.querySelector('.igx-input-group__bundle');
203+
expect(merged.target).toBe(bundleElement);
204+
expect(merged.target).not.toBe(select.getEditElement());
205+
});
206+
207+
it('should allow opt-in to IgxSelectOverlapPositionStrategy via overlaySettings', fakeAsync(() => {
208+
const overlapStrategy = new IgxSelectOverlapPositionStrategy(select);
209+
select.overlaySettings = { positionStrategy: overlapStrategy };
210+
expect(select.overlaySettings.positionStrategy).toBeInstanceOf(IgxSelectOverlapPositionStrategy);
211+
expect((select.overlaySettings.positionStrategy as IgxSelectOverlapPositionStrategy).isItemOverlapPositioning).toBeTrue();
212+
// The merged settings should switch the target to the raw input element
213+
const merged = (select as any).getMergedOverlaySettings();
214+
expect(merged.target).toBe(select.getEditElement());
215+
216+
// The select should still open correctly when using the overlap strategy
217+
select.open();
218+
tick();
219+
fixture.detectChanges();
220+
expect(select.collapsed).toBeFalsy();
221+
222+
select.close();
223+
tick();
224+
fixture.detectChanges();
225+
expect(select.collapsed).toBeTruthy();
226+
}));
227+
194228
it('should open dropdown on input click', () => {
195229
const inputGroup = fixture.debugElement.query(By.css('.' + CSS_CLASS_INPUT_GROUP));
196230
expect(select.collapsed).toBeTruthy();
@@ -287,6 +321,7 @@ describe('igxSelect', () => {
287321

288322
it('should properly emit opening/closing events on input click', fakeAsync(() => {
289323
const inputGroup = fixture.debugElement.query(By.css('.' + CSS_CLASS_INPUT_GROUP));
324+
const inputBundle = inputGroup.nativeElement.querySelector('.igx-input-group__bundle') as HTMLElement;
290325
expect(select).toBeTruthy();
291326

292327
spyOn(select.opening, 'emit');
@@ -297,19 +332,19 @@ describe('igxSelect', () => {
297332
spyOn(select, 'open').and.callThrough();
298333
spyOn(select, 'close').and.callThrough();
299334

300-
inputGroup.nativeElement.click();
335+
inputBundle.click();
301336
tick();
302337
fixture.detectChanges();
303338
verifyOpenCloseEvents(1, 0, 1);
304339

305-
inputGroup.nativeElement.click();
340+
inputBundle.click();
306341
tick();
307342
fixture.detectChanges();
308343
verifyOpenCloseEvents(1, 1, 2);
309344

310345
select.disabled = true;
311346
fixture.detectChanges();
312-
inputGroup.nativeElement.click();
347+
inputBundle.click();
313348
tick();
314349
fixture.detectChanges();
315350

@@ -2220,6 +2255,7 @@ describe('igxSelect', () => {
22202255
}));
22212256
});
22222257
describe('Positioning tests: ', () => {
2258+
describe('IgxSelectOverlapPositionStrategy positioning tests: ', () => {
22232259
const defaultWindowToListOffset = 16;
22242260
const defaultItemLeftPadding = 24;
22252261
const defaultItemTopPadding = 0;
@@ -2258,6 +2294,7 @@ describe('igxSelect', () => {
22582294
fixture = TestBed.createComponent(IgxSelectMiddleComponent);
22592295
select = fixture.componentInstance.select;
22602296
fixture.detectChanges();
2297+
select.overlaySettings = { positionStrategy: new IgxSelectOverlapPositionStrategy(select) };
22612298
inputElement = fixture.debugElement.query(By.css('.' + CSS_CLASS_INPUT));
22622299
selectList = fixture.debugElement.query(By.css('.' + CSS_CLASS_DROPDOWN_LIST_SCROLL));
22632300
addScrollDivToElement(fixture.nativeElement);
@@ -2358,6 +2395,7 @@ describe('igxSelect', () => {
23582395
fixture = TestBed.createComponent(IgxSelectTopComponent);
23592396
select = fixture.componentInstance.select;
23602397
fixture.detectChanges();
2398+
select.overlaySettings = { positionStrategy: new IgxSelectOverlapPositionStrategy(select) };
23612399
inputElement = fixture.debugElement.query(By.css('.' + CSS_CLASS_INPUT));
23622400
selectList = fixture.debugElement.query(By.css('.' + CSS_CLASS_DROPDOWN_LIST_SCROLL));
23632401
});
@@ -2407,6 +2445,7 @@ describe('igxSelect', () => {
24072445
fixture = TestBed.createComponent(IgxSelectBottomComponent);
24082446
select = fixture.componentInstance.select;
24092447
fixture.detectChanges();
2448+
select.overlaySettings = { positionStrategy: new IgxSelectOverlapPositionStrategy(select) };
24102449
inputElement = fixture.debugElement.query(By.css('.' + CSS_CLASS_INPUT));
24112450
selectList = fixture.debugElement.query(By.css('.' + CSS_CLASS_DROPDOWN_LIST_SCROLL));
24122451
});
@@ -2452,6 +2491,7 @@ describe('igxSelect', () => {
24522491
fixture = TestBed.createComponent(IgxSelectMiddleComponent);
24532492
fixture.detectChanges();
24542493
select = fixture.componentInstance.select;
2494+
select.overlaySettings = { positionStrategy: new IgxSelectOverlapPositionStrategy(select) };
24552495
inputElement = fixture.debugElement.query(By.css('.' + CSS_CLASS_INPUT));
24562496
selectList = fixture.debugElement.query(By.css('.' + CSS_CLASS_DROPDOWN_LIST_SCROLL));
24572497
addScrollDivToElement(fixture.nativeElement);
@@ -2557,6 +2597,30 @@ describe('igxSelect', () => {
25572597
verifyListPositioning();
25582598
}));
25592599
});
2600+
}); // end IgxSelectOverlapPositionStrategy positioning tests
2601+
2602+
describe('AutoPositionStrategy positioning tests: ', () => {
2603+
beforeEach(() => {
2604+
fixture = TestBed.createComponent(IgxSelectSimpleComponent);
2605+
select = fixture.componentInstance.select;
2606+
fixture.detectChanges();
2607+
inputElement = fixture.debugElement.query(By.css('.' + CSS_CLASS_INPUT));
2608+
selectList = fixture.debugElement.query(By.css('.' + CSS_CLASS_DROPDOWN_LIST_SCROLL));
2609+
});
2610+
2611+
it('should open and close correctly using the default AutoPositionStrategy', fakeAsync(() => {
2612+
expect((select as any)._overlayDefaults.positionStrategy).toBeInstanceOf(AutoPositionStrategy);
2613+
select.toggle();
2614+
tick();
2615+
fixture.detectChanges();
2616+
expect(select.collapsed).toBeFalsy();
2617+
2618+
select.toggle();
2619+
tick();
2620+
fixture.detectChanges();
2621+
expect(select.collapsed).toBeTruthy();
2622+
}));
2623+
});
25602624
});
25612625
describe('EditorProvider', () => {
25622626
beforeEach(() => {

0 commit comments

Comments
 (0)