Skip to content

Commit

Permalink
Add context menu
Browse files Browse the repository at this point in the history
  • Loading branch information
mattrunyon committed Dec 21, 2023
1 parent 97c2085 commit de05759
Show file tree
Hide file tree
Showing 4 changed files with 131 additions and 7 deletions.
31 changes: 31 additions & 0 deletions packages/code-studio/src/styleguide/Navigations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<NavTabList
tabs={tabs}
Expand All @@ -52,6 +82,7 @@ function NavTabListExample({
onReorder={handleReorder}
onClose={handleClose}
isReorderAllowed
makeContextItems={makeContextItems}
/>
);
}
Expand Down
2 changes: 1 addition & 1 deletion packages/components/src/context-actions/ContextActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
9 changes: 7 additions & 2 deletions packages/components/src/navigation/NavTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLDivElement>;
index: number;
isDraggable: boolean;
contextActions?: ResolvableContextAction[];
}

const NavTab = memo(
Expand All @@ -24,6 +27,7 @@ const NavTab = memo(
activeRef,
index,
isDraggable,
contextActions,
}: NavTabProps) => {
const { key, isClosable = false, title } = tab;

Expand Down Expand Up @@ -70,7 +74,7 @@ const NavTab = memo(
kind="ghost"
className="btn-nav-tab-close"
onClick={event => {
onClose(key);
onClose?.(key);
event.stopPropagation();
event.preventDefault();
}}
Expand All @@ -79,6 +83,7 @@ const NavTab = memo(
/>
)}
</div>
<ContextActions actions={contextActions} />
</div>
)}
</Draggable>
Expand Down
96 changes: 92 additions & 4 deletions packages/components/src/navigation/NavTabList.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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;
Expand All @@ -27,13 +34,23 @@ export interface NavTabItem {
isClosable?: boolean;
}

type NavTabListProps = {
type NavTabListProps<T extends NavTabItem = NavTabItem> = {
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 {
Expand All @@ -47,13 +64,70 @@ 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,
onSelect,
onReorder,
onClose,
isReorderAllowed,
makeContextItems,
}: NavTabListProps): React.ReactElement {
const containerRef = useRef<HTMLDivElement>();
const [isOverflowing, setIsOverflowing] = useState(true);
Expand Down Expand Up @@ -272,6 +346,19 @@ function NavTabList({
};
}, []);

const tabContextActionMap = useMemo(() => {
const tabContextActions = new Map<string, ResolvableContextAction[]>();
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<HTMLDivElement>(null);
const activeTab = tabs.find(tab => tab.key === activeKey) ?? tabs[0];
const navTabs = tabs.map((tab, index) => {
Expand All @@ -288,6 +375,7 @@ function NavTabList({
onSelect={onSelect}
onClose={onClose}
isDraggable={isReorderAllowed}
contextActions={tabContextActionMap.get(key)}
/>
);
});
Expand Down

0 comments on commit de05759

Please sign in to comment.