1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Improve dock tabs UX (#4754)

* Add separators and scroll button

Signed-off-by: DmitriyNoa <dmytro.zharkov@gmail.com>

* Add tabs controlls

Signed-off-by: DmitriyNoa <dmytro.zharkov@gmail.com>

* Update values on resize

Signed-off-by: DmitriyNoa <dmytro.zharkov@gmail.com>

* Fix right button

Signed-off-by: DmitriyNoa <dmytro.zharkov@gmail.com>

* Add change tab on keydown

Signed-off-by: DmitriyNoa <dmytro.zharkov@gmail.com>

* Fix flickering and arrows position

Signed-off-by: DmitriyNoa <dmytro.zharkov@gmail.com>

* Fix pr comments. Cleanup function. Simplify reaction

Signed-off-by: DmitriyNoa <dmytro.zharkov@gmail.com>

* Add disposer cleanup

Signed-off-by: DmitriyNoa <dmytro.zharkov@gmail.com>

* Add separators and scroll button

Signed-off-by: DmitriyNoa <dmytro.zharkov@gmail.com>

* Add tabs controlls

Signed-off-by: DmitriyNoa <dmytro.zharkov@gmail.com>

* Update values on resize

Signed-off-by: DmitriyNoa <dmytro.zharkov@gmail.com>

* Fix right button

Signed-off-by: DmitriyNoa <dmytro.zharkov@gmail.com>

* Add change tab on keydown

Signed-off-by: DmitriyNoa <dmytro.zharkov@gmail.com>

* Fix flickering and arrows position

Signed-off-by: DmitriyNoa <dmytro.zharkov@gmail.com>

* Fix pr comments. Cleanup function. Simplify reaction

Signed-off-by: DmitriyNoa <dmytro.zharkov@gmail.com>

* Add disposer cleanup

Signed-off-by: DmitriyNoa <dmytro.zharkov@gmail.com>

* PR fixes and improvements

Signed-off-by: DmitriyNoa <dmytro.zharkov@gmail.com>

* Add reaction cleanup

Signed-off-by: DmitriyNoa <dmytro.zharkov@gmail.com>

* Cleanup. Remove reaction.

Signed-off-by: DmitriyNoa <dmytro.zharkov@gmail.com>

* Active tab soft background

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Show close btn on hover

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Removing custom left/right arrow buttons

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Remove dock-tabs styles from dock.scss

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Add dock-tabs.module.scss

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Add useResizeObserver hook

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Set tabs scrollable on small window sizes

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Add custom scrollbar on hover

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Update scrollbar overflow on tabs change

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Adding shadow corners to scrollable area

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Update material icons font

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Change terminal and chart install icons

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Add hover tooltip

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Controls scrollable within Tabs

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Move tooltips to top

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Set dock tabs theme colors

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Mock ResizeObserver

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Increase tooltip show delay

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Scroll active tab into view

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Scroll horizontally with mouse wheel

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Add tiny shadow to cropped tab

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Remove dock-tab.scss

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Adding tab role attributes

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Handle dock open/closed state

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Increase shadow corner size

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Selecting next or previous tab after closing selected one

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Add tiny test

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Use scrollIntoViewIfNeeded in tabs

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Small cleaning

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Small fix for useResizeObserver deps array

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Fix plus button padding on empty dock

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Fix close button position in active tab

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Remove min-width for tab title

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Clean up

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

Co-authored-by: Alex Andreev <alex.andreev.email@gmail.com>
This commit is contained in:
Dmitriy Noa 2022-03-21 13:55:51 +01:00 committed by GitHub
parent a76fc6df84
commit 9f6c3e230a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 406 additions and 73 deletions

View File

@ -0,0 +1,87 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import directoryForUserDataInjectable from "../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
import { DockStore, DockTab, TabKind } from "../dock/store";
import dockStoreInjectable from "../dock/store.injectable";
import fse from "fs-extra";
const initialTabs: DockTab[] = [
{ id: "terminal", kind: TabKind.TERMINAL, title: "Terminal", pinned: false },
{ id: "create", kind: TabKind.CREATE_RESOURCE, title: "Create resource", pinned: false },
{ id: "edit", kind: TabKind.EDIT_RESOURCE, title: "Edit resource", pinned: false },
{ id: "install", kind: TabKind.INSTALL_CHART, title: "Install chart", pinned: false },
{ id: "logs", kind: TabKind.POD_LOGS, title: "Logs", pinned: false },
];
describe("DockStore", () => {
let dockStore: DockStore;
beforeEach(async () => {
const di = getDiForUnitTesting({ doGeneralOverrides: true });
di.override(
directoryForUserDataInjectable,
() => "some-test-suite-specific-directory-for-user-data",
);
await di.runSetups();
dockStore = di.inject(dockStoreInjectable);
await dockStore.whenReady;
});
afterEach(() => {
fse.remove("some-test-suite-specific-directory-for-user-data");
dockStore.closeAllTabs();
});
it("closes tab and selects one from right", () => {
dockStore.tabs = initialTabs;
dockStore.closeTab(dockStore.tabs[0].id);
expect(dockStore.selectedTabId).toBe("create");
dockStore.selectTab("edit");
dockStore.closeTab("edit");
expect(dockStore.selectedTabId).toBe("install");
});
it("closes last tab and selects one from right", () => {
dockStore.tabs = initialTabs;
dockStore.selectTab("logs");
dockStore.closeTab("logs");
expect(dockStore.selectedTabId).toBe("install");
});
it("closes tab and selects the last one", () => {
dockStore.tabs = [
{ id: "terminal", kind: TabKind.TERMINAL, title: "Terminal", pinned: false },
{ id: "create", kind: TabKind.CREATE_RESOURCE, title: "Create resource", pinned: false },
];
dockStore.closeTab("terminal");
expect(dockStore.selectedTabId).toBe("create");
});
it("closes last tab and selects none", () => {
dockStore.tabs = [
{ id: "create", kind: TabKind.CREATE_RESOURCE, title: "Create resource", pinned: false },
];
dockStore.closeTab("create");
expect(dockStore.selectedTabId).toBeUndefined();
});
it("doesn't change selected tab if other tab closed", () => {
dockStore.tabs = initialTabs;
dockStore.closeTab("install");
expect(dockStore.selectedTabId).toBe("terminal");
});
});

View File

@ -39,6 +39,14 @@ jest.mock("electron", () => ({
}, },
})); }));
Object.defineProperty(window, "ResizeObserver", {
writable: true,
value: jest.fn().mockImplementation(() => ({
observe: jest.fn(),
disconnect: jest.fn(),
})),
});
const initialTabs: DockTab[] = [ const initialTabs: DockTab[] = [
{ id: "terminal", kind: TabKind.TERMINAL, title: "Terminal", pinned: false }, { id: "terminal", kind: TabKind.TERMINAL, title: "Terminal", pinned: false },
{ id: "create", kind: TabKind.CREATE_RESOURCE, title: "Create resource", pinned: false }, { id: "create", kind: TabKind.CREATE_RESOURCE, title: "Create resource", pinned: false },

View File

@ -0,0 +1,98 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
.DockTab {
--color-active: var(--dockTabActiveBackground);
--color-text-active: var(--textColorAccent);
--color-border-active: var(--primary);
padding: var(--padding);
height: 32px;
position: relative;
border-right: 1px solid var(--dockTabBorderColor);
background-size: 1px 3ch;
overflow: hidden;
/* Allow tabs to shrink and take all parent space */
min-width: var(--min-tab-width);
flex-grow: 1;
flex-basis: 0;
max-width: fit-content;
&:last-child {
border-right: none;
}
&.pinned {
padding-right: var(--padding);
}
&:last-child {
padding-right: var(--padding);
}
&:global(.active) {
background-color: var(--color-active);
background-image: none;
border-bottom: 1px solid var(--color-border-active);
color: var(--color-text-active)!important;
.close {
opacity: 1;
}
&::before {
display: none;
}
}
&::before {
content: " ";
display: block;
position: absolute;
width: 8px;
height: 100%;
right: 0;
background: linear-gradient(90deg, transparent 0%, var(--dockHeadBackground) 65%);
}
&::after {
display: none;
}
&:not(:global(.active)):hover {
background-color: var(--dockTabActiveBackground);
background-image: none;
color: var(--textColorAccent);
.close {
opacity: 1;
background: linear-gradient(90deg, transparent 0%, var(--dockTabActiveBackground) 25%);
}
&::before {
display: none;
}
}
}
.close {
position: absolute;
right: 0px;
width: 5ch;
opacity: 0;
text-align: center;
background: linear-gradient(90deg, transparent 0%, var(--color-active) 25%);
}
.tabIcon {
opacity: 0;
}
.title {
overflow: hidden;
text-overflow: ellipsis;
margin-right: 2.5rem;
}

View File

@ -1,39 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
.DockTab {
padding: $padding;
padding-right: 0;
.Icon {
&.material {
--size: var(--small-size);
}
&.svg {
--size: 15px;
}
}
.label {
.title {
max-width: 250px;
overflow: hidden;
text-overflow: ellipsis;
}
}
&.pinned {
padding-right: $padding;
}
&:first-child {
padding-left: 0;
}
&:last-child {
padding-right: $padding;
}
}

View File

@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import "./dock-tab.scss"; import styles from "./dock-tab.module.scss";
import React from "react"; import React from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
@ -16,6 +16,7 @@ import { observable, makeObservable } from "mobx";
import { isMac } from "../../../common/vars"; import { isMac } from "../../../common/vars";
import { withInjectables } from "@ogre-tools/injectable-react"; import { withInjectables } from "@ogre-tools/injectable-react";
import dockStoreInjectable from "./dock/store.injectable"; import dockStoreInjectable from "./dock/store.injectable";
import { Tooltip, TooltipPosition } from "../tooltip";
export interface DockTabProps extends TabProps<DockTabModel> { export interface DockTabProps extends TabProps<DockTabModel> {
moreActions?: React.ReactNode; moreActions?: React.ReactNode;
@ -76,19 +77,26 @@ class NonInjectedDockTab extends React.Component<DockTabProps & Dependencies> {
} }
render() { render() {
const { className, moreActions, dockStore, ...tabProps } = this.props; const { className, moreActions, dockStore, active, ...tabProps } = this.props;
const { title, pinned } = tabProps.value; const { title, pinned } = tabProps.value;
const label = ( const label = (
<div className="flex gaps align-center" onAuxClick={isMiddleClick(prevDefault(this.close))}> <div className="flex align-center" onAuxClick={isMiddleClick(prevDefault(this.close))}>
<span className="title" title={title}>{title}</span> <span className={styles.title}>{title}</span>
{moreActions} {moreActions}
{!pinned && ( {!pinned && (
<Icon <div className={styles.close}>
small material="close" <Icon
tooltip={`Close ${isMac ? "⌘+W" : "Ctrl+W"}`} small material="close"
onClick={prevDefault(this.close)} tooltip={`Close ${isMac ? "⌘+W" : "Ctrl+W"}`}
/> onClick={prevDefault(this.close)}
/>
</div>
)} )}
<Tooltip
targetId={`tab-${this.tabId}`}
preferredPositions={[TooltipPosition.TOP, TooltipPosition.TOP_LEFT]}
style={{ transitionDelay: "700ms" }}
>{title}</Tooltip>
</div> </div>
); );
@ -97,7 +105,9 @@ class NonInjectedDockTab extends React.Component<DockTabProps & Dependencies> {
<Tab <Tab
{...tabProps} {...tabProps}
id={`tab-${this.tabId}`} id={`tab-${this.tabId}`}
className={cssNames("DockTab", className, { pinned })} className={cssNames(styles.DockTab, className, {
[styles.pinned]: pinned,
})}
onContextMenu={() => this.menuVisible = true} onContextMenu={() => this.menuVisible = true}
label={label} label={label}
/> />

View File

@ -0,0 +1,60 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
.dockTabs {
--min-tab-width: 120px;
overflow: hidden;
}
.tabs {
width: 100%;
display: flex;
overflow: hidden;
&:empty {
display: none;
}
&:global(.scrollable) {
overflow: auto;
overflow-x: overlay; /* Set scrollbar inside content area */
&::-webkit-scrollbar-thumb {
background-color: transparent;
}
&:hover {
&::-webkit-scrollbar {
width: 100%;
height: 3px;
}
&::-webkit-scrollbar-thumb {
border-radius: 0;
height: 3px;
background-color: rgba(106, 115, 125, 0.2);
}
}
&::before, &::after {
content: "\00A0";
position: sticky;
min-width: 8px;
z-index: 1;
}
&::before {
left: 0;
background: linear-gradient(270deg, transparent 0%, var(--dockHeadBackground) 65%);
}
&::after {
right: 0;
background: linear-gradient(90deg, transparent 0%, var(--dockHeadBackground) 65%);
}
}
}

View File

@ -3,14 +3,16 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import React, { Fragment } from "react"; import styles from "./dock-tabs.module.scss";
import { Icon } from "../icon"; import React, { Fragment, useEffect, useRef, useState } from "react";
import { Tabs } from "../tabs/tabs"; import { Tabs } from "../tabs/tabs";
import { DockTab } from "./dock-tab"; import { DockTab } from "./dock-tab";
import type { DockTab as DockTabModel } from "./dock/store"; import type { DockTab as DockTabModel } from "./dock/store";
import { TabKind } from "./dock/store"; import { TabKind } from "./dock/store";
import { TerminalTab } from "./terminal/dock-tab"; import { TerminalTab } from "./terminal/dock-tab";
import { useResizeObserver } from "../../hooks";
import { cssVar } from "../../utils";
export interface DockTabsProps { export interface DockTabsProps {
tabs: DockTabModel[]; tabs: DockTabModel[];
@ -20,6 +22,14 @@ export interface DockTabsProps {
} }
export const DockTabs = ({ tabs, autoFocus, selectedTab, onChangeTab }: DockTabsProps) => { export const DockTabs = ({ tabs, autoFocus, selectedTab, onChangeTab }: DockTabsProps) => {
const elem = useRef<HTMLDivElement>();
const minTabSize = useRef<number>(0);
const [showScrollbar, setShowScrollbar] = useState(false);
const getTabElements = (): HTMLDivElement[] => {
return Array.from(elem.current?.querySelectorAll(".Tabs .Tab"));
};
const renderTab = (tab?: DockTabModel) => { const renderTab = (tab?: DockTabModel) => {
if (!tab) { if (!tab) {
return null; return null;
@ -31,7 +41,7 @@ export const DockTabs = ({ tabs, autoFocus, selectedTab, onChangeTab }: DockTabs
return <DockTab value={tab} icon="edit" />; return <DockTab value={tab} icon="edit" />;
case TabKind.INSTALL_CHART: case TabKind.INSTALL_CHART:
case TabKind.UPGRADE_CHART: case TabKind.UPGRADE_CHART:
return <DockTab value={tab} icon={<Icon svg="install" />} />; return <DockTab value={tab} icon="install_desktop" />;
case TabKind.POD_LOGS: case TabKind.POD_LOGS:
return <DockTab value={tab} icon="subject" />; return <DockTab value={tab} icon="subject" />;
case TabKind.TERMINAL: case TabKind.TERMINAL:
@ -39,14 +49,49 @@ export const DockTabs = ({ tabs, autoFocus, selectedTab, onChangeTab }: DockTabs
} }
}; };
const scrollActiveTabIntoView = () => {
const tab = elem.current?.querySelector(".Tab.active");
tab?.scrollIntoView();
};
const updateScrollbarVisibility = () => {
const allTabsShrunk = getTabElements().every(tab => tab.offsetWidth == minTabSize.current);
setShowScrollbar(allTabsShrunk);
};
const scrollTabsWithMouseWheel = (left: number) => {
elem.current?.children[0]?.scrollBy({ left });
};
const onMouseWheel = (event: React.WheelEvent) => {
scrollTabsWithMouseWheel(event.deltaY);
};
useEffect(() => {
const cssVars = cssVar(elem.current);
minTabSize.current = cssVars.get("--min-tab-width").valueOf();
});
useResizeObserver(elem.current, () => {
scrollActiveTabIntoView();
updateScrollbarVisibility();
});
return ( return (
<Tabs <div className={styles.dockTabs} ref={elem} role="tablist">
className="DockTabs" <Tabs
autoFocus={autoFocus} autoFocus={autoFocus}
value={selectedTab} value={selectedTab}
onChange={onChangeTab} onChange={onChangeTab}
> onWheel={onMouseWheel}
{tabs.map(tab => <Fragment key={tab.id}>{renderTab(tab)}</Fragment>)} scrollable={showScrollbar}
</Tabs> className={styles.tabs}
>
{tabs.map(tab => <Fragment key={tab.id}>{renderTab(tab)}</Fragment>)}
</Tabs>
</div>
); );
}; };

View File

@ -34,7 +34,9 @@
height: auto !important; height: auto !important;
.Tab { .Tab {
--color-active: inherit; --color-active: var(--colorVague);
--color-text-active: inherit;
--color-border-active: transparent;
&:not(:focus):after { &:not(:focus):after {
display: none; display: none;
@ -55,6 +57,10 @@
min-height: $unit * 4; min-height: $unit * 4;
padding-left: $padding; padding-left: $padding;
user-select: none; user-select: none;
&.pl-0 {
padding-left: 0;
}
} }
} }

View File

@ -4,11 +4,9 @@
*/ */
import "./dock.scss"; import "./dock.scss";
import React from "react"; import React from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { cssNames } from "../../utils";
import { cssNames, prevDefault } from "../../utils";
import { Icon } from "../icon"; import { Icon } from "../icon";
import { MenuItem } from "../menu"; import { MenuItem } from "../menu";
import { MenuActions } from "../menu/menu-actions"; import { MenuActions } from "../menu/menu-actions";
@ -37,6 +35,11 @@ interface Dependencies {
dockStore: DockStore; dockStore: DockStore;
} }
enum Direction {
NEXT = 1,
PREV = -1,
}
@observer @observer
class NonInjectedDock extends React.Component<DockProps & Dependencies> { class NonInjectedDock extends React.Component<DockProps & Dependencies> {
private element = React.createRef<HTMLDivElement>(); private element = React.createRef<HTMLDivElement>();
@ -52,6 +55,7 @@ class NonInjectedDock extends React.Component<DockProps & Dependencies> {
onKeyDown = (evt: KeyboardEvent) => { onKeyDown = (evt: KeyboardEvent) => {
const { close, selectedTab, closeTab } = this.props.dockStore; const { close, selectedTab, closeTab } = this.props.dockStore;
const { code, ctrlKey, metaKey, shiftKey } = evt; const { code, ctrlKey, metaKey, shiftKey } = evt;
// Determine if user working inside <Dock/> or using any other areas in app // Determine if user working inside <Dock/> or using any other areas in app
const dockIsFocused = this.element?.current.contains(document.activeElement); const dockIsFocused = this.element?.current.contains(document.activeElement);
@ -65,6 +69,14 @@ class NonInjectedDock extends React.Component<DockProps & Dependencies> {
closeTab(selectedTab.id); closeTab(selectedTab.id);
this.element?.current.focus(); // Avoid loosing focus when closing tab this.element?.current.focus(); // Avoid loosing focus when closing tab
} }
if(ctrlKey && code === "Period") {
this.switchToNextTab();
}
if(ctrlKey && code === "Comma") {
this.switchToNextTab(Direction.PREV);
}
}; };
onChangeTab = (tab: DockTab) => { onChangeTab = (tab: DockTab) => {
@ -75,6 +87,19 @@ class NonInjectedDock extends React.Component<DockProps & Dependencies> {
this.element?.current.focus(); this.element?.current.focus();
}; };
switchToNextTab = (direction: Direction = Direction.NEXT) => {
const { tabs, selectedTab } = this.props.dockStore;
const currentIndex = tabs.indexOf(selectedTab);
const nextIndex = currentIndex + direction;
// check if moving to the next or previous tab is possible.
if (nextIndex >= tabs.length || nextIndex < 0) return;
const nextElement = tabs[nextIndex];
this.onChangeTab(nextElement);
};
renderTab(tab: DockTab) { renderTab(tab: DockTab) {
switch (tab.kind) { switch (tab.kind) {
case TabKind.CREATE_RESOURCE: case TabKind.CREATE_RESOURCE:
@ -125,14 +150,14 @@ class NonInjectedDock extends React.Component<DockProps & Dependencies> {
onMinExtentExceed={dockStore.open} onMinExtentExceed={dockStore.open}
onDrag={extent => dockStore.height = extent} onDrag={extent => dockStore.height = extent}
/> />
<div className="tabs-container flex align-center" onDoubleClick={prevDefault(toggle)}> <div className="tabs-container flex align-center">
<DockTabs <DockTabs
tabs={tabs} tabs={tabs}
selectedTab={selectedTab} selectedTab={selectedTab}
autoFocus={isOpen} autoFocus={isOpen}
onChangeTab={this.onChangeTab} onChangeTab={this.onChangeTab}
/> />
<div className="toolbar flex gaps align-center box grow"> <div className={cssNames("toolbar flex gaps align-center box grow", { "pl-0": tabs.length == 0 })}>
<div className="dock-menu box grow"> <div className="dock-menu box grow">
<MenuActions usePortal triggerIcon={{ material: "add", className: "new-dock-tab", tooltip: "New tab" }} closeOnScroll={false}> <MenuActions usePortal triggerIcon={{ material: "add", className: "new-dock-tab", tooltip: "New tab" }} closeOnScroll={false}>
<MenuItem className="create-terminal-tab" onClick={() => this.props.createTerminalTab()}> <MenuItem className="create-terminal-tab" onClick={() => this.props.createTerminalTab()}>

View File

@ -162,6 +162,10 @@ export class DockStore implements DockStorageState {
this.dependencies.storage.merge({ selectedTabId: tabId }); this.dependencies.storage.merge({ selectedTabId: tabId });
} }
@computed get tabsNumber() : number {
return this.tabs.length;
}
@computed get selectedTab() { @computed get selectedTab() {
return this.tabs.find(tab => tab.id === this.selectedTabId); return this.tabs.find(tab => tab.id === this.selectedTabId);
} }
@ -323,6 +327,7 @@ export class DockStore implements DockStorageState {
@action @action
closeTab(tabId: TabId) { closeTab(tabId: TabId) {
const tab = this.getTabById(tabId); const tab = this.getTabById(tabId);
const tabIndex = this.getTabIndex(tabId);
if (!tab || tab.pinned) { if (!tab || tab.pinned) {
return; return;
@ -333,7 +338,7 @@ export class DockStore implements DockStorageState {
if (this.selectedTabId === tab.id) { if (this.selectedTabId === tab.id) {
if (this.tabs.length) { if (this.tabs.length) {
const newTab = this.tabs.slice(-1)[0]; // last const newTab = tabIndex < this.tabsNumber ? this.tabs[tabIndex] : this.tabs[tabIndex - 1];
this.selectTab(newTab.id); this.selectTab(newTab.id);
} else { } else {

View File

@ -46,7 +46,7 @@ class NonInjectedTerminalTab extends React.Component<TerminalTabProps & Dependen
} }
render() { render() {
const tabIcon = <Icon svg="terminal"/>; const tabIcon = <Icon material="terminal"/>;
const className = cssNames("TerminalTab", this.props.className, { const className = cssNames("TerminalTab", this.props.className, {
disconnected: this.isDisconnected, disconnected: this.isDisconnected,
}); });

View File

@ -13,7 +13,6 @@
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
font-display: block; font-display: block;
src: url("./fonts/MaterialIcons-Regular.woff2") format("woff");
src: url("./fonts/MaterialIcons-Regular.ttf") format("truetype"); src: url("./fonts/MaterialIcons-Regular.ttf") format("truetype");
} }

View File

@ -81,10 +81,7 @@ export class Tab extends React.PureComponent<TabProps> {
} }
scrollIntoView() { scrollIntoView() {
this.ref.current?.scrollIntoView?.({ this.ref.current?.scrollIntoViewIfNeeded();
behavior: "smooth",
inline: "center",
});
} }
@boundMethod @boundMethod
@ -137,6 +134,7 @@ export class Tab extends React.PureComponent<TabProps> {
onClick={this.onClick} onClick={this.onClick}
onFocus={this.onFocus} onFocus={this.onFocus}
onKeyDown={this.onKeyDown} onKeyDown={this.onKeyDown}
role="tab"
ref={this.ref} ref={this.ref}
> >
{typeof icon === "string" ? <Icon small material={icon}/> : icon} {typeof icon === "string" ? <Icon small material={icon}/> : icon}

View File

@ -8,4 +8,5 @@
export * from "./useOnUnmount"; export * from "./useOnUnmount";
export * from "./useInterval"; export * from "./useInterval";
export * from "./useMutationObserver"; export * from "./useMutationObserver";
export * from "./useResizeObserver";
export * from "./use-toggle"; export * from "./use-toggle";

View File

@ -0,0 +1,26 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { useEffect } from "react";
export function useResizeObserver(
element: Element,
callback: ResizeObserverCallback,
) {
useEffect(() => {
if (element) {
const observer = new ResizeObserver(callback);
observer.observe(element);
return () => {
observer.disconnect();
};
}
return undefined;
}, [element, callback]);
}

View File

@ -73,6 +73,8 @@
"dockEditorComment": "#808080", "dockEditorComment": "#808080",
"dockEditorActiveLineBackground": "#3a3d41", "dockEditorActiveLineBackground": "#3a3d41",
"dockBadgeBackground": "#36393e", "dockBadgeBackground": "#36393e",
"dockTabBorderColor": "#43424d",
"dockTabActiveBackground": "#3a3e45",
"logsBackground": "#000000", "logsBackground": "#000000",
"logsForeground": "#ffffff", "logsForeground": "#ffffff",
"logRowHoverBackground": "#35373a", "logRowHoverBackground": "#35373a",

View File

@ -73,6 +73,8 @@
"dockEditorComment": "#808080", "dockEditorComment": "#808080",
"dockEditorActiveLineBackground": "#3a3d41", "dockEditorActiveLineBackground": "#3a3d41",
"dockBadgeBackground": "#dedede", "dockBadgeBackground": "#dedede",
"dockTabBorderColor": "#d5d4de",
"dockTabActiveBackground": "#ffffff",
"logsBackground": "#24292e", "logsBackground": "#24292e",
"logsForeground": "#ffffff", "logsForeground": "#ffffff",
"logRowHoverBackground": "#35373a", "logRowHoverBackground": "#35373a",