mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-24 17:23:05 -05:00
Fixing tabbing logic for tab headers (#19770)
* Fixing tabbing logic for tab headers * Renaming stuff Making null checks concise Adding comments * Renaming css class and interfaces from active to selected * Renaming styling classes and objects * Changing tabbing logic to match w3 behavior * Fixing focus logic in tab * Adding helper comment * Code cleanup
This commit is contained in:
@@ -70,15 +70,15 @@ panel {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tabbedPanel .tabList .tab-header:hover:not(.active) {
|
||||
.tabbedPanel .tabList .tab-header:hover:not(.selected) {
|
||||
background-color: #dcdcdc;
|
||||
outline: none;
|
||||
}
|
||||
.vs-dark .tabbedPanel .tabList .tab-header:hover:not(.active) {
|
||||
.vs-dark .tabbedPanel .tabList .tab-header:hover:not(.selected) {
|
||||
background-color: #2a2d2e;
|
||||
outline: none;
|
||||
}
|
||||
.hc-black .tabbedPanel .tabList .tab-header:hover:not(.active) {
|
||||
.hc-black .tabbedPanel .tabList .tab-header:hover:not(.selected) {
|
||||
background-color: initial;
|
||||
outline: 1px dashed #f38518;
|
||||
outline-offset: -3px;
|
||||
@@ -135,7 +135,7 @@ panel {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.tab > .tabLabel.active {
|
||||
.tab > .tabLabel.selected {
|
||||
border-bottom: 1px solid;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ tab-header .action-item {
|
||||
line-height: 1.4em;
|
||||
}
|
||||
|
||||
tab-header .tab-header.active .action-label, /* always show it for active tab */
|
||||
tab-header .tab-header.selected .action-label, /* always show it for selected 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;
|
||||
|
||||
@@ -57,10 +57,10 @@ let idPool = 0;
|
||||
<div *ngIf="_options.layout === NavigationBarLayout.vertical" class="vertical-tab-action-container">
|
||||
<button [attr.aria-expanded]="_tabExpanded" [title]="toggleTabPanelButtonAriaLabel" [attr.aria-label]="toggleTabPanelButtonAriaLabel" [ngClass]="toggleTabPanelButtonCssClass" tabindex="0" (click)="toggleTabPanel()"></button>
|
||||
</div>
|
||||
<div [style.display]="_tabExpanded ? 'flex': 'none'" [attr.aria-hidden]="_tabExpanded ? 'false': 'true'" class="tabList" role="tablist" (keydown)="onKey($event)">
|
||||
<div [style.display]="_tabExpanded ? 'flex': 'none'" [attr.aria-hidden]="_tabExpanded ? 'false': 'true'" class="tabList" role="tablist" (keydown)="onKey($event)" (focusout)="onTabHeaderFocusOut($event)">
|
||||
<div role="presentation" *ngFor="let tab of _tabs">
|
||||
<ng-container *ngIf="tab.type!=='group-header'">
|
||||
<tab-header role="presentation" [active]="_activeTab === tab" [tab]="tab" [showIcon]="_options.showIcon" (onSelectTab)='selectTab($event)' (onCloseTab)='closeTab($event)'></tab-header>
|
||||
<tab-header role="presentation" [selected]="_selectedTab === tab" [tab]="tab" [showIcon]="_options.showIcon" (onSelectTab)='selectTab($event)' (onCloseTab)='closeTab($event)'></tab-header>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="tab.type==='group-header' && _options.layout === NavigationBarLayout.vertical">
|
||||
<div class="tab-group-header">
|
||||
@@ -102,7 +102,7 @@ export class PanelComponent extends Disposable implements IThemable {
|
||||
@Output() public onTabChange = new EventEmitter<TabComponent>();
|
||||
@Output() public onTabClose = new EventEmitter<TabComponent>();
|
||||
|
||||
private _activeTab?: TabComponent;
|
||||
private _selectedTab?: TabComponent;
|
||||
private _actionbar?: ActionBar;
|
||||
private _mru: TabComponent[] = [];
|
||||
private _tabExpanded: boolean = true;
|
||||
@@ -218,39 +218,43 @@ export class PanelComponent extends Disposable implements IThemable {
|
||||
}
|
||||
});
|
||||
|
||||
if (this._activeTab && tab === this._activeTab) {
|
||||
if (this._selectedTab && tab === this._selectedTab) {
|
||||
this.onTabChange.emit(tab);
|
||||
return;
|
||||
}
|
||||
|
||||
this._zone.run(() => {
|
||||
if (this._activeTab) {
|
||||
this._activeTab.active = false;
|
||||
if (this._selectedTab) {
|
||||
this._selectedTab.selected = false;
|
||||
}
|
||||
|
||||
this._activeTab = tab;
|
||||
this._selectedTab = tab;
|
||||
this.setMostRecentlyUsed(tab);
|
||||
this._activeTab.active = true;
|
||||
this._selectedTab.selected = true;
|
||||
|
||||
this.onTabChange.emit(tab);
|
||||
});
|
||||
|
||||
this._tabHeaders?.forEach(tabHeader => {
|
||||
tabHeader.tabIndex = tabHeader.tab.identifier === foundTab.identifier ? 0 : -1;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the id of the active tab
|
||||
* Get the id of the selected tab
|
||||
*/
|
||||
public get getActiveTab(): string | undefined {
|
||||
return this._activeTab?.identifier;
|
||||
public get getSelectedTab(): string | undefined {
|
||||
return this._selectedTab?.identifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Select on the next tab
|
||||
*/
|
||||
public selectOnNextTab(): void {
|
||||
let activeIndex = this._tabs.toArray().findIndex(i => i === this._activeTab);
|
||||
let nextTabIndex = activeIndex + 1;
|
||||
let selectedIndex = this._tabs.toArray().findIndex(i => i === this._selectedTab);
|
||||
let nextTabIndex = selectedIndex + 1;
|
||||
if (nextTabIndex === this._tabs.length) {
|
||||
nextTabIndex = 0;
|
||||
}
|
||||
@@ -315,7 +319,7 @@ export class PanelComponent extends Disposable implements IThemable {
|
||||
}
|
||||
|
||||
public layout() {
|
||||
this._activeTab?.layout();
|
||||
this._selectedTab?.layout();
|
||||
}
|
||||
|
||||
onKey(e: KeyboardEvent): void {
|
||||
@@ -328,15 +332,34 @@ export class PanelComponent extends Disposable implements IThemable {
|
||||
this.focusPreviousTab();
|
||||
eventHandled = true;
|
||||
}
|
||||
|
||||
if (eventHandled) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
onTabHeaderFocusOut(e: Event): void {
|
||||
/**
|
||||
* Making the selected tab header focusable when the focus leaves the tab header div.
|
||||
* This fixes an issue when users press up/left arrow in vertical tab header and move up to
|
||||
* the previous tab header. The next focus was being set to the selected tab and then the tab
|
||||
* contents. Now, the focus will directly move to tab contents. And, when users press
|
||||
* shift-tab on the first focusable element of tab content, the focus will move back to
|
||||
* selected tab header.
|
||||
*/
|
||||
|
||||
if (!(<HTMLElement>e.currentTarget).contains((<any>e).relatedTarget)) {
|
||||
this._tabHeaders.forEach(th => {
|
||||
if (th.tab === this._selectedTab) {
|
||||
th.tabIndex = 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private focusPreviousTab(): void {
|
||||
const currentIndex = this.focusedTabHeaderIndex;
|
||||
this._tabHeaders.toArray()[currentIndex].tabIndex = -1;
|
||||
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);
|
||||
@@ -345,6 +368,7 @@ export class PanelComponent extends Disposable implements IThemable {
|
||||
|
||||
private focusNextTab(): void {
|
||||
const currentIndex = this.focusedTabHeaderIndex;
|
||||
this._tabHeaders.toArray()[currentIndex].tabIndex = -1;
|
||||
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);
|
||||
@@ -366,32 +390,36 @@ export class PanelComponent extends Disposable implements IThemable {
|
||||
style(styles: ITabbedPanelStyles) {
|
||||
if (this._styleElement) {
|
||||
const content: string[] = [];
|
||||
if (styles.titleInactiveForeground) {
|
||||
if (styles.titleUnSelectedForeground) {
|
||||
content.push(`.tabbedPanel.horizontal > .title .tabList .tab-header {
|
||||
color: ${styles.titleInactiveForeground}
|
||||
color: ${styles.titleUnSelectedForeground}
|
||||
}`);
|
||||
}
|
||||
if (styles.titleActiveBorder && styles.titleActiveForeground) {
|
||||
content.push(`.tabbedPanel.horizontal > .title .tabList .tab-header:focus,
|
||||
.tabbedPanel.horizontal > .title .tabList .tab-header.active {
|
||||
border-color: ${styles.titleActiveBorder};
|
||||
if (styles.titleSelectedBorder && styles.titleSelectedForeground) {
|
||||
content.push(`.tabbedPanel.horizontal > .title .tabList .tab-header.selected {
|
||||
border-color: ${styles.titleSelectedBorder};
|
||||
border-style: solid;
|
||||
color: ${styles.titleActiveForeground}
|
||||
color: ${styles.titleSelectedForeground}
|
||||
}`);
|
||||
|
||||
content.push(`.tabbedPanel.horizontal > .title .tabList .tab-header:focus,
|
||||
.tabbedPanel.horizontal > .title .tabList .tab-header.active {;
|
||||
border-width: 0 0 ${styles.activeTabContrastBorder ? '0' : '2'}px 0;
|
||||
content.push(`.tabbedPanel.horizontal > .title .tabList .tab-header.selected {;
|
||||
border-width: 0 0 ${styles.selectedTabContrastBorder ? '0' : '2'}px 0;
|
||||
}`);
|
||||
|
||||
content.push(`.tabbedPanel.horizontal > .title .tabList .tab-header:hover {
|
||||
color: ${styles.titleActiveForeground}
|
||||
color: ${styles.titleSelectedForeground}
|
||||
}`);
|
||||
|
||||
content.push(`.tabbedPanel.horizontal > .title .tabList .tab-header:focus {
|
||||
outline: 1px solid;
|
||||
outline-offset: 2px;
|
||||
outline-color: ${styles.titleSelectedBorder};
|
||||
}`);
|
||||
}
|
||||
|
||||
if (styles.activeBackgroundForVerticalLayout) {
|
||||
content.push(`.tabbedPanel.vertical > .title .tabList .tab-header.active {
|
||||
background-color:${styles.activeBackgroundForVerticalLayout}
|
||||
if (styles.selectedBackgroundForVerticalLayout) {
|
||||
content.push(`.tabbedPanel.vertical > .title .tabList .tab-header.selected {
|
||||
background-color:${styles.selectedBackgroundForVerticalLayout}
|
||||
}`);
|
||||
}
|
||||
|
||||
@@ -407,18 +435,14 @@ export class PanelComponent extends Disposable implements IThemable {
|
||||
}`);
|
||||
}
|
||||
|
||||
if (styles.activeTabContrastBorder) {
|
||||
if (styles.selectedTabContrastBorder) {
|
||||
content.push(`
|
||||
.tabbedPanel > .title .tabList .tab-header.active {
|
||||
.tabbedPanel > .title .tabList .tab-header.selected {
|
||||
outline: 1px solid;
|
||||
outline-offset: -3px;
|
||||
outline-color: ${styles.activeTabContrastBorder};
|
||||
outline-color: ${styles.selectedTabContrastBorder};
|
||||
}
|
||||
`);
|
||||
} else {
|
||||
content.push(`.tabbedPanel.horizontal > .title .tabList .tab-header:focus {
|
||||
outline-width: 0px;
|
||||
}`);
|
||||
}
|
||||
|
||||
const newStyles = content.join('\n');
|
||||
|
||||
@@ -16,14 +16,14 @@ import { Color } from 'vs/base/common/color';
|
||||
import { isUndefinedOrNull } from 'vs/base/common/types';
|
||||
|
||||
export interface ITabbedPanelStyles {
|
||||
titleActiveForeground?: Color;
|
||||
titleActiveBorder?: Color;
|
||||
titleInactiveForeground?: Color;
|
||||
titleSelectedForeground?: Color;
|
||||
titleSelectedBorder?: Color;
|
||||
titleUnSelectedForeground?: Color;
|
||||
focusBorder?: Color;
|
||||
outline?: Color;
|
||||
activeBackgroundForVerticalLayout?: Color;
|
||||
selectedBackgroundForVerticalLayout?: Color;
|
||||
border?: Color;
|
||||
activeTabContrastBorder?: Color;
|
||||
selectedTabContrastBorder?: Color;
|
||||
}
|
||||
|
||||
export interface IPanelOptions {
|
||||
@@ -108,7 +108,7 @@ export class TabbedPanel extends Disposable {
|
||||
return this.parent;
|
||||
}
|
||||
|
||||
public get activeTabId(): string | undefined {
|
||||
public get selectedTabId(): string | undefined {
|
||||
return this._shownTabId;
|
||||
}
|
||||
|
||||
@@ -209,8 +209,8 @@ export class TabbedPanel extends Disposable {
|
||||
if (this._shownTabId) {
|
||||
const shownTab = this._tabMap.get(this._shownTabId);
|
||||
if (shownTab) {
|
||||
shownTab.label.classList.remove('active');
|
||||
shownTab.header.classList.remove('active');
|
||||
shownTab.label.classList.remove('selected');
|
||||
shownTab.header.classList.remove('selected');
|
||||
shownTab.header.setAttribute('aria-selected', 'false');
|
||||
shownTab.header.tabIndex = -1;
|
||||
if (shownTab.body) {
|
||||
@@ -239,8 +239,8 @@ export class TabbedPanel extends Disposable {
|
||||
}
|
||||
this.body.appendChild(tab.body);
|
||||
this.body.setAttribute('aria-labelledby', tab.tab.identifier);
|
||||
tab.label.classList.add('active');
|
||||
tab.header.classList.add('active');
|
||||
tab.label.classList.add('selected');
|
||||
tab.header.classList.add('selected');
|
||||
tab.header.setAttribute('aria-selected', 'true');
|
||||
this._onTabChange.fire(id);
|
||||
if (tab.tab.view.onShow) {
|
||||
@@ -329,27 +329,27 @@ export class TabbedPanel extends Disposable {
|
||||
}`);
|
||||
}
|
||||
|
||||
if (styles.titleActiveForeground && styles.titleActiveBorder) {
|
||||
if (styles.titleSelectedForeground && styles.titleSelectedBorder) {
|
||||
content.push(`
|
||||
.tabbedPanel > .title .tabList .tab:hover .tabLabel,
|
||||
.tabbedPanel > .title .tabList .tab .tabLabel.active {
|
||||
color: ${styles.titleActiveForeground};
|
||||
border-bottom-color: ${styles.titleActiveBorder};
|
||||
.tabbedPanel > .title .tabList .tab .tabLabel.selected {
|
||||
color: ${styles.titleSelectedForeground};
|
||||
border-bottom-color: ${styles.titleSelectedBorder};
|
||||
border-bottom-width: 2px;
|
||||
}`);
|
||||
}
|
||||
|
||||
if (styles.titleInactiveForeground) {
|
||||
if (styles.titleUnSelectedForeground) {
|
||||
content.push(`
|
||||
.tabbedPanel > .title .tabList .tab .tabLabel {
|
||||
color: ${styles.titleInactiveForeground};
|
||||
color: ${styles.titleUnSelectedForeground};
|
||||
}`);
|
||||
}
|
||||
|
||||
if (styles.focusBorder && styles.titleActiveForeground) {
|
||||
if (styles.focusBorder && styles.titleSelectedForeground) {
|
||||
content.push(`
|
||||
.tabbedPanel > .title .tabList .tab .tabLabel:focus {
|
||||
color: ${styles.titleActiveForeground};
|
||||
color: ${styles.titleSelectedForeground};
|
||||
border-bottom-color: ${styles.focusBorder} !important;
|
||||
border-bottom: 1px solid;
|
||||
outline: none;
|
||||
@@ -358,7 +358,7 @@ export class TabbedPanel extends Disposable {
|
||||
|
||||
if (styles.outline) {
|
||||
content.push(`
|
||||
.tabbedPanel > .title .tabList .tab-header.active,
|
||||
.tabbedPanel > .title .tabList .tab-header.selected,
|
||||
.tabbedPanel > .title .tabList .tab-header:hover {
|
||||
outline-color: ${styles.outline};
|
||||
outline-width: 1px;
|
||||
@@ -367,7 +367,7 @@ export class TabbedPanel extends Disposable {
|
||||
outline-offset: -5px;
|
||||
}
|
||||
|
||||
.tabbedPanel > .title .tabList .tab-header:hover:not(.active) {
|
||||
.tabbedPanel > .title .tabList .tab-header:hover:not(.selected) {
|
||||
outline-style: dashed;
|
||||
}`);
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ export class TabComponent implements OnDestroy {
|
||||
@Input() public canClose!: boolean;
|
||||
@Input() public actions?: Array<Action>;
|
||||
@Input() public iconClass?: string;
|
||||
public _active = false;
|
||||
private _selected = false;
|
||||
@Input() public identifier!: string;
|
||||
@Input() public type: TabType = 'tab';
|
||||
@Input() private visibilityType: 'if' | 'visibility' = 'if';
|
||||
@@ -38,7 +38,7 @@ export class TabComponent implements OnDestroy {
|
||||
|
||||
@ContentChild(TabChild) public set child(tab: TabChild) {
|
||||
this._child = tab;
|
||||
if (this.active && this._child) {
|
||||
if (this.selected && this._child) {
|
||||
this._child.layout();
|
||||
}
|
||||
}
|
||||
@@ -47,21 +47,21 @@ export class TabComponent implements OnDestroy {
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) private _cd: ChangeDetectorRef
|
||||
) { }
|
||||
|
||||
public set active(val: boolean) {
|
||||
public set selected(val: boolean) {
|
||||
if (!this.destroyed) {
|
||||
this._active = val;
|
||||
if (this.active) {
|
||||
this._selected = val;
|
||||
if (this.selected) {
|
||||
this.rendered = true;
|
||||
}
|
||||
this._cd.detectChanges();
|
||||
if (this.active && this._child) {
|
||||
if (this.selected && this._child) {
|
||||
this._child.layout();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public get active(): boolean {
|
||||
return this._active;
|
||||
public get selected(): boolean {
|
||||
return this._selected;
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
@@ -72,7 +72,7 @@ export class TabComponent implements OnDestroy {
|
||||
}
|
||||
|
||||
shouldBeIfed(): boolean {
|
||||
if (this.active) {
|
||||
if (this.selected) {
|
||||
return true;
|
||||
} else if (this.visibilityType === 'visibility' && this.rendered) {
|
||||
return true;
|
||||
@@ -82,7 +82,7 @@ export class TabComponent implements OnDestroy {
|
||||
}
|
||||
|
||||
shouldBeHidden(): boolean {
|
||||
if (this.visibilityType === 'visibility' && !this.active) {
|
||||
if (this.visibilityType === 'visibility' && !this.selected) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
|
||||
@@ -19,10 +19,10 @@ import { CloseTabAction } from 'sql/base/browser/ui/panel/tabActions';
|
||||
@Component({
|
||||
selector: 'tab-header',
|
||||
template: `
|
||||
<div #actionHeader role="tab" [attr.aria-selected]="tab.active" [attr.aria-label]="tab.title" class="tab-header" style="flex: 0 0; flex-direction: row;" [class.active]="tab.active" tabindex="0" (click)="selectTab(tab)" (keyup)="onKey($event)">
|
||||
<div #actionHeader role="tab" [attr.aria-selected]="tab.selected" [attr.aria-label]="tab.title" class="tab-header" style="flex: 0 0; flex-direction: row;" [class.selected]="tab.selected" [attr.tabindex] = "_tabIndex" (click)="selectTab(tab)" (keyup)="onKey($event)">
|
||||
<div class="tab" role="presentation">
|
||||
<a #tabIcon *ngIf="showIcon && tab.iconClass" class="tabIcon codicon icon {{tab.iconClass}}"></a>
|
||||
<a class="tabLabel" [class.active]="tab.active" [title]="tab.title" #tabLabel>{{tab.title}}</a>
|
||||
<a class="tabLabel" [class.selected]="tab.selected" [title]="tab.title" #tabLabel>{{tab.title}}</a>
|
||||
</div>
|
||||
<div #actionbar style="flex: 0 0 auto; align-self: end; margin-top: auto; margin-bottom: auto;" ></div>
|
||||
</div>
|
||||
@@ -31,12 +31,13 @@ import { CloseTabAction } from 'sql/base/browser/ui/panel/tabActions';
|
||||
export class TabHeaderComponent extends Disposable implements AfterContentInit, OnDestroy {
|
||||
@Input() public tab!: TabComponent;
|
||||
@Input() public showIcon?: boolean;
|
||||
@Input() public active?: boolean;
|
||||
@Input() public selected?: boolean;
|
||||
@Output() public onSelectTab: EventEmitter<TabComponent> = new EventEmitter<TabComponent>();
|
||||
@Output() public onCloseTab: EventEmitter<TabComponent> = new EventEmitter<TabComponent>();
|
||||
@Output() public onFocusTab: EventEmitter<TabComponent> = new EventEmitter<TabComponent>();
|
||||
|
||||
private _actionbar!: ActionBar;
|
||||
private _tabIndex: number = -1;
|
||||
|
||||
@ViewChild('actionHeader', { read: ElementRef }) private _actionHeaderRef!: ElementRef;
|
||||
@ViewChild('actionbar', { read: ElementRef }) private _actionbarRef!: ElementRef;
|
||||
@@ -47,6 +48,15 @@ export class TabHeaderComponent extends Disposable implements AfterContentInit,
|
||||
super();
|
||||
}
|
||||
|
||||
public get tabIndex(): number {
|
||||
return this._tabIndex;
|
||||
}
|
||||
|
||||
public set tabIndex(value: number) {
|
||||
this._tabIndex = value;
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
public get nativeElement(): HTMLElement {
|
||||
return this._actionHeaderRef.nativeElement;
|
||||
}
|
||||
@@ -66,6 +76,9 @@ export class TabHeaderComponent extends Disposable implements AfterContentInit,
|
||||
this._actionbar.push(closeAction, { icon: true, label: false });
|
||||
}
|
||||
}
|
||||
if (this.tab.selected) {
|
||||
this.tabIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
|
||||
Reference in New Issue
Block a user