wally-ui 1.12.1 → 1.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/dist/cli.js +8 -5
  2. package/dist/cli.js.map +1 -1
  3. package/package.json +1 -1
  4. package/playground/showcase/src/app/app.routes.server.ts +4 -0
  5. package/playground/showcase/src/app/components/ai/ai-composer/ai-composer.html +164 -31
  6. package/playground/showcase/src/app/components/ai/ai-composer/ai-composer.ts +25 -3
  7. package/playground/showcase/src/app/components/ai/ai-prompt-input/ai-prompt-input.html +1 -1
  8. package/playground/showcase/src/app/components/badge/badge.css +0 -0
  9. package/playground/showcase/src/app/components/badge/badge.html +3 -0
  10. package/playground/showcase/src/app/components/badge/badge.ts +24 -0
  11. package/playground/showcase/src/app/components/button/button.html +1 -3
  12. package/playground/showcase/src/app/components/button/button.ts +4 -4
  13. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-content/dropdown-menu-content.css +0 -0
  14. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-content/dropdown-menu-content.html +9 -0
  15. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-content/dropdown-menu-content.spec.ts +23 -0
  16. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-content/dropdown-menu-content.ts +167 -0
  17. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-group/dropdown-menu-group.css +0 -0
  18. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-group/dropdown-menu-group.html +5 -0
  19. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-group/dropdown-menu-group.spec.ts +23 -0
  20. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-group/dropdown-menu-group.ts +10 -0
  21. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-item/dropdown-menu-item.css +0 -0
  22. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-item/dropdown-menu-item.html +6 -0
  23. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-item/dropdown-menu-item.spec.ts +23 -0
  24. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-item/dropdown-menu-item.ts +37 -0
  25. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-label/dropdown-menu-label.css +0 -0
  26. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-label/dropdown-menu-label.html +3 -0
  27. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-label/dropdown-menu-label.spec.ts +23 -0
  28. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-label/dropdown-menu-label.ts +11 -0
  29. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-portal/dropdown-menu-portal.css +0 -0
  30. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-portal/dropdown-menu-portal.html +1 -0
  31. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-portal/dropdown-menu-portal.spec.ts +23 -0
  32. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-portal/dropdown-menu-portal.ts +11 -0
  33. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-separator/dropdown-menu-separator.css +0 -0
  34. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-separator/dropdown-menu-separator.html +1 -0
  35. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-separator/dropdown-menu-separator.spec.ts +23 -0
  36. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-separator/dropdown-menu-separator.ts +11 -0
  37. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-sub/dropdown-menu-sub.css +0 -0
  38. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-sub/dropdown-menu-sub.html +3 -0
  39. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-sub/dropdown-menu-sub.spec.ts +23 -0
  40. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-sub/dropdown-menu-sub.ts +16 -0
  41. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-sub-content/dropdown-menu-sub-content.css +0 -0
  42. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-sub-content/dropdown-menu-sub-content.html +9 -0
  43. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-sub-content/dropdown-menu-sub-content.spec.ts +23 -0
  44. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-sub-content/dropdown-menu-sub-content.ts +140 -0
  45. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-sub-trigger/dropdown-menu-sub-trigger.css +0 -0
  46. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-sub-trigger/dropdown-menu-sub-trigger.html +13 -0
  47. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-sub-trigger/dropdown-menu-sub-trigger.spec.ts +23 -0
  48. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-sub-trigger/dropdown-menu-sub-trigger.ts +40 -0
  49. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-sub.service.spec.ts +16 -0
  50. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-sub.service.ts +23 -0
  51. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-trigger/dropdown-menu-trigger.css +0 -0
  52. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-trigger/dropdown-menu-trigger.html +8 -0
  53. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-trigger/dropdown-menu-trigger.spec.ts +23 -0
  54. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-trigger/dropdown-menu-trigger.ts +55 -0
  55. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu.css +0 -0
  56. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu.html +3 -0
  57. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu.service.spec.ts +16 -0
  58. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu.service.ts +31 -0
  59. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu.ts +69 -0
  60. package/playground/showcase/src/app/components/tooltip/tooltip.ts +195 -80
  61. package/playground/showcase/src/app/pages/documentation/components/components.html +110 -51
  62. package/playground/showcase/src/app/pages/documentation/components/components.routes.ts +4 -0
  63. package/playground/showcase/src/app/pages/documentation/components/dropdown-menu-docs/dropdown-menu-docs.css +1 -0
  64. package/playground/showcase/src/app/pages/documentation/components/dropdown-menu-docs/dropdown-menu-docs.examples.ts +404 -0
  65. package/playground/showcase/src/app/pages/documentation/components/dropdown-menu-docs/dropdown-menu-docs.html +612 -0
  66. package/playground/showcase/src/app/pages/documentation/components/dropdown-menu-docs/dropdown-menu-docs.ts +127 -0
  67. package/playground/showcase/src/app/pages/home/home.html +10 -6
