diff --git a/live-editing/configs/DropDownConfigGenerator.ts b/live-editing/configs/DropDownConfigGenerator.ts index 7608d9256..2167953f6 100644 --- a/live-editing/configs/DropDownConfigGenerator.ts +++ b/live-editing/configs/DropDownConfigGenerator.ts @@ -21,7 +21,8 @@ import { import { AppModuleConfig, Config, IConfigGenerator } from 'igniteui-live-editing'; export class DropDownConfigGenerator implements IConfigGenerator { public additionalImports = { - RemoteNWindService: '../../src/app/services/remoteNwind.service' + RemoteNWindService: '../../src/app/services/remoteNwind.service', + MultiLevelDirective: '../../src/app/data-entries/dropdown/dropdown-multi-level-menu/multi-level.directive' }; public generateConfigs(): Config[] { const configs = new Array(); @@ -101,6 +102,20 @@ export class DropDownConfigGenerator implements IConfigGenerator { shortenComponentPathBy: '/data-entries/dropdown/' })); + configs.push(new Config({ + component: 'DropdownMultiLevelMenuComponent', + additionalFiles: ['/src/app/data-entries/dropdown/dropdown-multi-level-menu/data.ts', + '/src/app/data-entries/dropdown/dropdown-multi-level-menu/multi-level.directive.ts', + '/src/app/data-entries/dropdown/dropdown-multi-level-menu/multi-level.service.ts'], + appModuleConfig: new AppModuleConfig({ + imports: ['DropdownMultiLevelMenuComponent', 'MultiLevelDirective', + 'IgxDropDownModule', 'IgxIconModule', 'IgxNavbarModule', 'IgxButtonModule', 'IgxToggleModule'], + ngDeclarations: ['DropdownMultiLevelMenuComponent', 'MultiLevelDirective'], + ngImports: ['IgxDropDownModule', 'IgxIconModule', 'IgxNavbarModule', 'IgxButtonModule', 'IgxToggleModule'] + }), + shortenComponentPathBy: '/data-entries/dropdown/' + })); + configs.push(new Config({ component: 'DropDownVirtualComponent', appModuleConfig: new AppModuleConfig({ diff --git a/src/app/data-entries/data-entries-routes-data.ts b/src/app/data-entries/data-entries-routes-data.ts index 497b0d971..77eb99b25 100644 --- a/src/app/data-entries/data-entries-routes-data.ts +++ b/src/app/data-entries/data-entries-routes-data.ts @@ -28,6 +28,7 @@ export const dataEntriesRoutesData = { "dropdown-remote": { displayName: "Virtual Dropdown - Remote Data", parentName: "Dropdown" }, "dropdown-virtual": { displayName: "Virtual Dropdown", parentName: "Dropdown" }, "dropdown-menu": { displayName: "Dropdown as Menu", parentName: "Dropdown" }, + "dropdown-multi-level-menu": { displayName: "Multi-Level Dropdown Menu", parentName: "Dropdown" }, "dropdown-sample-1": { displayName: "Simple Dropdown", parentName: "Dropdown" }, "dropdown-sample-2": { displayName: "Dropdown Selection", parentName: "Dropdown" }, "dropdown-sample-3": { displayName: "Dropdown Headers", parentName: "Dropdown" }, diff --git a/src/app/data-entries/data-entries-routing.module.ts b/src/app/data-entries/data-entries-routing.module.ts index e495e5404..4e990fae0 100644 --- a/src/app/data-entries/data-entries-routing.module.ts +++ b/src/app/data-entries/data-entries-routing.module.ts @@ -28,6 +28,7 @@ import { dataEntriesRoutesData } from './data-entries-routes-data'; import { DropDownRemoteComponent } from './dropdown/drop-down-remote-virtual/drop-down-remote.component'; import { DropDownVirtualComponent } from './dropdown/drop-down-virtual/drop-down-virtual.component'; import { DropdownMenuComponent } from './dropdown/dropdown-menu/dropdown-menu.component'; +import { DropdownMultiLevelMenuComponent } from './dropdown/dropdown-multi-level-menu/dropdown-multi-level-menu.component'; import { DropDownSample1Component } from './dropdown/dropdown-sample-1/dropdown-sample-1.component'; import { DropDownSample2Component } from './dropdown/dropdown-sample-2/dropdown-sample-2.component'; import { DropDownSample3Component } from './dropdown/dropdown-sample-3/dropdown-sample-3.component'; @@ -191,6 +192,11 @@ export const dataEntriesRoutes: Routes = [ data: dataEntriesRoutesData['dropdown-menu'], path: 'dropdown-menu' }, + { + component: DropdownMultiLevelMenuComponent, + data: dataEntriesRoutesData['dropdown-multi-level-menu'], + path: 'dropdown-multi-level-menu' + }, { component: DropDownSample1Component, data: dataEntriesRoutesData['dropdown-sample-1'], diff --git a/src/app/data-entries/data-entries.module.ts b/src/app/data-entries/data-entries.module.ts index d9b4f7398..937e96b7b 100644 --- a/src/app/data-entries/data-entries.module.ts +++ b/src/app/data-entries/data-entries.module.ts @@ -37,6 +37,8 @@ import { DataEntriesRoutingModule } from './data-entries-routing.module'; import { DropDownRemoteComponent } from './dropdown/drop-down-remote-virtual/drop-down-remote.component'; import { DropDownVirtualComponent } from './dropdown/drop-down-virtual/drop-down-virtual.component'; import { DropdownMenuComponent } from './dropdown/dropdown-menu/dropdown-menu.component'; +import { DropdownMultiLevelMenuComponent } from './dropdown/dropdown-multi-level-menu/dropdown-multi-level-menu.component'; +import { MultiLevelDirective } from './dropdown/dropdown-multi-level-menu/multi-level.directive'; import { DropDownSample1Component } from './dropdown/dropdown-sample-1/dropdown-sample-1.component'; import { DropDownSample2Component } from './dropdown/dropdown-sample-2/dropdown-sample-2.component'; import { DropDownSample3Component } from './dropdown/dropdown-sample-3/dropdown-sample-3.component'; @@ -99,6 +101,8 @@ import { ReactiveFormCustomValidationComponent } from './input-group/reactive-fo DropDownRemoteComponent, DropDownVirtualComponent, DropdownMenuComponent, + DropdownMultiLevelMenuComponent, + MultiLevelDirective, DropDownSample1Component, DropDownSample2Component, DropDownSample3Component, diff --git a/src/app/data-entries/dropdown/dropdown-multi-level-menu/data.ts b/src/app/data-entries/dropdown/dropdown-multi-level-menu/data.ts new file mode 100644 index 000000000..6b971ffe7 --- /dev/null +++ b/src/app/data-entries/dropdown/dropdown-multi-level-menu/data.ts @@ -0,0 +1,44 @@ +export const SUPPORT_DATA = [ + "Help & Support Documents", + "Blogs", + "Forums", + "Product Ideas", + "Reference Applications", + "Customer Stories", + "Webinars", + "eBook & Whitepapers" +]; + +export const DESKTOP_DATA = [ + "Ultimate UI for Windows Forms", + "Ultimate UI for WPF" +]; + +export const CROSS_PLATFORM_DATA = [ + "Ultimate UI for Uno", + "Ultimate UI for UWP", + "Ultimate UI for WinUI", + "Ultimate UI for Xamarin" +]; + +export const DESIGN_TO_CODE_DATA = [ + "Indigo.Design", + "App Builder", + "Design System & UI Kits" +]; + +export const TESTING_TOOLS_DATA = [ + "Test automation for Micro Focus UFT: Win Forms", + "Test automation for Micro Focus UFT: WPF", + "Test automation for IBM RFT: Windows Forms" +]; + +export const IGNITE_UI_DATA = [ + "Angular", + "ASP.NET Core", + "ASP.NET MVC", + "Blazor", + "jQuery", + "React", + "Web Components" +]; diff --git a/src/app/data-entries/dropdown/dropdown-multi-level-menu/dropdown-multi-level-menu.component.html b/src/app/data-entries/dropdown/dropdown-multi-level-menu/dropdown-multi-level-menu.component.html new file mode 100644 index 000000000..701f8f805 --- /dev/null +++ b/src/app/data-entries/dropdown/dropdown-multi-level-menu/dropdown-multi-level-menu.component.html @@ -0,0 +1,104 @@ +
+ +
+ +
+ + + + + +
+ +
+

{{ selection }}

+
+ + + + + Web chevron_right + + + + Desktop chevron_right + + + + Cross Platform chevron_right + + + + Design to Code chevron_right + + + + Testing Tools chevron_right + + + + + Indigo.Design + App Builder + + + + + {{ item }} + + + + + Product Pricing + Contact Us + + + + + + App Builder + + + Ignite UI chevron_right + + + + + + {{ item }} + + + + + + {{ item }} + + + + + + {{ item }} + + + + + + {{ item }} + + + + + + + {{ item }} + + +
diff --git a/src/app/data-entries/dropdown/dropdown-multi-level-menu/dropdown-multi-level-menu.component.scss b/src/app/data-entries/dropdown/dropdown-multi-level-menu/dropdown-multi-level-menu.component.scss new file mode 100644 index 000000000..b0e94eb48 --- /dev/null +++ b/src/app/data-entries/dropdown/dropdown-multi-level-menu/dropdown-multi-level-menu.component.scss @@ -0,0 +1,39 @@ +@use "../../../../variables" as *; + +.container { + padding: 16px; +} + +igx-icon { + --size: 18px; +} + +[igxButton] { + margin-inline-start: 0px; +} + +[multiLevel] { + cursor: default; +} + +$custom-navbar-theme: navbar-theme( + $background: #f8f9fa, +); + +$custom-button-theme: button-theme( + $foreground: #666, + $hover-foreground: #0099ff, + $focus-foreground: #0099ff, + $active-foreground: #0099ff, + $background: transparent, + $hover-background: transparent, + $focus-background: transparent, + $active-background: transparent, +); + +:host::ng-deep { + --ig-button-font-size: 0.75rem; + + @include css-vars($custom-navbar-theme); + @include css-vars($custom-button-theme); +} diff --git a/src/app/data-entries/dropdown/dropdown-multi-level-menu/dropdown-multi-level-menu.component.ts b/src/app/data-entries/dropdown/dropdown-multi-level-menu/dropdown-multi-level-menu.component.ts new file mode 100644 index 000000000..2cfcf1c6b --- /dev/null +++ b/src/app/data-entries/dropdown/dropdown-multi-level-menu/dropdown-multi-level-menu.component.ts @@ -0,0 +1,79 @@ +import { AfterViewInit, Component, QueryList, ViewChild, ViewChildren } from '@angular/core'; +import { + IgxDropDownComponent, + OverlaySettings, + ConnectedPositioningStrategy, + HorizontalAlignment, + VerticalAlignment +} from 'igniteui-angular'; +import { + CROSS_PLATFORM_DATA, + DESIGN_TO_CODE_DATA, + DESKTOP_DATA, + IGNITE_UI_DATA, + SUPPORT_DATA, + TESTING_TOOLS_DATA +} from './data'; +import { MultiLevelService } from './multi-level.service'; + +@Component({ + selector: 'app-dropdown-multi-level-menu', + templateUrl: './dropdown-multi-level-menu.component.html', + styleUrls: ['./dropdown-multi-level-menu.component.scss'], + providers: [MultiLevelService] +}) +export class DropdownMultiLevelMenuComponent implements AfterViewInit { + @ViewChildren(IgxDropDownComponent, { read: IgxDropDownComponent }) + private _dropdowns: QueryList; + + @ViewChild('dropdown1', { read: IgxDropDownComponent }) + private _multiLevelDropdown: IgxDropDownComponent; + + public supportData: string[] = SUPPORT_DATA; + public desktopData: string[] = DESKTOP_DATA; + public crossPlatformData: string[] = CROSS_PLATFORM_DATA; + public designToCodeData: string[] = DESIGN_TO_CODE_DATA; + public testingToolsData: string[] = TESTING_TOOLS_DATA; + public igniteUIData: string[] = IGNITE_UI_DATA; + + public overlaySettings: OverlaySettings = { + modal: false, + positionStrategy: new ConnectedPositioningStrategy({ + horizontalStartPoint: HorizontalAlignment.Center, + horizontalDirection: HorizontalAlignment.Center, + verticalStartPoint: VerticalAlignment.Bottom, + closeAnimation: undefined + }) + }; + + public selection: string = ''; + + constructor(private _multiLevelService: MultiLevelService) { } + + public ngAfterViewInit(): void { + this._dropdowns.forEach((dropdown) => { + dropdown.selectionChanging.subscribe((args) => { + args.cancel = true; + const value = args.newSelection.value; + const categories = this._multiLevelService.categories; + + if (categories.includes(value)) { + this.selection = ''; + return; + } + + if (this._multiLevelService.isMultiLevel(dropdown)) { + this._multiLevelService.handleSelection(); + } else { + dropdown.close(); + } + + this.selection = value; + }); + }); + + this._multiLevelDropdown.closing.subscribe((args) => { + this._multiLevelService.handleClosing(args); + }); + } +} diff --git a/src/app/data-entries/dropdown/dropdown-multi-level-menu/multi-level.directive.ts b/src/app/data-entries/dropdown/dropdown-multi-level-menu/multi-level.directive.ts new file mode 100644 index 000000000..78f26a8f5 --- /dev/null +++ b/src/app/data-entries/dropdown/dropdown-multi-level-menu/multi-level.directive.ts @@ -0,0 +1,83 @@ +import { AfterViewInit, Directive, HostListener, Input, OnDestroy } from '@angular/core'; +import { Subject, fromEvent } from 'rxjs'; +import { map, take, takeUntil } from 'rxjs/operators'; +import { MultiLevelService } from './multi-level.service'; +import { + IgxDropDownComponent, + OverlaySettings, + ConnectedPositioningStrategy, + IgxDropDownItemComponent, + HorizontalAlignment, + VerticalAlignment +} from 'igniteui-angular'; + +@Directive({ + selector: '[multiLevel]' +}) +export class MultiLevelDirective implements AfterViewInit, OnDestroy { + @Input() + public innerDropdown!: IgxDropDownComponent; + + private _destroy$ = new Subject(); + + private _overlaySettings: OverlaySettings = { + closeOnOutsideClick: false, + modal: false, + positionStrategy: new ConnectedPositioningStrategy({ + horizontalStartPoint: HorizontalAlignment.Right, + horizontalDirection: HorizontalAlignment.Right, + verticalStartPoint: VerticalAlignment.Top, + openAnimation: undefined, + closeAnimation: undefined + }) + }; + + constructor( + private _host: IgxDropDownItemComponent, + private _multiLevelService: MultiLevelService + ) { } + + public ngAfterViewInit(): void { + this._multiLevelService.add(this.innerDropdown); + + this.innerDropdown.opening + .pipe( + take(1), + map((args) => args.owner.toggleDirective.element) + ) + .subscribe((element) => { + fromEvent(element, 'mouseleave') + .pipe(takeUntil(this._destroy$)) + .subscribe((event: any) => { + this._multiLevelService.handleHover(event, this._host, this.innerDropdown); + }); + }); + } + + public ngOnDestroy(): void { + this._destroy$.next(); + this._destroy$.complete(); + } + + @HostListener('mouseenter') + public mouseenter() { + if (this.innerDropdown.collapsed) { + this.innerDropdown.open({ + ...this._overlaySettings, + target: this._host.element.nativeElement + }); + } + } + + @HostListener('mouseleave', ['$event']) + public mouseleave(event: MouseEvent) { + const target = event.relatedTarget as any; + const innerDropdownItem = this.innerDropdown.items.some( + (item) => item.id === target?.id + ); + + if (!this.innerDropdown.collapsed && !innerDropdownItem) { + this.innerDropdown.close(); + } + } +} diff --git a/src/app/data-entries/dropdown/dropdown-multi-level-menu/multi-level.service.ts b/src/app/data-entries/dropdown/dropdown-multi-level-menu/multi-level.service.ts new file mode 100644 index 000000000..f288ffadd --- /dev/null +++ b/src/app/data-entries/dropdown/dropdown-multi-level-menu/multi-level.service.ts @@ -0,0 +1,71 @@ +import { Injectable } from '@angular/core'; +import { IgxDropDownComponent, IgxDropDownItemComponent } from 'igniteui-angular'; + +@Injectable() +export class MultiLevelService { + private _dropdowns: IgxDropDownComponent[] = []; + + private _categories: string[] = [ + 'Web', + 'Desktop', + 'Cross Platform', + 'Design to Code', + 'Testing Tools', + 'Ignite UI' + ]; + + public get categories(): string[] { + return this._categories; + } + + public add(dropdown: IgxDropDownComponent): void { + this._dropdowns.push(dropdown); + } + + public isMultiLevel(dropdown: IgxDropDownComponent): boolean { + return this._dropdowns.some((d) => d.id === dropdown.id); + } + + public handleSelection(): void { + // close all inner dropdowns on selection + this.closeAll(); + } + + public handleClosing(args: any): void { + // do not close the main dropdown if a host item is selected + if (args && args.event) { + const target = args.event.composedPath() + .find((e) => (e as HTMLElement).tagName?.toLowerCase() === 'igx-drop-down-item'); + + if (target?.hasAttribute('multiLevel')) { + args.cancel = true; + } + } + } + + public handleHover( + event: any, + host: IgxDropDownItemComponent, + dropdown: IgxDropDownComponent + ): void { + const target = event.relatedTarget; + + // hover outside of dropdown -> close all inner dropdowns + if (!target?.id) { + this.closeAll(); + + } else if (target?.hasAttribute('multiLevel') && target?.id !== host.id) { + // hover back to parent dropdown + // if the target is not the host -> close inner dropdown + dropdown.close(); + } + } + + private closeAll() { + this._dropdowns.forEach((dropdown) => { + if (!dropdown.collapsed) { + dropdown.close(); + } + }); + } +}