From 2f46ffc11bf6f03d87869bcd4624714059a8749f Mon Sep 17 00:00:00 2001 From: Stephannie Jimenez Date: Tue, 6 Dec 2022 14:49:21 -0500 Subject: [PATCH 01/18] Make menubar responsive --- packages/widgets/src/menubar.ts | 78 +++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/packages/widgets/src/menubar.ts b/packages/widgets/src/menubar.ts index fbf9d066b..1d81183a8 100644 --- a/packages/widgets/src/menubar.ts +++ b/packages/widgets/src/menubar.ts @@ -15,6 +15,8 @@ import { getKeyboardLayout } from '@lumino/keyboard'; import { Message, MessageLoop } from '@lumino/messaging'; +import { CommandRegistry } from '@lumino/commands'; + import { ElementARIAAttrs, ElementDataset, @@ -47,6 +49,8 @@ export class MenuBar extends Widget { forceX: true, forceY: true }; + this._hamburgerMenu = null; + this._menuItemSizes = []; } /** @@ -347,6 +351,9 @@ export class MenuBar extends Widget { event.preventDefault(); event.stopPropagation(); break; + case 'resize': + this._evtResize(event); + break; } } @@ -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); } /** @@ -382,6 +390,74 @@ export class MenuBar extends Widget { } } + /** + * A message handler invoked on an `'resize'` message. + */ + protected _evtResize(event: Event): void { + let itemMenus = this.node.getElementsByClassName('lm-MenuBar-item'); + let screenSize = this.node.offsetWidth; + let totalMenuSize = 0; + let index = -1; + let n = itemMenus.length; + let first = false; + + if (this._menuItemSizes.length == 0) { + // Check if it is the first resize + first = true; + } + + for (let i = 0; i < n; i++) { + let item = itemMenus[i] as HTMLLIElement; + totalMenuSize += item.offsetWidth; + if (first) { + // Add sizes to array + this._menuItemSizes.push(item.offsetWidth); + } + if (totalMenuSize > screenSize) { + index = i; + break; + } + } + if (first) { + first = false; + } + console.log(this._menuItemSizes); + if (index > -1) { + // Create hamburger menu + const commands = new CommandRegistry(); + if (this._hamburgerMenu === null) { + this._hamburgerMenu = new Menu({ commands }); + this._hamburgerMenu.title.label = '...'; + this._hamburgerMenu.title.mnemonic = 0; + this.addMenu(this._hamburgerMenu); + } + + // Move menus + for (let i = index; i < n - 1; i++) { + let submenu = this.menus[i]; + submenu.title.mnemonic = 0; + this._hamburgerMenu.insertItem(0, { + type: 'submenu', + submenu: submenu + }); + this.removeMenuAt(i); + } + } else if (this._hamburgerMenu !== null) { + let i = n - 1; + let hamburgerMenuItems = this._hamburgerMenu.items; + console.log(screenSize - totalMenuSize, this._menuItemSizes[i]); + if (screenSize - totalMenuSize > this._menuItemSizes[i]) { + let menu = hamburgerMenuItems[0].submenu as Menu; + this._hamburgerMenu.removeItemAt(0); + this.insertMenu(i, menu); + } + if (this._hamburgerMenu.items.length === 0) { + this.removeMenu(this._hamburgerMenu); + this._hamburgerMenu = null; + } + } + } + /** * A message handler invoked on an `'update-request'` message. */ @@ -730,6 +806,8 @@ export class MenuBar extends Widget { private _forceItemsPosition: Menu.IOpenOptions; private _menus: Menu[] = []; private _childMenu: Menu | null = null; + private _hamburgerMenu: Menu | null = null; + private _menuItemSizes: number[] = []; } /** From 59291de604febbba2ac143ac4814256a958fbad8 Mon Sep 17 00:00:00 2001 From: Stephannie Jimenez Date: Wed, 7 Dec 2022 10:53:09 -0500 Subject: [PATCH 02/18] Add review changes --- packages/widgets/src/menubar.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/widgets/src/menubar.ts b/packages/widgets/src/menubar.ts index 1d81183a8..56c4666b2 100644 --- a/packages/widgets/src/menubar.ts +++ b/packages/widgets/src/menubar.ts @@ -378,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(); } @@ -421,12 +422,10 @@ export class MenuBar extends Widget { if (first) { first = false; } - console.log(this._menuItemSizes); if (index > -1) { // Create hamburger menu - const commands = new CommandRegistry(); if (this._hamburgerMenu === null) { - this._hamburgerMenu = new Menu({ commands }); + this._hamburgerMenu = new Menu({ commands: new CommandRegistry() }); this._hamburgerMenu.title.label = '...'; this._hamburgerMenu.title.mnemonic = 0; this.addMenu(this._hamburgerMenu); @@ -445,7 +444,6 @@ export class MenuBar extends Widget { } else if (this._hamburgerMenu !== null) { let i = n - 1; let hamburgerMenuItems = this._hamburgerMenu.items; - console.log(screenSize - totalMenuSize, this._menuItemSizes[i]); if (screenSize - totalMenuSize > this._menuItemSizes[i]) { let menu = hamburgerMenuItems[0].submenu as Menu; this._hamburgerMenu.removeItemAt(0); From 4f9f0e1be7deabadcd68d94b90901fcbfbeda463 Mon Sep 17 00:00:00 2001 From: Stephannie Jimenez Date: Tue, 13 Dec 2022 14:52:02 -0500 Subject: [PATCH 03/18] Use cached item sizes --- packages/widgets/src/menubar.ts | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/packages/widgets/src/menubar.ts b/packages/widgets/src/menubar.ts index 56c4666b2..80103572e 100644 --- a/packages/widgets/src/menubar.ts +++ b/packages/widgets/src/menubar.ts @@ -400,28 +400,31 @@ export class MenuBar extends Widget { let totalMenuSize = 0; let index = -1; let n = itemMenus.length; - let first = false; if (this._menuItemSizes.length == 0) { - // Check if it is the first resize - first = true; - } - - for (let i = 0; i < n; i++) { - let item = itemMenus[i] as HTMLLIElement; - totalMenuSize += item.offsetWidth; - if (first) { + // 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; + let found = false; // Add sizes to array + totalMenuSize += item.offsetWidth; this._menuItemSizes.push(item.offsetWidth); + if (totalMenuSize > screenSize && !found) { + index = i; + found = true; + } } - if (totalMenuSize > screenSize) { - index = i; - break; + } else { + // Calculate current menu size + for (let i = 0; i < this._menuItemSizes.length; i++) { + totalMenuSize += this._menuItemSizes[i]; + if (totalMenuSize > screenSize) { + index = i; + break; + } } } - if (first) { - first = false; - } + if (index > -1) { // Create hamburger menu if (this._hamburgerMenu === null) { From dfe9abd077fb05ac3a4785dec90623a98c731086 Mon Sep 17 00:00:00 2001 From: Stephannie Jimenez Date: Tue, 13 Dec 2022 14:54:28 -0500 Subject: [PATCH 04/18] Invalidate cached item sizes in onUpdateRequest --- packages/widgets/src/menubar.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/widgets/src/menubar.ts b/packages/widgets/src/menubar.ts index 80103572e..8753e088f 100644 --- a/packages/widgets/src/menubar.ts +++ b/packages/widgets/src/menubar.ts @@ -481,6 +481,7 @@ export class MenuBar extends Widget { } }); } + this._menuItemSizes = []; VirtualDOM.render(content, this.contentNode); } From b5402a85c9215b609cfc6767aeffb4de18fdb5d7 Mon Sep 17 00:00:00 2001 From: Stephannie Jimenez Date: Fri, 16 Dec 2022 10:42:29 -0500 Subject: [PATCH 05/18] Add review changes --- packages/widgets/src/menubar.ts | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/packages/widgets/src/menubar.ts b/packages/widgets/src/menubar.ts index 8753e088f..d69354205 100644 --- a/packages/widgets/src/menubar.ts +++ b/packages/widgets/src/menubar.ts @@ -49,7 +49,7 @@ export class MenuBar extends Widget { forceX: true, forceY: true }; - this._hamburgerMenu = null; + this._overflowMenu = null; this._menuItemSizes = []; } @@ -395,6 +395,7 @@ export class MenuBar extends Widget { * A message handler invoked on an `'resize'` message. */ protected _evtResize(event: Event): void { + // Get elements visible in the main menu bar let itemMenus = this.node.getElementsByClassName('lm-MenuBar-item'); let screenSize = this.node.offsetWidth; let totalMenuSize = 0; @@ -405,13 +406,11 @@ export class MenuBar extends Widget { // 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; - let found = false; // Add sizes to array totalMenuSize += item.offsetWidth; this._menuItemSizes.push(item.offsetWidth); - if (totalMenuSize > screenSize && !found) { + if (totalMenuSize > screenSize && index === -1) { index = i; - found = true; } } } else { @@ -427,34 +426,34 @@ export class MenuBar extends Widget { if (index > -1) { // Create hamburger menu - if (this._hamburgerMenu === null) { - this._hamburgerMenu = new Menu({ commands: new CommandRegistry() }); - this._hamburgerMenu.title.label = '...'; - this._hamburgerMenu.title.mnemonic = 0; - this.addMenu(this._hamburgerMenu); + 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._hamburgerMenu.insertItem(0, { + this._overflowMenu.insertItem(0, { type: 'submenu', submenu: submenu }); this.removeMenuAt(i); } - } else if (this._hamburgerMenu !== null) { + } else if (this._overflowMenu !== null) { let i = n - 1; - let hamburgerMenuItems = this._hamburgerMenu.items; + let hamburgerMenuItems = this._overflowMenu.items; if (screenSize - totalMenuSize > this._menuItemSizes[i]) { let menu = hamburgerMenuItems[0].submenu as Menu; - this._hamburgerMenu.removeItemAt(0); + this._overflowMenu.removeItemAt(0); this.insertMenu(i, menu); } - if (this._hamburgerMenu.items.length === 0) { - this.removeMenu(this._hamburgerMenu); - this._hamburgerMenu = null; + if (this._overflowMenu.items.length === 0) { + this.removeMenu(this._overflowMenu); + this._overflowMenu = null; } } } @@ -808,7 +807,7 @@ export class MenuBar extends Widget { private _forceItemsPosition: Menu.IOpenOptions; private _menus: Menu[] = []; private _childMenu: Menu | null = null; - private _hamburgerMenu: Menu | null = null; + private _overflowMenu: Menu | null = null; private _menuItemSizes: number[] = []; } From f7501b7b900514e256386d82415d4c0021841399 Mon Sep 17 00:00:00 2001 From: Stephannie Jimenez Date: Wed, 28 Dec 2022 23:16:49 -0500 Subject: [PATCH 06/18] Move logic to onUpdateRequest --- examples/example-dockpanel/src/index.ts | 3 +- packages/widgets/src/menubar.ts | 149 +++++++++++++++--------- 2 files changed, 99 insertions(+), 53 deletions(-) diff --git a/examples/example-dockpanel/src/index.ts b/examples/example-dockpanel/src/index.ts index 81579a194..c97cd2333 100644 --- a/examples/example-dockpanel/src/index.ts +++ b/examples/example-dockpanel/src/index.ts @@ -10,7 +10,7 @@ |----------------------------------------------------------------------------*/ import { CommandRegistry } from '@lumino/commands'; -import { Message } from '@lumino/messaging'; +import { Message, MessageLoop } from '@lumino/messaging'; import { BoxPanel, @@ -446,6 +446,7 @@ function main(): void { main.addWidget(dock); window.onresize = () => { + MessageLoop.postMessage(bar, new Widget.ResizeMessage(-1, -1)); main.update(); }; diff --git a/packages/widgets/src/menubar.ts b/packages/widgets/src/menubar.ts index d69354205..d66062a6e 100644 --- a/packages/widgets/src/menubar.ts +++ b/packages/widgets/src/menubar.ts @@ -51,6 +51,7 @@ export class MenuBar extends Widget { }; this._overflowMenu = null; this._menuItemSizes = []; + this._overflowIndex = -1; } /** @@ -187,8 +188,8 @@ export class MenuBar extends Widget { * #### Notes * If the menu is already added to the menu bar, it will be moved. */ - addMenu(menu: Menu): void { - this.insertMenu(this._menus.length, menu); + addMenu(menu: Menu, update: boolean = true): void { + this.insertMenu(this._menus.length, menu, update); } /** @@ -203,7 +204,7 @@ export class MenuBar extends Widget { * * If the menu is already added to the menu bar, it will be moved. */ - insertMenu(index: number, menu: Menu): void { + insertMenu(index: number, menu: Menu, update: boolean = true): void { // Close the child menu before making changes. this._closeChildMenu(); @@ -227,7 +228,9 @@ export class MenuBar extends Widget { menu.title.changed.connect(this._onTitleChanged, this); // Schedule an update of the items. - this.update(); + if (update) { + this.update(); + } // There is nothing more to do. return; @@ -249,7 +252,9 @@ export class MenuBar extends Widget { ArrayExt.move(this._menus, i, j); // Schedule an update of the items. - this.update(); + if (update) { + this.update(); + } } /** @@ -260,8 +265,8 @@ export class MenuBar extends Widget { * #### Notes * This is a no-op if the menu is not in the menu bar. */ - removeMenu(menu: Menu): void { - this.removeMenuAt(this._menus.indexOf(menu)); + removeMenu(menu: Menu, update: boolean = true): void { + this.removeMenuAt(this._menus.indexOf(menu), update); } /** @@ -272,7 +277,7 @@ export class MenuBar extends Widget { * #### Notes * This is a no-op if the index is out of range. */ - removeMenuAt(index: number): void { + removeMenuAt(index: number, update: boolean = true): void { // Close the child menu before making changes. this._closeChildMenu(); @@ -293,7 +298,9 @@ export class MenuBar extends Widget { menu.removeClass('lm-MenuBar-menu'); // Schedule an update of the items. - this.update(); + if (update) { + this.update(); + } } /** @@ -351,9 +358,6 @@ export class MenuBar extends Widget { event.preventDefault(); event.stopPropagation(); break; - case 'resize': - this._evtResize(event); - break; } } @@ -366,7 +370,6 @@ export class MenuBar extends Widget { this.node.addEventListener('mousemove', this); this.node.addEventListener('mouseleave', this); this.node.addEventListener('contextmenu', this); - window.addEventListener('resize', this); } /** @@ -378,7 +381,6 @@ 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(); } @@ -392,9 +394,13 @@ export class MenuBar extends Widget { } /** - * A message handler invoked on an `'resize'` message. + * A message handler invoked on a `'resize'` message. */ - protected _evtResize(event: Event): void { + protected onResize(msg: Widget.ResizeMessage): void { + this.update(); + } + + protected updateOverflowIndex(): void { // Get elements visible in the main menu bar let itemMenus = this.node.getElementsByClassName('lm-MenuBar-item'); let screenSize = this.node.offsetWidth; @@ -423,39 +429,7 @@ export class MenuBar extends Widget { } } } - - 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; - } - } + this._overflowIndex = index; } /** @@ -465,8 +439,15 @@ export class MenuBar extends Widget { let menus = this._menus; let renderer = this.renderer; let activeIndex = this._activeIndex; - let content = new Array(menus.length); - for (let i = 0, n = menus.length; i < n; ++i) { + let length = this._overflowIndex > -1 ? this._overflowIndex : menus.length; + let content = new Array(length); + let totalMenuSize = 0; + + // Check that the overflow menu doesn't count + length = this._overflowMenu !== null ? length - 1 : length; + + // Render visible menus + for (let i = 0; i < length; ++i) { let title = menus[i].title; let active = i === activeIndex; if (active && menus[i].items.length == 0) { @@ -479,9 +460,72 @@ export class MenuBar extends Widget { this.activeIndex = i; } }); + // Calculate size of current menu + totalMenuSize += this._menuItemSizes[i]; + } + // Render overflow menu if needed + if (this._overflowIndex > -1) { + // Create overflow 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, false); + } + // Move menus to overflow menu + for (let i = length; i < menus.length - 1; ++i) { + let submenu = this.menus[i]; + submenu.title.mnemonic = 0; + this._overflowMenu.insertItem(0, { + type: 'submenu', + submenu: submenu + }); + this.removeMenu(submenu, false); + } + let title = this._overflowMenu.title; + let active = length === activeIndex; + if (active && menus[length].items.length == 0) { + active = false; + } + content[length] = renderer.renderItem({ + title, + active, + onfocus: () => { + this.activeIndex = length; + } + }); + } else if (this._overflowMenu !== null) { + // Remove submenus from overflow menu + let overflowMenuItems = this._overflowMenu.items; + let screenSize = this.node.offsetWidth; + let n = this._overflowMenu.items.length; + for (let i = 0; i < n; ++i) { + let index = menus.length - 1 - i; + if (screenSize - totalMenuSize > this._menuItemSizes[index]) { + let menu = overflowMenuItems[0].submenu as Menu; + this._overflowMenu.removeItemAt(0); + this.insertMenu(length, menu, false); + let title = menu.title; + let active = false; + content[length] = renderer.renderItem({ + title, + active, + onfocus: () => { + this.activeIndex = length; + } + }); + length++; + } + } + if (this._overflowMenu.items.length === 0) { + this.removeMenu(this._overflowMenu, false); + content.pop(); + this._overflowMenu = null; + this._overflowIndex = -1; + } } - this._menuItemSizes = []; VirtualDOM.render(content, this.contentNode); + this.updateOverflowIndex(); } /** @@ -809,6 +853,7 @@ export class MenuBar extends Widget { private _childMenu: Menu | null = null; private _overflowMenu: Menu | null = null; private _menuItemSizes: number[] = []; + private _overflowIndex: number; } /** From 1e1c4940346f86ac38ac45bd80ee4cc3851a1b49 Mon Sep 17 00:00:00 2001 From: Stephannie Jimenez Date: Wed, 28 Dec 2022 23:22:39 -0500 Subject: [PATCH 07/18] Remove redundant initialization --- packages/widgets/src/menubar.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/widgets/src/menubar.ts b/packages/widgets/src/menubar.ts index d66062a6e..6fb3039e8 100644 --- a/packages/widgets/src/menubar.ts +++ b/packages/widgets/src/menubar.ts @@ -49,9 +49,6 @@ export class MenuBar extends Widget { forceX: true, forceY: true }; - this._overflowMenu = null; - this._menuItemSizes = []; - this._overflowIndex = -1; } /** From de210eda0af5db7b67d13fb897f2d3aecee4bd4d Mon Sep 17 00:00:00 2001 From: Stephannie Jimenez Date: Thu, 29 Dec 2022 12:49:18 -0500 Subject: [PATCH 08/18] Initialize overflowIndex and add submenus in the correct order --- packages/widgets/src/menubar.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/widgets/src/menubar.ts b/packages/widgets/src/menubar.ts index 6fb3039e8..0a9a73df6 100644 --- a/packages/widgets/src/menubar.ts +++ b/packages/widgets/src/menubar.ts @@ -470,7 +470,7 @@ export class MenuBar extends Widget { this.addMenu(this._overflowMenu, false); } // Move menus to overflow menu - for (let i = length; i < menus.length - 1; ++i) { + for (let i = menus.length - 2; i >= length; i--) { let submenu = this.menus[i]; submenu.title.mnemonic = 0; this._overflowMenu.insertItem(0, { @@ -850,7 +850,7 @@ export class MenuBar extends Widget { private _childMenu: Menu | null = null; private _overflowMenu: Menu | null = null; private _menuItemSizes: number[] = []; - private _overflowIndex: number; + private _overflowIndex: number = -1; } /** From 4e8c6253f1df8b5b5cf7ca10d699af082673dd99 Mon Sep 17 00:00:00 2001 From: Stephannie Jimenez Date: Mon, 23 Jan 2023 17:26:19 -0500 Subject: [PATCH 09/18] Create interface in renderer and change updateOverflowIndex as a private function --- packages/widgets/src/menubar.ts | 100 +++++++++++++++++++++----------- 1 file changed, 65 insertions(+), 35 deletions(-) diff --git a/packages/widgets/src/menubar.ts b/packages/widgets/src/menubar.ts index 0a9a73df6..d828eeb7c 100644 --- a/packages/widgets/src/menubar.ts +++ b/packages/widgets/src/menubar.ts @@ -397,38 +397,6 @@ export class MenuBar extends Widget { this.update(); } - protected updateOverflowIndex(): void { - // Get elements visible in the main menu bar - let itemMenus = this.node.getElementsByClassName('lm-MenuBar-item'); - let screenSize = this.node.offsetWidth; - let totalMenuSize = 0; - let index = -1; - let n = itemMenus.length; - - if (this._menuItemSizes.length == 0) { - // 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 { - // Calculate current menu size - for (let i = 0; i < this._menuItemSizes.length; i++) { - totalMenuSize += this._menuItemSizes[i]; - if (totalMenuSize > screenSize) { - index = i; - break; - } - } - } - this._overflowIndex = index; - } - /** * A message handler invoked on an `'update-request'` message. */ @@ -471,7 +439,7 @@ export class MenuBar extends Widget { } // Move menus to overflow menu for (let i = menus.length - 2; i >= length; i--) { - let submenu = this.menus[i]; + const submenu = this.menus[i]; submenu.title.mnemonic = 0; this._overflowMenu.insertItem(0, { type: 'submenu', @@ -484,7 +452,7 @@ export class MenuBar extends Widget { if (active && menus[length].items.length == 0) { active = false; } - content[length] = renderer.renderItem({ + content[length] = renderer.renderOverflowItem({ title, active, onfocus: () => { @@ -522,7 +490,42 @@ export class MenuBar extends Widget { } } VirtualDOM.render(content, this.contentNode); - this.updateOverflowIndex(); + this._updateOverflowIndex(); + } + + /** + * Calculate and update the current overflow index. + */ + private _updateOverflowIndex(): void { + // Get elements visible in the main menu bar + let itemMenus = this.node.getElementsByClassName('lm-MenuBar-item'); + let screenSize = this.node.offsetWidth; + let totalMenuSize = 0; + let index = -1; + let n = itemMenus.length; + + if (this._menuItemSizes.length == 0) { + // 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 { + // Calculate current menu size + for (let i = 0; i < this._menuItemSizes.length; i++) { + totalMenuSize += this._menuItemSizes[i]; + if (totalMenuSize > screenSize) { + index = i; + break; + } + } + } + this._overflowIndex = index; } /** @@ -908,6 +911,15 @@ export namespace MenuBar { * @returns A virtual element representing the item. */ renderItem(data: IRenderData): VirtualElement; + + /** + * Render the virtual element for an overflow menu in the menu bar. + * + * @param data - The data to use for rendering the overflow item. + * + * @returns A virtual element representing the item. + */ + renderOverflowItem(data: IRenderData): VirtualElement; } /** @@ -935,6 +947,24 @@ export namespace MenuBar { ); } + /** + * Render the virtual element for an overflow menu in the menu bar. + * + * @param data - The data to use for rendering the overflow item. + * + * @returns A virtual element representing the item. + */ + renderOverflowItem(data: IRenderData): VirtualElement { + let className = this.createItemClass(data); + let dataset = this.createItemDataset(data); + let aria = this.createItemARIA(data); + return h.li( + { className, dataset, tabindex: '0', onfocus: data.onfocus, ...aria }, + this.renderIcon(data), + this.renderLabel(data) + ); + } + /** * Render the icon element for a menu bar item. * From c14d733b21c040596667bf9b339a9d3ebb155f13 Mon Sep 17 00:00:00 2001 From: Stephannie Jimenez Date: Mon, 23 Jan 2023 17:40:22 -0500 Subject: [PATCH 10/18] Get elements by children nodes without their classname --- packages/widgets/src/menubar.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/widgets/src/menubar.ts b/packages/widgets/src/menubar.ts index d828eeb7c..ce1614aa1 100644 --- a/packages/widgets/src/menubar.ts +++ b/packages/widgets/src/menubar.ts @@ -498,7 +498,7 @@ export class MenuBar extends Widget { */ private _updateOverflowIndex(): void { // Get elements visible in the main menu bar - let itemMenus = this.node.getElementsByClassName('lm-MenuBar-item'); + const itemMenus = this.contentNode.childNodes; let screenSize = this.node.offsetWidth; let totalMenuSize = 0; let index = -1; From fcc357b720e293882a50514a5b55c07656589821 Mon Sep 17 00:00:00 2001 From: krassowski <5832902+krassowski@users.noreply.github.com> Date: Wed, 25 Jan 2023 19:23:03 +0000 Subject: [PATCH 11/18] Collapse logic to reduce code complexity --- packages/widgets/src/menubar.ts | 24 ++++++------------------ 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/packages/widgets/src/menubar.ts b/packages/widgets/src/menubar.ts index ce1614aa1..3132fc1c6 100644 --- a/packages/widgets/src/menubar.ts +++ b/packages/widgets/src/menubar.ts @@ -413,14 +413,9 @@ export class MenuBar extends Widget { // Render visible menus for (let i = 0; i < length; ++i) { - let title = menus[i].title; - let active = i === activeIndex; - if (active && menus[i].items.length == 0) { - active = false; - } content[i] = renderer.renderItem({ - title, - active, + title: menus[i].title, + active: i === activeIndex && menus[i].items.length !== 0, onfocus: () => { this.activeIndex = i; } @@ -447,14 +442,9 @@ export class MenuBar extends Widget { }); this.removeMenu(submenu, false); } - let title = this._overflowMenu.title; - let active = length === activeIndex; - if (active && menus[length].items.length == 0) { - active = false; - } content[length] = renderer.renderOverflowItem({ - title, - active, + title: this._overflowMenu.title, + active: length === activeIndex && menus[length].items.length !== 0, onfocus: () => { this.activeIndex = length; } @@ -470,11 +460,9 @@ export class MenuBar extends Widget { let menu = overflowMenuItems[0].submenu as Menu; this._overflowMenu.removeItemAt(0); this.insertMenu(length, menu, false); - let title = menu.title; - let active = false; content[length] = renderer.renderItem({ - title, - active, + title: menu.title, + active: false, onfocus: () => { this.activeIndex = length; } From aae3aca928767faee77bce04f4178d9a7222aa72 Mon Sep 17 00:00:00 2001 From: Stephannie Jimenez Date: Wed, 1 Feb 2023 00:57:18 -0500 Subject: [PATCH 12/18] Undo dedicated IRenderer and add overflowMenuOptions --- packages/widgets/src/menubar.ts | 158 +++++++++++++++++--------------- 1 file changed, 82 insertions(+), 76 deletions(-) diff --git a/packages/widgets/src/menubar.ts b/packages/widgets/src/menubar.ts index 3132fc1c6..9c008b663 100644 --- a/packages/widgets/src/menubar.ts +++ b/packages/widgets/src/menubar.ts @@ -49,6 +49,10 @@ export class MenuBar extends Widget { forceX: true, forceY: true }; + this._overflowMenuOptions = options.overflowMenuOptions || { + overflowMenuVisible: true, + title: '...' + }; } /** @@ -423,58 +427,61 @@ export class MenuBar extends Widget { // Calculate size of current menu totalMenuSize += this._menuItemSizes[i]; } - // Render overflow menu if needed - if (this._overflowIndex > -1) { - // Create overflow 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, false); - } - // Move menus to overflow menu - for (let i = menus.length - 2; i >= length; i--) { - const submenu = this.menus[i]; - submenu.title.mnemonic = 0; - this._overflowMenu.insertItem(0, { - type: 'submenu', - submenu: submenu - }); - this.removeMenu(submenu, false); - } - content[length] = renderer.renderOverflowItem({ - title: this._overflowMenu.title, - active: length === activeIndex && menus[length].items.length !== 0, - onfocus: () => { - this.activeIndex = length; + // Render overflow menu if needed and active + if (this._overflowMenuOptions.overflowMenuVisible) { + if (this._overflowIndex > -1) { + // Create overflow menu + if (this._overflowMenu === null) { + this._overflowMenu = new Menu({ commands: new CommandRegistry() }); + this._overflowMenu.title.label = this._overflowMenuOptions.title; + this._overflowMenu.title.mnemonic = 0; + this.addMenu(this._overflowMenu, false); } - }); - } else if (this._overflowMenu !== null) { - // Remove submenus from overflow menu - let overflowMenuItems = this._overflowMenu.items; - let screenSize = this.node.offsetWidth; - let n = this._overflowMenu.items.length; - for (let i = 0; i < n; ++i) { - let index = menus.length - 1 - i; - if (screenSize - totalMenuSize > this._menuItemSizes[index]) { - let menu = overflowMenuItems[0].submenu as Menu; - this._overflowMenu.removeItemAt(0); - this.insertMenu(length, menu, false); - content[length] = renderer.renderItem({ - title: menu.title, - active: false, - onfocus: () => { - this.activeIndex = length; - } + // Move menus to overflow menu + for (let i = menus.length - 2; i >= length; i--) { + const submenu = this.menus[i]; + submenu.title.mnemonic = 0; + this._overflowMenu.insertItem(0, { + type: 'submenu', + submenu: submenu }); - length++; + this.removeMenu(submenu, false); + } + content[length] = renderer.renderItem({ + title: this._overflowMenu.title, + active: length === activeIndex && menus[length].items.length !== 0, + onfocus: () => { + this.activeIndex = length; + } + }); + length++; + } else if (this._overflowMenu !== null) { + // Remove submenus from overflow menu + let overflowMenuItems = this._overflowMenu.items; + let screenSize = this.node.offsetWidth; + let n = this._overflowMenu.items.length; + for (let i = 0; i < n; ++i) { + let index = menus.length - 1 - i; + if (screenSize - totalMenuSize > this._menuItemSizes[index]) { + let menu = overflowMenuItems[0].submenu as Menu; + this._overflowMenu.removeItemAt(0); + this.insertMenu(length, menu, false); + content[length] = renderer.renderItem({ + title: menu.title, + active: false, + onfocus: () => { + this.activeIndex = length; + } + }); + length++; + } + } + if (this._overflowMenu.items.length === 0) { + this.removeMenu(this._overflowMenu, false); + content.pop(); + this._overflowMenu = null; + this._overflowIndex = -1; } - } - if (this._overflowMenu.items.length === 0) { - this.removeMenu(this._overflowMenu, false); - content.pop(); - this._overflowMenu = null; - this._overflowIndex = -1; } } VirtualDOM.render(content, this.contentNode); @@ -837,6 +844,7 @@ export class MenuBar extends Widget { private _activeIndex = -1; private _forceItemsPosition: Menu.IOpenOptions; + private _overflowMenuOptions: IOverflowMenuOptions; private _menus: Menu[] = []; private _childMenu: Menu | null = null; private _overflowMenu: Menu | null = null; @@ -868,6 +876,15 @@ export namespace MenuBar { * The default is `true`. */ forceItemsPosition?: Menu.IOpenOptions; + /** + * Whether to add a overflow menu if there's overflow. + * + * Setting to `true` will enable the logic that creates an overflow menu + * to show the menu items that don't fit entirely on the screen. + * + * The default is `true`. + */ + overflowMenuOptions?: IOverflowMenuOptions; } /** @@ -899,15 +916,6 @@ export namespace MenuBar { * @returns A virtual element representing the item. */ renderItem(data: IRenderData): VirtualElement; - - /** - * Render the virtual element for an overflow menu in the menu bar. - * - * @param data - The data to use for rendering the overflow item. - * - * @returns A virtual element representing the item. - */ - renderOverflowItem(data: IRenderData): VirtualElement; } /** @@ -935,24 +943,6 @@ export namespace MenuBar { ); } - /** - * Render the virtual element for an overflow menu in the menu bar. - * - * @param data - The data to use for rendering the overflow item. - * - * @returns A virtual element representing the item. - */ - renderOverflowItem(data: IRenderData): VirtualElement { - let className = this.createItemClass(data); - let dataset = this.createItemDataset(data); - let aria = this.createItemARIA(data); - return h.li( - { className, dataset, tabindex: '0', onfocus: data.onfocus, ...aria }, - this.renderIcon(data), - this.renderLabel(data) - ); - } - /** * Render the icon element for a menu bar item. * @@ -1067,6 +1057,22 @@ export namespace MenuBar { export const defaultRenderer = new Renderer(); } +/** + * Options for overflow menu. + */ +export interface IOverflowMenuOptions { + /** + * Determines if a overflow menu appears when the menu items overflow. + */ + overflowMenuVisible: boolean; + /** + * Determines the title of the overflow menu. + * + * Default: `...`. + */ + title: string; +} + /** * The namespace for the module implementation details. */ From 36cefff062afc0ae69c30e9c00b52af2f5c6ff9b Mon Sep 17 00:00:00 2001 From: Stephannie Jimenez Date: Wed, 8 Feb 2023 13:46:16 -0500 Subject: [PATCH 13/18] Fix double menu bug --- packages/widgets/src/menubar.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/widgets/src/menubar.ts b/packages/widgets/src/menubar.ts index 9c008b663..d770b06e8 100644 --- a/packages/widgets/src/menubar.ts +++ b/packages/widgets/src/menubar.ts @@ -409,11 +409,12 @@ export class MenuBar extends Widget { let renderer = this.renderer; let activeIndex = this._activeIndex; let length = this._overflowIndex > -1 ? this._overflowIndex : menus.length; - let content = new Array(length); let totalMenuSize = 0; + let overflowMenuVisible = false; // Check that the overflow menu doesn't count length = this._overflowMenu !== null ? length - 1 : length; + let content = new Array(length); // Render visible menus for (let i = 0; i < length; ++i) { @@ -426,10 +427,15 @@ export class MenuBar extends Widget { }); // Calculate size of current menu totalMenuSize += this._menuItemSizes[i]; + // Check if overflow menu is already rendered + if (menus[i].title.label === this._overflowMenuOptions.title) { + overflowMenuVisible = true; + length--; + } } // Render overflow menu if needed and active if (this._overflowMenuOptions.overflowMenuVisible) { - if (this._overflowIndex > -1) { + if (this._overflowIndex > -1 && !overflowMenuVisible) { // Create overflow menu if (this._overflowMenu === null) { this._overflowMenu = new Menu({ commands: new CommandRegistry() }); From 92fbf60bb10881d178e98f0454e2d4653bf87ee1 Mon Sep 17 00:00:00 2001 From: Stephannie Jimenez Date: Wed, 15 Feb 2023 17:20:59 -0500 Subject: [PATCH 14/18] Add test to check the hamburger menu is rendering --- packages/widgets/src/menubar.ts | 14 ++++++++++++++ packages/widgets/tests/src/menubar.spec.ts | 12 ++++++++++++ 2 files changed, 26 insertions(+) diff --git a/packages/widgets/src/menubar.ts b/packages/widgets/src/menubar.ts index d770b06e8..105b8b880 100644 --- a/packages/widgets/src/menubar.ts +++ b/packages/widgets/src/menubar.ts @@ -79,6 +79,20 @@ export class MenuBar extends Widget { return this._childMenu; } + /** + * The overflow index of the menu bar. + */ + get overflowIndex(): number { + return this._overflowIndex; + } + + /** + * The overflow menu of the menu bar. + */ + get overflowMenu(): Menu | null { + return this._overflowMenu; + } + /** * Get the menu bar content node. * diff --git a/packages/widgets/tests/src/menubar.spec.ts b/packages/widgets/tests/src/menubar.spec.ts index d582bb5c0..ed8aa91ee 100644 --- a/packages/widgets/tests/src/menubar.spec.ts +++ b/packages/widgets/tests/src/menubar.spec.ts @@ -846,6 +846,18 @@ describe('@lumino/widgets', () => { expect(child.className).to.contain('lm-MenuBar-item'); bar.dispose(); }); + + it('should render the overflow menu', () => { + let bar = createMenuBar(); + expect(bar.overflowIndex).to.equal(-1); + expect(bar.overflowMenu).to.equal(null); + bar.node.style.maxWidth = '70px'; + MessageLoop.sendMessage(bar, Widget.Msg.UpdateRequest); + requestAnimationFrame(() => { + expect(bar.overflowMenu).to.not.equal(null); + bar.dispose(); + }); + }); }); context('`menuRequested` signal', () => { From 3f0c8e650258fad7e33a3c192a98c4160d604b51 Mon Sep 17 00:00:00 2001 From: Stephannie Jimenez Date: Thu, 16 Feb 2023 13:51:45 -0500 Subject: [PATCH 15/18] Test that the overflow menu renders and hides --- packages/widgets/tests/src/menubar.spec.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/widgets/tests/src/menubar.spec.ts b/packages/widgets/tests/src/menubar.spec.ts index ea16c8622..7a01f6774 100644 --- a/packages/widgets/tests/src/menubar.spec.ts +++ b/packages/widgets/tests/src/menubar.spec.ts @@ -902,7 +902,7 @@ describe('@lumino/widgets', () => { bar.dispose(); }); - it('should render the overflow menu', () => { + it('should render and hide the overflow menu', () => { let bar = createMenuBar(); expect(bar.overflowIndex).to.equal(-1); expect(bar.overflowMenu).to.equal(null); @@ -910,6 +910,13 @@ describe('@lumino/widgets', () => { MessageLoop.sendMessage(bar, Widget.Msg.UpdateRequest); requestAnimationFrame(() => { expect(bar.overflowMenu).to.not.equal(null); + expect(bar.overflowIndex).to.not.equal(-1); + }); + bar.node.style.maxWidth = '400px'; + MessageLoop.sendMessage(bar, Widget.Msg.UpdateRequest); + requestAnimationFrame(() => { + expect(bar.overflowMenu).to.equal(null); + expect(bar.overflowIndex).to.equal(-1); bar.dispose(); }); }); From 76b31255243d2486a6d6a5b01b8aaf2af63cf801 Mon Sep 17 00:00:00 2001 From: "Afshin T. Darian" Date: Sat, 18 Feb 2023 03:36:59 +0000 Subject: [PATCH 16/18] Update API documentation --- review/api/widgets.api.md | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/review/api/widgets.api.md b/review/api/widgets.api.md index 43dc5fa81..faa6b85b8 100644 --- a/review/api/widgets.api.md +++ b/review/api/widgets.api.md @@ -582,6 +582,12 @@ export namespace GridLayout { export function setCellConfig(widget: Widget, value: Partial): void; } +// @public +export interface IOverflowMenuOptions { + overflowMenuVisible: boolean; + title: string; +} + // @public export abstract class Layout implements Iterable, IDisposable { abstract [Symbol.iterator](): IterableIterator; @@ -751,21 +757,24 @@ export class MenuBar extends Widget { set activeIndex(value: number); get activeMenu(): Menu | null; set activeMenu(value: Menu | null); - addMenu(menu: Menu): void; + addMenu(menu: Menu, update?: boolean): void; get childMenu(): Menu | null; clearMenus(): void; get contentNode(): HTMLUListElement; dispose(): void; handleEvent(event: Event): void; - insertMenu(index: number, menu: Menu): void; + insertMenu(index: number, menu: Menu, update?: boolean): void; get menus(): ReadonlyArray; protected onActivateRequest(msg: Message): void; protected onAfterDetach(msg: Message): void; protected onBeforeAttach(msg: Message): void; + protected onResize(msg: Widget.ResizeMessage): void; protected onUpdateRequest(msg: Message): void; openActiveMenu(): void; - removeMenu(menu: Menu): void; - removeMenuAt(index: number): void; + get overflowIndex(): number; + get overflowMenu(): Menu | null; + removeMenu(menu: Menu, update?: boolean): void; + removeMenuAt(index: number, update?: boolean): void; readonly renderer: MenuBar.IRenderer; } @@ -773,6 +782,7 @@ export class MenuBar extends Widget { export namespace MenuBar { export interface IOptions { forceItemsPosition?: Menu.IOpenOptions; + overflowMenuOptions?: IOverflowMenuOptions; renderer?: IRenderer; } export interface IRenderData { From f1f3879b1ccbfdf9c910fc9ab4dccabeafdb26fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Collonval?= Date: Wed, 22 Feb 2023 15:27:27 +0100 Subject: [PATCH 17/18] Update packages/widgets/src/menubar.ts --- packages/widgets/src/menubar.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/widgets/src/menubar.ts b/packages/widgets/src/menubar.ts index 7359b3bfd..5a45743cf 100644 --- a/packages/widgets/src/menubar.ts +++ b/packages/widgets/src/menubar.ts @@ -418,6 +418,7 @@ export class MenuBar extends Widget { */ protected onResize(msg: Widget.ResizeMessage): void { this.update(); + super.onResize(msg); } /** From 705a85507d9bf2acffaf55c2755d1d63d9c6a87e Mon Sep 17 00:00:00 2001 From: Stephannie Jimenez Date: Wed, 22 Feb 2023 12:04:49 -0500 Subject: [PATCH 18/18] Break test into two to avoid having two animation frames --- packages/widgets/tests/src/menubar.spec.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/widgets/tests/src/menubar.spec.ts b/packages/widgets/tests/src/menubar.spec.ts index 7a01f6774..69fbe56dc 100644 --- a/packages/widgets/tests/src/menubar.spec.ts +++ b/packages/widgets/tests/src/menubar.spec.ts @@ -902,7 +902,7 @@ describe('@lumino/widgets', () => { bar.dispose(); }); - it('should render and hide the overflow menu', () => { + it('should render the overflow menu', () => { let bar = createMenuBar(); expect(bar.overflowIndex).to.equal(-1); expect(bar.overflowMenu).to.equal(null); @@ -911,7 +911,16 @@ describe('@lumino/widgets', () => { requestAnimationFrame(() => { expect(bar.overflowMenu).to.not.equal(null); expect(bar.overflowIndex).to.not.equal(-1); + bar.dispose(); }); + }); + + it('should hide the overflow menu', () => { + let bar = createMenuBar(); + expect(bar.overflowIndex).to.equal(-1); + expect(bar.overflowMenu).to.equal(null); + bar.node.style.maxWidth = '70px'; + MessageLoop.sendMessage(bar, Widget.Msg.UpdateRequest); bar.node.style.maxWidth = '400px'; MessageLoop.sendMessage(bar, Widget.Msg.UpdateRequest); requestAnimationFrame(() => {