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:
parent
a76fc6df84
commit
9f6c3e230a
87
src/renderer/components/dock/__test__/dock-store.test.ts
Normal file
87
src/renderer/components/dock/__test__/dock-store.test.ts
Normal 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");
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -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 },
|
||||||
|
|||||||
98
src/renderer/components/dock/dock-tab.module.scss
Normal file
98
src/renderer/components/dock/dock-tab.module.scss
Normal 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;
|
||||||
|
}
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
60
src/renderer/components/dock/dock-tabs.module.scss
Normal file
60
src/renderer/components/dock/dock-tabs.module.scss
Normal 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%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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()}>
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@ -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}
|
||||||
|
|||||||
@ -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";
|
||||||
|
|||||||
26
src/renderer/hooks/useResizeObserver.ts
Normal file
26
src/renderer/hooks/useResizeObserver.ts
Normal 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]);
|
||||||
|
}
|
||||||
@ -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",
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user