Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
gabalafou committed Jul 23, 2023
1 parent 07d4bab commit 8b797a2
Show file tree
Hide file tree
Showing 3 changed files with 64 additions and 38 deletions.
12 changes: 11 additions & 1 deletion packages/default-theme/style/menubar.css
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
border-right: 1px solid transparent;
}

.lm-MenuBar-item.lm-mod-active {
.lm-MenuBar-item.lm-mod-active,
.lm-MenuBar-item:hover {
background: #e5e5e5;
}

Expand All @@ -41,3 +42,12 @@
border-right: 1px solid #c0c0c0;
box-shadow: 0px 0px 6px rgba(0, 0, 0, 0.2);
}

.lm-MenuBar-item:focus {
text-decoration: underline;
}

.lm-MenuBar-item:focus-visible {
outline: 2px solid #333;
outline-offset: -2px;
}
70 changes: 42 additions & 28 deletions packages/widgets/src/menubar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,19 +153,6 @@ export class MenuBar extends Widget {
// Update the active index.
this._activeIndex = value;

// Update the focus index.
if (value !== -1) {
this._tabFocusIndex = value;
}

// Update focus to new active index
if (
this._activeIndex >= 0 &&
this.contentNode.childNodes[this._activeIndex]
) {
(this.contentNode.childNodes[this._activeIndex] as HTMLElement).focus();
}

// Schedule an update of the items.
this.update();
}
Expand Down Expand Up @@ -370,8 +357,8 @@ export class MenuBar extends Widget {
case 'mousemove':
this._evtMouseMove(event as MouseEvent);
break;
case 'mouseleave':
this._evtMouseLeave(event as MouseEvent);
case 'focusout':
this._evtFocusOut(event as FocusEvent);
break;
case 'contextmenu':
event.preventDefault();
Expand All @@ -387,7 +374,7 @@ export class MenuBar extends Widget {
this.node.addEventListener('keydown', this);
this.node.addEventListener('mousedown', this);
this.node.addEventListener('mousemove', this);
this.node.addEventListener('mouseleave', this);
this.node.addEventListener('focusout', this);
this.node.addEventListener('contextmenu', this);
}

Expand All @@ -398,7 +385,7 @@ export class MenuBar extends Widget {
this.node.removeEventListener('keydown', this);
this.node.removeEventListener('mousedown', this);
this.node.removeEventListener('mousemove', this);
this.node.removeEventListener('mouseleave', this);
this.node.removeEventListener('focusout', this);
this.node.removeEventListener('contextmenu', this);
this._closeChildMenu();
}
Expand All @@ -408,7 +395,7 @@ export class MenuBar extends Widget {
*/
protected onActivateRequest(msg: Message): void {
if (this.isAttached) {
this.activeIndex = 0;
this._focusItemAt(0);
}
}

Expand Down Expand Up @@ -446,6 +433,7 @@ export class MenuBar extends Widget {
active: i === activeIndex && menus[i].items.length !== 0,
tabbable: i === tabFocusIndex,
onfocus: () => {
this._tabFocusIndex = i;
this.activeIndex = i;
}
});
Expand Down Expand Up @@ -483,6 +471,7 @@ export class MenuBar extends Widget {
active: length === activeIndex && menus[length].items.length !== 0,
tabbable: length === tabFocusIndex,
onfocus: () => {
this._tabFocusIndex = length;
this.activeIndex = length;
}
});
Expand All @@ -503,6 +492,7 @@ export class MenuBar extends Widget {
active: false,
tabbable: length === tabFocusIndex,
onfocus: () => {
this._tabFocusIndex = length;
this.activeIndex = length;
}
});
Expand Down Expand Up @@ -582,31 +572,33 @@ export class MenuBar extends Widget {

// Enter, Space, Up Arrow, Down Arrow
if (kc === 13 || kc === 32 || kc === 38 || kc === 40) {
// The active index may have changed (for example, user hovers over an
// item with the mouse), so be sure to use the focus index.
this.activeIndex = this._tabFocusIndex;
this.openActiveMenu();
return;
}

// Escape
if (kc === 27) {
this._closeChildMenu();
this.activeIndex = -1;
this.node.blur();
this._focusItemAt(this.activeIndex);
return;
}

// Left Arrow
if (kc === 37) {
let i = this._activeIndex;
let n = this._menus.length;
this.activeIndex = i === 0 ? n - 1 : i - 1;
this._focusItemAt(i === 0 ? n - 1 : i - 1);
return;
}

// Right Arrow
if (kc === 39) {
let i = this._activeIndex;
let n = this._menus.length;
this.activeIndex = i === n - 1 ? 0 : i + 1;
this._focusItemAt(i === n - 1 ? 0 : i + 1);
return;
}

Expand All @@ -631,8 +623,10 @@ export class MenuBar extends Widget {
this.openActiveMenu();
} else if (result.index !== -1) {
this.activeIndex = result.index;
this._focusItemAt(this.activeIndex);
} else if (result.auto !== -1) {
this.activeIndex = result.auto;
this._focusItemAt(this.activeIndex);
}
}

