Skip to content

Commit

Permalink
feat: add subMenuOpenByEvent option to open sub-menus via mouseover
Browse files Browse the repository at this point in the history
- so I thought it was complex to add open sub-menus via mouseover (not just click event) but it turns out to be quite easy with the latest code I've put in place for sub-menus.
- this new mouseover will be the default but user could change it to only work with a click event by changing `subMenuOpenByEvent: 'click'`
  • Loading branch information
ghiscoding-SE committed Oct 23, 2023
1 parent 9aebf45 commit 3064be7
Show file tree
Hide file tree
Showing 11 changed files with 94 additions and 27 deletions.
8 changes: 4 additions & 4 deletions cypress/e2e/example-grid-menu.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ describe('Example - Grid Menu', () => {

it('should expect "Clear Sorting" command to become hidden from Grid Menu when disabling feature', () => {
cy.get('#toggle-sorting')
.click();
.click();

cy.get('#myGrid')
.find('button.slick-gridmenu-button')
Expand All @@ -276,7 +276,7 @@ describe('Example - Grid Menu', () => {

it('should expect "Clear Sorting" command to become visible agaom in Grid Menu when toggling feature again', () => {
cy.get('#toggle-sorting')
.click();
.click();

cy.get('#myGrid')
.find('button.slick-gridmenu-button')
Expand Down Expand Up @@ -336,7 +336,7 @@ describe('Example - Grid Menu', () => {

cy.get('.slick-submenu').should('have.length', 1);
cy.get('.slick-gridmenu.slick-menu-level-1 .slick-gridmenu-command-list')
.find('.slick-gridmenu-item')
.find('.slick-gridmenu-item')
.contains('Excel')
.click();

Expand Down Expand Up @@ -387,7 +387,7 @@ describe('Example - Grid Menu', () => {
.find('.slick-gridmenu-item')
.contains('Feedback')
.should('exist')
.click();
.trigger('mouseover'); // mouseover or click should work

cy.get('.slick-submenu').should('have.length', 1);
cy.get('.slick-gridmenu.slick-menu-level-1')
Expand Down
6 changes: 3 additions & 3 deletions cypress/e2e/example-plugin-contextmenu.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,7 @@ describe('Example - Context Menu & Cell Menu', () => {
cy.get('.slick-cell-menu.slick-menu-level-0 .slick-cell-menu-command-list')
.find('.slick-cell-menu-item')
.contains('Export')
.click();
.trigger('mouseover'); // mouseover or click should work

cy.get('.slick-cell-menu.slick-menu-level-1 .slick-cell-menu-command-list')
.should('exist')
Expand Down Expand Up @@ -458,7 +458,7 @@ describe('Example - Context Menu & Cell Menu', () => {
.find('.slick-cell-menu-item')
.contains('Feedback')
.should('exist')
.click();
.trigger('mouseover'); // mouseover or click should work

cy.get('.slick-submenu').should('have.length', 1);
cy.get('.slick-cell-menu.slick-menu-level-1')
Expand Down Expand Up @@ -884,7 +884,7 @@ describe('Example - Context Menu & Cell Menu', () => {
.find('.slick-context-menu-item')
.contains('Feedback')
.should('exist')
.click();
.trigger('mouseover'); // mouseover or click should work

cy.get('.slick-submenu').should('have.length', 1);
cy.get('.slick-context-menu.slick-menu-level-1')
Expand Down
2 changes: 1 addition & 1 deletion cypress/e2e/example-plugin-headermenu.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ describe('Example - Header Menu', () => {
.find('.slick-header-menuitem.slick-header-menuitem')
.contains('Feedback')
.should('exist')
.click();
.trigger('mouseover'); // mouseover or click should work

cy.get('.slick-submenu').should('have.length', 1);
cy.get('.slick-header-menu.slick-menu-level-1')
Expand Down
15 changes: 14 additions & 1 deletion src/controls/slick.gridmenu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ const Utils = IIFE_ONLY ? Slick.Utils : Utils_;
* menuUsabilityOverride: Callback method that user can override the default behavior of enabling/disabling the menu from being usable (must be combined with a custom formatter)
* marginBottom: Margin to use at the bottom of the grid menu, only in effect when height is undefined (defaults to 15)
* subItemChevronClass: CSS class that can be added on the right side of a sub-item parent (typically a chevron-right icon)
* subMenuOpenByEvent: defaults to "mouseover", what event type shoud we use to open sub-menu(s), 2 options are available: "mouseover" or "click"
*
* Available custom menu item options:
* action: Optionally define a callback function that gets executed when item is chosen (and/or use the onCommand event)
Expand Down Expand Up @@ -153,6 +154,7 @@ export class SlickGridMenu {
menuWidth: 18,
contentMinWidth: 0,
resizeOnShowHeaderRow: false,
subMenuOpenByEvent: 'mouseover',
syncResizeTitle: 'Synchronous resize',
useClickToRepositionMenu: true,
headerColumnValueExtractor: (columnDef: Column) => columnDef.name as string,
Expand All @@ -161,7 +163,7 @@ export class SlickGridMenu {
constructor(protected columns: Column[], protected readonly grid: SlickGrid, gridOptions: GridOption) {
this._gridUid = grid.getUID();
this._gridOptions = gridOptions;
this._gridMenuOptions = Utils.extend({}, gridOptions.gridMenu);
this._gridMenuOptions = Utils.extend({}, this._defaults, gridOptions.gridMenu);
this._bindingEventService = new BindingEventService();

// when a grid optionally changes from a regular grid to a frozen grid, we need to destroy & recreate the grid menu
Expand Down Expand Up @@ -451,6 +453,17 @@ export class SlickGridMenu {
this._bindingEventService.bind(liElm, 'click', this.handleMenuItemClick.bind(this, item, args.level) as EventListener);
}

// optionally open sub-menu(s) by mouseover
if (this._gridMenuOptions?.subMenuOpenByEvent === 'mouseover') {
this._bindingEventService.bind(liElm, 'mouseover', ((e: DOMMouseOrTouchEvent<HTMLDivElement>) => {
if ((item as GridMenuItem).customItems) {
this.repositionSubMenu(item, args.level, e);
} else if (!isSubMenu) {
this.destroySubMenus();
}
}) as EventListener);
}

// the option/command item could be a sub-menu if it has another list of commands/options
if ((item as GridMenuItem).customItems) {
const chevronElm = document.createElement('span');
Expand Down
3 changes: 3 additions & 0 deletions src/models/cellMenuOption.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ export interface CellMenuOption {
/** CSS class that can be added on the right side of a sub-item parent (typically a chevron-right icon) */
subItemChevronClass?: string;

/** Defaults to "mouseover", what event type shoud we use to open sub-menu(s), 2 options are available: "mouseover" or "click" */
subMenuOpenByEvent?: 'mouseover' | 'click';

// --
// action/override callbacks

Expand Down
3 changes: 3 additions & 0 deletions src/models/contextMenuOption.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ export interface ContextMenuOption {
/** CSS class that can be added on the right side of a sub-item parent (typically a chevron-right icon) */
subItemChevronClass?: string;

/** Defaults to "mouseover", what event type shoud we use to open sub-menu(s), 2 options are available: "mouseover" or "click" */
subMenuOpenByEvent?: 'mouseover' | 'click';

// --
// action/override callbacks

Expand Down
3 changes: 3 additions & 0 deletions src/models/gridMenuOption.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ export interface GridMenuOption {
/** CSS class that can be added on the right side of a sub-item parent (typically a chevron-right icon) */
subItemChevronClass?: string;

/** Defaults to "mouseover", what event type shoud we use to open sub-menu(s), 2 options are available: "mouseover" or "click" */
subMenuOpenByEvent?: 'mouseover' | 'click';

/** Defaults to "Synchronous resize" which is 1 of the last 2 checkbox title shown at the end of the picker list */
syncResizeTitle?: string;

Expand Down
3 changes: 3 additions & 0 deletions src/models/headerMenuOption.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ export interface HeaderMenuOption {
/** CSS class that can be added on the right side of a sub-item parent (typically a chevron-right icon) */
subItemChevronClass?: string;

/** Defaults to "mouseover", what event type shoud we use to open sub-menu(s), 2 options are available: "mouseover" or "click" */
subMenuOpenByEvent?: 'mouseover' | 'click';

/** Menu item text. */
title?: string;

Expand Down
14 changes: 14 additions & 0 deletions src/plugins/slick.cellmenu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ const Utils = IIFE_ONLY ? Slick.Utils : Utils_;
* autoAlignSideOffset: Optionally add an offset to the left/right side auto-align (defaults to 0)
* menuUsabilityOverride: Callback method that user can override the default behavior of enabling/disabling the menu from being usable (must be combined with a custom formatter)
* subItemChevronClass: CSS class that can be added on the right side of a sub-item parent (typically a chevron-right icon)
* subMenuOpenByEvent: defaults to "mouseover", what event type shoud we use to open sub-menu(s), 2 options are available: "mouseover" or "click"
*
*
* Available menu Command/Option item properties:
Expand Down Expand Up @@ -183,6 +184,7 @@ export class SlickCellMenu implements SlickPlugin {
hideMenuOnScroll: true,
maxHeight: 'none',
width: 'auto',
subMenuOpenByEvent: 'mouseover',
};

constructor(optionProperties: Partial<CellMenuOption>) {
Expand Down Expand Up @@ -704,6 +706,18 @@ export class SlickCellMenu implements SlickPlugin {
this._bindingEventService.bind(liElm, 'click', this.handleMenuItemClick.bind(this, item, itemType, args.level) as EventListener);
}

// optionally open sub-menu(s) by mouseover
if (this._cellMenuProperties.subMenuOpenByEvent === 'mouseover') {
this._bindingEventService.bind(liElm, 'mouseover', ((e: DOMMouseOrTouchEvent<HTMLDivElement>) => {
if ((item as MenuCommandItem).commandItems || (item as MenuOptionItem).optionItems) {
this.repositionSubMenu(item, itemType, args.level, e);
this._lastMenuTypeClicked = itemType;
} else if (!isSubMenu) {
this.destroySubMenus();
}
}) 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) {
const chevronElm = document.createElement('span');
Expand Down
16 changes: 15 additions & 1 deletion src/plugins/slick.contextmenu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ const Utils = IIFE_ONLY ? Slick.Utils : Utils_;
* autoAlignSideOffset: Optionally add an offset to the left/right side auto-align (defaults to 0)
* menuUsabilityOverride: Callback method that user can override the default behavior of enabling/disabling the menu from being usable (must be combined with a custom formatter)
* subItemChevronClass: CSS class that can be added on the right side of a sub-item parent (typically a chevron-right icon)
* subMenuOpenByEvent: defaults to "mouseover", what event type shoud we use to open sub-menu(s), 2 options are available: "mouseover" or "click"
*
*
* Available menu Command/Option item properties:
Expand Down Expand Up @@ -192,6 +193,7 @@ export class SlickContextMenu implements SlickPlugin {
width: 'auto',
optionShownOverColumnIds: [],
commandShownOverColumnIds: [],
subMenuOpenByEvent: 'mouseover',
};

constructor(optionProperties: Partial<ContextMenuOption>) {
Expand Down Expand Up @@ -283,7 +285,7 @@ export class SlickContextMenu implements SlickPlugin {
return this._menuElm;
}

protected createMenu(commandItems: Array<MenuCommandItem | 'divider'>, optionItems: Array<MenuOptionItem | 'divider'>, level = 0, item?: MenuCommandItem | MenuOptionItem | 'divider') {
protected createMenu(commandItems: Array<MenuCommandItem | 'divider'>, optionItems: Array<MenuOptionItem | 'divider'>, level = 0, item?: MenuCommandItem | MenuOptionItem | 'divider') {
const columnDef = this._grid.getColumns()[this._currentCell];
const dataContext = this._grid.getDataItem(this._currentRow);
const isColumnOptionAllowed = this.checkIsColumnAllowed(this._contextMenuProperties.optionShownOverColumnIds ?? [], columnDef.id);
Expand Down Expand Up @@ -632,6 +634,18 @@ export class SlickContextMenu implements SlickPlugin {
this._bindingEventService.bind(liElm, 'click', this.handleMenuItemClick.bind(this, item, itemType, args.level) as EventListener);
}

// optionally open sub-menu(s) by mouseover
if (this._contextMenuProperties.subMenuOpenByEvent === 'mouseover') {
this._bindingEventService.bind(liElm, 'mouseover', ((e: DOMMouseOrTouchEvent<HTMLDivElement>) => {
if ((item as MenuCommandItem).commandItems || (item as MenuOptionItem).optionItems) {
this.repositionSubMenu(item, itemType, args.level, e);
this._lastMenuTypeClicked = itemType;
} else if (!isSubMenu) {
this.destroySubMenus();
}
}) 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) {
const chevronElm = document.createElement('span');
Expand Down
48 changes: 31 additions & 17 deletions src/plugins/slick.headermenu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ const Utils = IIFE_ONLY ? Slick.Utils : Utils_;
* buttonImage: a url to the menu button image
* menuUsabilityOverride: Callback method that user can override the default behavior of enabling/disabling the menu from being usable (must be combined with a custom formatter)
* minWidth: Minimum width that the drop menu will have
* subItemChevronClass: CSS class that can be added on the right side of a sub-item parent (typically a chevron-right icon)
* subMenuOpenByEvent: defaults to "mouseover", what event type shoud we use to open sub-menu(s), 2 options are available: "mouseover" or "click"
*
*
* Available menu item options:
Expand Down Expand Up @@ -124,7 +126,8 @@ export class SlickHeaderMenu implements SlickPlugin {
buttonImage: undefined,
minWidth: 100,
autoAlign: true,
autoAlignOffset: 0
autoAlignOffset: 0,
subMenuOpenByEvent: 'mouseover',
};
protected _options: HeaderMenuOption;
protected _activeHeaderColumnElm?: HTMLDivElement | null;
Expand Down Expand Up @@ -305,12 +308,13 @@ export class SlickHeaderMenu implements SlickPlugin {
// to avoid having multiple sub-menu trees opened,
// we need to somehow keep trace of which parent menu the tree belongs to
// and we should keep ref of only the first sub-menu parent, we can use the command name (remove any whitespaces though)
const isSubMenu = level > 0;
const subMenuCommand = (item as HeaderMenuCommandItem)?.command;
let subMenuId = (level === 1 && subMenuCommand) ? subMenuCommand.replaceAll(' ', '') : '';
if (subMenuId) {
this._subMenuParentId = subMenuId;
}
if (level > 1) {
if (isSubMenu) {
subMenuId = this._subMenuParentId;
}

Expand Down Expand Up @@ -369,34 +373,34 @@ export class SlickHeaderMenu implements SlickPlugin {
(item as HeaderMenuCommandItem).disabled = isItemUsable ? false : true;
}

const menuItem = document.createElement('div');
menuItem.className = 'slick-header-menuitem';
menuItem.role = 'menuitem';
const menuItemElm = document.createElement('div');
menuItemElm.className = 'slick-header-menuitem';
menuItemElm.role = 'menuitem';

if ((item as HeaderMenuCommandItem).divider || item === 'divider') {
menuItem.classList.add('slick-header-menuitem-divider');
menuItemElm.classList.add('slick-header-menuitem-divider');
addClickListener = false;
}

if ((item as HeaderMenuCommandItem).disabled) {
menuItem.classList.add('slick-header-menuitem-disabled');
menuItemElm.classList.add('slick-header-menuitem-disabled');
}

if ((item as HeaderMenuCommandItem).hidden) {
menuItem.classList.add('slick-header-menuitem-hidden');
menuItemElm.classList.add('slick-header-menuitem-hidden');
}

if ((item as HeaderMenuCommandItem).cssClass) {
menuItem.classList.add(...(item as HeaderMenuCommandItem).cssClass!.split(' '));
menuItemElm.classList.add(...(item as HeaderMenuCommandItem).cssClass!.split(' '));
}

if ((item as HeaderMenuCommandItem).tooltip) {
menuItem.title = (item as HeaderMenuCommandItem).tooltip || '';
menuItemElm.title = (item as HeaderMenuCommandItem).tooltip || '';
}

const iconElm = document.createElement('div');
iconElm.className = 'slick-header-menuicon';
menuItem.appendChild(iconElm);
menuItemElm.appendChild(iconElm);

if ((item as HeaderMenuCommandItem).iconCssClass) {
iconElm.classList.add(...(item as HeaderMenuCommandItem).iconCssClass!.split(' '));
Expand All @@ -409,15 +413,26 @@ export class SlickHeaderMenu implements SlickPlugin {
const textElm = document.createElement('span');
textElm.className = 'slick-header-menucontent';
textElm.textContent = (item as HeaderMenuCommandItem).title || '';
menuItem.appendChild(textElm);
menuItemElm.appendChild(textElm);

if ((item as HeaderMenuCommandItem).textCssClass) {
textElm.classList.add(...(item as HeaderMenuCommandItem).textCssClass!.split(' '));
}
menuElm.appendChild(menuItem);
menuElm.appendChild(menuItemElm);

if (addClickListener) {
this._bindingEventService.bind(menuItem, 'click', this.handleMenuItemClick.bind(this, item, columnDef, level) as EventListener);
this._bindingEventService.bind(menuItemElm, 'click', this.handleMenuItemClick.bind(this, item, columnDef, level) as EventListener);
}

// optionally open sub-menu(s) by mouseover
if (this._options.subMenuOpenByEvent === 'mouseover') {
this._bindingEventService.bind(menuItemElm, 'mouseover', ((e: DOMMouseOrTouchEvent<HTMLDivElement>) => {
if ((item as HeaderMenuCommandItem).items) {
this.repositionSubMenu(item as HeaderMenuCommandItem, columnDef, level, e);
} else if (!isSubMenu) {
this.destroySubMenus();
}
}) as EventListener);
}

// the option/command item could be a sub-menu if it has another list of commands/options
Expand All @@ -430,9 +445,8 @@ export class SlickHeaderMenu implements SlickPlugin {
chevronElm.textContent = '⮞'; // ⮞ or ▸
}

menuItem.classList.add('slick-submenu-item');
menuItem.appendChild(chevronElm);
continue;
menuItemElm.classList.add('slick-submenu-item');
menuItemElm.appendChild(chevronElm);
}
}

Expand Down

0 comments on commit 3064be7

Please sign in to comment.