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

Collapse main menu options in a hamburger menu #489

Merged
merged 20 commits into from
Feb 23, 2023
Merged
Changes from 5 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
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
79 changes: 79 additions & 0 deletions packages/widgets/src/menubar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import { getKeyboardLayout } from '@lumino/keyboard';

import { Message, MessageLoop } from '@lumino/messaging';

import { CommandRegistry } from '@lumino/commands';

import {
ElementARIAAttrs,
ElementDataset,
Expand Down Expand Up @@ -47,6 +49,8 @@ export class MenuBar extends Widget {
forceX: true,
forceY: true
};
this._overflowMenu = null;
this._menuItemSizes = [];
steff456 marked this conversation as resolved.
Show resolved Hide resolved
}

/**
Expand Down Expand Up @@ -347,6 +351,9 @@ export class MenuBar extends Widget {
event.preventDefault();
event.stopPropagation();
break;
case 'resize':
this._evtResize(event);
break;
}
}

Expand All @@ -359,6 +366,7 @@ export class MenuBar extends Widget {
this.node.addEventListener('mousemove', this);
this.node.addEventListener('mouseleave', this);
this.node.addEventListener('contextmenu', this);
window.addEventListener('resize', this);
steff456 marked this conversation as resolved.
Show resolved Hide resolved
}

/**
Expand All @@ -370,6 +378,7 @@ export class MenuBar extends Widget {
this.node.removeEventListener('mousemove', this);
this.node.removeEventListener('mouseleave', this);
this.node.removeEventListener('contextmenu', this);
window.removeEventListener('resize', this);
this._closeChildMenu();
}

Expand All @@ -382,6 +391,73 @@ export class MenuBar extends Widget {
}
}

/**
* A message handler invoked on an `'resize'` message.
steff456 marked this conversation as resolved.
Show resolved Hide resolved
*/
protected _evtResize(event: Event): void {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a resize event on initial page load? In other words, if the user makes their browser really skinny first, and then loads JupyterLab (and then never resizes their browser again), will this code get executed and put stuff into the "..." menu or will the user have to first resize their browser window?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, there's not a resize event on the initial page load, so users will first have to resize their window. I'm not sure if there's a signal that exists when the page is loaded that I can connect this method to. If you know of it please let me know.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@afshin @fcollonval is there a good signal for initial load?

This seems bad from an end user's perspective, doesn't it? For example, if I've set my browser to a higher zoom and then load the JupyterLab UI, as an end user I don't want to have to jiggle the window resize handle to get the menu bar to render properly.

That said, just to be clear I'm not asking us to block on this, because it's something that can be added later.

// Get elements visible in the main menu bar
let itemMenus = this.node.getElementsByClassName('lm-MenuBar-item');
steff456 marked this conversation as resolved.
Show resolved Hide resolved
steff456 marked this conversation as resolved.
Show resolved Hide resolved
let screenSize = this.node.offsetWidth;
let totalMenuSize = 0;
let index = -1;
let n = itemMenus.length;

if (this._menuItemSizes.length == 0) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To reduce the number of lines in this resize handler, maybe you want to replace this._menuItemSizes with a _getMenuItemSizes method that handles this caching of the menu item sizes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can move this block of code to another function but I would need to send almost all the variables defined in the function as a parameter, so I don't know what's best...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might just be me, but would it be better if the entire function were moved into a separate method? Like so:

_evtResize(event: Event): void {
  this._handleOverflow();
}

I'm not sure that's the best name for the method, but there are a few reasons why I think having such a method would be a good idea:

  • I think it might be prudent to provide a way for clients to opt out of this behavior. I don't know all of the places that the Lumino menu bar class is used. I'm not 100% sure this behavior should be forced on downstream consumers always. Pulling the body of this function into its own method will make it easy to isolate that code and exit early if the client has passed some kind of "no overflow" option to the menu bar.
  • Once we figure out how to hook into page load and where, we will be able to call this method (rather than this._evtResize) from there.
  • Slightly simplified unit testing, since you don't have to trigger a resize event (also it makes it clear that you're not actually using the event object)
  • Friendlier to future programmers if they want to add more stuff to the resize handler (that doesn't have to do with overflow)

// Check if it is the first resize and get info about menu items sizes
for (let i = 0; i < n; i++) {
let item = itemMenus[i] as HTMLLIElement;
// Add sizes to array
totalMenuSize += item.offsetWidth;
this._menuItemSizes.push(item.offsetWidth);
if (totalMenuSize > screenSize && index === -1) {
index = i;
}
}
} else {
github-advanced-security[bot] marked this conversation as resolved.
Fixed
Show resolved Hide resolved
// Calculate current menu size
for (let i = 0; i < this._menuItemSizes.length; i++) {
Fixed Show fixed Hide fixed
totalMenuSize += this._menuItemSizes[i];
if (totalMenuSize > screenSize) {
index = i;
break;
}
}
}

if (index > -1) {
// Create hamburger menu
if (this._overflowMenu === null) {
this._overflowMenu = new Menu({ commands: new CommandRegistry() });
this._overflowMenu.title.label = '...';
this._overflowMenu.title.mnemonic = 0;
this.addMenu(this._overflowMenu);
}

// Move menus
for (let i = index; i < n - 1; i++) {
let submenu = this.menus[i];
submenu.title.mnemonic = 0;
this._overflowMenu.insertItem(0, {
type: 'submenu',
submenu: submenu
});
this.removeMenuAt(i);
}
} else if (this._overflowMenu !== null) {
let i = n - 1;
let hamburgerMenuItems = this._overflowMenu.items;
if (screenSize - totalMenuSize > this._menuItemSizes[i]) {
let menu = hamburgerMenuItems[0].submenu as Menu;
this._overflowMenu.removeItemAt(0);
this.insertMenu(i, menu);
}
if (this._overflowMenu.items.length === 0) {
this.removeMenu(this._overflowMenu);
this._overflowMenu = null;
}
}
}

/**
* A message handler invoked on an `'update-request'` message.
*/
Expand All @@ -404,6 +480,7 @@ export class MenuBar extends Widget {
}
});
}
this._menuItemSizes = [];
VirtualDOM.render(content, this.contentNode);
}

Expand Down Expand Up @@ -730,6 +807,8 @@ export class MenuBar extends Widget {
private _forceItemsPosition: Menu.IOpenOptions;
private _menus: Menu[] = [];
private _childMenu: Menu | null = null;
private _overflowMenu: Menu | null = null;
private _menuItemSizes: number[] = [];
}

/**
Expand Down