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[] = [
|
||||
{ id: "terminal", kind: TabKind.TERMINAL, title: "Terminal", 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.
|
||||
*/
|
||||
|
||||
import "./dock-tab.scss";
|
||||
import styles from "./dock-tab.module.scss";
|
||||
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
@ -16,6 +16,7 @@ import { observable, makeObservable } from "mobx";
|
||||
import { isMac } from "../../../common/vars";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import dockStoreInjectable from "./dock/store.injectable";
|
||||
import { Tooltip, TooltipPosition } from "../tooltip";
|
||||
|
||||
export interface DockTabProps extends TabProps<DockTabModel> {
|
||||
moreActions?: React.ReactNode;
|
||||
@ -76,19 +77,26 @@ class NonInjectedDockTab extends React.Component<DockTabProps & Dependencies> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { className, moreActions, dockStore, ...tabProps } = this.props;
|
||||
const { className, moreActions, dockStore, active, ...tabProps } = this.props;
|
||||
const { title, pinned } = tabProps.value;
|
||||
const label = (
|
||||
<div className="flex gaps align-center" onAuxClick={isMiddleClick(prevDefault(this.close))}>
|
||||
<span className="title" title={title}>{title}</span>
|
||||
<div className="flex align-center" onAuxClick={isMiddleClick(prevDefault(this.close))}>
|
||||
<span className={styles.title}>{title}</span>
|
||||
{moreActions}
|
||||
{!pinned && (
|
||||
<div className={styles.close}>
|
||||
<Icon
|
||||
small material="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>
|
||||
);
|
||||
|
||||
@ -97,7 +105,9 @@ class NonInjectedDockTab extends React.Component<DockTabProps & Dependencies> {
|
||||
<Tab
|
||||
{...tabProps}
|
||||
id={`tab-${this.tabId}`}
|
||||
className={cssNames("DockTab", className, { pinned })}
|
||||
className={cssNames(styles.DockTab, className, {
|
||||
[styles.pinned]: pinned,
|
||||
})}
|
||||
onContextMenu={() => this.menuVisible = true}
|
||||
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.
|
||||
*/
|
||||
|
||||
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 { DockTab } from "./dock-tab";
|
||||
import type { DockTab as DockTabModel } from "./dock/store";
|
||||
import { TabKind } from "./dock/store";
|
||||
import { TerminalTab } from "./terminal/dock-tab";
|
||||
import { useResizeObserver } from "../../hooks";
|
||||
import { cssVar } from "../../utils";
|
||||
|
||||
export interface DockTabsProps {
|
||||
tabs: DockTabModel[];
|
||||
@ -20,6 +22,14 @@ export interface 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) => {
|
||||
if (!tab) {
|
||||
return null;
|
||||
@ -31,7 +41,7 @@ export const DockTabs = ({ tabs, autoFocus, selectedTab, onChangeTab }: DockTabs
|
||||
return <DockTab value={tab} icon="edit" />;
|
||||
case TabKind.INSTALL_CHART:
|
||||
case TabKind.UPGRADE_CHART:
|
||||
return <DockTab value={tab} icon={<Icon svg="install" />} />;
|
||||
return <DockTab value={tab} icon="install_desktop" />;
|
||||
case TabKind.POD_LOGS:
|
||||
return <DockTab value={tab} icon="subject" />;
|
||||
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 (
|
||||
<div className={styles.dockTabs} ref={elem} role="tablist">
|
||||
<Tabs
|
||||
className="DockTabs"
|
||||
autoFocus={autoFocus}
|
||||
value={selectedTab}
|
||||
onChange={onChangeTab}
|
||||
onWheel={onMouseWheel}
|
||||
scrollable={showScrollbar}
|
||||
className={styles.tabs}
|
||||
>
|
||||
{tabs.map(tab => <Fragment key={tab.id}>{renderTab(tab)}</Fragment>)}
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -34,7 +34,9 @@
|
||||
height: auto !important;
|
||||
|
||||
.Tab {
|
||||
--color-active: inherit;
|
||||
--color-active: var(--colorVague);
|
||||
--color-text-active: inherit;
|
||||
--color-border-active: transparent;
|
||||
|
||||
&:not(:focus):after {
|
||||
display: none;
|
||||
@ -55,6 +57,10 @@
|
||||
min-height: $unit * 4;
|
||||
padding-left: $padding;
|
||||
user-select: none;
|
||||
|
||||
&.pl-0 {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -4,11 +4,9 @@
|
||||
*/
|
||||
|
||||
import "./dock.scss";
|
||||
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
|
||||
import { cssNames, prevDefault } from "../../utils";
|
||||
import { cssNames } from "../../utils";
|
||||
import { Icon } from "../icon";
|
||||
import { MenuItem } from "../menu";
|
||||
import { MenuActions } from "../menu/menu-actions";
|
||||
@ -37,6 +35,11 @@ interface Dependencies {
|
||||
dockStore: DockStore;
|
||||
}
|
||||
|
||||
enum Direction {
|
||||
NEXT = 1,
|
||||
PREV = -1,
|
||||
}
|
||||
|
||||
@observer
|
||||
class NonInjectedDock extends React.Component<DockProps & Dependencies> {
|
||||
private element = React.createRef<HTMLDivElement>();
|
||||
@ -52,6 +55,7 @@ class NonInjectedDock extends React.Component<DockProps & Dependencies> {
|
||||
onKeyDown = (evt: KeyboardEvent) => {
|
||||
const { close, selectedTab, closeTab } = this.props.dockStore;
|
||||
const { code, ctrlKey, metaKey, shiftKey } = evt;
|
||||
|
||||
// Determine if user working inside <Dock/> or using any other areas in app
|
||||
const dockIsFocused = this.element?.current.contains(document.activeElement);
|
||||
|
||||
@ -65,6 +69,14 @@ class NonInjectedDock extends React.Component<DockProps & Dependencies> {
|
||||
closeTab(selectedTab.id);
|
||||
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) => {
|
||||
@ -75,6 +87,19 @@ class NonInjectedDock extends React.Component<DockProps & Dependencies> {
|
||||
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) {
|
||||
switch (tab.kind) {
|
||||
case TabKind.CREATE_RESOURCE:
|
||||
@ -125,14 +150,14 @@ class NonInjectedDock extends React.Component<DockProps & Dependencies> {
|
||||
onMinExtentExceed={dockStore.open}
|
||||
onDrag={extent => dockStore.height = extent}
|
||||
/>
|
||||
<div className="tabs-container flex align-center" onDoubleClick={prevDefault(toggle)}>
|
||||
<div className="tabs-container flex align-center">
|
||||
<DockTabs
|
||||
tabs={tabs}
|
||||
selectedTab={selectedTab}
|
||||
autoFocus={isOpen}
|
||||
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">
|
||||
<MenuActions usePortal triggerIcon={{ material: "add", className: "new-dock-tab", tooltip: "New tab" }} closeOnScroll={false}>
|
||||
<MenuItem className="create-terminal-tab" onClick={() => this.props.createTerminalTab()}>
|
||||
|
||||
@ -162,6 +162,10 @@ export class DockStore implements DockStorageState {
|
||||
this.dependencies.storage.merge({ selectedTabId: tabId });
|
||||
}
|
||||
|
||||
@computed get tabsNumber() : number {
|
||||
return this.tabs.length;
|
||||
}
|
||||
|
||||
@computed get selectedTab() {
|
||||
return this.tabs.find(tab => tab.id === this.selectedTabId);
|
||||
}
|
||||
@ -323,6 +327,7 @@ export class DockStore implements DockStorageState {
|
||||
@action
|
||||
closeTab(tabId: TabId) {
|
||||
const tab = this.getTabById(tabId);
|
||||
const tabIndex = this.getTabIndex(tabId);
|
||||
|
||||
if (!tab || tab.pinned) {
|
||||
return;
|
||||
@ -333,7 +338,7 @@ export class DockStore implements DockStorageState {
|
||||
|
||||
if (this.selectedTabId === tab.id) {
|
||||
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);
|
||||
} else {
|
||||
|
||||
@ -46,7 +46,7 @@ class NonInjectedTerminalTab extends React.Component<TerminalTabProps & Dependen
|
||||
}
|
||||
|
||||
render() {
|
||||
const tabIcon = <Icon svg="terminal"/>;
|
||||
const tabIcon = <Icon material="terminal"/>;
|
||||
const className = cssNames("TerminalTab", this.props.className, {
|
||||
disconnected: this.isDisconnected,
|
||||
});
|
||||
|
||||
@ -13,7 +13,6 @@
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: block;
|
||||
src: url("./fonts/MaterialIcons-Regular.woff2") format("woff");
|
||||
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() {
|
||||
this.ref.current?.scrollIntoView?.({
|
||||
behavior: "smooth",
|
||||
inline: "center",
|
||||
});
|
||||
this.ref.current?.scrollIntoViewIfNeeded();
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
@ -137,6 +134,7 @@ export class Tab extends React.PureComponent<TabProps> {
|
||||
onClick={this.onClick}
|
||||
onFocus={this.onFocus}
|
||||
onKeyDown={this.onKeyDown}
|
||||
role="tab"
|
||||
ref={this.ref}
|
||||
>
|
||||
{typeof icon === "string" ? <Icon small material={icon}/> : icon}
|
||||
|
||||
@ -8,4 +8,5 @@
|
||||
export * from "./useOnUnmount";
|
||||
export * from "./useInterval";
|
||||
export * from "./useMutationObserver";
|
||||
export * from "./useResizeObserver";
|
||||
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",
|
||||
"dockEditorActiveLineBackground": "#3a3d41",
|
||||
"dockBadgeBackground": "#36393e",
|
||||
"dockTabBorderColor": "#43424d",
|
||||
"dockTabActiveBackground": "#3a3e45",
|
||||
"logsBackground": "#000000",
|
||||
"logsForeground": "#ffffff",
|
||||
"logRowHoverBackground": "#35373a",
|
||||
|
||||
@ -73,6 +73,8 @@
|
||||
"dockEditorComment": "#808080",
|
||||
"dockEditorActiveLineBackground": "#3a3d41",
|
||||
"dockBadgeBackground": "#dedede",
|
||||
"dockTabBorderColor": "#d5d4de",
|
||||
"dockTabActiveBackground": "#ffffff",
|
||||
"logsBackground": "#24292e",
|
||||
"logsForeground": "#ffffff",
|
||||
"logRowHoverBackground": "#35373a",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user