diff --git a/extensions/machine-learning-services/src/test/views/utils.ts b/extensions/machine-learning-services/src/test/views/utils.ts index 7f36593597..0d7467203f 100644 --- a/extensions/machine-learning-services/src/test/views/utils.ts +++ b/extensions/machine-learning-services/src/test/views/utils.ts @@ -182,6 +182,7 @@ export function createViewContext(): ViewTestContext { loadingComponent: () => loadingBuilder, fileBrowserTree: undefined!, hyperlink: undefined!, + tabbedPanel: undefined!, separator: undefined! } }; diff --git a/extensions/mssql/package.json b/extensions/mssql/package.json index 5698d67ed1..2d0413b8b3 100644 --- a/extensions/mssql/package.json +++ b/extensions/mssql/package.json @@ -52,7 +52,7 @@ "command": "mssqlCluster.task.newNotebook", "title": "%notebook.command.new%", "icon": { - "dark": "resources/dark/new_notebook_inverse.svg", + "dark": "resources/dark/new_notebook.svg", "light": "resources/light/new_notebook.svg" } }, diff --git a/extensions/mssql/resources/dark/database_inverse.svg b/extensions/mssql/resources/dark/database_inverse.svg new file mode 100644 index 0000000000..fbb38a2836 --- /dev/null +++ b/extensions/mssql/resources/dark/database_inverse.svg @@ -0,0 +1 @@ +Database_Inverse@2x \ No newline at end of file diff --git a/extensions/mssql/resources/dark/new_notebook.svg b/extensions/mssql/resources/dark/new_notebook.svg new file mode 100644 index 0000000000..6557616999 --- /dev/null +++ b/extensions/mssql/resources/dark/new_notebook.svg @@ -0,0 +1,3 @@ + + + diff --git a/extensions/mssql/resources/dark/new_notebook_inverse.svg b/extensions/mssql/resources/dark/new_notebook_inverse.svg deleted file mode 100644 index e0072afee1..0000000000 --- a/extensions/mssql/resources/dark/new_notebook_inverse.svg +++ /dev/null @@ -1 +0,0 @@ -new_notebook_inverse \ No newline at end of file diff --git a/extensions/mssql/resources/light/database.svg b/extensions/mssql/resources/light/database.svg new file mode 100644 index 0000000000..c11f929ce4 --- /dev/null +++ b/extensions/mssql/resources/light/database.svg @@ -0,0 +1 @@ +Database@2x \ No newline at end of file diff --git a/extensions/mssql/resources/light/new_notebook.svg b/extensions/mssql/resources/light/new_notebook.svg index 9618487568..6557616999 100644 --- a/extensions/mssql/resources/light/new_notebook.svg +++ b/extensions/mssql/resources/light/new_notebook.svg @@ -1 +1,3 @@ -new_notebook \ No newline at end of file + + + diff --git a/src/sql/azdata.proposed.d.ts b/src/sql/azdata.proposed.d.ts index c1de32754c..aeafcb309d 100644 --- a/src/sql/azdata.proposed.d.ts +++ b/src/sql/azdata.proposed.d.ts @@ -127,6 +127,7 @@ declare module 'azdata' { export interface ModelBuilder { radioCardGroup(): ComponentBuilder; + tabbedPanel(): TabbedPanelComponentBuilder; separator(): ComponentBuilder; } @@ -200,6 +201,78 @@ declare module 'azdata' { export interface ImageComponentProperties extends ComponentProperties, ComponentWithIconProperties { } + /** + * Panel component with tabs + */ + export interface TabbedPanelComponent extends Container { + /** + * An event triggered when the selected tab is changed. + * The event argument is the id of the selected tab. + */ + onTabChanged: vscode.Event; + } + + /** + * Defines the tab orientation of TabbedPanelComponent + */ + export enum TabOrientation { + Vertical = 'vertical', + Horizontal = 'horizontal' + } + + /** + * Layout of TabbedPanelComponent, can be used to initialize the component when using ModelBuilder + */ + export interface TabbedPanelLayout { + orientation: TabOrientation; + } + + /** + * Represents the tab of TabbedPanelComponent + */ + export interface Tab { + /** + * Title of the tab + */ + title: string; + + /** + * Content component of the tab + */ + content: Component; + + /** + * Id of the tab + */ + id: string; + } + + /** + * Represents the tab group of TabbedPanelComponent + */ + export interface TabGroup { + /** + * Title of the tab group + */ + title: string; + + /** + * children of the tab group + */ + tabs: Tab[]; + } + + /** + * Builder for TabbedPannelComponent + */ + export interface TabbedPanelComponentBuilder extends ContainerBuilder { + /** + * Add the tabs to the component + * @param tabs tabs/tab groups to be added + */ + withTabs(tabs: (Tab | TabGroup)[]): ContainerBuilder; + } + export interface InputBoxProperties extends ComponentProperties { validationErrorMessage?: string; } diff --git a/src/sql/base/browser/ui/panel/media/collapse.svg b/src/sql/base/browser/ui/panel/media/collapse.svg new file mode 100644 index 0000000000..b263806e32 --- /dev/null +++ b/src/sql/base/browser/ui/panel/media/collapse.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/sql/base/browser/ui/panel/media/collapse_inverse.svg b/src/sql/base/browser/ui/panel/media/collapse_inverse.svg new file mode 100644 index 0000000000..76b943a430 --- /dev/null +++ b/src/sql/base/browser/ui/panel/media/collapse_inverse.svg @@ -0,0 +1,12 @@ + + + + background + + + + Layer 1 + + + + \ No newline at end of file diff --git a/src/sql/base/browser/ui/panel/media/expand.svg b/src/sql/base/browser/ui/panel/media/expand.svg new file mode 100644 index 0000000000..8e0ad52a9f --- /dev/null +++ b/src/sql/base/browser/ui/panel/media/expand.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/sql/base/browser/ui/panel/media/expand_inverse.svg b/src/sql/base/browser/ui/panel/media/expand_inverse.svg new file mode 100644 index 0000000000..210b0f3b46 --- /dev/null +++ b/src/sql/base/browser/ui/panel/media/expand_inverse.svg @@ -0,0 +1,12 @@ + + + + background + + + + Layer 1 + + + + \ No newline at end of file diff --git a/src/sql/base/browser/ui/panel/media/panel.css b/src/sql/base/browser/ui/panel/media/panel.css index 5338c597ba..b68539e8b1 100644 --- a/src/sql/base/browser/ui/panel/media/panel.css +++ b/src/sql/base/browser/ui/panel/media/panel.css @@ -59,7 +59,7 @@ panel { display: flex; padding-left: 5px; padding-right: 5px; - min-width: 65px; + cursor: pointer; } .tabbedPanel.vertical .tabList .tab-header { @@ -67,17 +67,16 @@ panel { text-transform: none; text-overflow: ellipsis; overflow: hidden; - width: 65px; + width: auto; height: 50px; line-height: 45px; } -.tabbedPanel .tabList .tab .tabLabel.codicon { +.tabbedPanel .tabList .tab .tabIcon.codicon { background-repeat: no-repeat; - background-position: center center; - background-size: 20px; - padding: 20px 25px; - line-height: 50px; + background-position: 2px center; + background-size: 16px; + padding: 2px 2px 2px 22px; } .tabbedPanel .tabList .actions-container { @@ -117,24 +116,20 @@ panel { height: 100%; } -.tabbedPanel.vertical .tabList { - flex-direction: column; -} - .tabbedPanel > .tab-content { flex: 1; position: relative; } -.tabbedPanel.vertical > .title > .tabList { +.tabbedPanel.vertical > .title > .tabContainer > .monaco-scrollable-element > .tabList { flex-flow: column; } -.tabbedPanel.horizontal > .title > .tabList { +.tabbedPanel.horizontal > .title > .tabContainer > .monaco-scrollable-element > .tabList { flex-flow: row; } -.tabbedPanel > .title > .monaco-scrollable-element { +.tabbedPanel > .title > .tabContainer > .monaco-scrollable-element { flex: 0 1 auto; width: inherit; } @@ -147,3 +142,47 @@ panel { width: 100%; height: 100%; } + +.tabbedPanel .tab-group-header { + font-weight: bold; + margin: 15px 5px 3px 5px; + line-height: 35px; + height: 35px; + border-style: solid; + border-width: 0 0 1px 0; + border-color: rgb(214, 214, 214); +} + +.tabbedPanel .action-container { + display: flex; + flex-flow: row-reverse; +} + +.tabbedPanel .tab-action { + width: 15px; + height: 15px; + padding: 0px; + border: 0px; + background-color: transparent; + background-position: 2px center; + background-repeat: no-repeat; + background-size: 11px 11px; +} + +.vs .tabbedPanel .tab-action.collapse{ + background-image: url("collapse.svg"); +} + +.vs-dark .tabbedPanel .tab-action.collapse, +.hc-black .tabbedPanel .tab-action.collapse { + background-image: url("collapse_inverse.svg"); +} + +.vs .tabbedPanel .tab-action.expand { + background-image: url("expand.svg"); +} + +.vs-dark .tabbedPanel .tab-action.expand, +.hc-black .tabbedPanel .tab-action.expand { + background-image: url("expand_inverse.svg"); +} diff --git a/src/sql/base/browser/ui/panel/media/tabHeader.css b/src/sql/base/browser/ui/panel/media/tabHeader.css index bbf06dda0b..35fa24986f 100644 --- a/src/sql/base/browser/ui/panel/media/tabHeader.css +++ b/src/sql/base/browser/ui/panel/media/tabHeader.css @@ -17,4 +17,4 @@ tab-header .tab-header.active .action-label, /* always show it for active tab tab-header .tab-header:hover .action-label, /* always show it on hover */ tab-header .tab-header:focus .action-label { /* always show it on focus */ opacity: 1; -} \ No newline at end of file +} diff --git a/src/sql/base/browser/ui/panel/panel.component.ts b/src/sql/base/browser/ui/panel/panel.component.ts index 4ac6ddaf18..fd658c22e7 100644 --- a/src/sql/base/browser/ui/panel/panel.component.ts +++ b/src/sql/base/browser/ui/panel/panel.component.ts @@ -5,7 +5,7 @@ import { Component, ContentChildren, QueryList, Inject, forwardRef, NgZone, - Input, EventEmitter, Output, ViewChild, ElementRef + Input, EventEmitter, Output, ViewChild, ElementRef, ChangeDetectorRef, ViewChildren } from '@angular/core'; import { TabComponent } from 'sql/base/browser/ui/panel/tab.component'; @@ -19,6 +19,10 @@ import { mixin } from 'vs/base/common/objects'; import { Disposable } from 'vs/base/common/lifecycle'; import { ScrollbarVisibility } from 'vs/base/common/scrollable'; import { firstIndex } from 'vs/base/common/arrays'; +import * as nls from 'vs/nls'; +import { TabHeaderComponent } from 'sql/base/browser/ui/panel/tabHeader.component'; +import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { KeyCode } from 'vs/base/common/keyCodes'; export interface IPanelOptions { /** @@ -48,9 +52,19 @@ let idPool = 0;
-
+
+ +
+
- + + + + +
+ {{tab.title}} +
+
@@ -71,6 +85,7 @@ export class PanelComponent extends Disposable { @Input() public options?: IPanelOptions; @Input() public actions?: Array; @ContentChildren(TabComponent) private readonly _tabs!: QueryList; + @ViewChildren(TabHeaderComponent) private readonly _tabHeaders!: QueryList; @ViewChild(ScrollableDirective) private scrollable?: ScrollableDirective; @Output() public onTabChange = new EventEmitter(); @@ -79,16 +94,32 @@ export class PanelComponent extends Disposable { private _activeTab?: TabComponent; private _actionbar?: ActionBar; private _mru: TabComponent[] = []; + private _tabExpanded: boolean = true; protected AutoScrollbarVisibility = ScrollbarVisibility.Auto; // used by angular template protected HiddenScrollbarVisibility = ScrollbarVisibility.Hidden; // used by angular template protected NavigationBarLayout = NavigationBarLayout; // used by angular template @ViewChild('panelActionbar', { read: ElementRef }) private _actionbarRef!: ElementRef; - constructor(@Inject(forwardRef(() => NgZone)) private _zone: NgZone) { + constructor( + @Inject(forwardRef(() => NgZone)) private _zone: NgZone, + @Inject(forwardRef(() => ChangeDetectorRef)) private _cd: ChangeDetectorRef) { super(); } + public get toggleTabPanelButtonCssClass(): string { + return this._tabExpanded ? 'tab-action collapse' : 'tab-action expand'; + } + + public get toggleTabPanelButtonAriaLabel(): string { + return this._tabExpanded ? nls.localize('hideTextLabel', "Hide text labels") : nls.localize('showTextLabel', "Show text labels"); + } + + toggleTabPanel(): void { + this._tabExpanded = !this._tabExpanded; + this._cd.detectChanges(); + } + ngOnInit(): void { this.options = mixin(this.options || {}, defaultOptions, false); } @@ -245,4 +276,49 @@ export class PanelComponent extends Disposable { public layout() { this._activeTab?.layout(); } + + onKey(e: KeyboardEvent): void { + const event = new StandardKeyboardEvent(e); + let eventHandled: boolean = false; + if (event.equals(KeyCode.DownArrow) || event.equals(KeyCode.RightArrow)) { + this.focusNextTab(); + eventHandled = true; + } else if (event.equals(KeyCode.UpArrow) || event.equals(KeyCode.LeftArrow)) { + this.focusPreviousTab(); + eventHandled = true; + } + + if (eventHandled) { + event.preventDefault(); + event.stopPropagation(); + } + } + + private focusPreviousTab(): void { + const currentIndex = this.focusedTabHeaderIndex; + if (currentIndex !== -1) { + // Move to the previous tab, if we are at the first tab then move to the last tab. + this.focusOnTabHeader(currentIndex === 0 ? this._tabHeaders.length - 1 : currentIndex - 1); + } + } + + private focusNextTab(): void { + const currentIndex = this.focusedTabHeaderIndex; + if (currentIndex !== -1) { + // Move to the next tab, if we are at the last tab then move to the first tab. + this.focusOnTabHeader(currentIndex === this._tabHeaders.length - 1 ? 0 : currentIndex + 1); + } + } + + private focusOnTabHeader(index: number): void { + if (index >= 0 && index <= this._tabHeaders.length - 1) { + this._tabHeaders.toArray()[index].focusOnTabHeader(); + } + } + + private get focusedTabHeaderIndex(): number { + return this._tabHeaders.toArray().findIndex((header) => { + return header.nativeElement === document.activeElement; + }); + } } diff --git a/src/sql/base/browser/ui/panel/tab.component.ts b/src/sql/base/browser/ui/panel/tab.component.ts index 226b331bdd..0cca4bc2c0 100644 --- a/src/sql/base/browser/ui/panel/tab.component.ts +++ b/src/sql/base/browser/ui/panel/tab.component.ts @@ -12,6 +12,8 @@ export abstract class TabChild extends AngularDisposable { public abstract layout(): void; } +export type TabType = 'tab' | 'group-header'; + @Component({ selector: 'tab', template: ` @@ -29,6 +31,7 @@ export class TabComponent implements OnDestroy { @Input() public iconClass?: string; public _active = false; @Input() public identifier!: string; + @Input() public type: TabType = 'tab'; @Input() private visibilityType: 'if' | 'visibility' = 'if'; private rendered = false; private destroyed: boolean = false; diff --git a/src/sql/base/browser/ui/panel/tabHeader.component.ts b/src/sql/base/browser/ui/panel/tabHeader.component.ts index a10ad3e4d5..4781c4b935 100644 --- a/src/sql/base/browser/ui/panel/tabHeader.component.ts +++ b/src/sql/base/browser/ui/panel/tabHeader.component.ts @@ -19,10 +19,13 @@ import { CloseTabAction } from 'sql/base/browser/ui/panel/tabActions'; @Component({ selector: 'tab-header', template: ` -