Skip to content

Commit 182db07

Browse files
committed
activitybar treated as unit for kb nav
1 parent 124cb3e commit 182db07

5 files changed

Lines changed: 128 additions & 16 deletions

File tree

src/vs/base/browser/ui/actionbar/actionbar.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,7 @@ export interface IActionBarOptions {
406406
animated?: boolean;
407407
triggerKeys?: ActionTrigger;
408408
allowContextMenu?: boolean;
409+
preventLoopNavigation?: boolean;
409410
}
410411

411412
const defaultOptions: IActionBarOptions = {
@@ -509,9 +510,9 @@ export class ActionBar extends Disposable implements IActionRunner {
509510
let eventHandled = true;
510511

511512
if (event.equals(previousKey)) {
512-
this.focusPrevious();
513+
eventHandled = this.focusPrevious();
513514
} else if (event.equals(nextKey)) {
514-
this.focusNext();
515+
eventHandled = this.focusNext();
515516
} else if (event.equals(KeyCode.Escape)) {
516517
this._onDidCancel.fire();
517518
} else if (this.isTriggerKeyEvent(event)) {
@@ -724,7 +725,7 @@ export class ActionBar extends Disposable implements IActionRunner {
724725

725726
if (selectFirst && typeof this.focusedItem === 'undefined') {
726727
// Focus the first enabled item
727-
this.focusedItem = this.viewItems.length - 1;
728+
this.focusedItem = -1;
728729
this.focusNext();
729730
} else {
730731
if (index !== undefined) {
@@ -735,7 +736,7 @@ export class ActionBar extends Disposable implements IActionRunner {
735736
}
736737
}
737738

738-
protected focusNext(): void {
739+
protected focusNext(): boolean {
739740
if (typeof this.focusedItem === 'undefined') {
740741
this.focusedItem = this.viewItems.length - 1;
741742
}
@@ -744,6 +745,11 @@ export class ActionBar extends Disposable implements IActionRunner {
744745
let item: IActionViewItem;
745746

746747
do {
748+
if (this.options.preventLoopNavigation && this.focusedItem + 1 >= this.viewItems.length) {
749+
this.focusedItem = startIndex;
750+
return false;
751+
}
752+
747753
this.focusedItem = (this.focusedItem + 1) % this.viewItems.length;
748754
item = this.viewItems[this.focusedItem];
749755
} while (this.focusedItem !== startIndex && !item.isEnabled());
@@ -753,9 +759,10 @@ export class ActionBar extends Disposable implements IActionRunner {
753759
}
754760

755761
this.updateFocus();
762+
return true;
756763
}
757764

758-
protected focusPrevious(): void {
765+
protected focusPrevious(): boolean {
759766
if (typeof this.focusedItem === 'undefined') {
760767
this.focusedItem = 0;
761768
}
@@ -767,6 +774,11 @@ export class ActionBar extends Disposable implements IActionRunner {
767774
this.focusedItem = this.focusedItem - 1;
768775

769776
if (this.focusedItem < 0) {
777+
if (this.options.preventLoopNavigation) {
778+
this.focusedItem = startIndex;
779+
return false;
780+
}
781+
770782
this.focusedItem = this.viewItems.length - 1;
771783
}
772784

@@ -778,6 +790,7 @@ export class ActionBar extends Disposable implements IActionRunner {
778790
}
779791

780792
this.updateFocus(true);
793+
return true;
781794
}
782795

783796
protected updateFocus(fromRight?: boolean, preventScroll?: boolean): void {

src/vs/base/browser/ui/menu/menubar.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -326,7 +326,15 @@ export class MenuBar extends Disposable {
326326
let event = new StandardKeyboardEvent(e as KeyboardEvent);
327327
let eventHandled = true;
328328

329-
if ((event.equals(KeyCode.DownArrow) || event.equals(KeyCode.Enter) || (this.options.compactMode !== undefined && event.equals(KeyCode.Space))) && !this.isOpen) {
329+
const triggerKeys = [KeyCode.Enter];
330+
if (this.options.compactMode === undefined) {
331+
triggerKeys.push(KeyCode.DownArrow);
332+
} else {
333+
triggerKeys.push(KeyCode.Space);
334+
triggerKeys.push(this.options.compactMode === Direction.Right ? KeyCode.RightArrow : KeyCode.LeftArrow);
335+
}
336+
337+
if ((triggerKeys.some(k => event.equals(k)) && !this.isOpen)) {
330338
this.focusedMenu = { index: MenuBar.OVERFLOW_INDEX };
331339
this.openedViaKeyboard = true;
332340
this.focusState = MenubarState.OPEN;

src/vs/workbench/browser/parts/activitybar/activitybarPart.ts

Lines changed: 91 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { IThemeService, IColorTheme } from 'vs/platform/theme/common/themeServic
1818
import { ACTIVITY_BAR_BACKGROUND, ACTIVITY_BAR_BORDER, ACTIVITY_BAR_FOREGROUND, ACTIVITY_BAR_ACTIVE_BORDER, ACTIVITY_BAR_BADGE_BACKGROUND, ACTIVITY_BAR_BADGE_FOREGROUND, ACTIVITY_BAR_INACTIVE_FOREGROUND, ACTIVITY_BAR_ACTIVE_BACKGROUND, ACTIVITY_BAR_DRAG_AND_DROP_BORDER } from 'vs/workbench/common/theme';
1919
import { contrastBorder } from 'vs/platform/theme/common/colorRegistry';
2020
import { CompositeBar, ICompositeBarItem, CompositeDragAndDrop } from 'vs/workbench/browser/parts/compositeBar';
21-
import { Dimension, addClass, removeNode, createCSSRule, asCSSUrl, toggleClass } from 'vs/base/browser/dom';
21+
import { Dimension, addClass, removeNode, createCSSRule, asCSSUrl, toggleClass, addDisposableListener, EventType } from 'vs/base/browser/dom';
2222
import { IStorageService, StorageScope, IWorkspaceStorageChangeEvent } from 'vs/platform/storage/common/storage';
2323
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
2424
import { URI, UriComponents } from 'vs/base/common/uri';
@@ -39,6 +39,8 @@ import { Before2D } from 'vs/workbench/browser/dnd';
3939
import { Codicon, iconRegistry } from 'vs/base/common/codicons';
4040
import { Action } from 'vs/base/common/actions';
4141
import { Event } from 'vs/base/common/event';
42+
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
43+
import { KeyCode } from 'vs/base/common/keyCodes';
4244

4345
interface IPlaceholderViewContainer {
4446
id: string;
@@ -92,16 +94,20 @@ export class ActivitybarPart extends Part implements IActivityBarService {
9294
private menuBarContainer: HTMLElement | undefined;
9395

9496
private compositeBar: CompositeBar;
97+
private compositeBarContainer: HTMLElement | undefined;
9598

9699
private globalActivityAction: ActivityAction | undefined;
97100
private globalActivityActionBar: ActionBar | undefined;
98101
private readonly globalActivity: ICompositeActivity[] = [];
102+
private globalActivitiesContainer: HTMLElement | undefined;
99103

100104
private accountsActivityAction: ActivityAction | undefined;
101105

102106
private readonly compositeActions = new Map<string, { activityAction: ViewContainerActivityAction, pinnedAction: ToggleCompositePinnedAction }>();
103107
private readonly viewContainerDisposables = new Map<string, IDisposable>();
104108

109+
private readonly keyboardNavigationDisposables = new DisposableStore();
110+
105111
private readonly location = ViewContainerLocation.Sidebar;
106112

107113
constructor(
@@ -136,6 +142,7 @@ export class ActivitybarPart extends Part implements IActivityBarService {
136142
this.compositeBar = this._register(this.instantiationService.createInstance(CompositeBar, cachedItems, {
137143
icon: true,
138144
orientation: ActionsOrientation.VERTICAL,
145+
preventLoopNavigation: true,
139146
openComposite: (compositeId: string) => this.viewsService.openViewContainer(compositeId, true),
140147
getActivityAction: (compositeId: string) => this.getCompositeActions(compositeId).activityAction,
141148
getCompositePinnedAction: (compositeId: string) => this.getCompositeActions(compositeId).pinnedAction,
@@ -375,10 +382,13 @@ export class ActivitybarPart extends Part implements IActivityBarService {
375382
private uninstallMenubar() {
376383
if (this.menuBar) {
377384
this.menuBar.dispose();
385+
this.menuBar = undefined;
378386
}
379387

380388
if (this.menuBarContainer) {
381389
removeNode(this.menuBarContainer);
390+
this.menuBarContainer = undefined;
391+
this.registerKeyboardNavigationListeners();
382392
}
383393
}
384394

@@ -392,6 +402,8 @@ export class ActivitybarPart extends Part implements IActivityBarService {
392402
// Menubar: install a custom menu bar depending on configuration
393403
this.menuBar = this._register(this.instantiationService.createInstance(CustomMenubarControl));
394404
this.menuBar.create(this.menuBarContainer);
405+
406+
this.registerKeyboardNavigationListeners();
395407
}
396408

397409
createContentArea(parent: HTMLElement): HTMLElement {
@@ -420,18 +432,87 @@ export class ActivitybarPart extends Part implements IActivityBarService {
420432
}
421433

422434
// View Containers action bar
423-
this.compositeBar.create(this.content);
435+
this.compositeBarContainer = this.compositeBar.create(this.content);
424436

425437
// Global action bar
426-
const globalActivities = document.createElement('div');
427-
addClass(globalActivities, 'global-activity');
428-
this.content.appendChild(globalActivities);
438+
this.globalActivitiesContainer = document.createElement('div');
439+
addClass(this.globalActivitiesContainer, 'global-activity');
440+
this.content.appendChild(this.globalActivitiesContainer);
429441

430-
this.createGlobalActivityActionBar(globalActivities);
442+
this.createGlobalActivityActionBar(this.globalActivitiesContainer);
443+
444+
this.registerKeyboardNavigationListeners();
431445

432446
return this.content;
433447
}
434448

449+
private registerKeyboardNavigationListeners(): void {
450+
this.keyboardNavigationDisposables.clear();
451+
452+
// Down arrow on home indicator
453+
if (this.homeBarContainer) {
454+
this.keyboardNavigationDisposables.add(addDisposableListener(this.homeBarContainer, EventType.KEY_DOWN, e => {
455+
const kbEvent = new StandardKeyboardEvent(e);
456+
if (kbEvent.equals(KeyCode.DownArrow)) {
457+
if (this.menuBar) {
458+
this.menuBar.toggleFocus();
459+
} else if (this.compositeBar) {
460+
this.compositeBar.focus();
461+
}
462+
}
463+
}));
464+
}
465+
466+
// Up/Down arrow on compact menu
467+
if (this.menuBarContainer) {
468+
this.keyboardNavigationDisposables.add(addDisposableListener(this.menuBarContainer, EventType.KEY_DOWN, e => {
469+
const kbEvent = new StandardKeyboardEvent(e);
470+
if (kbEvent.equals(KeyCode.DownArrow)) {
471+
if (this.compositeBar) {
472+
this.compositeBar.focus();
473+
}
474+
} else if (kbEvent.equals(KeyCode.UpArrow)) {
475+
if (this.homeBar) {
476+
this.homeBar.focus();
477+
}
478+
}
479+
}));
480+
}
481+
482+
// Up/Down on Activity Icons
483+
if (this.compositeBarContainer) {
484+
this.keyboardNavigationDisposables.add(addDisposableListener(this.compositeBarContainer, EventType.KEY_DOWN, e => {
485+
const kbEvent = new StandardKeyboardEvent(e);
486+
if (kbEvent.equals(KeyCode.DownArrow)) {
487+
if (this.globalActivityActionBar) {
488+
this.globalActivityActionBar.focus(true);
489+
}
490+
} else if (kbEvent.equals(KeyCode.UpArrow)) {
491+
if (this.menuBar) {
492+
this.menuBar.toggleFocus();
493+
} else if (this.homeBar) {
494+
this.homeBar.focus();
495+
}
496+
}
497+
}));
498+
}
499+
500+
// Up arrow on global icons
501+
if (this.globalActivitiesContainer) {
502+
this.keyboardNavigationDisposables.add(addDisposableListener(this.globalActivitiesContainer, EventType.KEY_DOWN, e => {
503+
const kbEvent = new StandardKeyboardEvent(e);
504+
if (kbEvent.equals(KeyCode.UpArrow)) {
505+
if (this.compositeBar) {
506+
this.compositeBar.focus(this.getVisibleViewContainerIds().length - 1);
507+
}
508+
}
509+
}));
510+
}
511+
512+
513+
514+
}
515+
435516
private createHomeBar(href: string, title: string, icon: Codicon): void {
436517
this.homeBarContainer = document.createElement('div');
437518
this.homeBarContainer.setAttribute('aria-label', nls.localize('homeIndicator', "Home"));
@@ -443,7 +524,8 @@ export class ActivitybarPart extends Part implements IActivityBarService {
443524
animated: false,
444525
ariaLabel: nls.localize('home', "Home"),
445526
actionViewItemProvider: action => new HomeActionViewItem(action),
446-
allowContextMenu: true
527+
allowContextMenu: true,
528+
preventLoopNavigation: true
447529
}));
448530

449531
const homeBarIconBadge = document.createElement('div');
@@ -495,7 +577,8 @@ export class ActivitybarPart extends Part implements IActivityBarService {
495577
},
496578
orientation: ActionsOrientation.VERTICAL,
497579
ariaLabel: nls.localize('manage', "Manage"),
498-
animated: false
580+
animated: false,
581+
preventLoopNavigation: true
499582
}));
500583

501584
this.globalActivityAction = new ActivityAction({

src/vs/workbench/browser/parts/compositeBar.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ export interface ICompositeBarOptions {
146146
readonly compositeSize: number;
147147
readonly overflowActionSize: number;
148148
readonly dndHandler: ICompositeDragAndDrop;
149+
readonly preventLoopNavigation?: boolean;
149150

150151
getActivityAction: (compositeId: string) => ActivityAction;
151152
getCompositePinnedAction: (compositeId: string) => Action;
@@ -225,6 +226,7 @@ export class CompositeBar extends Widget implements ICompositeBar {
225226
orientation: this.options.orientation,
226227
ariaLabel: nls.localize('activityBarAriaLabel', "Active View Switcher"),
227228
animated: false,
229+
preventLoopNavigation: this.options.preventLoopNavigation
228230
}));
229231

230232
// Contextmenu for composites
@@ -293,9 +295,9 @@ export class CompositeBar extends Widget implements ICompositeBar {
293295
return { verticallyBefore: front, horizontallyBefore: front };
294296
}
295297

296-
focus(): void {
298+
focus(index?: number): void {
297299
if (this.compositeSwitcherBar) {
298-
this.compositeSwitcherBar.focus();
300+
this.compositeSwitcherBar.focus(index);
299301
}
300302
}
301303

src/vs/workbench/browser/parts/titlebar/menubarControl.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -734,4 +734,10 @@ export class CustomMenubarControl extends MenubarControl {
734734

735735
this.menubar?.update(this.getMenuBarOptions());
736736
}
737+
738+
toggleFocus() {
739+
if (this.menubar) {
740+
this.menubar.toggleFocus();
741+
}
742+
}
737743
}

0 commit comments

Comments
 (0)