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.
- package/dist/cli.js +8 -5
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/playground/showcase/src/app/app.routes.server.ts +4 -0
- package/playground/showcase/src/app/components/ai/ai-composer/ai-composer.html +164 -31
- package/playground/showcase/src/app/components/ai/ai-composer/ai-composer.ts +25 -3
- package/playground/showcase/src/app/components/ai/ai-prompt-input/ai-prompt-input.html +1 -1
- package/playground/showcase/src/app/components/badge/badge.css +0 -0
- package/playground/showcase/src/app/components/badge/badge.html +3 -0
- package/playground/showcase/src/app/components/badge/badge.ts +24 -0
- package/playground/showcase/src/app/components/button/button.html +1 -3
- package/playground/showcase/src/app/components/button/button.ts +4 -4
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-content/dropdown-menu-content.css +0 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-content/dropdown-menu-content.html +9 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-content/dropdown-menu-content.spec.ts +23 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-content/dropdown-menu-content.ts +167 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-group/dropdown-menu-group.css +0 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-group/dropdown-menu-group.html +5 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-group/dropdown-menu-group.spec.ts +23 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-group/dropdown-menu-group.ts +10 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-item/dropdown-menu-item.css +0 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-item/dropdown-menu-item.html +6 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-item/dropdown-menu-item.spec.ts +23 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-item/dropdown-menu-item.ts +37 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-label/dropdown-menu-label.css +0 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-label/dropdown-menu-label.html +3 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-label/dropdown-menu-label.spec.ts +23 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-label/dropdown-menu-label.ts +11 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-portal/dropdown-menu-portal.css +0 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-portal/dropdown-menu-portal.html +1 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-portal/dropdown-menu-portal.spec.ts +23 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-portal/dropdown-menu-portal.ts +11 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-separator/dropdown-menu-separator.css +0 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-separator/dropdown-menu-separator.html +1 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-separator/dropdown-menu-separator.spec.ts +23 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-separator/dropdown-menu-separator.ts +11 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-sub/dropdown-menu-sub.css +0 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-sub/dropdown-menu-sub.html +3 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-sub/dropdown-menu-sub.spec.ts +23 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-sub/dropdown-menu-sub.ts +16 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-sub-content/dropdown-menu-sub-content.css +0 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-sub-content/dropdown-menu-sub-content.html +9 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-sub-content/dropdown-menu-sub-content.spec.ts +23 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-sub-content/dropdown-menu-sub-content.ts +140 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-sub-trigger/dropdown-menu-sub-trigger.css +0 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-sub-trigger/dropdown-menu-sub-trigger.html +13 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-sub-trigger/dropdown-menu-sub-trigger.spec.ts +23 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-sub-trigger/dropdown-menu-sub-trigger.ts +40 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-sub.service.spec.ts +16 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-sub.service.ts +23 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-trigger/dropdown-menu-trigger.css +0 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-trigger/dropdown-menu-trigger.html +8 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-trigger/dropdown-menu-trigger.spec.ts +23 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-trigger/dropdown-menu-trigger.ts +55 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu.css +0 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu.html +3 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu.service.spec.ts +16 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu.service.ts +31 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu.ts +69 -0
- package/playground/showcase/src/app/components/tooltip/tooltip.ts +195 -80
- package/playground/showcase/src/app/pages/documentation/components/components.html +110 -51
- package/playground/showcase/src/app/pages/documentation/components/components.routes.ts +4 -0
- package/playground/showcase/src/app/pages/documentation/components/dropdown-menu-docs/dropdown-menu-docs.css +1 -0
- package/playground/showcase/src/app/pages/documentation/components/dropdown-menu-docs/dropdown-menu-docs.examples.ts +404 -0
- package/playground/showcase/src/app/pages/documentation/components/dropdown-menu-docs/dropdown-menu-docs.html +612 -0
- package/playground/showcase/src/app/pages/documentation/components/dropdown-menu-docs/dropdown-menu-docs.ts +127 -0
- 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
|
+
}
|
|
File without changes
|
|
@@ -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
|
+
}
|
|
File without changes
|
|
@@ -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
|
+
});
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<p>dropdown-menu-portal works!</p>
|
|
@@ -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
|
+
});
|
|
File without changes
|
|
@@ -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
|
+
}
|
package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-sub/dropdown-menu-sub.css
ADDED
|
File without changes
|
|
@@ -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
|
+
});
|
package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-sub/dropdown-menu-sub.ts
ADDED
|
@@ -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
|
+
}
|
|
File without changes
|
|
@@ -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
|
+
}
|
|
File without changes
|
|
@@ -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
|
+
}
|
package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-sub.service.spec.ts
ADDED
|
@@ -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
|
+
}
|
|
File without changes
|
|
@@ -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
|
+
}
|
|
File without changes
|