@@ -0,0 +1,10 @@
1
+ import { Component, input } from '@angular/core';
2
+
3
+ @Component({
4
+ selector: 'wally-dropdown-menu-group',
5
+ templateUrl: './dropdown-menu-group.html',
6
+ styleUrl: './dropdown-menu-group.css'
7
+ })
8
+ export class DropdownMenuGroup {
9
+ ariaLabel = input<string>('');
10
+ }
@@ -0,0 +1,6 @@
1
+ <div (click)="handleClick($event)" class="p-1" role="menuitem" tabindex="0">
2
+ <div class="text-base text-[#0a0a0a] hover:bg-neutral-100 dark:hover:bg-neutral-700/50 dark:text-white py-2 px-4 rounded-lg"
3
+ [ngClass]="{'text-neutral-400 dark:!text-white/50 pointer-events-none': disabled(), 'cursor-pointer': !disabled()}">
4
+ <ng-content></ng-content>
5
+ </div>
6
+ </div>
@@ -0,0 +1,23 @@
1
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+
3
+ import { DropdownMenuItem } from './dropdown-menu-item';
4
+
5
+ describe('DropdownMenuItem', () => {
6
+ let component: DropdownMenuItem;
7
+ let fixture: ComponentFixture<DropdownMenuItem>;
8
+
9
+ beforeEach(async () => {
10
+ await TestBed.configureTestingModule({
11
+ imports: [DropdownMenuItem]
12
+ })
13
+ .compileComponents();
14
+
15
+ fixture = TestBed.createComponent(DropdownMenuItem);
16
+ component = fixture.componentInstance;
17
+ fixture.detectChanges();
18
+ });
19
+
20
+ it('should create', () => {
21
+ expect(component).toBeTruthy();
22
+ });
23
+ });
@@ -0,0 +1,37 @@
1
+ import { Component, input, InputSignal, output, OutputEmitterRef } from '@angular/core';
2
+
3
+ import { DropdownMenuService } from '../dropdown-menu.service';
4
+ import { CommonModule } from '@angular/common';
5
+
6
+ @Component({
7
+ selector: 'wally-dropdown-menu-item',
8
+ imports: [
9
+ CommonModule
10
+ ],
11
+ templateUrl: './dropdown-menu-item.html',
12
+ styleUrl: './dropdown-menu-item.css'
13
+ })
14
+ export class DropdownMenuItem {
15
+ disabled: InputSignal<boolean> = input<boolean>(false);
16
+
17
+ click: OutputEmitterRef<void> = output<void>();
18
+
19
+ constructor(
20
+ private dropdownMenuService: DropdownMenuService
21
+ ) {}
22
+
23
+ /**
24
+ * Handles click event on menu item.
25
+ * Stops event propagation to prevent double-firing, emits click event, and closes dropdown.
26
+ * Does nothing if item is disabled.
27
+ */
28
+ handleClick(event: MouseEvent): void {
29
+ if (this.disabled()) {
30
+ return;
31
+ }
32
+
33
+ event?.stopPropagation();
34
+ this.click.emit();
35
+ this.dropdownMenuService.close();
36
+ }
37
+ }
@@ -0,0 +1,3 @@
1
+ <div class="px-5 py-4 text-base font-medium text-[#0a0a0a] dark:text-white">
2
+ <ng-content></ng-content>
3
+ </div>
@@ -0,0 +1,23 @@
1
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+
3
+ import { DropdownMenuLabel } from './dropdown-menu-label';
4
+
5
+ describe('DropdownMenuLabel', () => {
6
+ let component: DropdownMenuLabel;
7
+ let fixture: ComponentFixture<DropdownMenuLabel>;
8
+
9
+ beforeEach(async () => {
10
+ await TestBed.configureTestingModule({
11
+ imports: [DropdownMenuLabel]
12
+ })
13
+ .compileComponents();
14
+
15
+ fixture = TestBed.createComponent(DropdownMenuLabel);
16
+ component = fixture.componentInstance;
17
+ fixture.detectChanges();
18
+ });
19
+
20
+ it('should create', () => {
21
+ expect(component).toBeTruthy();
22
+ });
23
+ });
@@ -0,0 +1,11 @@
1
+ import { Component } from '@angular/core';
2
+
3
+ @Component({
4
+ selector: 'wally-dropdown-menu-label',
5
+ imports: [],
6
+ templateUrl: './dropdown-menu-label.html',
7
+ styleUrl: './dropdown-menu-label.css'
8
+ })
9
+ export class DropdownMenuLabel {
10
+
11
+ }
@@ -0,0 +1,23 @@
1
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+
3
+ import { DropdownMenuPortal } from './dropdown-menu-portal';
4
+
5
+ describe('DropdownMenuPortal', () => {
6
+ let component: DropdownMenuPortal;
7
+ let fixture: ComponentFixture<DropdownMenuPortal>;
8
+
9
+ beforeEach(async () => {
10
+ await TestBed.configureTestingModule({
11
+ imports: [DropdownMenuPortal]
12
+ })
13
+ .compileComponents();
14
+
15
+ fixture = TestBed.createComponent(DropdownMenuPortal);
16
+ component = fixture.componentInstance;
17
+ fixture.detectChanges();
18
+ });
19
+
20
+ it('should create', () => {
21
+ expect(component).toBeTruthy();
22
+ });
23
+ });
@@ -0,0 +1,11 @@
1
+ import { Component } from '@angular/core';
2
+
3
+ @Component({
4
+ selector: 'wally-dropdown-menu-portal',
5
+ imports: [],
6
+ templateUrl: './dropdown-menu-portal.html',
7
+ styleUrl: './dropdown-menu-portal.css'
8
+ })
9
+ export class DropdownMenuPortal {
10
+
11
+ }
@@ -0,0 +1 @@
1
+ <div class="h-px bg-neutral-300 dark:bg-neutral-700"></div>
@@ -0,0 +1,23 @@
1
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+
3
+ import { DropdownMenuSeparator } from './dropdown-menu-separator';
4
+
5
+ describe('DropdownMenuSeparator', () => {
6
+ let component: DropdownMenuSeparator;
7
+ let fixture: ComponentFixture<DropdownMenuSeparator>;
8
+
9
+ beforeEach(async () => {
10
+ await TestBed.configureTestingModule({
11
+ imports: [DropdownMenuSeparator]
12
+ })
13
+ .compileComponents();
14
+
15
+ fixture = TestBed.createComponent(DropdownMenuSeparator);
16
+ component = fixture.componentInstance;
17
+ fixture.detectChanges();
18
+ });
19
+
20
+ it('should create', () => {
21
+ expect(component).toBeTruthy();
22
+ });
23
+ });
@@ -0,0 +1,11 @@
1
+ import { Component } from '@angular/core';
2
+
3
+ @Component({
4
+ selector: 'wally-dropdown-menu-separator',
5
+ imports: [],
6
+ templateUrl: './dropdown-menu-separator.html',
7
+ styleUrl: './dropdown-menu-separator.css'
8
+ })
9
+ export class DropdownMenuSeparator {
10
+
11
+ }
@@ -0,0 +1,3 @@
1
+ <div class="relative">
2
+ <ng-content></ng-content>
3
+ </div>
@@ -0,0 +1,23 @@
1
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+
3
+ import { DropdownMenuSub } from './dropdown-menu-sub';
4
+
5
+ describe('DropdownMenuSub', () => {
6
+ let component: DropdownMenuSub;
7
+ let fixture: ComponentFixture<DropdownMenuSub>;
8
+
9
+ beforeEach(async () => {
10
+ await TestBed.configureTestingModule({
11
+ imports: [DropdownMenuSub]
12
+ })
13
+ .compileComponents();
14
+
15
+ fixture = TestBed.createComponent(DropdownMenuSub);
16
+ component = fixture.componentInstance;
17
+ fixture.detectChanges();
18
+ });
19
+
20
+ it('should create', () => {
21
+ expect(component).toBeTruthy();
22
+ });
23
+ });
@@ -0,0 +1,16 @@
1
+ import { Component } from '@angular/core';
2
+
3
+ import { DropdownMenuSubService } from '../dropdown-menu-sub.service';
4
+
5
+ @Component({
6
+ selector: 'wally-dropdown-menu-sub',
7
+ imports: [],
8
+ providers: [
9
+ DropdownMenuSubService
10
+ ],
11
+ templateUrl: './dropdown-menu-sub.html',
12
+ styleUrl: './dropdown-menu-sub.css'
13
+ })
14
+ export class DropdownMenuSub {
15
+
16
+ }
@@ -0,0 +1,9 @@
1
+ @if (subService.isOpen() && isPositioned()) {
2
+ <div
3
+ (mouseenter)="onMouseEnter()"
4
+ (mouseleave)="onMouseLeave()"
5
+ [class]="'absolute bg-white dark:bg-[#1b1b1b] rounded-xl shadow-2xl border border-neutral-300 dark:border-neutral-700 min-w-56 z-50 transition-all duration-200 ease-out ' + positionClasses()"
6
+ role="menu">
7
+ <ng-content></ng-content>
8
+ </div>
9
+ }
@@ -0,0 +1,23 @@
1
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+
3
+ import { DropdownMenuSubContent } from './dropdown-menu-sub-content';
4
+
5
+ describe('DropdownMenuSubContent', () => {
6
+ let component: DropdownMenuSubContent;
7
+ let fixture: ComponentFixture<DropdownMenuSubContent>;
8
+
9
+ beforeEach(async () => {
10
+ await TestBed.configureTestingModule({
11
+ imports: [DropdownMenuSubContent]
12
+ })
13
+ .compileComponents();
14
+
15
+ fixture = TestBed.createComponent(DropdownMenuSubContent);
16
+ component = fixture.componentInstance;
17
+ fixture.detectChanges();
18
+ });
19
+
20
+ it('should create', () => {
21
+ expect(component).toBeTruthy();
22
+ });
23
+ });
@@ -0,0 +1,140 @@
1
+ import { Component, computed, effect, ElementRef, signal } from '@angular/core';
2
+ import { DropdownMenuSubService } from '../dropdown-menu-sub.service';
3
+
4
+ export type SubmenuPosition = 'right' | 'left' | 'bottom' | 'top';
5
+
6
+ @Component({
7
+ selector: 'wally-dropdown-menu-sub-content',
8
+ imports: [],
9
+ templateUrl: './dropdown-menu-sub-content.html',
10
+ styleUrl: './dropdown-menu-sub-content.css'
11
+ })
12
+ export class DropdownMenuSubContent {
13
+ calculatedPosition = signal<SubmenuPosition>('right');
14
+ isPositioned = signal<boolean>(false);
15
+
16
+ positionClasses = computed(() => {
17
+ const position = this.calculatedPosition();
18
+
19
+ const positionMap = {
20
+ right: 'top-0 left-full ml-1',
21
+ left: 'top-0 right-full mr-1',
22
+ bottom: 'left-0 top-full mt-1',
23
+ top: 'left-0 bottom-full mb-1'
24
+ };
25
+
26
+ return positionMap[position];
27
+ });
28
+
29
+ constructor(
30
+ public subService: DropdownMenuSubService,
31
+ private elementRef: ElementRef
32
+ ) {
33
+ effect(() => {
34
+ if (this.subService.isOpen()) {
35
+ this.isPositioned.set(false);
36
+ setTimeout(() => {
37
+ const bestPosition = this.calculateBestPosition();
38
+ this.calculatedPosition.set(bestPosition);
39
+ this.isPositioned.set(true);
40
+ }, 0);
41
+ } else {
42
+ this.isPositioned.set(false);
43
+ }
44
+ });
45
+ }
46
+
47
+ /**
48
+ * Measures available space around the trigger element in all directions.
49
+ * @returns Object containing trigger dimensions and available space, or null if trigger not found
50
+ */
51
+ private measureAvailableSpace(): {
52
+ triggerRect: DOMRect;
53
+ spaceAbove: number;
54
+ spaceBelow: number;
55
+ spaceLeft: number;
56
+ spaceRight: number;
57
+ } | null {
58
+ const triggerElement = this.elementRef.nativeElement.parentElement;
59
+
60
+ if (!triggerElement) {
61
+ return null;
62
+ }
63
+
64
+ const triggerRect = triggerElement.getBoundingClientRect();
65
+ const viewportWidth = window.innerWidth;
66
+ const viewportHeight = window.innerHeight;
67
+
68
+ return {
69
+ triggerRect,
70
+ spaceAbove: triggerRect.top,
71
+ spaceBelow: viewportHeight - triggerRect.bottom,
72
+ spaceLeft: triggerRect.left,
73
+ spaceRight: viewportWidth - triggerRect.right
74
+ };
75
+ }
76
+
77
+ /**
78
+ * Calculates the best position for the submenu based on available viewport space.
79
+ * Prioritizes right/left (horizontal) over top/bottom (vertical) for submenus.
80
+ * Always uses the same priority order: right → left → bottom → top
81
+ * @returns The optimal submenu position
82
+ */
83
+ private calculateBestPosition(): SubmenuPosition {
84
+ const space = this.measureAvailableSpace();
85
+
86
+ if (!space) {
87
+ return 'right';
88
+ }
89
+
90
+ const menuDimensions = this.getMenuDimensions();
91
+ const MENU_MIN_HEIGHT = menuDimensions.height + 20;
92
+ const MENU_MIN_WIDTH = menuDimensions.width + 20;
93
+
94
+ // Always use same priority for submenus: right → left → bottom → top
95
+ if (space.spaceRight >= MENU_MIN_WIDTH) return 'right';
96
+ if (space.spaceLeft >= MENU_MIN_WIDTH) return 'left';
97
+ if (space.spaceBelow >= MENU_MIN_HEIGHT) return 'bottom';
98
+ return 'top';
99
+ }
100
+
101
+ /**
102
+ * Gets the submenu dimensions from the DOM.
103
+ * @returns Height and width of the submenu
104
+ */
105
+ private getMenuDimensions(): {
106
+ height: number;
107
+ width: number;
108
+ } {
109
+ const menuElement = this.elementRef.nativeElement.querySelector('[role="menu"]');
110
+
111
+ if (!menuElement) {
112
+ return {
113
+ height: 200,
114
+ width: 224
115
+ };
116
+ }
117
+
118
+ const rect = menuElement.getBoundingClientRect();
119
+
120
+ return {
121
+ height: rect.height || 200,
122
+ width: rect.width || 224
123
+ };
124
+ }
125
+
126
+ onMouseEnter(): void {
127
+ this.subService.setHoveringContent(true);
128
+ this.subService.open();
129
+ }
130
+
131
+ onMouseLeave(): void {
132
+ this.subService.setHoveringContent(false);
133
+
134
+ setTimeout(() => {
135
+ if (!this.subService.isHoveringContent()) {
136
+ this.subService.close();
137
+ }
138
+ }, 100);
139
+ }
140
+ }
@@ -0,0 +1,13 @@
1
+ <div (mouseenter)="onMouseEnter()" (mouseleave)="onMouseLeave()" class="p-1" role="menuitem" [attr.aria-haspopup]="true"
2
+ [attr.aria-expanded]="subService.isOpen()">
3
+
4
+ <div
5
+ class="flex justify-center items-center text-base text-[#0a0a0a] hover:bg-neutral-100 dark:hover:bg-neutral-700/50 dark:text-white py-2 px-4 rounded-lg cursor-pointer">
6
+ <ng-content></ng-content>
7
+
8
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"
9
+ class="size-4 ml-auto text-neutral-500 dark:text-neutral-400">
10
+ <path stroke-linecap="round" stroke-linejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5" />
11
+ </svg>
12
+ </div>
13
+ </div>
@@ -0,0 +1,23 @@
1
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+
3
+ import { DropdownMenuSubTrigger } from './dropdown-menu-sub-trigger';
4
+
5
+ describe('DropdownMenuSubTrigger', () => {
6
+ let component: DropdownMenuSubTrigger;
7
+ let fixture: ComponentFixture<DropdownMenuSubTrigger>;
8
+
9
+ beforeEach(async () => {
10
+ await TestBed.configureTestingModule({
11
+ imports: [DropdownMenuSubTrigger]
12
+ })
13
+ .compileComponents();
14
+
15
+ fixture = TestBed.createComponent(DropdownMenuSubTrigger);
16
+ component = fixture.componentInstance;
17
+ fixture.detectChanges();
18
+ });
19
+
20
+ it('should create', () => {
21
+ expect(component).toBeTruthy();
22
+ });
23
+ });
@@ -0,0 +1,40 @@
1
+ import { Component } from '@angular/core';
2
+ import { DropdownMenuSubService } from '../dropdown-menu-sub.service';
3
+
4
+ @Component({
5
+ selector: 'wally-dropdown-menu-sub-trigger',
6
+ imports: [],
7
+ templateUrl: './dropdown-menu-sub-trigger.html',
8
+ styleUrl: './dropdown-menu-sub-trigger.css'
9
+ })
10
+ export class DropdownMenuSubTrigger {
11
+ private hoverTimeout: any = null;
12
+
13
+ constructor(public subService: DropdownMenuSubService) {}
14
+
15
+ /**
16
+ * Opens submenu after 150ms delay to prevent accidental triggers.
17
+ */
18
+ onMouseEnter(): void {
19
+ this.hoverTimeout = setTimeout(() => {
20
+ this.subService.open();
21
+ }, 150);
22
+ }
23
+
24
+ /**
25
+ * Closes submenu with 300ms delay, allowing user to move mouse to submenu content.
26
+ * Cancels pending open timeout if mouse leaves quickly.
27
+ */
28
+ onMouseLeave(): void {
29
+ if (this.hoverTimeout) {
30
+ clearTimeout(this.hoverTimeout);
31
+ this.hoverTimeout = null;
32
+ }
33
+
34
+ setTimeout(() => {
35
+ if (!this.subService.isHoveringContent()) {
36
+ this.subService.close();
37
+ }
38
+ }, 300);
39
+ }
40
+ }
@@ -0,0 +1,16 @@
1
+ import { TestBed } from '@angular/core/testing';
2
+
3
+ import { DropdownMenuSubService } from './dropdown-menu-sub.service';
4
+
5
+ describe('DropdownMenuSubService', () => {
6
+ let service: DropdownMenuSubService;
7
+
8
+ beforeEach(() => {
9
+ TestBed.configureTestingModule({});
10
+ service = TestBed.inject(DropdownMenuSubService);
11
+ });
12
+
13
+ it('should be created', () => {
14
+ expect(service).toBeTruthy();
15
+ });
16
+ });
@@ -0,0 +1,23 @@
1
+ import { Injectable, signal } from '@angular/core';
2
+
3
+ @Injectable()
4
+ export class DropdownMenuSubService {
5
+ isOpen = signal(false);
6
+ private hoveringContent = signal(false);
7
+
8
+ open(): void {
9
+ this.isOpen.set(true);
10
+ }
11
+
12
+ close(): void {
13
+ this.isOpen.set(false);
14
+ }
15
+
16
+ setHoveringContent(value: boolean): void {
17
+ this.hoveringContent.set(value);
18
+ }
19
+
20
+ isHoveringContent(): boolean {
21
+ return this.hoveringContent();
22
+ }
23
+ }
@@ -0,0 +1,8 @@
1
+ <div
2
+ (click)="toggle()"
3
+ (mouseenter)="onMouseEnter()"
4
+ (mouseleave)="onMouseLeave()"
5
+ class="cursor-pointer"
6
+ role="button">
7
+ <ng-content></ng-content>
8
+ </div>
@@ -0,0 +1,23 @@
1
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+
3
+ import { DropdownMenuTrigger } from './dropdown-menu-trigger';
4
+
5
+ describe('DropdownMenuTrigger', () => {
6
+ let component: DropdownMenuTrigger;
7
+ let fixture: ComponentFixture<DropdownMenuTrigger>;
8
+
9
+ beforeEach(async () => {
10
+ await TestBed.configureTestingModule({
11
+ imports: [DropdownMenuTrigger]
12
+ })
13
+ .compileComponents();
14
+
15
+ fixture = TestBed.createComponent(DropdownMenuTrigger);
16
+ component = fixture.componentInstance;
17
+ fixture.detectChanges();
18
+ });
19
+
20
+ it('should create', () => {
21
+ expect(component).toBeTruthy();
22
+ });
23
+ });
@@ -0,0 +1,55 @@
1
+ import { Component } from '@angular/core';
2
+
3
+ import { DropdownMenuService } from '../dropdown-menu.service';
4
+
5
+ @Component({
6
+ selector: 'wally-dropdown-menu-trigger',
7
+ imports: [],
8
+ templateUrl: './dropdown-menu-trigger.html',
9
+ styleUrl: './dropdown-menu-trigger.css'
10
+ })
11
+ export class DropdownMenuTrigger {
12
+ private hoverTimeout: any = null;
13
+
14
+ constructor(
15
+ public dropdownMenuService: DropdownMenuService
16
+ ) {}
17
+
18
+ toggle(): void {
19
+ if (this.dropdownMenuService.triggerMode() === 'click') {
20
+ this.dropdownMenuService.toggle();
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Handles mouse enter event for hover mode.
26
+ * Opens the dropdown after a 200ms delay to prevent accidental triggers.
27
+ */
28
+ onMouseEnter(): void {
29
+ if (this.dropdownMenuService.triggerMode() === 'hover') {
30
+ this.hoverTimeout = setTimeout(() => {
31
+ this.dropdownMenuService.open();
32
+ }, 200);
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Handles mouse leave event for hover mode.
38
+ * Cancels pending open timeout and closes dropdown after 100ms delay,
39
+ * allowing user to move mouse to dropdown content without it closing.
40
+ */
41
+ onMouseLeave(): void {
42
+ if (this.dropdownMenuService.triggerMode() === 'hover') {
43
+ if (this.hoverTimeout) {
44
+ clearTimeout(this.hoverTimeout);
45
+ this.hoverTimeout = null;
46
+ }
47
+
48
+ setTimeout(() => {
49
+ if (!this.dropdownMenuService.isHoveringContent()) {
50
+ this.dropdownMenuService.close();
51
+ }
52
+ }, 100);
53
+ }
54
+ }
55
+ }
@@ -0,0 +1,3 @@
1
+ <div class="relative inline-block">
2
+ <ng-content></ng-content>
3
+ </div>