diff --git a/packages/code-studio/src/styleguide/Navigations.tsx b/packages/code-studio/src/styleguide/Navigations.tsx
index 8da683b937..e54b028c42 100644
--- a/packages/code-studio/src/styleguide/Navigations.tsx
+++ b/packages/code-studio/src/styleguide/Navigations.tsx
@@ -44,6 +44,36 @@ function NavTabListExample({
setTabs(t => t.filter(tab => tab.key !== key));
}, []);
+ const makeContextItems = useCallback(
+ (tab: NavTabItem) => [
+ {
+ title: 'Select Tab to the Left',
+ group: 10,
+ order: 10,
+ disabled: tabs[0].key === tab.key,
+ action: () => {
+ const index = tabs.findIndex(t => t.key === tab.key);
+ if (index > 0) {
+ setActiveKey(tabs[index - 1].key);
+ }
+ },
+ },
+ {
+ title: 'Select Tab to the Right',
+ group: 30,
+ order: 10,
+ disabled: tabs[tabs.length - 1].key === tab.key,
+ action: () => {
+ const index = tabs.findIndex(t => t.key === tab.key);
+ if (index < tabs.length - 1) {
+ setActiveKey(tabs[index + 1].key);
+ }
+ },
+ },
+ ],
+ [tabs]
+ );
+
return (
);
}
diff --git a/packages/components/src/context-actions/ContextActions.tsx b/packages/components/src/context-actions/ContextActions.tsx
index b192e1ec4b..ab45275490 100644
--- a/packages/components/src/context-actions/ContextActions.tsx
+++ b/packages/components/src/context-actions/ContextActions.tsx
@@ -14,7 +14,7 @@ import './ContextActions.scss';
const log = Log.module('ContextActions');
interface ContextActionsProps {
- actions: ResolvableContextAction | ResolvableContextAction[];
+ actions?: ResolvableContextAction | ResolvableContextAction[];
ignoreClassNames?: string[];
'data-testid'?: string;
}
diff --git a/packages/components/src/navigation/NavTab.tsx b/packages/components/src/navigation/NavTab.tsx
index 797fac58c7..adaf25218a 100644
--- a/packages/components/src/navigation/NavTab.tsx
+++ b/packages/components/src/navigation/NavTab.tsx
@@ -4,15 +4,18 @@ import { Draggable } from 'react-beautiful-dnd';
import { vsClose } from '@deephaven/icons';
import type { NavTabItem } from './NavTabList';
import Button from '../Button';
+import ContextActions from '../context-actions/ContextActions';
+import { ResolvableContextAction } from '../context-actions';
interface NavTabProps {
tab: NavTabItem;
onSelect: (key: string) => void;
- onClose: (key: string) => void;
+ onClose?: (key: string) => void;
isActive: boolean;
activeRef: React.RefObject;
index: number;
isDraggable: boolean;
+ contextActions?: ResolvableContextAction[];
}
const NavTab = memo(
@@ -24,6 +27,7 @@ const NavTab = memo(
activeRef,
index,
isDraggable,
+ contextActions,
}: NavTabProps) => {
const { key, isClosable = false, title } = tab;
@@ -70,7 +74,7 @@ const NavTab = memo(
kind="ghost"
className="btn-nav-tab-close"
onClick={event => {
- onClose(key);
+ onClose?.(key);
event.stopPropagation();
event.preventDefault();
}}
@@ -79,6 +83,7 @@ const NavTab = memo(
/>
)}
+
)}
diff --git a/packages/components/src/navigation/NavTabList.tsx b/packages/components/src/navigation/NavTabList.tsx
index ad5c51ded2..dd6359cec8 100644
--- a/packages/components/src/navigation/NavTabList.tsx
+++ b/packages/components/src/navigation/NavTabList.tsx
@@ -1,4 +1,10 @@
-import React, { useCallback, useEffect, useRef, useState } from 'react';
+import React, {
+ useCallback,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+} from 'react';
import classNames from 'classnames';
import clamp from 'lodash.clamp';
import {
@@ -13,6 +19,7 @@ import DragUtils from '../DragUtils';
import Button from '../Button';
import NavTab from './NavTab';
import './NavTabList.scss';
+import { ResolvableContextAction } from '../context-actions';
// mouse hold timeout to act as hold instead of click
const CLICK_TIMEOUT = 500;
@@ -27,13 +34,23 @@ export interface NavTabItem {
isClosable?: boolean;
}
-type NavTabListProps = {
+type NavTabListProps = {
activeKey: string;
- tabs: NavTabItem[];
+ tabs: T[];
onSelect: (key: string) => void;
- onClose: (key: string) => void;
+ onClose?: (key: string) => void;
onReorder: (sourceIndex: number, destinationIndex: number) => void;
isReorderAllowed: boolean;
+
+ /**
+ * Context items to add to the tab in addition to the default items.
+ * The default items are Close, Close to the Right, and Close All.
+ * The default items have a group value of 20.
+ *
+ * @param tab The tab to make context items for
+ * @returns Additional context items for the tab
+ */
+ makeContextItems?: (tab: T) => ResolvableContextAction[];
};
function isScrolledLeft(element: HTMLElement): boolean {
@@ -47,6 +64,62 @@ function isScrolledRight(element: HTMLElement): boolean {
);
}
+function makeBaseContextItems(
+ tab: NavTabItem,
+ tabs: NavTabItem[],
+ onClose: ((key: string) => void) | undefined
+): ResolvableContextAction[] {
+ const { isClosable, key } = tab;
+ const contextActions: ResolvableContextAction[] = [];
+ if (isClosable != null && onClose != null) {
+ contextActions.push({
+ title: 'Close',
+ order: 10,
+ group: 20,
+ action: () => {
+ onClose(key);
+ },
+ });
+
+ contextActions.push(() => ({
+ title: 'Close to the Right',
+ order: 20,
+ group: 20,
+ action: () => {
+ for (let i = tabs.length - 1; i > 0; i -= 1) {
+ if (tabs[i].key === key) break;
+ if (tabs[i].isClosable === true) onClose(tabs[i].key);
+ }
+ },
+ // IIFE to run when called
+ disabled: (() => {
+ let disable = true;
+ for (let i = tabs.length - 1; i > 0; i -= 1) {
+ if (tabs[i].key === tab.key) break;
+ if (tabs[i].isClosable === true) {
+ disable = false;
+ break;
+ }
+ }
+ return disable;
+ })(),
+ }));
+
+ contextActions.push({
+ title: 'Close All',
+ order: 30,
+ group: 20,
+ action: () => {
+ tabs.forEach(t => {
+ if (t.isClosable === true) onClose(t.key);
+ });
+ },
+ });
+ }
+
+ return contextActions;
+}
+
function NavTabList({
activeKey,
tabs,
@@ -54,6 +127,7 @@ function NavTabList({
onReorder,
onClose,
isReorderAllowed,
+ makeContextItems,
}: NavTabListProps): React.ReactElement {
const containerRef = useRef();
const [isOverflowing, setIsOverflowing] = useState(true);
@@ -272,6 +346,19 @@ function NavTabList({
};
}, []);
+ const tabContextActionMap = useMemo(() => {
+ const tabContextActions = new Map();
+ tabs.forEach(tab => {
+ const { key } = tab;
+ const contextActions = [
+ ...makeBaseContextItems(tab, tabs, onClose),
+ ...(makeContextItems?.(tab) ?? []),
+ ];
+ tabContextActions.set(key, contextActions);
+ });
+ return tabContextActions;
+ }, [makeContextItems, tabs, onClose]);
+
const activeTabRef = useRef(null);
const activeTab = tabs.find(tab => tab.key === activeKey) ?? tabs[0];
const navTabs = tabs.map((tab, index) => {
@@ -288,6 +375,7 @@ function NavTabList({
onSelect={onSelect}
onClose={onClose}
isDraggable={isReorderAllowed}
+ contextActions={tabContextActionMap.get(key)}
/>
);
});