Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add subMenuOpenByEvent option to open sub-menus via mouseover #1161

Merged
merged 2 commits into from
Oct 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ describe('CellMenu Plugin', () => {
autoAdjustDropOffset: 0,
autoAlignSideOffset: 0,
hideMenuOnScroll: true,
subMenuOpenByEvent: 'mouseover'
});
});

Expand Down Expand Up @@ -627,7 +628,7 @@ describe('CellMenu Plugin', () => {
const commandContentElm2 = subCommands1Elm.querySelector('.slick-menu-content') as HTMLDivElement;
const commandChevronElm = commandList1Elm.querySelector('.sub-item-chevron') as HTMLSpanElement;

subCommands1Elm!.dispatchEvent(new Event('click'));
subCommands1Elm!.dispatchEvent(new Event('mouseover')); // mouseover or click should work
const cellMenu2Elm = document.body.querySelector('.slick-cell-menu.slickgrid12345.slick-menu-level-1') as HTMLDivElement;
const commandList2Elm = cellMenu2Elm.querySelector('.slick-menu-command-list') as HTMLDivElement;
const subCommand3Elm = commandList2Elm.querySelector('[data-command="command3"]') as HTMLDivElement;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ describe('ContextMenu Plugin', () => {
hideMenuOnScroll: false,
optionShownOverColumnIds: [],
commandShownOverColumnIds: [],
subMenuOpenByEvent: 'mouseover'
});
});

