Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 3 additions & 134 deletions src/components/contextMenu/ContextMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,11 @@
import { useState } from 'react';
import useHotkeys from '../../hooks/useHotKeys';
import Menu, { MenuItemsType } from '../menu/Menu';

const MENU_BUTTON_HEIGHT = 40;

export type MenuItemType<T> =
| { separator: true }
| {
separator?: false;
name: string;
icon?: React.ForwardRefExoticComponent<{ size?: number | string }>;
keyboardShortcutOptions?: {
keyboardShortcutIcon?: React.ForwardRefExoticComponent<{ size?: number | string }>;
keyboardShortcutText?: string;
};
action: (target: T) => void | Promise<void>;
disabled?: (target: T) => boolean;
isTitle?: (target: T) => boolean;
};

export type MenuType<T> = Array<MenuItemType<T>>;

export interface ContextMenuProps<T> {
item: T;
menuItemsRef: React.MutableRefObject<HTMLDivElement | null>;
menu?: MenuType<T>;
menu?: MenuItemsType<T>;
openedFromRightClick: boolean;
posX: number;
posY: number;
Expand Down Expand Up @@ -81,77 +63,6 @@ const ContextMenu = <T,>({
genericEnterKey,
handleMenuClose,
}: ContextMenuProps<T>): JSX.Element => {
const [selectedIndex, setSelectedIndex] = useState<number | null>(null);

const handleMouseEnter = (index: number) => {
setSelectedIndex(index);
};

const isUnClickableItem = (menuItem: MenuItemType<T>) =>
menuItem?.separator || menuItem?.disabled?.(item) || menuItem?.isTitle?.(item);

const handleArrowDown = () => {
menu &&
setSelectedIndex((prevIndex) => {
const getNextEnabledIndex = (startIndex: number): number => {
let newIndex = startIndex;
let menuItem = menu[newIndex];
while (isUnClickableItem(menuItem)) {
newIndex = (newIndex + 1) % menu.length;
menuItem = menu[newIndex];
if (startIndex === newIndex) break;
}
return newIndex;
};

const newCurrentIndex = prevIndex === null ? 0 : (prevIndex + 1) % menu.length;
const newIndex = getNextEnabledIndex(newCurrentIndex);
const menuItem = menu[newIndex];
return isUnClickableItem(menuItem) ? null : newIndex;
});
};

const handleArrowUp = () => {
menu &&
setSelectedIndex((prevIndex) => {
const getPreviousEnabledIndex = (startIndex: number): number => {
let newIndex = startIndex;
let menuItem = menu[newIndex];
while (isUnClickableItem(menuItem)) {
newIndex = (newIndex - 1 + menu.length) % menu.length;
menuItem = menu[newIndex];
if (startIndex === newIndex) break;
}
return newIndex;
};

const newCurrentIndex = prevIndex === null ? menu.length - 1 : (prevIndex - 1 + menu.length) % menu.length;
const newIndex = getPreviousEnabledIndex(newCurrentIndex);
const menuItem = menu[newIndex];
return isUnClickableItem(menuItem) ? null : newIndex;
});
};

const handleEnterKey = () => {
setSelectedIndex((prevIndex) => {
if (prevIndex !== null) {
const menuItem = menu ? menu[prevIndex] : undefined;
if (menuItem && 'action' in menuItem) menuItem.action(item);
} else genericEnterKey();
handleMenuClose();
return null;
});
};

useHotkeys(
{
arrowdown: handleArrowDown,
arrowup: handleArrowUp,
enter: handleEnterKey,
},
[],
);

return (
<div
className="z-20 mt-0 flex flex-col rounded-lg bg-surface py-1.5 shadow-subtle-hard outline-none dark:bg-gray-5"
Expand All @@ -167,49 +78,7 @@ const ContextMenu = <T,>({
}
ref={menuItemsRef}
>
{menu?.map((option, i) => (
<div key={i}>
{option && option.separator ? (
<div className="my-0.5 flex w-full flex-row px-4">
<div className="h-px w-full bg-gray-10" />
</div>
) : (
option && (
<div
className={`${isUnClickableItem(option) ? 'pointer-events-none' : ''}`}
onClick={(e) => {
if (!isUnClickableItem(option)) {
e.stopPropagation();
option.action?.(item);
handleMenuClose();
}
}}
onMouseEnter={() => handleMouseEnter(i)}
>
<div
className={`flex cursor-pointer flex-row whitespace-nowrap px-4 py-1.5 text-base
${option.disabled?.(item) ? 'font-medium text-gray-50' : ''}
${option.isTitle?.(item) && !option.disabled?.(item) ? 'font-medium text-gray-100' : ''}
${selectedIndex === i && !option.disabled?.(item) ? 'bg-gray-5 text-gray-100 dark:bg-gray-10' : ''}
${!option.disabled?.(item) && !option.isTitle?.(item) && selectedIndex !== i ? 'text-gray-80' : ''}
`}
>
<div className="flex flex-row items-center space-x-2">
{option.icon && <option.icon size={20} />}
<span>{option.name}</span>
</div>
<span className="ml-5 flex grow items-center justify-end text-sm text-gray-40">
{option.keyboardShortcutOptions?.keyboardShortcutIcon && (
<option.keyboardShortcutOptions.keyboardShortcutIcon size={14} />
)}
{option.keyboardShortcutOptions?.keyboardShortcutText ?? ''}
</span>
</div>
</div>
)
)}
</div>
))}
<Menu item={item} genericEnterKey={genericEnterKey} handleMenuClose={handleMenuClose} menu={menu} />
</div>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,105 @@ exports[`ContextMenu Component > should match snapshot 1`] = `
<div
class="z-20 mt-0 flex flex-col rounded-lg bg-surface py-1.5 shadow-subtle-hard outline-none dark:bg-gray-5"
style="position: absolute; left: 100px; top: 100px; z-index: 99;"
>
<div
class="z-20 mt-0 flex flex-col rounded-lg bg-surface py-1.5 shadow-subtle-hard outline-none dark:bg-gray-5"
>
<div>
<div
class=""
>
<div
class="flex cursor-pointer flex-row whitespace-nowrap px-4 py-1.5 text-base



text-gray-80
"
>
<div
class="flex flex-row items-center space-x-2"
>
<span>
Option 1
</span>
</div>
<span
class="ml-5 flex grow items-center justify-end text-sm text-gray-40"
/>
</div>
</div>
</div>
<div>
<div
class=""
>
<div
class="flex cursor-pointer flex-row whitespace-nowrap px-4 py-1.5 text-base



text-gray-80
"
>
<div
class="flex flex-row items-center space-x-2"
>
<span>
Option 2
</span>
</div>
<span
class="ml-5 flex grow items-center justify-end text-sm text-gray-40"
/>
</div>
</div>
</div>
<div>
<div
class="my-0.5 flex w-full flex-row px-4"
>
<div
class="h-px w-full bg-gray-10"
/>
</div>
</div>
<div>
<div
class=""
>
<div
class="flex cursor-pointer flex-row whitespace-nowrap px-4 py-1.5 text-base



text-gray-80
"
>
<div
class="flex flex-row items-center space-x-2"
>
<span>
Option 3
</span>
</div>
<span
class="ml-5 flex grow items-center justify-end text-sm text-gray-40"
/>
</div>
</div>
</div>
</div>
</div>
</div>
</body>,
"container": <div>
<div
class="z-20 mt-0 flex flex-col rounded-lg bg-surface py-1.5 shadow-subtle-hard outline-none dark:bg-gray-5"
style="position: absolute; left: 100px; top: 100px; z-index: 99;"
>
<div
class="z-20 mt-0 flex flex-col rounded-lg bg-surface py-1.5 shadow-subtle-hard outline-none dark:bg-gray-5"
>
<div>
<div
Expand Down Expand Up @@ -95,97 +194,6 @@ exports[`ContextMenu Component > should match snapshot 1`] = `
</div>
</div>
</div>
</body>,
"container": <div>
<div
class="z-20 mt-0 flex flex-col rounded-lg bg-surface py-1.5 shadow-subtle-hard outline-none dark:bg-gray-5"
style="position: absolute; left: 100px; top: 100px; z-index: 99;"
>
<div>
<div
class=""
>
<div
class="flex cursor-pointer flex-row whitespace-nowrap px-4 py-1.5 text-base



text-gray-80
"
>
<div
class="flex flex-row items-center space-x-2"
>
<span>
Option 1
</span>
</div>
<span
class="ml-5 flex grow items-center justify-end text-sm text-gray-40"
/>
</div>
</div>
</div>
<div>
<div
class=""
>
<div
class="flex cursor-pointer flex-row whitespace-nowrap px-4 py-1.5 text-base



text-gray-80
"
>
<div
class="flex flex-row items-center space-x-2"
>
<span>
Option 2
</span>
</div>
<span
class="ml-5 flex grow items-center justify-end text-sm text-gray-40"
/>
</div>
</div>
</div>
<div>
<div
class="my-0.5 flex w-full flex-row px-4"
>
<div
class="h-px w-full bg-gray-10"
/>
</div>
</div>
<div>
<div
class=""
>
<div
class="flex cursor-pointer flex-row whitespace-nowrap px-4 py-1.5 text-base



text-gray-80
"
>
<div
class="flex flex-row items-center space-x-2"
>
<span>
Option 3
</span>
</div>
<span
class="ml-5 flex grow items-center justify-end text-sm text-gray-40"
/>
</div>
</div>
</div>
</div>
</div>,
"debug": [Function],
"findAllByAltText": [Function],
Expand Down
1 change: 0 additions & 1 deletion src/components/dialog/__test__/Dialog.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@ describe('Dialog', () => {
it('disables buttons when isLoading is true', () => {
const { getAllByRole } = renderDialog({ isLoading: true });
const elements = getAllByRole('button');
screen.debug();
expect(elements[0]).toBeDisabled();
expect(elements[1]).toBeDisabled();
});
Expand Down
Loading
Loading