Dashboard toolbar overflow (#9796)

* initial changes for actionbar collapsing

* fix more not always all showing after resizing

* collapse toolbar if window size is already small when dashboard is opened

* make wrapping default behavior and collapse opt in

* fix so keyboard navigation works in overflow

* more keyboard fixing so that the actions in overflow get triggered

* change overflow background with theme

* change margin

* udpate more button

* use icon for ...

* addressing comments

* overflow css changes to match portal

* arrow navigation working

* handle tab and shift tab in overflow

* keep arrow navigation within overflow

* move reused code to helper methods

* set roles for overflow

* use actionsList instead of document.getElementById all the time

* move collapsible action bar to its own class

* renamve to overflowActionBar

* fix focus rectangle around more element

* hide overflow after an action is executed

* hide overflow when clicking an action

* hide overflow when focus leaves and loop focus within overflow when using arrow keys

* fix double down arrow to move focus in overflow

* update comment

* fix clicking more not hiding overflow

* add box-shadow for themes

* fix hygiene error

* fix hygiene error

* widen focused outline for overflow actions
This commit is contained in:
Kim Santiago
2020-04-09 16:31:52 -07:00
committed by GitHub
parent 433049d1b2
commit 8ff53281f9
8 changed files with 474 additions and 26 deletions

View File

@@ -27,18 +27,18 @@ const defaultOptions: IActionBarOptions = {
*/
export class ActionBar extends ActionRunner implements IActionRunner {
private _options: IActionBarOptions;
private _actionRunner: IActionRunner;
private _context: any;
protected _options: IActionBarOptions;
protected _actionRunner: IActionRunner;
protected _context: any;
// Items
private _items: IActionViewItem[];
private _focusedItem?: number;
private _focusTracker: DOM.IFocusTracker;
protected _items: IActionViewItem[];
protected _focusedItem?: number;
protected _focusTracker: DOM.IFocusTracker;
// Elements
private _domNode: HTMLElement;
private _actionsList: HTMLElement;
protected _domNode: HTMLElement;
protected _actionsList: HTMLElement;
constructor(container: HTMLElement, options: IActionBarOptions = defaultOptions) {
super();
@@ -128,6 +128,7 @@ export class ActionBar extends ActionRunner implements IActionRunner {
this._actionsList = document.createElement('ul');
this._actionsList.className = 'actions-container';
this._actionsList.setAttribute('role', 'toolbar');
this._actionsList.id = 'actions-container';
if (this._options.ariaLabel) {
this._actionsList.setAttribute('aria-label', this._options.ariaLabel);
}
@@ -145,7 +146,7 @@ export class ActionBar extends ActionRunner implements IActionRunner {
}
}
private updateFocusedItem(): void {
protected updateFocusedItem(): void {
let actionIndex = 0;
for (let i = 0; i < this._actionsList.children.length; i++) {
let elem = this._actionsList.children[i];
@@ -155,7 +156,7 @@ export class ActionBar extends ActionRunner implements IActionRunner {
break;
}
if (elem.classList.contains('action-item')) {
if (elem.classList.contains('action-item') && i !== this._actionsList.children.length - 1) {
actionIndex++;
}
}
@@ -268,7 +269,7 @@ export class ActionBar extends ActionRunner implements IActionRunner {
this.updateFocus();
}
private focusNext(): void {
protected focusNext(): void {
if (typeof this._focusedItem === 'undefined') {
this._focusedItem = this._items.length - 1;
}
@@ -288,7 +289,7 @@ export class ActionBar extends ActionRunner implements IActionRunner {
this.updateFocus();
}
private focusPrevious(): void {
protected focusPrevious(): void {
if (typeof this._focusedItem === 'undefined') {
this._focusedItem = 0;
}
@@ -313,7 +314,7 @@ export class ActionBar extends ActionRunner implements IActionRunner {
this.updateFocus();
}
private updateFocus(): void {
protected updateFocus(): void {
if (typeof this._focusedItem === 'undefined') {
this._domNode.focus();
return;
@@ -329,7 +330,7 @@ export class ActionBar extends ActionRunner implements IActionRunner {
actionItem.focus();
}
} else {
if (types.isFunction(actionItem.blur)) {
if (actionItem && types.isFunction(actionItem.blur)) {
actionItem.blur();
}
}
@@ -349,7 +350,7 @@ export class ActionBar extends ActionRunner implements IActionRunner {
}
}
private cancel(): void {
protected cancel(): void {
if (document.activeElement instanceof HTMLElement) {
(<HTMLElement>document.activeElement).blur(); // remove focus from focussed action
}

View File

@@ -47,6 +47,10 @@
margin-right: 5px;
}
.carbon-taskbar .action-item.more {
padding-right: 15px;
}
.carbon-taskbar .action-label {
background-repeat: no-repeat;
background-position: 0% 50%;
@@ -86,3 +90,52 @@
.carbon-taskbar .codicon {
background-size: 11px;
}
.carbon-taskbar .overflow {
position:absolute;
right:0;
display:none;
z-index: 3;
padding-left: 0;
margin-top: 6px;
margin-right: 5px;
}
.vs-dark .carbon-taskbar .overflow {
box-sizing: border-box;
}
.hc-black .carbon-taskbar .overflow {
box-shadow: none;
}
.carbon-taskbar .overflow li {
padding: 5px;
margin-right: 0;
}
.carbon-taskbar .overflow li.focused a.action-label {
outline: none;
}
.carbon-taskbar .overflow a {
padding-left: 20px;
}
.carbon-taskbar .actions-container .action-item .action-label.moreActionsElement {
height: 18px;
margin-top: 3px;
background-position: center;
padding-right: 20px;
}
.carbon-taskbar .overflow .action-item .action-label{
background-size: 16px;
background-position: left;
}
.overflow .taskbarSeparator {
height: 1px;
width: 100%;
margin-left: 0;
}

View File

@@ -0,0 +1,328 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'sql/base/browser/ui/taskbar/overflowActionbarStyles';
import { IAction } from 'vs/base/common/actions';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import {
IActionBarOptions, ActionsOrientation, IActionViewItem,
IActionOptions
} from 'vs/base/browser/ui/actionbar/actionbar';
import * as DOM from 'vs/base/browser/dom';
import * as types from 'vs/base/common/types';
import * as nls from 'vs/nls';
import { debounce } from 'vs/base/common/decorators';
import { ActionBar } from 'sql/base/browser/ui/taskbar/actionbar';
const defaultOptions: IActionBarOptions = {
orientation: ActionsOrientation.HORIZONTAL,
context: null
};
/**
* Extends Actionbar so that it overflows when the window is resized to be smaller than the actionbar instead of wrapping
*/
export class OverflowActionBar extends ActionBar {
// Elements
private _overflow: HTMLElement;
private _moreItemElement: HTMLElement;
private _moreActionsElement: HTMLElement;
constructor(container: HTMLElement, options: IActionBarOptions = defaultOptions) {
super(container, options);
this._register(DOM.addDisposableListener(window, DOM.EventType.RESIZE, e => {
if (this._actionsList) {
this.resizeToolbar();
}
}));
this._overflow = document.createElement('ul');
this._overflow.id = 'overflow';
this._overflow.className = 'overflow';
this._overflow.setAttribute('role', 'menu');
this._domNode.appendChild(this._overflow);
this._register(DOM.addDisposableListener(this._overflow, DOM.EventType.FOCUS_OUT, e => {
if (this._overflow && !DOM.isAncestor(e.relatedTarget as HTMLElement, this._overflow) && e.relatedTarget !== this._moreActionsElement) {
this.hideOverflowDisplay();
}
}));
this._actionsList.style.flexWrap = 'nowrap';
container.appendChild(this._domNode);
}
@debounce(300)
private resizeToolbar() {
let width = this._actionsList.offsetWidth;
let fullWidth = this._actionsList.scrollWidth;
// collapse actions that are beyond the width of the toolbar
if (width < fullWidth) {
// create '•••' more element if it doesn't exist yet
if (!this._moreItemElement) {
this.createMoreItemElement();
}
this._moreItemElement.style.display = 'block';
while (width < fullWidth) {
let index = this._actionsList.childNodes.length - 2; // remove the last toolbar action before the more actions '...'
if (index > -1) {
this.collapseItem();
fullWidth = this._actionsList.scrollWidth;
} else {
break;
}
}
} else if (this._overflow?.hasChildNodes()) { // uncollapse actions if there is space for it
while (width === fullWidth && this._overflow.hasChildNodes()) {
// move placeholder in this._items
let placeHolderItem = this._items.splice(this._actionsList.childNodes.length - 1, 1);
this._items.splice(this._actionsList.childNodes.length, 0, placeHolderItem[0]);
let item = this._overflow.removeChild(this._overflow.firstChild);
// change role back to button when it's in the toolbar
if ((<HTMLElement>item).className !== 'taskbarSeparator') {
(<HTMLElement>item.firstChild).setAttribute('role', 'button');
}
this._actionsList.insertBefore(item, this._actionsList.lastChild);
// if the action was too wide, collapse it again
if (this._actionsList.scrollWidth > this._actionsList.offsetWidth) {
// move placeholder in this._items
this.collapseItem();
break;
} else if (!this._overflow.hasChildNodes()) {
this._moreItemElement.style.display = 'none';
}
}
}
}
private collapseItem(): void {
// move placeholder in this._items
let placeHolderItem = this._items.splice(this._actionsList.childNodes.length - 1, 1);
this._items.splice(this._actionsList.childNodes.length - 2, 0, placeHolderItem[0]);
let index = this._actionsList.childNodes.length - 2; // remove the last toolbar action before the more actions '...'
let item = this._actionsList.removeChild(this._actionsList.childNodes[index]);
this._overflow.insertBefore(item, this._overflow.firstChild);
this._register(DOM.addDisposableListener(item, DOM.EventType.CLICK, (e => { this.hideOverflowDisplay(); })));
// change role to menuItem when it's in the overflow
if ((<HTMLElement>this._overflow.firstChild).className !== 'taskbarSeparator') {
(<HTMLElement>this._overflow.firstChild.firstChild).setAttribute('role', 'menuItem');
}
}
private createMoreItemElement(): void {
this._moreItemElement = document.createElement('li');
this._moreItemElement.className = 'action-item more';
this._moreItemElement.setAttribute('role', 'presentation');
this._moreActionsElement = document.createElement('a');
this._moreActionsElement.className = 'moreActionsElement action-label codicon toggle-more';
this._moreActionsElement.setAttribute('role', 'button');
this._moreActionsElement.title = nls.localize('toggleMore', "Toggle More");
this._moreActionsElement.tabIndex = 0;
this._moreActionsElement.setAttribute('aria-haspopup', 'true');
this._register(DOM.addDisposableListener(this._moreActionsElement, DOM.EventType.CLICK, (e => {
this.moreElementOnClick(e);
})));
this._register(DOM.addDisposableListener(this._moreActionsElement, DOM.EventType.KEY_UP, (ev => {
let event = new StandardKeyboardEvent(ev);
if (event.keyCode === KeyCode.Enter || event.keyCode === KeyCode.Space) {
this.moreElementOnClick(event);
}
})));
this._register(DOM.addDisposableListener(this._overflow, DOM.EventType.KEY_DOWN, (e: KeyboardEvent) => {
let event = new StandardKeyboardEvent(e);
// Close overflow if Escape is pressed
if (event.equals(KeyCode.Escape)) {
this.hideOverflowDisplay();
this._moreActionsElement.focus();
} else if (event.equals(KeyCode.UpArrow)) {
// up arrow on first element in overflow should move focus to the bottom of the overflow
if (this._focusedItem === this._actionsList.childElementCount) {
this._focusedItem = this._actionsList.childElementCount + this._overflow.childElementCount - 2;
this.updateFocus();
} else {
this.focusPrevious();
}
} else if (event.equals(KeyCode.DownArrow)) {
// down arrow on last element should move focus to the first element of the overflow
if (this._focusedItem === this._actionsList.childNodes.length + this._overflow.childNodes.length - 2) {
this._focusedItem = this._actionsList.childElementCount;
this.updateFocus();
} else {
this.focusNext();
}
} else if (event.equals(KeyMod.Shift | KeyCode.Tab)) {
this.hideOverflowDisplay();
this._focusedItem = this._actionsList.childElementCount - 1;
this.updateFocus();
} else if (event.equals(KeyCode.Tab)) {
this.hideOverflowDisplay();
}
DOM.EventHelper.stop(event, true);
}));
this._moreItemElement.appendChild(this._moreActionsElement);
this._actionsList.appendChild(this._moreItemElement);
this._items.push(undefined); // add place holder for more item element
}
private moreElementOnClick(event: MouseEvent | StandardKeyboardEvent): void {
this._overflow.style.display = this._overflow.style.display === 'block' ? 'none' : 'block';
if (this._overflow.style.display === 'block') {
this._focusedItem = this._actionsList.childElementCount;
this.updateFocus();
}
DOM.EventHelper.stop(event, true);
}
private hideOverflowDisplay(): void {
this._overflow.style.display = 'none';
this._focusedItem = this._actionsList.childElementCount - 1;
}
protected updateFocusedItem(): void {
let actionIndex = 0;
for (let i = 0; i < this._actionsList.children.length; i++) {
let elem = this._actionsList.children[i];
if (DOM.isAncestor(document.activeElement, elem)) {
this._focusedItem = actionIndex;
break;
}
if (elem.classList.contains('action-item') && i !== this._actionsList.children.length - 1) {
actionIndex++;
}
}
// move focus to overflow items if there are any
if (this._overflow) {
for (let i = 0; i < this._overflow.children.length; i++) {
let elem = this._overflow.children[i];
if (DOM.isAncestor(document.activeElement, elem)) {
this._focusedItem = actionIndex;
break;
}
if (elem.classList.contains('action-item')) {
actionIndex++;
}
}
}
}
/**
* Push an HTML Element onto the action bar UI in the position specified by options.
* Pushes to the last position if no options are provided.
*/
public pushElement(element: HTMLElement, options: IActionOptions = {}): void {
super.pushElement(element, options);
this.resizeToolbar();
}
/**
* Push an action onto the action bar UI in the position specified by options.
* Pushes to the last position if no options are provided.
*/
public pushAction(arg: IAction | IAction[], options: IActionOptions = {}): void {
super.pushAction(arg, options);
this.resizeToolbar();
}
protected focusNext(): void {
if (typeof this._focusedItem === 'undefined') {
this._focusedItem = this._items.length - 1;
}
let startIndex = this._focusedItem;
let item: IActionViewItem;
do {
this._focusedItem = (this._focusedItem + 1) % this._items.length;
item = this._items[this._focusedItem];
} while (this._focusedItem !== startIndex && item && !item.isEnabled());
if (this._focusedItem === startIndex && item && !item.isEnabled()) {
this._focusedItem = undefined;
}
this.updateFocus();
}
protected focusPrevious(): void {
if (typeof this._focusedItem === 'undefined') {
this._focusedItem = 0;
}
let startIndex = this._focusedItem;
let item: IActionViewItem;
do {
this._focusedItem = this._focusedItem - 1;
if (this._focusedItem < 0) {
this._focusedItem = this._items.length - 1;
}
item = this._items[this._focusedItem];
} while (this._focusedItem !== startIndex && item && !item.isEnabled());
if (this._focusedItem === startIndex && item && !item.isEnabled()) {
this._focusedItem = undefined;
}
this.updateFocus();
}
protected updateFocus(): void {
if (typeof this._focusedItem === 'undefined') {
this._domNode.focus();
return;
}
for (let i = 0; i < this._items.length; i++) {
let item = this._items[i];
let actionItem = <any>item;
if (i === this._focusedItem) {
// placeholder for location of moreActionsElement
if (!actionItem) {
this._moreActionsElement.focus();
}
else if (types.isFunction(actionItem.focus)) {
actionItem.focus();
}
} else {
if (actionItem && types.isFunction(actionItem.blur)) {
actionItem.blur();
}
}
}
}
protected cancel(): void {
super.cancel();
if (this._overflow) {
this.hideOverflowDisplay();
}
}
public run(action: IAction, context?: any): Promise<any> {
this.hideOverflowDisplay();
return this._actionRunner.run(action, context);
}
}

View File

@@ -0,0 +1,43 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// eslint-disable-next-line code-import-patterns
import { registerThemingParticipant, IColorTheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService';
// eslint-disable-next-line code-import-patterns
import { EDITOR_PANE_BACKGROUND, DASHBOARD_BORDER, TOOLBAR_OVERFLOW_SHADOW } from 'vs/workbench/common/theme';
// eslint-disable-next-line code-import-patterns
import { focusBorder } from 'vs/platform/theme/common/colorRegistry';
registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => {
const overflowBackground = theme.getColor(EDITOR_PANE_BACKGROUND);
if (overflowBackground) {
collector.addRule(`.carbon-taskbar .overflow {
background-color: ${overflowBackground};
}`);
}
const overflowShadow = theme.getColor(TOOLBAR_OVERFLOW_SHADOW);
if (overflowShadow) {
collector.addRule(`.carbon-taskbar .overflow {
box-shadow: 0px 4px 4px ${overflowShadow};
}`);
}
const border = theme.getColor(DASHBOARD_BORDER);
if (border) {
collector.addRule(`.carbon-taskbar .overflow {
border: 1px solid ${border};
}`);
}
const activeOutline = theme.getColor(focusBorder);
if (activeOutline) {
collector.addRule(`.carbon-taskbar .overflow li.focused {
outline: 1px solid;
outline-offset: -3px;
outline-color: ${activeOutline}
}`);
}
});

View File

@@ -11,6 +11,7 @@ import { ActionBar } from './actionbar';
import { IActionRunner, IAction } from 'vs/base/common/actions';
import { ActionsOrientation, IActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar';
import { IToolBarOptions } from 'vs/base/browser/ui/toolbar/toolbar';
import { OverflowActionBar } from 'sql/base/browser/ui/taskbar/overflowActionbar';
/**
* A wrapper for the different types of content a QueryTaskbar can display
@@ -33,20 +34,36 @@ export class Taskbar {
private options: IToolBarOptions;
private actionBar: ActionBar;
constructor(container: HTMLElement, options: IToolBarOptions = { orientation: ActionsOrientation.HORIZONTAL }) {
constructor(container: HTMLElement, options: IToolBarOptions = { orientation: ActionsOrientation.HORIZONTAL }, collapseOverflow: boolean = false) {
this.options = options;
let element = document.createElement('div');
element.className = 'monaco-toolbar carbon-taskbar';
container.appendChild(element);
this.actionBar = new ActionBar(element, {
orientation: options.orientation,
ariaLabel: options.ariaLabel,
actionViewItemProvider: (action: IAction): IActionViewItem | undefined => {
return options.actionViewItemProvider ? options.actionViewItemProvider(action) : undefined;
}
});
if (collapseOverflow) {
this.actionBar = new OverflowActionBar(
element,
{
orientation: options.orientation,
ariaLabel: options.ariaLabel,
actionViewItemProvider: (action: IAction): IActionViewItem | undefined => {
return options.actionViewItemProvider ? options.actionViewItemProvider(action) : undefined;
}
}
);
} else {
this.actionBar = new ActionBar(
element,
{
orientation: options.orientation,
ariaLabel: options.ariaLabel,
actionViewItemProvider: (action: IAction): IActionViewItem | undefined => {
return options.actionViewItemProvider ? options.actionViewItemProvider(action) : undefined;
}
}
);
}
}
/**