Expand Down Expand Up @@ -657,6 +658,7 @@ describe('ContextMenu Plugin', () => {

it('should create a Context Menu item with commands sub-menu items and expect sub-menu list to show in the DOM element when sub-menu is clicked', () => {
const actionMock = jest.fn();
const disposeSubMenuSpy = jest.spyOn(plugin, 'disposeSubMenus');
jest.spyOn(getEditorLockMock, 'commitCurrentEdit').mockReturnValue(true);

plugin.dispose();
Expand All @@ -667,6 +669,7 @@ describe('ContextMenu Plugin', () => {

let contextMenu1Elm = document.body.querySelector('.slick-context-menu.slickgrid12345.slick-menu-level-0') as HTMLDivElement;
const commandList1Elm = contextMenu1Elm.querySelector('.slick-menu-command-list') as HTMLDivElement;
const deleteRowCommandElm = commandList1Elm.querySelector('[data-command="delete-row"]') as HTMLDivElement;
const subCommands1Elm = commandList1Elm.querySelector('[data-command="sub-commands"]') as HTMLDivElement;
const commandContentElm2 = subCommands1Elm.querySelector('.slick-menu-content') as HTMLDivElement;
const commandChevronElm = commandList1Elm.querySelector('.sub-item-chevron') as HTMLSpanElement;
Expand All @@ -677,7 +680,7 @@ describe('ContextMenu Plugin', () => {
const subCommand3Elm = commandList2Elm.querySelector('[data-command="command3"]') as HTMLDivElement;
const subCommands2Elm = commandList2Elm.querySelector('[data-command="more-sub-commands"]') as HTMLDivElement;

subCommands2Elm!.dispatchEvent(new Event('click'));
subCommands2Elm!.dispatchEvent(new Event('mouseover')); // mouseover or click should work
const contextMenu3Elm = document.body.querySelector('.slick-context-menu.slickgrid12345.slick-menu-level-2') as HTMLDivElement;
const commandList3Elm = contextMenu3Elm.querySelector('.slick-menu-command-list') as HTMLDivElement;
const subCommand5Elm = commandList3Elm.querySelector('[data-command="command5"]') as HTMLDivElement;
Expand Down Expand Up @@ -705,6 +708,10 @@ describe('ContextMenu Plugin', () => {
document.body.dispatchEvent(subCommandEvent);
contextMenu2Elm = document.body.querySelector('.slick-context-menu.slickgrid12345.slick-menu-level-1') as HTMLDivElement;
expect(contextMenu2Elm).toBeTruthy();

// calling another command on parent menu should dispose sub-menus
deleteRowCommandElm!.dispatchEvent(new Event('mouseover'));
expect(disposeSubMenuSpy).toHaveBeenCalledTimes(4);
});

it('should create a Context Menu and expect the button click handler & "action" callback to be executed when defined', () => {
Expand Down
11 changes: 9 additions & 2 deletions packages/common/src/extensions/__tests__/slickGridMenu.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -925,6 +925,7 @@ describe('GridMenuControl', () => {
const commandList1Elm = gridMenu1Elm.querySelector('.slick-menu-command-list') as HTMLDivElement;
Object.defineProperty(commandList1Elm, 'clientWidth', { writable: true, configurable: true, value: 70 });
const subCommands1Elm = commandList1Elm.querySelector('[data-command="sub-commands"]') as HTMLDivElement;
const helpCommandElm = commandList1Elm.querySelector('[data-command="help"]') as HTMLDivElement;
Object.defineProperty(subCommands1Elm, 'clientWidth', { writable: true, configurable: true, value: 70 });
const commandContentElm2 = subCommands1Elm.querySelector('.slick-menu-content') as HTMLDivElement;
const commandChevronElm = commandList1Elm.querySelector('.sub-item-chevron') as HTMLSpanElement;
Expand All @@ -935,7 +936,7 @@ describe('GridMenuControl', () => {
const subCommand3Elm = commandList2Elm.querySelector('[data-command="command3"]') as HTMLDivElement;
const subCommands2Elm = commandList2Elm.querySelector('[data-command="more-sub-commands"]') as HTMLDivElement;

subCommands2Elm!.dispatchEvent(new Event('click'));
subCommands2Elm!.dispatchEvent(new Event('mouseover')); // mouseover or click should work
const cellMenu3Elm = document.body.querySelector('.slick-grid-menu.slick-menu-level-2') as HTMLDivElement;
const commandList3Elm = cellMenu3Elm.querySelector('.slick-menu-command-list') as HTMLDivElement;
const subCommand5Elm = commandList3Elm.querySelector('[data-command="command5"]') as HTMLDivElement;
Expand All @@ -957,9 +958,15 @@ describe('GridMenuControl', () => {
subCommands1Elm!.dispatchEvent(new Event('click'));
expect(disposeSubMenuSpy).toHaveBeenCalledTimes(0);
const subCommands12Elm = commandList1Elm.querySelector('[data-command="sub-commands2"]') as HTMLDivElement;
subCommands12Elm!.dispatchEvent(new Event('click'));
subCommands12Elm!.dispatchEvent(new Event('mouseover'));
expect(disposeSubMenuSpy).toHaveBeenCalledTimes(1);
expect(disposeSubMenuSpy).toHaveBeenCalled();
subCommands1Elm!.dispatchEvent(new Event('mouseover'));
expect(disposeSubMenuSpy).toHaveBeenCalledTimes(2);

// calling another command on parent menu should dispose sub-menus
helpCommandElm!.dispatchEvent(new Event('mouseover'));
expect(disposeSubMenuSpy).toHaveBeenCalledTimes(3);
});

it('should create a Cell Menu item with commands sub-menu items and expect sub-menu list to show in the DOM element align right when sub-menu is clicked', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ describe('HeaderMenu Plugin', () => {
hideSortCommands: false,
minWidth: 100,
title: '',
subMenuOpenByEvent: 'mouseover'
});
});

Expand Down Expand Up @@ -651,6 +652,7 @@ describe('HeaderMenu Plugin', () => {
const headerMenu1Elm = gridContainerDiv.querySelector('.slick-header-menu.slick-menu-level-0') as HTMLDivElement;
const commandList1Elm = headerMenu1Elm.querySelector('.slick-menu-command-list') as HTMLDivElement;
Object.defineProperty(commandList1Elm, 'clientWidth', { writable: true, configurable: true, value: 70 });
const helpCommandElm = commandList1Elm.querySelector('[data-command="help"]') as HTMLDivElement;
const subCommands1Elm = commandList1Elm.querySelector('[data-command="sub-commands"]') as HTMLDivElement;
Object.defineProperty(subCommands1Elm, 'clientWidth', { writable: true, configurable: true, value: 70 });
const commandContentElm2 = subCommands1Elm.querySelector('.slick-menu-content') as HTMLDivElement;
Expand All @@ -662,7 +664,7 @@ describe('HeaderMenu Plugin', () => {
const subCommand3Elm = commandList2Elm.querySelector('[data-command="command3"]') as HTMLDivElement;
const subCommands2Elm = commandList2Elm.querySelector('[data-command="more-sub-commands"]') as HTMLDivElement;

subCommands2Elm!.dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false }));
subCommands2Elm!.dispatchEvent(new Event('mouseover', { bubbles: true, cancelable: true, composed: false })); // mouseover or click should work
const cellMenu3Elm = document.body.querySelector('.slick-header-menu.slick-menu-level-2') as HTMLDivElement;
const commandList3Elm = cellMenu3Elm.querySelector('.slick-menu-command-list') as HTMLDivElement;
const subCommand5Elm = commandList3Elm.querySelector('[data-command="command5"]') as HTMLDivElement;
Expand All @@ -687,6 +689,10 @@ describe('HeaderMenu Plugin', () => {
subCommands12Elm!.dispatchEvent(new Event('click'));
expect(disposeSubMenuSpy).toHaveBeenCalledTimes(2);
expect(disposeSubMenuSpy).toHaveBeenCalled();

// calling another command on parent menu should dispose sub-menus
helpCommandElm!.dispatchEvent(new Event('mouseover'));
expect(disposeSubMenuSpy).toHaveBeenCalledTimes(3);
});

it('should create a Header Menu item with commands sub-menu commandItems and expect sub-menu list to show in the DOM element align right when sub-menu is clicked', () => {
Expand Down
37 changes: 29 additions & 8 deletions packages/common/src/extensions/menuBaseClass.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,12 +135,12 @@ export class MenuBaseClass<M extends CellMenu | ContextMenu | GridMenu | HeaderM
// protected functions
// ------------------

protected addSubMenuTitleWhenExists(item: MenuCommandItem | MenuOptionItem | GridMenuItem | 'divider', commandOrOptionMenu: HTMLDivElement) {
if (item !== 'divider' && item?.subMenuTitle) {
protected addSubMenuTitleWhenExists(item: ExtractMenuType<ExtendableItemTypes, MenuType>, commandOrOptionMenu: HTMLDivElement) {
if (item !== 'divider' && (item as MenuCommandItem | MenuOptionItem | GridMenuItem)?.subMenuTitle) {
const subMenuTitleElm = document.createElement('div');
subMenuTitleElm.className = 'slick-menu-title';
subMenuTitleElm.textContent = item.subMenuTitle as string;
const subMenuTitleClass = item.subMenuTitleCssClass as string;
subMenuTitleElm.textContent = (item as MenuCommandItem | MenuOptionItem | GridMenuItem).subMenuTitle as string;
const subMenuTitleClass = (item as MenuCommandItem | MenuOptionItem | GridMenuItem).subMenuTitleCssClass as string;
if (subMenuTitleClass) {
subMenuTitleElm.classList.add(...subMenuTitleClass.split(' '));
}
Expand All @@ -155,11 +155,12 @@ export class MenuBaseClass<M extends CellMenu | ContextMenu | GridMenu | HeaderM
commandOrOptionMenuElm: HTMLElement,
commandOrOptionItems: Array<ExtractMenuType<ExtendableItemTypes, MenuType>>,
args: unknown,
itemClickCallback: (event: DOMMouseOrTouchEvent<HTMLDivElement>, type: MenuType, item: ExtractMenuType<ExtendableItemTypes, MenuType>, level: number, columnDef?: Column) => void
itemClickCallback: (e: DOMMouseOrTouchEvent<HTMLDivElement>, type: MenuType, item: ExtractMenuType<ExtendableItemTypes, MenuType>, level: number, columnDef?: Column) => void,
itemMouseoverCallback?: (e: DOMMouseOrTouchEvent<HTMLElement>, type: MenuType, item: ExtractMenuType<ExtendableItemTypes, MenuType>, level: number, columnDef?: Column) => void
) {
if (args && commandOrOptionItems && menuOptions) {
for (const item of commandOrOptionItems) {
this.populateSingleCommandOrOptionItem(itemType, menuOptions, commandOrOptionMenuElm, item, args, itemClickCallback);
this.populateSingleCommandOrOptionItem(itemType, menuOptions, commandOrOptionMenuElm, item, args, itemClickCallback, itemMouseoverCallback);
}
}
}
Expand Down Expand Up @@ -196,7 +197,8 @@ export class MenuBaseClass<M extends CellMenu | ContextMenu | GridMenu | HeaderM
commandOrOptionMenuElm: HTMLElement | null,
item: ExtractMenuType<ExtendableItemTypes, MenuType>,
args: any,
itemClickCallback: (event: DOMMouseOrTouchEvent<HTMLDivElement>, type: MenuType, item: ExtractMenuType<ExtendableItemTypes, MenuType>, level: number, columnDef?: Column) => void
itemClickCallback: (e: DOMMouseOrTouchEvent<HTMLDivElement>, type: MenuType, item: ExtractMenuType<ExtendableItemTypes, MenuType>, level: number, columnDef?: Column) => void,
itemMouseoverCallback?: (e: DOMMouseOrTouchEvent<HTMLElement>, type: MenuType, item: ExtractMenuType<ExtendableItemTypes, MenuType>, level: number, columnDef?: Column) => void
): HTMLLIElement | null {
let commandLiElm: HTMLLIElement | null = null;

Expand Down Expand Up @@ -279,14 +281,33 @@ export class MenuBaseClass<M extends CellMenu | ContextMenu | GridMenu | HeaderM
}

// execute command callback on menu item clicked
const eventGroupName = level > 0 ? 'sub-menu' : 'parent-menu';
this._bindEventService.bind(
commandLiElm,
'click',
((e: DOMMouseOrTouchEvent<HTMLDivElement>) => itemClickCallback.call(this, e, itemType, item, level, args?.column)) as EventListener,
undefined,
level > 0 ? 'sub-menu' : 'parent-menu'
eventGroupName
);

// optionally open sub-menu(s) by mouseover
if ((this._addonOptions as CellMenu | ContextMenu | GridMenu | HeaderMenu)?.subMenuOpenByEvent === 'mouseover' && typeof itemMouseoverCallback === 'function') {
this._bindEventService.bind(
commandLiElm,
'mouseover',
((e: DOMMouseOrTouchEvent<HTMLDivElement>) => itemMouseoverCallback.call(this, e, itemType, item as ExtractMenuType<ExtendableItemTypes, MenuType>, level)) as EventListener,
undefined,
eventGroupName
);
// this._bindEventService.bind(commandLiElm, 'mouseover', ((e: DOMMouseOrTouchEvent<HTMLDivElement>) => {
// if ((item as MenuCommandItem).commandItems || (item as MenuOptionItem).optionItems || (item as HeaderMenuCommandItem).items) {
// (this as any).repositionSubMenu(item, itemType, args.level, e);
// } else if (level === 0) {
// this.disposeSubMenus();
// }
// }) as EventListener);
}

// the option/command item could be a sub-menu if it has another list of commands/options
if ((item as MenuCommandItem).commandItems || (item as MenuOptionItem).optionItems || (item as HeaderMenuCommandItem).items) {
const chevronElm = document.createElement('span');
Expand Down
20 changes: 16 additions & 4 deletions packages/common/src/extensions/menuFromCellBaseClass.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {
CellMenu,
ContextMenu,
DOMMouseOrTouchEvent,
HeaderMenuCommandItem,
MenuCallbackArgs,
MenuCommandItem,
MenuCommandItemCallbackArgs,
Expand Down Expand Up @@ -89,7 +90,7 @@ export class MenuFromCellBaseClass<M extends CellMenu | ContextMenu> extends Men
* @param item - command, option or divider
* @returns menu DOM element
*/
createMenu(commandItems: Array<MenuCommandItem | 'divider'>, optionItems: Array<MenuOptionItem | 'divider'>, level = 0, item?: MenuCommandItem | MenuOptionItem | 'divider') {
createMenu(commandItems: Array<MenuCommandItem | 'divider'>, optionItems: Array<MenuOptionItem | 'divider'>, level = 0, item?: ExtractMenuType<ExtendableItemTypes, MenuType>) {
const columnDef = this.grid.getColumns()[this._currentCell];
const dataContext = this.grid.getDataItem(this._currentRow);

Expand Down Expand Up @@ -177,6 +178,7 @@ export class MenuFromCellBaseClass<M extends CellMenu | ContextMenu> extends Men
optionItems,
{ cell: this._currentCell, row: this._currentRow, column: columnDef, dataContext, grid: this.grid, level } as MenuCallbackArgs,
this.handleMenuItemCommandClick,
this.handleMenuItemMouseOver
);
}

Expand All @@ -200,6 +202,7 @@ export class MenuFromCellBaseClass<M extends CellMenu | ContextMenu> extends Men
commandItems,
{ cell: this._currentCell, row: this._currentRow, column: columnDef, dataContext, grid: this.grid, level } as MenuCallbackArgs,
this.handleMenuItemCommandClick,
this.handleMenuItemMouseOver
);
}

Expand Down Expand Up @@ -259,6 +262,15 @@ export class MenuFromCellBaseClass<M extends CellMenu | ContextMenu> extends Men
}
}

protected handleMenuItemMouseOver(e: DOMMouseOrTouchEvent<HTMLElement>, type: MenuType, item: ExtractMenuType<ExtendableItemTypes, MenuType>, level = 0) {
if ((item as MenuCommandItem).commandItems || (item as MenuOptionItem).optionItems || (item as HeaderMenuCommandItem).items) {
this.repositionSubMenu(item, type, level, e);
this._lastMenuTypeClicked = type;
} else if (level === 0) {
this.disposeSubMenus();
}
}

protected handleMenuItemCommandClick(event: DOMMouseOrTouchEvent<HTMLDivElement>, type: MenuType, item: ExtractMenuType<ExtendableItemTypes, MenuType>, level = 0) {
if ((item as never)?.[type] !== undefined && item !== 'divider' && !item.disabled && !(item as MenuCommandItem | MenuOptionItem).divider && this._currentCell !== undefined && this._currentRow !== undefined) {
if (type === 'option' && !this.grid.getEditorLock().commitCurrentEdit()) {
Expand Down Expand Up @@ -303,7 +315,7 @@ export class MenuFromCellBaseClass<M extends CellMenu | ContextMenu> extends Men
this.closeMenu(event, { cell, row, grid: this.grid });
}
} else if ((item as MenuCommandItem).commandItems || (item as MenuOptionItem).optionItems) {
this.repositionSubMenu(item as any, type, level, event);
this.repositionSubMenu(item as MenuCommandItem | MenuOptionItem | 'divider', type, level, event);
}
this._lastMenuTypeClicked = type;
}
Expand All @@ -317,7 +329,7 @@ export class MenuFromCellBaseClass<M extends CellMenu | ContextMenu> extends Men
commandOrOptionMenuHeaderElm.classList.add('with-close');
}

protected repositionSubMenu(item: MenuCommandItem | MenuOptionItem | 'divider', type: MenuType, level: number, e: DOMMouseOrTouchEvent<HTMLDivElement>) {
protected repositionSubMenu(item: ExtractMenuType<ExtendableItemTypes, MenuType>, type: MenuType, level: number, e: DOMMouseOrTouchEvent<HTMLElement>) {
// when we're clicking a grid cell OR our last menu type (command/option) differs then we know that we need to start fresh and close any sub-menus that might still be open
if (e.target.classList.contains('slick-cell') || this._lastMenuTypeClicked !== type) {
this.disposeSubMenus();
Expand All @@ -332,7 +344,7 @@ export class MenuFromCellBaseClass<M extends CellMenu | ContextMenu> extends Men
}
}

protected repositionMenu(event: DOMMouseOrTouchEvent<HTMLDivElement>, menuElm?: HTMLElement) {
protected repositionMenu(event: DOMMouseOrTouchEvent<HTMLElement>, menuElm?: HTMLElement) {
const isSubMenu = menuElm?.classList.contains('slick-submenu');
const parentElm = isSubMenu
? event.target.closest(`.${this._menuCssPrefix}-item`) as HTMLDivElement
Expand Down
1 change: 1 addition & 0 deletions packages/common/src/extensions/slickCellMenu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export class SlickCellMenu extends MenuFromCellBaseClass<CellMenu> {
autoAdjustDropOffset: 0,
autoAlignSideOffset: 0,
hideMenuOnScroll: true,
subMenuOpenByEvent: 'mouseover',
} as unknown as CellMenuOption;
pluginName: 'CellMenu' = 'CellMenu' as const;

Expand Down
1 change: 1 addition & 0 deletions packages/common/src/extensions/slickContextMenu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export class SlickContextMenu extends MenuFromCellBaseClass<ContextMenu> {
hideMenuOnScroll: false,
optionShownOverColumnIds: [],
commandShownOverColumnIds: [],
subMenuOpenByEvent: 'mouseover',
} as unknown as ContextMenuOption;
pluginName: 'ContextMenu' = 'ContextMenu' as const;

Expand Down
Loading