Skip to content

Commit 860df87

Browse files
feat(module:menu): add nzTriggerSubMenuAction to support click trigger for submenu (#8461)
1 parent b9c511d commit 860df87

8 files changed

+85
-25
lines changed

components/menu/demo/horizontal.ts

+6
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@ import { NzMenuModule } from 'ng-zorro-antd/menu';
3535
<li nz-menu-item>Option 6</li>
3636
</ul>
3737
</li>
38+
<li nz-submenu nzTitle="Click me" [nzTriggerSubMenuAction]="'click'">
39+
<ul>
40+
<li nz-menu-item nzDisabled>Option 5</li>
41+
<li nz-menu-item>Option 6</li>
42+
</ul>
43+
</li>
3844
<li nz-submenu nzDisabled nzTitle="Disabled Sub Menu">
3945
<ul>
4046
<li nz-menu-item>Option 5</li>

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

+10-9
Original file line numberDiff line numberDiff line change
@@ -70,15 +70,16 @@ You can set the title of `[nz-submenu]` in the following ways.
7070
<ng-template #titleTpl><span nz-icon nzType="appstore"></span><span>SubTitle</span></ng-template>
7171
```
7272

73-
| Param | Description | Type | Default value |
74-
| ------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------- | -------------- |
75-
| `[nzPlacement]` | placement of pop menu | `'bottomLeft' \| 'bottomCenter' \| 'bottomRight' \| 'topLeft' \| 'topCenter' \| 'topRight'` | `'bottomLeft'` |
76-
| `[nzOpen]` | whether sub menu is open or not, double binding | `boolean` | `false` |
77-
| `[nzDisabled]` | whether sub menu is disabled or not | `boolean` | `false` |
78-
| `[nzTitle]` | set submenu title | `string \| TemplateRef<void>` | - |
79-
| `[nzIcon]` | `icon` type in title | `string` | - |
80-
| `[nzMenuClassName]` | Custom the submenu container's class name | `string` | - |
81-
| `(nzOpenChange)` | nzOpen callback | `EventEmitter<boolean>` | - |
73+
| Param | Description | Type | Default value |
74+
| -------------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------- | -------------- |
75+
| `[nzPlacement]` | placement of pop menu | `'bottomLeft' \| 'bottomCenter' \| 'bottomRight' \| 'topLeft' \| 'topCenter' \| 'topRight'` | `'bottomLeft'` |
76+
| `[nzOpen]` | whether sub menu is open or not, double binding | `boolean` | `false` |
77+
| `[nzDisabled]` | whether sub menu is disabled or not | `boolean` | `false` |
78+
| `[nzTitle]` | set submenu title | `string \| TemplateRef<void>` | - |
79+
| `[nzIcon]` | `icon` type in title | `string` | - |
80+
| `[nzMenuClassName]` | Custom the submenu container's class name | `string` | - |
81+
| `[nzTriggerSubMenuAction]` | Which action can trigger submenu open/close | `'hover' \| 'click'` | `'hover'` |
82+
| `(nzOpenChange)` | nzOpen callback | `EventEmitter<boolean>` | - |
8283

8384
### [nz-menu-group]:standalone
8485

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

+10-9
Original file line numberDiff line numberDiff line change
@@ -71,15 +71,16 @@ import { NzMenuModule } from 'ng-zorro-antd/menu';
7171
<ng-template #titleTpl><span nz-icon nzType="appstore"></span><span>SubTitle</span></ng-template>
7272
```
7373

74-
| 参数 | 说明 | 类型 | 默认值 |
75-
| ------------------- | -------------------- | ------------------------------------------------------------------------------------------- | -------------- |
76-
| `[nzPlacement]` | 菜单弹出位置 | `'bottomLeft' \| 'bottomCenter' \| 'bottomRight' \| 'topLeft' \| 'topCenter' \| 'topRight'` | `'bottomLeft'` |
77-
| `[nzOpen]` | 是否展开,可双向绑定 | `boolean` | `false` |
78-
| `[nzDisabled]` | 是否禁用 | `boolean` | `false` |
79-
| `[nzTitle]` | 标题内容 | `string \| TemplateRef<void>` | - |
80-
| `[nzIcon]` | 标题中 `icon` 类型 | `string` | - |
81-
| `[nzMenuClassName]` | 自定义子菜单容器类名 | `string` | - |
82-
| `(nzOpenChange)` | 展开回调 | `EventEmitter<boolean>` | - |
74+
| 参数 | 说明 | 类型 | 默认值 |
75+
| -------------------------- | --------------------------- | ------------------------------------------------------------------------------------------- | -------------- |
76+
| `[nzPlacement]` | 菜单弹出位置 | `'bottomLeft' \| 'bottomCenter' \| 'bottomRight' \| 'topLeft' \| 'topCenter' \| 'topRight'` | `'bottomLeft'` |
77+
| `[nzOpen]` | 是否展开,可双向绑定 | `boolean` | `false` |
78+
| `[nzDisabled]` | 是否禁用 | `boolean` | `false` |
79+
| `[nzTitle]` | 标题内容 | `string \| TemplateRef<void>` | - |
80+
| `[nzIcon]` | 标题中 `icon` 类型 | `string` | - |
81+
| `[nzMenuClassName]` | 自定义子菜单容器类名 | `string` | - |
82+
| `[nzTriggerSubMenuAction]` | SubMenu 展开/关闭的触发行为 | `'hover' \| 'click'` | `'hover'` |
83+
| `(nzOpenChange)` | 展开回调 | `EventEmitter<boolean>` | - |
8384

8485
### [nz-menu-group]:standalone
8586

components/menu/menu.spec.ts

+45-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { NzButtonModule } from 'ng-zorro-antd/button';
99
import { dispatchFakeEvent } from 'ng-zorro-antd/core/testing';
1010
import { NzIconModule } from 'ng-zorro-antd/icon';
1111
import { provideNzIconsTesting } from 'ng-zorro-antd/icon/testing';
12+
import { NzSubmenuTrigger } from 'ng-zorro-antd/menu/menu.types';
1213

1314
import { NzMenuItemComponent } from './menu-item.component';
1415
import { NzMenuDirective } from './menu.directive';
@@ -271,6 +272,42 @@ describe('menu', () => {
271272
expect(mouseenterCallback).toHaveBeenCalledWith(true);
272273
expect(mouseenterCallback).toHaveBeenCalledTimes(1);
273274
});
275+
it('should have "hover" as default trigger', () => {
276+
fixture.detectChanges();
277+
const mouseenterCallback = jasmine.createSpy('mouseenter callback');
278+
const subs = testComponent.subs.toArray();
279+
const title = submenu.nativeElement.querySelector('.ant-menu-submenu-title');
280+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
281+
(subs[0].nzSubmenuService as any).isMouseEnterTitleOrOverlay$.subscribe(mouseenterCallback);
282+
dispatchFakeEvent(title, 'mouseenter');
283+
fixture.detectChanges();
284+
expect(mouseenterCallback).toHaveBeenCalledWith(true);
285+
expect(mouseenterCallback).toHaveBeenCalledTimes(1);
286+
});
287+
it('should have not open with mouse hover if trigger is set to "click"', () => {
288+
testComponent.nzTriggerSubMenuAction = 'click';
289+
fixture.detectChanges();
290+
const mouseenterCallback = jasmine.createSpy('mouseenter callback');
291+
const subs = testComponent.subs.toArray();
292+
const title = submenu.nativeElement.querySelector('.ant-menu-submenu-title');
293+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
294+
(subs[0].nzSubmenuService as any).isMouseEnterTitleOrOverlay$.subscribe(mouseenterCallback);
295+
dispatchFakeEvent(title, 'mouseenter');
296+
fixture.detectChanges();
297+
expect(mouseenterCallback).toHaveBeenCalledTimes(0);
298+
});
299+
it('should open with mouse click if trigger is set to "click"', () => {
300+
testComponent.nzTriggerSubMenuAction = 'click';
301+
fixture.detectChanges();
302+
const mouseenterCallback = jasmine.createSpy('mouseenter callback');
303+
const subs = testComponent.subs.toArray();
304+
const title = submenu.nativeElement.querySelector('.ant-menu-submenu-title');
305+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
306+
(subs[0].nzSubmenuService as any).isMouseEnterTitleOrOverlay$.subscribe(mouseenterCallback);
307+
title.click();
308+
fixture.detectChanges();
309+
expect(mouseenterCallback).toHaveBeenCalledTimes(1);
310+
});
274311
it('should submenu mouseleave work', () => {
275312
fixture.detectChanges();
276313
const mouseleaveCallback = jasmine.createSpy('mouseleave callback');
@@ -524,7 +561,13 @@ describe('menu', () => {
524561
imports: [NzIconModule, NzMenuModule],
525562
template: `
526563
<ul nz-menu [nzMode]="'horizontal'">
527-
<li nz-submenu nzMenuClassName="submenu" [nzOpen]="open" [style.width.px]="width">
564+
<li
565+
nz-submenu
566+
[nzTriggerSubMenuAction]="nzTriggerSubMenuAction"
567+
nzMenuClassName="submenu"
568+
[nzOpen]="open"
569+
[style.width.px]="width"
570+
>
528571
<span title>
529572
<span nz-icon nzType="setting"></span>
530573
Navigation Three - Submenu
@@ -567,6 +610,7 @@ export class NzTestMenuHorizontalComponent {
567610
width = 200;
568611
open = false;
569612
disabled = false;
613+
nzTriggerSubMenuAction: NzSubmenuTrigger = 'hover';
570614
@ViewChildren(NzSubMenuComponent) subs!: QueryList<NzSubMenuComponent>;
571615
@ViewChild('menuitem', { static: false, read: ElementRef }) menuitem!: ElementRef;
572616
@ViewChild('menuitem1', { static: false, read: ElementRef }) menuitem1!: ElementRef;

components/menu/menu.types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@
55

66
export type NzMenuModeType = 'vertical' | 'horizontal' | 'inline';
77
export type NzMenuThemeType = 'light' | 'dark';
8+
export type NzSubmenuTrigger = 'hover' | 'click';

components/menu/submenu-non-inline-child.component.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import { takeUntil } from 'rxjs/operators';
2424
import { slideMotion, zoomBigMotion } from 'ng-zorro-antd/core/animation';
2525
import { NzSafeAny } from 'ng-zorro-antd/core/types';
2626

27-
import { NzMenuModeType, NzMenuThemeType } from './menu.types';
27+
import { NzMenuModeType, NzMenuThemeType, NzSubmenuTrigger } from './menu.types';
2828

2929
@Component({
3030
selector: '[nz-submenu-none-inline-child]',
@@ -68,6 +68,7 @@ export class NzSubmenuNoneInlineChildComponent implements OnDestroy, OnInit, OnC
6868
@Input() templateOutlet: TemplateRef<NzSafeAny> | null = null;
6969
@Input() isMenuInsideDropDown = false;
7070
@Input() mode: NzMenuModeType = 'vertical';
71+
@Input() nzTriggerSubMenuAction: NzSubmenuTrigger = 'hover';
7172
@Input() position = 'right';
7273
@Input() nzDisabled = false;
7374
@Input() nzOpen = false;
@@ -76,7 +77,7 @@ export class NzSubmenuNoneInlineChildComponent implements OnDestroy, OnInit, OnC
7677
constructor(private directionality: Directionality) {}
7778

7879
setMouseState(state: boolean): void {
79-
if (!this.nzDisabled) {
80+
if (!this.nzDisabled && this.nzTriggerSubMenuAction === 'hover') {
8081
this.subMenuMouseState.next(state);
8182
}
8283
}

components/menu/submenu-title.component.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import { takeUntil } from 'rxjs/operators';
2222
import { NzOutletModule } from 'ng-zorro-antd/core/outlet';
2323
import { NzIconModule } from 'ng-zorro-antd/icon';
2424

25-
import { NzMenuModeType } from './menu.types';
25+
import { NzMenuModeType, NzSubmenuTrigger } from './menu.types';
2626

2727
@Component({
2828
selector: '[nz-submenu-title]',
@@ -71,6 +71,7 @@ export class NzSubMenuTitleComponent implements OnDestroy, OnInit {
7171
@Input() nzDisabled = false;
7272
@Input() paddingLeft: number | null = null;
7373
@Input() mode: NzMenuModeType = 'vertical';
74+
@Input() nzTriggerSubMenuAction: NzSubmenuTrigger = 'hover';
7475
@Output() readonly toggleSubMenu = new EventEmitter();
7576
@Output() readonly subMenuMouseState = new EventEmitter<boolean>();
7677

@@ -95,12 +96,13 @@ export class NzSubMenuTitleComponent implements OnDestroy, OnInit {
9596
}
9697

9798
setMouseState(state: boolean): void {
98-
if (!this.nzDisabled) {
99+
if (!this.nzDisabled && this.nzTriggerSubMenuAction === 'hover') {
99100
this.subMenuMouseState.next(state);
100101
}
101102
}
102103
clickTitle(): void {
103-
if (this.mode === 'inline' && !this.nzDisabled) {
104+
if ((this.mode === 'inline' || this.nzTriggerSubMenuAction === 'click') && !this.nzDisabled) {
105+
this.subMenuMouseState.next(true);
104106
this.toggleSubMenu.emit();
105107
}
106108
}

components/menu/submenu.component.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ import { getPlacementName, POSITION_MAP, POSITION_TYPE_HORIZONTAL } from 'ng-zor
3737
import { NzMenuItemComponent } from './menu-item.component';
3838
import { MenuService } from './menu.service';
3939
import { NzIsMenuInsideDropDownToken } from './menu.token';
40-
import { NzMenuModeType, NzMenuThemeType } from './menu.types';
40+
import { NzMenuModeType, NzMenuThemeType, NzSubmenuTrigger } from './menu.types';
4141
import { NzSubmenuInlineChildComponent } from './submenu-inline-child.component';
4242
import { NzSubmenuNoneInlineChildComponent } from './submenu-non-inline-child.component';
4343
import { NzSubMenuTitleComponent } from './submenu-title.component';
@@ -76,6 +76,7 @@ const listOfHorizontalPositions = [
7676
[nzDisabled]="nzDisabled"
7777
[isMenuInsideDropDown]="isMenuInsideDropDown"
7878
[paddingLeft]="nzPaddingLeft || inlinePaddingLeft"
79+
[nzTriggerSubMenuAction]="nzTriggerSubMenuAction"
7980
(subMenuMouseState)="setMouseEnterState($event)"
8081
(toggleSubMenu)="toggleSubMenu()"
8182
>
@@ -102,6 +103,7 @@ const listOfHorizontalPositions = [
102103
[cdkConnectedOverlayWidth]="triggerWidth!"
103104
[cdkConnectedOverlayOpen]="nzOpen"
104105
[cdkConnectedOverlayTransformOriginOn]="'.ant-menu-submenu'"
106+
(overlayOutsideClick)="setMouseEnterState(false)"
105107
>
106108
<div
107109
nz-submenu-none-inline-child
@@ -111,6 +113,7 @@ const listOfHorizontalPositions = [
111113
[position]="position"
112114
[nzDisabled]="nzDisabled"
113115
[isMenuInsideDropDown]="isMenuInsideDropDown"
116+
[nzTriggerSubMenuAction]="nzTriggerSubMenuAction"
114117
[templateOutlet]="subMenuTemplate"
115118
[menuClass]="nzMenuClassName"
116119
[@.disabled]="!!noAnimation?.nzNoAnimation"
@@ -157,6 +160,7 @@ export class NzSubMenuComponent implements OnInit, OnDestroy, AfterContentInit,
157160
@Input() nzPaddingLeft: number | null = null;
158161
@Input() nzTitle: string | TemplateRef<void> | null = null;
159162
@Input() nzIcon: string | null = null;
163+
@Input() nzTriggerSubMenuAction: NzSubmenuTrigger = 'hover';
160164
@Input({ transform: booleanAttribute }) nzOpen = false;
161165
@Input({ transform: booleanAttribute }) nzDisabled = false;
162166
@Input() nzPlacement: POSITION_TYPE_HORIZONTAL = 'bottomLeft';

0 commit comments

Comments
 (0)