diff --git a/CHANGELOG.md b/CHANGELOG.md index c5e6df5c44e..241ba097e14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ All notable changes for each version of this project will be documented in this ### New Features +- `IgxSelectComponent` + - 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. + - Added `IgxSelectOverlapPositionStrategy` - a new publicly exported strategy that preserves the previous behavior of aligning the selected item's text over the input text. Consumers can opt into this behavior by passing `overlaySettings = { positionStrategy: new IgxSelectOverlapPositionStrategy(select) }`. + - **Theming** - Added derived themes for the Grid and related internal components. When a parent component theme is included, its internal controls now derive their tokens from the parent theme colors, keeping nested buttons, icons, inputs, dropdowns, checkboxes, scrollbars, chips, and other helper components visually aligned. diff --git a/package-lock.json b/package-lock.json index 6a0a6e90e3a..193c3dba46d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1182,9 +1182,6 @@ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1199,9 +1196,6 @@ "arm" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1216,9 +1210,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1233,9 +1224,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1250,9 +1238,6 @@ "loong64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1267,9 +1252,6 @@ "loong64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1284,9 +1266,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1301,9 +1280,6 @@ "ppc64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1318,9 +1294,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1335,9 +1308,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1352,9 +1322,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1369,9 +1336,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1386,9 +1350,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -5048,9 +5009,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5068,9 +5026,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -5088,9 +5043,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5108,9 +5060,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5128,9 +5077,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5148,9 +5094,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5168,9 +5111,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -5778,6 +5718,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5798,6 +5739,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5818,6 +5760,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5838,6 +5781,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5858,6 +5802,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5878,6 +5823,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -23186,6 +23132,33 @@ "sassdoc-extras": "^2.5.0" } }, + "node_modules/sassdoc-theme-default/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, "node_modules/sassdoc-theme-default/node_modules/commander": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", @@ -23214,6 +23187,21 @@ "jsonfile": "^2.1.0" } }, + "node_modules/sassdoc-theme-default/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/sassdoc-theme-default/node_modules/jsonfile": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", @@ -23250,6 +23238,36 @@ } } }, + "node_modules/sassdoc-theme-default/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/sassdoc-theme-default/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/sassdoc/node_modules/argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", diff --git a/projects/igniteui-angular/select/src/select/public_api.ts b/projects/igniteui-angular/select/src/select/public_api.ts index 07d7e47894a..c15b16ec1ba 100644 --- a/projects/igniteui-angular/select/src/select/public_api.ts +++ b/projects/igniteui-angular/select/src/select/public_api.ts @@ -6,6 +6,7 @@ import { IgxSelectComponent, IgxSelectFooterDirective, IgxSelectHeaderDirective, export * from './select-group.component'; export * from './select-item.component'; export * from './select.component'; +export * from './select-overlap-positioning-strategy'; /* NOTE: Select directives collection for ease-of-use import in standalone components scenario */ export const IGX_SELECT_DIRECTIVES = [ diff --git a/projects/igniteui-angular/select/src/select/select-positioning-strategy.ts b/projects/igniteui-angular/select/src/select/select-overlap-positioning-strategy.ts similarity index 93% rename from projects/igniteui-angular/select/src/select/select-positioning-strategy.ts rename to projects/igniteui-angular/select/src/select/select-overlap-positioning-strategy.ts index 3a448070a39..c2d16ad7b1f 100644 --- a/projects/igniteui-angular/select/src/select/select-positioning-strategy.ts +++ b/projects/igniteui-angular/select/src/select/select-overlap-positioning-strategy.ts @@ -1,13 +1,14 @@ import { VerticalAlignment, HorizontalAlignment, PositionSettings, ConnectedFit, Point, Size, BaseFitPositionStrategy, Util } from 'igniteui-angular/core'; import { IPositionStrategy } from 'igniteui-angular/core'; -import { IgxSelectBase } from './select.common'; -import { PlatformUtil } from 'igniteui-angular/core'; -import { Optional } from '@angular/core'; +import type { IgxSelectComponent } from './select.component'; import { fadeIn, fadeOut } from 'igniteui-angular/animations'; -/** @hidden @internal */ -export class SelectPositioningStrategy extends BaseFitPositionStrategy implements IPositionStrategy { +/** + * Positions the select dropdown so that the active item's text overlaps + * the value displayed in the select input box. + */ +export class IgxSelectOverlapPositionStrategy extends BaseFitPositionStrategy implements IPositionStrategy { private _selectDefaultSettings = { horizontalDirection: HorizontalAlignment.Right, verticalDirection: VerticalAlignment.Bottom, @@ -22,7 +23,10 @@ export class SelectPositioningStrategy extends BaseFitPositionStrategy implement private global_xOffset = 0; private global_styles: SelectStyles = {}; - constructor(public select: IgxSelectBase, settings?: PositionSettings, @Optional() protected platform?: PlatformUtil) { + /** @hidden @internal */ + public ownsScrollPositioning = true; + + constructor(private select: IgxSelectComponent, settings?: PositionSettings) { super(); this.settings = Object.assign({}, this._selectDefaultSettings, settings); } @@ -87,7 +91,7 @@ export class SelectPositioningStrategy extends BaseFitPositionStrategy implement /** * Obtain the selected item if there is such one or otherwise use the first one */ - public getInteractionItemElement(): HTMLElement { + private getInteractionItemElement(): HTMLElement { let itemElement; if (this.select.selectedItem) { itemElement = this.select.selectedItem.element.nativeElement; @@ -103,7 +107,7 @@ export class SelectPositioningStrategy extends BaseFitPositionStrategy implement * * @param selectFit selectFit to use for computation. */ - protected fitInViewport(_contentElement: HTMLElement, selectFit: SelectFit) { + protected override fitInViewport(_contentElement: HTMLElement, selectFit: SelectFit) { const footer = selectFit.scrollContainerRect.bottom - selectFit.contentElementRect.bottom; const header = selectFit.scrollContainerRect.top - selectFit.contentElementRect.top; const lastItemFitSize = selectFit.targetRect.bottom + selectFit.styles.itemTextToInputTextDiff - footer; @@ -212,7 +216,7 @@ export class SelectPositioningStrategy extends BaseFitPositionStrategy implement } /** @hidden */ -export interface SelectFit extends ConnectedFit { +interface SelectFit extends ConnectedFit { itemElement?: HTMLElement; scrollContainer: HTMLElement; scrollContainerRect: ClientRect; @@ -222,7 +226,7 @@ export interface SelectFit extends ConnectedFit { } /** @hidden */ -export interface SelectStyles { +interface SelectStyles { itemTextPadding?: number; itemTextIndent?: number; itemTextToInputTextDiff?: number; diff --git a/projects/igniteui-angular/select/src/select/select.component.spec.ts b/projects/igniteui-angular/select/src/select/select.component.spec.ts index b54cbd6239c..4db58d1f918 100644 --- a/projects/igniteui-angular/select/src/select/select.component.spec.ts +++ b/projects/igniteui-angular/select/src/select/select.component.spec.ts @@ -9,13 +9,14 @@ import { IGX_DROPDOWN_BASE, IgxDropDownItemComponent, ISelectionEventArgs } from import { IgxHintDirective, IgxInputState, IgxLabelDirective, IgxPrefixDirective, IgxSuffixDirective } from '../../../input-group/src/public_api'; import { IgxSelectComponent, IgxSelectFooterDirective, IgxSelectHeaderDirective } from './select.component'; import { IgxSelectItemComponent } from './select-item.component'; -import { HorizontalAlignment, VerticalAlignment, ConnectedPositioningStrategy, AbsoluteScrollStrategy, IgxSelectionAPIService } from 'igniteui-angular/core'; +import { HorizontalAlignment, VerticalAlignment, ConnectedPositioningStrategy, AbsoluteScrollStrategy, AutoPositionStrategy, IgxSelectionAPIService } from 'igniteui-angular/core'; import { UIInteractions } from '../../../test-utils/ui-interactions.spec'; import { IgxButtonDirective } from '../../../directives/src/directives/button/button.directive'; import { IgxIconComponent } from 'igniteui-angular/icon'; import { IgxSelectGroupComponent } from './select-group.component'; import { IgxDropDownItemBaseDirective } from '../../../drop-down/src/drop-down/drop-down-item.base'; import { addScrollDivToElement } from 'igniteui-angular/core/src/services/overlay/overlay.spec'; +import { IgxSelectOverlapPositionStrategy } from './select-overlap-positioning-strategy'; const CSS_CLASS_INPUT_GROUP = 'igx-input-group'; const CSS_CLASS_INPUT = 'igx-input-group__input'; @@ -191,6 +192,31 @@ describe('igxSelect', () => { expect(select.disabled).toBeTruthy(); }); + it('should use AutoPositionStrategy as the default position strategy', () => { + // The public overlaySettings input is undefined by default + expect(select.overlaySettings).toBeUndefined(); + // The internal _overlayDefaults should use AutoPositionStrategy + expect((select as any)._overlayDefaults.positionStrategy).toBeInstanceOf(AutoPositionStrategy); + }); + + it('should allow opt-in to IgxSelectOverlapPositionStrategy via overlaySettings', fakeAsync(() => { + const overlapStrategy = new IgxSelectOverlapPositionStrategy(select); + select.overlaySettings = { positionStrategy: overlapStrategy }; + expect(select.overlaySettings.positionStrategy).toBeInstanceOf(IgxSelectOverlapPositionStrategy); + expect((select.overlaySettings.positionStrategy as IgxSelectOverlapPositionStrategy).ownsScrollPositioning).toBeTrue(); + + // The select should still open correctly when using the overlap strategy + select.open(); + tick(); + fixture.detectChanges(); + expect(select.collapsed).toBeFalsy(); + + select.close(); + tick(); + fixture.detectChanges(); + expect(select.collapsed).toBeTruthy(); + })); + it('should open dropdown on input click', () => { const inputGroup = fixture.debugElement.query(By.css('.' + CSS_CLASS_INPUT_GROUP)); expect(select.collapsed).toBeTruthy(); @@ -2220,6 +2246,7 @@ describe('igxSelect', () => { })); }); describe('Positioning tests: ', () => { + describe('IgxSelectOverlapPositionStrategy positioning tests: ', () => { const defaultWindowToListOffset = 16; const defaultItemLeftPadding = 24; const defaultItemTopPadding = 0; @@ -2258,6 +2285,7 @@ describe('igxSelect', () => { fixture = TestBed.createComponent(IgxSelectMiddleComponent); select = fixture.componentInstance.select; fixture.detectChanges(); + select.overlaySettings = { positionStrategy: new IgxSelectOverlapPositionStrategy(select) }; inputElement = fixture.debugElement.query(By.css('.' + CSS_CLASS_INPUT)); selectList = fixture.debugElement.query(By.css('.' + CSS_CLASS_DROPDOWN_LIST_SCROLL)); addScrollDivToElement(fixture.nativeElement); @@ -2358,6 +2386,7 @@ describe('igxSelect', () => { fixture = TestBed.createComponent(IgxSelectTopComponent); select = fixture.componentInstance.select; fixture.detectChanges(); + select.overlaySettings = { positionStrategy: new IgxSelectOverlapPositionStrategy(select) }; inputElement = fixture.debugElement.query(By.css('.' + CSS_CLASS_INPUT)); selectList = fixture.debugElement.query(By.css('.' + CSS_CLASS_DROPDOWN_LIST_SCROLL)); }); @@ -2407,6 +2436,7 @@ describe('igxSelect', () => { fixture = TestBed.createComponent(IgxSelectBottomComponent); select = fixture.componentInstance.select; fixture.detectChanges(); + select.overlaySettings = { positionStrategy: new IgxSelectOverlapPositionStrategy(select) }; inputElement = fixture.debugElement.query(By.css('.' + CSS_CLASS_INPUT)); selectList = fixture.debugElement.query(By.css('.' + CSS_CLASS_DROPDOWN_LIST_SCROLL)); }); @@ -2452,6 +2482,7 @@ describe('igxSelect', () => { fixture = TestBed.createComponent(IgxSelectMiddleComponent); fixture.detectChanges(); select = fixture.componentInstance.select; + select.overlaySettings = { positionStrategy: new IgxSelectOverlapPositionStrategy(select) }; inputElement = fixture.debugElement.query(By.css('.' + CSS_CLASS_INPUT)); selectList = fixture.debugElement.query(By.css('.' + CSS_CLASS_DROPDOWN_LIST_SCROLL)); addScrollDivToElement(fixture.nativeElement); @@ -2557,6 +2588,30 @@ describe('igxSelect', () => { verifyListPositioning(); })); }); + }); // end IgxSelectOverlapPositionStrategy positioning tests + + describe('AutoPositionStrategy positioning tests: ', () => { + beforeEach(() => { + fixture = TestBed.createComponent(IgxSelectSimpleComponent); + select = fixture.componentInstance.select; + fixture.detectChanges(); + inputElement = fixture.debugElement.query(By.css('.' + CSS_CLASS_INPUT)); + selectList = fixture.debugElement.query(By.css('.' + CSS_CLASS_DROPDOWN_LIST_SCROLL)); + }); + + it('should open and close correctly using the default AutoPositionStrategy', fakeAsync(() => { + expect((select as any)._overlayDefaults.positionStrategy).toBeInstanceOf(AutoPositionStrategy); + select.toggle(); + tick(); + fixture.detectChanges(); + expect(select.collapsed).toBeFalsy(); + + select.toggle(); + tick(); + fixture.detectChanges(); + expect(select.collapsed).toBeTruthy(); + })); + }); }); describe('EditorProvider', () => { beforeEach(() => { diff --git a/projects/igniteui-angular/select/src/select/select.component.ts b/projects/igniteui-angular/select/src/select/select.component.ts index 63ae442d0f1..68b9523006b 100644 --- a/projects/igniteui-angular/select/src/select/select.component.ts +++ b/projects/igniteui-angular/select/src/select/select.component.ts @@ -9,10 +9,13 @@ import { IBaseCancelableBrowserEventArgs, IBaseEventArgs, AbsoluteScrollStrategy, + AutoPositionStrategy, + HorizontalAlignment, + VerticalAlignment, OverlaySettings } from 'igniteui-angular/core'; +import { fadeIn, fadeOut } from 'igniteui-angular/animations'; import { IgxSelectItemComponent } from './select-item.component'; -import { SelectPositioningStrategy } from './select-positioning-strategy'; import { IgxSelectBase } from './select.common'; import { IgxHintDirective, IgxInputGroupType, IgxPrefixDirective, IGX_INPUT_GROUP_TYPE, IgxInputGroupComponent, IgxInputDirective, IgxInputState, IgxLabelDirective, IgxReadOnlyInputDirective, IgxSuffixDirective } from 'igniteui-angular/input-group'; import { ToggleViewCancelableEventArgs, ToggleViewEventArgs, IgxToggleDirective } from 'igniteui-angular/directives'; @@ -414,7 +417,14 @@ export class IgxSelectComponent extends IgxDropDownComponent implements IgxSelec this._overlayDefaults = { target: this.getEditElement(), modal: false, - positionStrategy: new SelectPositioningStrategy(this), + positionStrategy: new AutoPositionStrategy({ + horizontalDirection: HorizontalAlignment.Right, + verticalDirection: VerticalAlignment.Bottom, + horizontalStartPoint: HorizontalAlignment.Left, + verticalStartPoint: VerticalAlignment.Top, + openAnimation: fadeIn, + closeAnimation: fadeOut + }), scrollStrategy: new AbsoluteScrollStrategy(), excludeFromOutsideClick: [this.inputGroup.element.nativeElement as HTMLElement] }; @@ -447,7 +457,7 @@ export class IgxSelectComponent extends IgxDropDownComponent implements IgxSelec /** @hidden @internal */ public override onToggleContentAppended(event: ToggleViewEventArgs) { const info = this.overlayService.getOverlayById(event.id); - if (info?.settings?.positionStrategy instanceof SelectPositioningStrategy) { + if ((info?.settings?.positionStrategy as { ownsScrollPositioning?: boolean })?.ownsScrollPositioning) { return; } super.onToggleContentAppended(event);