Skip to content
Merged
10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@internxt/ui",
"version": "0.0.7",
"version": "0.0.8",
"description": "Library of Internxt components",
"repository": {
"type": "git",
Expand Down Expand Up @@ -68,7 +68,9 @@
"vite-plugin-dts": "^3.7.3",
"vite-plugin-svgr": "^4.2.0",
"vite-tsconfig-paths": "^5.1.3",
"vitest": "^2.1.8"
"vitest": "^2.1.8",
"react-dnd": "14.0.5",
"react-dnd-html5-backend": "^14.0.0"
},
"scripts": {
"build:tsc": "tsc",
Expand All @@ -90,9 +92,7 @@
"dependencies": {
"@phosphor-icons/react": "^2.1.5",
"@radix-ui/react-switch": "^1.1.0",
"@radix-ui/themes": "^3.0.0",
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1"
"@radix-ui/themes": "^3.0.0"
},
"lint-staged": {
"*.{ts,tsx}": [
Expand Down
13 changes: 12 additions & 1 deletion src/components/contextMenu/ContextMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export interface ContextMenuProps<T> {
menuItemsRef: React.MutableRefObject<HTMLDivElement | null>;
menu?: MenuItemsType<T>;
openedFromRightClick: boolean;
isOpen: boolean;
posX: number;
posY: number;
isContextMenuCutOff: boolean;
Expand Down Expand Up @@ -34,6 +35,9 @@ export interface ContextMenuProps<T> {
* - Indicates whether the context menu was opened via a right-click (`true`).
* Determines whether the menu's position is set based on the click location or a predefined position.
*
* @property {boolean} [isOpen]
* - To know is Menu is visible.
*
* @property {number} posX
* - X-coordinate for the menu's position, used if `openedFromRightClick` is `true`.
*
Expand All @@ -60,6 +64,7 @@ const ContextMenu = <T,>({
posX,
posY,
isContextMenuCutOff,
isOpen,
genericEnterKey,
handleMenuClose,
}: ContextMenuProps<T>): JSX.Element => {
Expand All @@ -78,7 +83,13 @@ const ContextMenu = <T,>({
}
ref={menuItemsRef}
>
<Menu item={item} genericEnterKey={genericEnterKey} handleMenuClose={handleMenuClose} menu={menu} />
<Menu
item={item}
isOpen={isOpen}
genericEnterKey={genericEnterKey}
handleMenuClose={handleMenuClose}
menu={menu}
/>
</div>
);
};
Expand Down
1 change: 1 addition & 0 deletions src/components/contextMenu/__test__/ContextMenu.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ describe('ContextMenu Component', () => {
posX: 100,
posY: 100,
isContextMenuCutOff: false,
isOpen: true,
genericEnterKey: vi.fn(),
handleMenuClose: vi.fn(),
};
Expand Down
23 changes: 20 additions & 3 deletions src/components/dropdown/Dropdown.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState, ReactNode } from 'react';
import { useState, ReactNode, useEffect, useRef } from 'react';
import { Menu, MenuItemType } from '../';

export type DropdownProps<T> = {
Expand Down Expand Up @@ -56,6 +56,23 @@ const Dropdown = <T,>({
}: DropdownProps<T>): JSX.Element => {
const [isOpen, setIsOpen] = useState(false);
const direction = openDirection === 'left' ? 'origin-top-left' : 'origin-top-right';
const containerRef = useRef<HTMLDivElement>(null);

useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
setIsOpen(false);
}
};

document.addEventListener('mousedown', handleClickOutside);
document.addEventListener('contextmenu', handleClickOutside);

return () => {
document.removeEventListener('mousedown', handleClickOutside);
document.removeEventListener('contextmenu', handleClickOutside);
};
}, []);

const group1: Array<MenuItemType<T>> = options
? options.map((option) => ({
Expand All @@ -78,7 +95,7 @@ const Dropdown = <T,>({
const closeMenu = () => setIsOpen(false);

return (
<div className="relative outline-none">
<div className="relative outline-none" ref={containerRef}>
<button
className={`cursor-pointer outline-none ${classButton}`}
onClick={toggleMenu}
Expand All @@ -96,7 +113,7 @@ const Dropdown = <T,>({
data-testid="menu-dropdown"
>
<div className={`absolute ${classMenuItems}`}>
<Menu item={item} handleMenuClose={closeMenu} menu={allItems} />
<Menu item={item} isOpen={isOpen} handleMenuClose={closeMenu} menu={allItems} />
</div>
</div>
</div>
Expand Down
1 change: 1 addition & 0 deletions src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export * from './modal';
export * from './popover';
export * from './radioButton';
export * from './slider';
export * from './skeletonLoader';
export * from './switch';
export * from './textArea';
export * from './tooltip';
4 changes: 3 additions & 1 deletion src/components/list/List.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ const List = <T extends { id: number }, F extends keyof T>({
};

const onOrderableColumnClicked = (field: HeaderProps<T, F>) => {
onCloseContextMenu();
if (!field.orderable || !onOrderByChanged) return;

const columnWasAlreadySelected = orderBy?.field === field.name;
Expand Down Expand Up @@ -307,6 +308,7 @@ const List = <T extends { id: number }, F extends keyof T>({
displayMenuDiv={displayMenuDiv}
isVerticalScrollbarVisible={isVerticalScrollbarVisible}
checkboxDataCy={checkboxDataCy}
onClose={onCloseContextMenu}
/>
) : null}

Expand All @@ -318,7 +320,7 @@ const List = <T extends { id: number }, F extends keyof T>({
<InfiniteScroll
handleNextPage={handleNextPage}
hasMoreItems={!!hasMoreItems}
loader={<></>}
loader={loader}
scrollableTarget="scrollableList"
>
{items.map((item, index) => (
Expand Down
4 changes: 3 additions & 1 deletion src/components/list/ListHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ interface ListHeaderProps<T, F> {
checkboxDataCy?: string;
onTopSelectionCheckboxClick: () => void;
onOrderableColumnClicked: (column: HeaderProps<T, F>) => void;
onClose?: () => void;
}

const ListHeader = <T, F extends keyof T>({
Expand All @@ -34,9 +35,10 @@ const ListHeader = <T, F extends keyof T>({
displayMenuDiv,
isVerticalScrollbarVisible,
checkboxDataCy,
onClose,
}: ListHeaderProps<T, F>) => {
return (
<div className="flex h-12 shrink-0 flex-row px-5">
<div onClick={onClose} onContextMenu={onClose} className="flex h-12 shrink-0 flex-row px-5">
{/* COLUMN */}
<div className="flex h-full min-w-full flex-row items-center border-b border-gray-10">
{/* SELECTION CHECKBOX */}
Expand Down
1 change: 1 addition & 0 deletions src/components/list/ListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ const ListItem = <T extends { id: number }>({
menu={menu}
menuItemsRef={menuItemsRef}
openedFromRightClick={openedFromRightClick}
isOpen={isOpen}
posX={posX}
posY={posY}
isContextMenuCutOff={isContextMenuCutOff}
Expand Down
34 changes: 23 additions & 11 deletions src/components/menu/Menu.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ReactNode, useEffect, useState } from 'react';
import { isValidElement, ReactNode, useEffect, useState } from 'react';
import useHotkeys from '../../hooks/useHotKeys';

export type MenuItemType<T> =
Expand All @@ -23,6 +23,7 @@ export type MenuItemsType<T> = Array<MenuItemType<T>>;

export interface MenuProps<T> {
item?: T;
isOpen: boolean;
menu?: MenuItemsType<T>;
handleMenuClose: () => void;
genericEnterKey?: () => void;
Expand All @@ -37,6 +38,9 @@ export interface MenuProps<T> {
* @property {T} [item]
* - Optional item that may be used in menu actions (e.g., data passed for actions).
*
* @property {boolean} [isOpen]
* - To know is Menu is visible.
*
* @property {MenuItemsType<T>} [menu]
* - Optional array of menu items. Each item can define a separator, title, icon, action, etc.
*
Expand All @@ -58,7 +62,7 @@ export interface MenuProps<T> {
* It features a dynamic index for item selection, with keyboard and mouse-based navigation.
*/

const Menu = <T,>({ item, menu, genericEnterKey, handleMenuClose }: MenuProps<T>): JSX.Element => {
const Menu = <T,>({ item, menu, isOpen, genericEnterKey, handleMenuClose }: MenuProps<T>): JSX.Element => {
const [selectedIndex, setSelectedIndex] = useState<number | null>(null);
const [enterPressed, setEnterPressed] = useState<boolean>(false);
const handleMouseEnter = (index: number) => {
Expand All @@ -70,6 +74,7 @@ const Menu = <T,>({ item, menu, genericEnterKey, handleMenuClose }: MenuProps<T>

const handleArrowDown = () => {
menu &&
isOpen &&
setSelectedIndex((prevIndex) => {
const getNextEnabledIndex = (startIndex: number): number => {
let newIndex = startIndex;
Expand All @@ -91,6 +96,7 @@ const Menu = <T,>({ item, menu, genericEnterKey, handleMenuClose }: MenuProps<T>

const handleArrowUp = () => {
menu &&
isOpen &&
setSelectedIndex((prevIndex) => {
const getPreviousEnabledIndex = (startIndex: number): number => {
let newIndex = startIndex;
Expand All @@ -111,14 +117,20 @@ const Menu = <T,>({ item, menu, genericEnterKey, handleMenuClose }: MenuProps<T>
};

const handleEnterKey = () => {
setSelectedIndex((prevIndex) => {
if (prevIndex !== null) {
const menuItem = menu ? menu[prevIndex] : undefined;
if (item && menuItem && 'action' in menuItem && menuItem.action) menuItem.action(item);
} else if (genericEnterKey) genericEnterKey();
setEnterPressed(true);
return null;
});
menu &&
isOpen &&
setSelectedIndex((prevIndex) => {
if (prevIndex !== null) {
const menuItem = menu ? menu[prevIndex] : undefined;
if (item && menuItem && 'action' in menuItem && menuItem.action) menuItem.action(item);
if (item && menuItem && 'node' in menuItem && menuItem.node && isValidElement(menuItem.node)) {
const onClick = menuItem.node.props.onClick;
onClick && onClick();
}
} else if (genericEnterKey) genericEnterKey();
setEnterPressed(true);
return null;
});
};

useEffect(() => {
Expand All @@ -134,7 +146,7 @@ const Menu = <T,>({ item, menu, genericEnterKey, handleMenuClose }: MenuProps<T>
arrowup: handleArrowUp,
enter: handleEnterKey,
},
[],
[isOpen],
);

return (
Expand Down
1 change: 1 addition & 0 deletions src/components/menu/__test__/Menu.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ describe('Menu Component', () => {
const defaultProps: MenuProps<{ id: number; name: string }> = {
item: { id: 1, name: 'Sample Item' },
menu: menuItems,
isOpen: true,
handleMenuClose,
genericEnterKey,
};
Expand Down
6 changes: 6 additions & 0 deletions src/hooks/useHotKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ type KeyMap = { [key: string]: () => void };

const useHotkeys = (keyMap: KeyMap, dependencies: unknown[] = []) => {
const handleKeyDown = (event: KeyboardEvent) => {
const isInputElement = ['input', 'textarea'].includes(document.activeElement?.tagName.toLowerCase() ?? '');

if (isInputElement && event.key.toLowerCase() !== 'escape') {
return;
}

const keyCombination = `${event.ctrlKey ? 'ctrl+' : ''}${event.metaKey ? 'meta+' : ''}${event.key.toLowerCase()}`;
if (keyMap[keyCombination]) {
event.preventDefault();
Expand Down
1 change: 1 addition & 0 deletions src/stories/components/breadcrumbs/breadcrumbs.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ const menuBreadcrumbs = (props: BreadcrumbsMenuProps): JSX.Element => {
<Menu
item={{ id: 1, name: 'Sample Item' }}
handleMenuClose={handleClick}
isOpen={false}
menu={[
{ name: 'Title', isTitle: () => true },
{ separator: true },
Expand Down
1 change: 1 addition & 0 deletions src/stories/components/contextMenu/ContextMenu.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ const ContextMenuWithNotifications = () => {
isContextMenuCutOff={false}
genericEnterKey={() => {}}
handleMenuClose={() => {}}
isOpen={true}
/>
</div>
</div>
Expand Down
13 changes: 11 additions & 2 deletions src/stories/components/dropdown/Dropdown.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,18 @@ const defaultArgs: DropdownProps<unknown> = {
{ text: 'Option 1', onClick: () => alert('Option 1 selected') },
{ text: 'Option 2', onClick: () => alert('Option 2 selected') },
],
menuItems: ['Item 1', 'Item 2'],
menuItems: [
<div onClick={() => alert('Item 1 selected')}>{'Item 1'}</div>,
<div onClick={() => alert('Item 2 selected')}>{'Item 2'}</div>,
],
dropdownActionsContext: [
{ name: 'Action 1', action: () => alert('Launched action 1') },
{
name: 'Action 1',
action: () => {
console.log('llamada');
alert('Launched action 1');
},
},
{ name: 'Action 2', action: () => alert('Launched action 2') },
{ separator: true },
{ name: 'Action 3', action: () => alert('Launched action 3') },
Expand Down
4 changes: 3 additions & 1 deletion src/stories/components/menu/Menu.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const withCloseHandler: Decorator = (Story, context) => {
...context,
args: {
...context.allArgs,
handleMenuClose: () => setArgs({ ...context.args, isOpen: false }),
handleMenuClose: () => setArgs({ ...context.args, isOpen: true }),
},
});
};
Expand Down Expand Up @@ -59,6 +59,7 @@ const ExampleIconGreen = React.forwardRef<SVGSVGElement, { size?: number | strin
export const Default: Story = {
args: {
item: { id: 1, name: 'Sample Item' },
isOpen: true,
menu: [
{ name: 'Title', isTitle: () => true },
{ separator: true },
Expand All @@ -76,6 +77,7 @@ export const Default: Story = {
export const WithIcons: Story = {
args: {
item: { id: 1, name: 'Sample Item' },
isOpen: true,
menu: [
{ name: 'Title', isTitle: () => true },
{ separator: true },
Expand Down
Loading
Loading