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)} /> ); });