Expand All @@ -648,7 +642,6 @@ export class MenuBar extends Widget {

// Stop the propagation of the event. Immediate propagation is
// also stopped so that an open menu does not handle the event.
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();

Expand All @@ -673,6 +666,9 @@ export class MenuBar extends Widget {
this._closeChildMenu();
this.activeIndex = index;
} else {
// If we don't call preventDefault() here, then the item in the menu
// bar will take focus over the menu that is being opened.
// event.preventDefault();
const position = this._positionForMenu(index);
Menu.saveWindowData();
// Begin DOM modifications.
Expand Down Expand Up @@ -737,15 +733,28 @@ export class MenuBar extends Widget {
}

/**
* Handle the `'mouseleave'` event for the menu bar.
* Handle the `'focusout'` event for the menu bar.
*/
private _evtMouseLeave(event: MouseEvent): void {
// Reset the active index if there is no open menu.
if (!this._childMenu) {
private _evtFocusOut(event: FocusEvent): void {
// Reset the active index if there is no open menu and the menubar is losing focus.
if (!this._childMenu && !this.node.contains(event.relatedTarget as Node)) {
this.activeIndex = -1;
}
}

/**
* Focus an item in the menu bar.
*
* #### Notes
* Does not open the associated menu.
*/
private _focusItemAt(index: number): void {
const itemNode = this.contentNode.childNodes[index] as HTMLElement | void;
if (itemNode) {
itemNode.focus();
}
}

/**
* Open the child menu at the active index immediately.
*
Expand Down Expand Up @@ -776,6 +785,11 @@ export class MenuBar extends Widget {
document.addEventListener('mousedown', this, true);
}

// Update the tab focus index. Note: it's important to follow this with an
// update message so that the tabIndex value on each menubar item gets
// properly updated.
this._tabFocusIndex = this.activeIndex;

// Ensure the menu bar is updated and look up the item node.
MessageLoop.sendMessage(this, Widget.Msg.UpdateRequest);

Expand Down Expand Up @@ -880,7 +894,7 @@ export class MenuBar extends Widget {
this.update();
}

// Track the index of the item that is currently focused. -1 means nothing focused.
// Track the index of the item that is currently focused or hovered. -1 means nothing focused or hovered.
private _activeIndex = -1;
// Track which item can be focused using the TAB key. Unlike _activeIndex will always point to a menuitem.
private _tabFocusIndex = 0;
Expand Down
20 changes: 11 additions & 9 deletions packages/widgets/tests/src/menubar.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -768,16 +768,16 @@ describe('@lumino/widgets', () => {
});
});

context('mouseleave', () => {
context('focusout', () => {
it('should reset the active index if there is no open menu', () => {
bar.node.dispatchEvent(new MouseEvent('mouseleave', { bubbles }));
bar.node.dispatchEvent(new FocusEvent('focusout', { bubbles }));
expect(bar.activeIndex).to.equal(-1);
});

it('should be a no-op if there is an open menu', () => {
bar.openActiveMenu();
let menu = bar.activeMenu!;
bar.node.dispatchEvent(new MouseEvent('mouseleave', { bubbles }));
bar.node.dispatchEvent(new FocusEvent('focusous', { bubbles }));
expect(bar.activeIndex).to.equal(0);
expect(menu.isAttached).to.equal(true);
});
Expand All @@ -794,7 +794,8 @@ describe('@lumino/widgets', () => {
context('focus', () => {
it('should lose focus on tab key', () => {
let bar = createUnfocusedMenuBar();
bar.activeIndex = 0;
bar.activate();
MessageLoop.flush();
expect(bar.contentNode.contains(document.activeElement)).to.equal(
true
);
Expand All @@ -806,7 +807,8 @@ describe('@lumino/widgets', () => {

it('should lose focus on shift-tab key', () => {
let bar = createUnfocusedMenuBar();
bar.activeIndex = 0;
bar.activate();
MessageLoop.flush();
expect(bar.contentNode.contains(document.activeElement)).to.equal(
true
);
Expand Down Expand Up @@ -834,8 +836,8 @@ describe('@lumino/widgets', () => {
expect(bar.events.indexOf('mousedown')).to.not.equal(-1);
node.dispatchEvent(new MouseEvent('mousemove', { bubbles }));
expect(bar.events.indexOf('mousemove')).to.not.equal(-1);
node.dispatchEvent(new MouseEvent('mouseleave', { bubbles }));
expect(bar.events.indexOf('mouseleave')).to.not.equal(-1);
node.dispatchEvent(new FocusEvent('focusout', { bubbles }));
expect(bar.events.indexOf('focusout')).to.not.equal(-1);
node.dispatchEvent(new MouseEvent('contextmenu', { bubbles }));
expect(bar.events.indexOf('contextmenu')).to.not.equal(-1);
bar.dispose();
Expand All @@ -855,8 +857,8 @@ describe('@lumino/widgets', () => {
expect(bar.events.indexOf('mousedown')).to.equal(-1);
node.dispatchEvent(new MouseEvent('mousemove', { bubbles }));
expect(bar.events.indexOf('mousemove')).to.equal(-1);
node.dispatchEvent(new MouseEvent('mouseleave', { bubbles }));
expect(bar.events.indexOf('mouseleave')).to.equal(-1);
node.dispatchEvent(new FocusEvent('focusout', { bubbles }));
expect(bar.events.indexOf('focusout')).to.equal(-1);
node.dispatchEvent(new MouseEvent('contextmenu', { bubbles }));
expect(bar.events.indexOf('contextmenu')).to.equal(-1);
bar.dispose();
Expand Down

0 comments on commit 8b797a2

Please sign in to comment.