Skip to content

Commit 3686b73

Browse files
authored
feat(module:dropdown): display arrow for content dropdown (#9329)
1 parent 190c4c2 commit 3686b73

7 files changed

Lines changed: 156 additions & 2 deletions

File tree

components/dropdown/demo/arrow.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
order: 9
3+
title:
4+
zh-CN: 箭头
5+
en-US: Arrow
6+
---
7+
8+
## zh-CN
9+
10+
可以展示一个箭头
11+
12+
## en-US
13+
14+
You could display an arrow

components/dropdown/demo/arrow.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { Component } from '@angular/core';
2+
3+
import { NzButtonModule } from 'ng-zorro-antd/button';
4+
import { NzDropDownModule, NzPlacementType } from 'ng-zorro-antd/dropdown';
5+
import { NzFlexModule } from 'ng-zorro-antd/flex';
6+
7+
@Component({
8+
selector: 'nz-demo-dropdown-arrow',
9+
imports: [NzDropDownModule, NzButtonModule, NzFlexModule],
10+
template: `
11+
<div nz-flex [nzGap]="8" nzWrap="wrap">
12+
@for (position of listOfPosition; track position) {
13+
<button nz-button nz-dropdown [nzDropdownMenu]="menu" [nzPlacement]="position" nzArrow>{{ position }}</button>
14+
<nz-dropdown-menu #menu="nzDropdownMenu">
15+
<ul nz-menu>
16+
<li nz-menu-item>1st menu item length</li>
17+
<li nz-menu-item>2nd menu item length</li>
18+
<li nz-menu-item>3rd menu item length</li>
19+
</ul>
20+
</nz-dropdown-menu>
21+
}
22+
</div>
23+
`
24+
})
25+
export class NzDemoDropdownArrowComponent {
26+
listOfPosition: NzPlacementType[] = ['bottomLeft', 'bottomCenter', 'bottomRight', 'topLeft', 'topCenter', 'topRight'];
27+
}

components/dropdown/doc/index.en-US.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ If there are too many operations to display, you can wrap them in a `Dropdown`.
2525
| `[nzOverlayClassName]` | Class name of the dropdown root element | `string` | - |
2626
| `[nzOverlayStyle]` | Style of the dropdown root element | `object` | - |
2727
| `(nzVisibleChange)` | a callback function takes an argument: `nzVisible`, is executed when the visible state is changed | `EventEmitter<boolean>` | - |
28+
| `[nzArrow]` | Whether the dropdown arrow should be visible | `boolean` | `false` |
2829

2930
You should use [nz-menu](/components/menu/en) in `nz-dropdown`. The menu items and dividers are also available by using `nz-menu-item` and `nz-menu-divider`.
3031

components/dropdown/doc/index.zh-CN.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ description: 向下弹出的列表。
2626
| `[nzOverlayClassName]` | 下拉根元素的类名称 | `string` | - |
2727
| `[nzOverlayStyle]` | 下拉根元素的样式 | `object` | - |
2828
| `(nzVisibleChange)` | 菜单显示状态改变时调用,参数为 nzVisible | `EventEmitter<boolean>` | - |
29+
| `[nzArrow]` | 下拉框箭头是否显示 | `boolean` | `false` |
2930

3031
菜单使用 [nz-menu](/components/menu/zh),还包括菜单项 `[nz-menu-item]`,分割线 `[nz-menu-divider]`
3132

components/dropdown/dropdown-menu.component.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,13 @@ export type NzPlacementType = 'bottomLeft' | 'bottomCenter' | 'bottomRight' | 't
4848
<div
4949
class="ant-dropdown"
5050
[class.ant-dropdown-rtl]="dir === 'rtl'"
51+
[class.ant-dropdown-show-arrow]="nzArrow"
52+
[class.ant-dropdown-placement-bottomLeft]="placement === 'bottomLeft'"
53+
[class.ant-dropdown-placement-bottomRight]="placement === 'bottomRight'"
54+
[class.ant-dropdown-placement-bottom]="placement === 'bottom'"
55+
[class.ant-dropdown-placement-topLeft]="placement === 'topLeft'"
56+
[class.ant-dropdown-placement-topRight]="placement === 'topRight'"
57+
[class.ant-dropdown-placement-top]="placement === 'top'"
5158
[class]="nzOverlayClassName"
5259
[style]="nzOverlayStyle"
5360
@slideMotion
@@ -57,6 +64,9 @@ export type NzPlacementType = 'bottomLeft' | 'bottomCenter' | 'bottomRight' | 't
5764
(mouseenter)="setMouseState(true)"
5865
(mouseleave)="setMouseState(false)"
5966
>
67+
@if (nzArrow) {
68+
<div class="ant-dropdown-arrow"></div>
69+
}
6070
<ng-content></ng-content>
6171
</div>
6272
</ng-template>
@@ -79,6 +89,8 @@ export class NzDropdownMenuComponent implements AfterContentInit, OnInit {
7989
animationStateChange$ = new EventEmitter<AnimationEvent>();
8090
nzOverlayClassName: string = '';
8191
nzOverlayStyle: IndexableObject = {};
92+
nzArrow: boolean = false;
93+
placement: NzPlacementType | 'bottom' | 'top' = 'bottomLeft';
8294
@ViewChild(TemplateRef, { static: true }) templateRef!: TemplateRef<NzSafeAny>;
8395

8496
dir: Direction = 'ltr';

components/dropdown/dropdown.directive.spec.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,59 @@ describe('dropdown', () => {
3939
overlayContainer.ngOnDestroy();
4040
}));
4141

42+
it('should render arrow when nzArrow is true and apply placement classes', fakeAsync(() => {
43+
const fixture = createComponent(NzTestDropdownArrowComponent);
44+
fixture.componentInstance.arrow = true;
45+
fixture.componentInstance.placement = 'bottomLeft';
46+
fixture.detectChanges();
47+
const dropdownElement = fixture.debugElement.query(By.directive(NzDropDownDirective)).nativeElement;
48+
dispatchFakeEvent(dropdownElement, 'mouseenter');
49+
tick(1000);
50+
fixture.detectChanges();
51+
const dropdown = overlayContainerElement.querySelector('.ant-dropdown') as HTMLElement;
52+
expect(dropdown).not.toBeNull();
53+
expect(dropdown.classList.contains('ant-dropdown-show-arrow')).toBeTrue();
54+
expect(dropdown.classList.contains('ant-dropdown-placement-bottomLeft')).toBeTrue();
55+
expect(dropdown.querySelector('.ant-dropdown-arrow')).not.toBeNull();
56+
57+
// Change placement while open should update placement class
58+
fixture.componentInstance.placement = 'topRight';
59+
fixture.detectChanges();
60+
tick(0);
61+
fixture.detectChanges();
62+
expect(dropdown.classList.contains('ant-dropdown-placement-topRight')).toBeTrue();
63+
}));
64+
65+
it('should map center placements to top/bottom classes', fakeAsync(() => {
66+
const fixture = createComponent(NzTestDropdownArrowComponent);
67+
fixture.componentInstance.arrow = true;
68+
fixture.componentInstance.placement = 'bottomCenter';
69+
fixture.detectChanges();
70+
const dropdownElement = fixture.debugElement.query(By.directive(NzDropDownDirective)).nativeElement;
71+
dispatchFakeEvent(dropdownElement, 'mouseenter');
72+
tick(1000);
73+
fixture.detectChanges();
74+
const dropdown = overlayContainerElement.querySelector('.ant-dropdown') as HTMLElement;
75+
expect(dropdown).not.toBeNull();
76+
expect(dropdown.classList.contains('ant-dropdown-show-arrow')).toBeTrue();
77+
const isBottomFamily =
78+
dropdown.classList.contains('ant-dropdown-placement-bottom') ||
79+
dropdown.classList.contains('ant-dropdown-placement-bottomLeft') ||
80+
dropdown.classList.contains('ant-dropdown-placement-bottomRight');
81+
expect(isBottomFamily).toBeTrue();
82+
83+
// Switch to topCenter
84+
fixture.componentInstance.placement = 'topCenter';
85+
fixture.detectChanges();
86+
tick(0);
87+
fixture.detectChanges();
88+
const isTopFamily =
89+
dropdown.classList.contains('ant-dropdown-placement-top') ||
90+
dropdown.classList.contains('ant-dropdown-placement-topLeft') ||
91+
dropdown.classList.contains('ant-dropdown-placement-topRight');
92+
expect(isTopFamily).toBeTrue();
93+
}));
94+
4295
it('should hover correct', fakeAsync(() => {
4396
const fixture = createComponent(NzTestDropdownComponent);
4497
fixture.componentInstance.trigger = 'hover';
@@ -241,3 +294,21 @@ export class NzTestDropdownVisibleComponent {
241294
visible = false;
242295
triggerVisible = jasmine.createSpy('visibleChange');
243296
}
297+
298+
@Component({
299+
imports: [NzDropDownModule, NzMenuModule],
300+
template: `
301+
<a nz-dropdown [nzDropdownMenu]="menu" [nzArrow]="arrow" [nzPlacement]="placement" [nzTrigger]="'hover'">
302+
Trigger
303+
</a>
304+
<nz-dropdown-menu #menu="nzDropdownMenu">
305+
<ul nz-menu>
306+
<li nz-menu-item>1st menu item</li>
307+
</ul>
308+
</nz-dropdown-menu>
309+
`
310+
})
311+
export class NzTestDropdownArrowComponent {
312+
arrow = false;
313+
placement: NzPlacementType = 'bottomLeft';
314+
}

components/dropdown/dropdown.directive.ts

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import { BehaviorSubject, EMPTY, Subject, combineLatest, fromEvent, merge } from
2727
import { auditTime, distinctUntilChanged, filter, map, switchMap } from 'rxjs/operators';
2828

2929
import { NzConfigKey, NzConfigService, WithConfig } from 'ng-zorro-antd/core/config';
30-
import { POSITION_MAP } from 'ng-zorro-antd/core/overlay';
30+
import { POSITION_MAP, getPlacementName } from 'ng-zorro-antd/core/overlay';
3131
import { IndexableObject } from 'ng-zorro-antd/core/types';
3232

3333
import { NzDropdownMenuComponent, NzPlacementType } from './dropdown-menu.component';
@@ -41,6 +41,17 @@ const listOfPositions = [
4141
POSITION_MAP.topLeft
4242
];
4343

44+
const normalizePlacementForClass = (p: NzPlacementType): NzDropdownMenuComponent['placement'] => {
45+
// Map center placements to generic top/bottom classes for styling
46+
if (p === 'topCenter') {
47+
return 'top';
48+
}
49+
if (p === 'bottomCenter') {
50+
return 'bottom';
51+
}
52+
return p as NzDropdownMenuComponent['placement'];
53+
};
54+
4455
@Directive({
4556
selector: '[nz-dropdown]',
4657
exportAs: 'nzDropdown',
@@ -76,6 +87,7 @@ export class NzDropDownDirective implements AfterViewInit, OnChanges {
7687
@Input({ transform: booleanAttribute }) nzClickHide = true;
7788
@Input({ transform: booleanAttribute }) nzDisabled = false;
7889
@Input({ transform: booleanAttribute }) nzVisible = false;
90+
@Input({ transform: booleanAttribute }) nzArrow = false;
7991
@Input() nzOverlayClassName: string = '';
8092
@Input() nzOverlayStyle: IndexableObject = {};
8193
@Input() nzPlacement: NzPlacementType = 'bottomLeft';
@@ -154,6 +166,13 @@ export class NzDropDownDirective implements AfterViewInit, OnChanges {
154166
hasBackdrop: this.nzBackdrop && this.nzTrigger === 'click',
155167
scrollStrategy: this.overlay.scrollStrategies.reposition()
156168
});
169+
// Listen for placement changes to update the menu classes (arrow position)
170+
this.positionStrategy.positionChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(change => {
171+
const placement = getPlacementName(change) as NzPlacementType | undefined;
172+
if (placement) {
173+
this.setDropdownMenuValue('placement', normalizePlacementForClass(placement));
174+
}
175+
});
157176
merge(
158177
this.overlayRef.backdropClick(),
159178
this.overlayRef.detachments(),
@@ -177,6 +196,9 @@ export class NzDropDownDirective implements AfterViewInit, OnChanges {
177196
if (!this.portal || this.portal.templateRef !== this.nzDropdownMenu!.templateRef) {
178197
this.portal = new TemplatePortal(this.nzDropdownMenu!.templateRef, this.viewContainerRef);
179198
}
199+
// Initialize arrow and placement on open
200+
this.setDropdownMenuValue('nzArrow', this.nzArrow);
201+
this.setDropdownMenuValue('placement', normalizePlacementForClass(this.nzPlacement));
180202
this.overlayRef.attach(this.portal);
181203
} else {
182204
/** detach overlayRef if needed **/
@@ -198,7 +220,7 @@ export class NzDropDownDirective implements AfterViewInit, OnChanges {
198220
}
199221

200222
ngOnChanges(changes: SimpleChanges): void {
201-
const { nzVisible, nzDisabled, nzOverlayClassName, nzOverlayStyle, nzTrigger } = changes;
223+
const { nzVisible, nzDisabled, nzOverlayClassName, nzOverlayStyle, nzTrigger, nzArrow, nzPlacement } = changes;
202224
if (nzTrigger) {
203225
this.nzTrigger$.next(this.nzTrigger);
204226
}
@@ -220,5 +242,11 @@ export class NzDropDownDirective implements AfterViewInit, OnChanges {
220242
if (nzOverlayStyle) {
221243
this.setDropdownMenuValue('nzOverlayStyle', this.nzOverlayStyle);
222244
}
245+
if (nzArrow) {
246+
this.setDropdownMenuValue('nzArrow', this.nzArrow);
247+
}
248+
if (nzPlacement) {
249+
this.setDropdownMenuValue('placement', normalizePlacementForClass(this.nzPlacement));
250+
}
223251
}
224252
}

0 commit comments

Comments
 (0)