Skip to content

Commit

Permalink
Merge pull request #27 from Bandmators/feat/contextmenu
Browse files Browse the repository at this point in the history
Feat/contextmenu
  • Loading branch information
kyechan99 authored Sep 26, 2024
2 parents 6bf2595 + f2f1b54 commit 778c150
Show file tree
Hide file tree
Showing 15 changed files with 7,630 additions and 6,012 deletions.
13,416 changes: 7,413 additions & 6,003 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

26 changes: 26 additions & 0 deletions src/components/ContextMenu/ContextMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import styled from '@emotion/styled';
import * as React from 'react';

import { PortalContent } from '@/components/Portal/PortalContent';

interface ModalProps extends React.ComponentPropsWithoutRef<'div'> {
width?: React.CSSProperties['width'];
}

/**
* ContextMenu
* @returns
*/
export const ContextMenu = React.forwardRef<HTMLDivElement, ModalProps>(({ width, children, ...props }, ref) => {
return (
<PortalContent width={width} ref={ref} role="group" {...props}>
<ContextMenuListBox>{children}</ContextMenuListBox>
</PortalContent>
);
});
ContextMenu.displayName = 'ContextMenu';

const ContextMenuListBox = styled.ul`
margin: 0px;
padding: 0px;
`;
57 changes: 57 additions & 0 deletions src/components/ContextMenu/ContextMenuItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { css } from '@emotion/react';
import styled from '@emotion/styled';
import * as React from 'react';

import { PortalContext } from '@/components/Portal/PortalContext';
import useContext from '@/hooks/useContext';
import { composeEventHandlers } from '@/libs/event';

interface ContextMenuItemProps extends React.ComponentPropsWithoutRef<'li'> {
disabled?: boolean;
}
export const ContextMenuItem = React.forwardRef<HTMLLIElement, ContextMenuItemProps>(
({ disabled = false, ...props }, ref) => {
const { setShowModal } = useContext(PortalContext);

const onClickHandler = () => {
if (!disabled) setShowModal(false);
};

return (
<ContextMenuItemStyled
ref={ref}
tabIndex={0}
role={'menuitem'}
disabled={disabled}
onClick={composeEventHandlers(props.onClick, onClickHandler)}
data-focus-enabled="true"
{...props}
></ContextMenuItemStyled>
);
},
);
ContextMenuItem.displayName = 'ContextMenuItem';

const ContextMenuItemStyled = styled.li<{ disabled: boolean }>`
display: flex;
position: relative;
align-items: center;
padding: 0.375rem 0.5rem;
border-radius: 0.25rem;
font-size: 0.875rem;
line-height: 1.25rem;
outline: 2px solid transparent;
outline-offset: 2px;
cursor: default;
${({ disabled }) =>
disabled
? css`
opacity: 0.5;
`
: css`
&:hover,
&:focus {
background-color: var(--gray-100);
}
`}
`;
54 changes: 54 additions & 0 deletions src/components/ContextMenu/ContextMenuProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import * as React from 'react';

import { PortalProvider } from '@/components/Portal/PortalProvider';
import useContext from '@/hooks/useContext';
import { composeEventHandlers } from '@/libs/event';
import { composeRefs } from '@/libs/ref';
import { AlignType } from '@/types/align';

import { PortalContext } from '../Portal/PortalContext';

interface ContextMenuProviderProps extends React.PropsWithChildren {
align?: AlignType;
space?: number;
}

/**
* Displays a list of menus.
* @returns
*/
export const ContextMenuProvider = ({ align = 'start', space = 0, children }: ContextMenuProviderProps) => {
return (
<PortalProvider align={align} space={space}>
<ContextMenuContainer>{children}</ContextMenuContainer>
</PortalProvider>
);
};

// interface ContextMenuContainerProps extends React.PropsWithChildren {}

export const ContextMenuContainer = React.forwardRef<HTMLDivElement, React.ComponentPropsWithoutRef<'div'>>(
({ onContextMenu, ...props }, ref) => {
const { setShowModal, setReorgPos } = useContext(PortalContext);

const compRef = React.useRef<HTMLDivElement | null>(null);
return (
<div
ref={composeRefs(compRef, ref)}
onContextMenu={composeEventHandlers(onContextMenu, evt => {
if (evt.button !== 2) return;
evt.preventDefault();
// if (compRef.current) {
// const rect = compRef.current;
// setToggleElment(rect);
// }
setReorgPos({ x: evt.clientX, y: evt.clientY });

setShowModal(true);
})}
{...props}
/>
);
},
);
ContextMenuContainer.displayName = 'ContextMenuContainer';
5 changes: 5 additions & 0 deletions src/components/ContextMenu/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// export * from './_ContextMenu2';
// export * from './_ContextMenuContext2';
export * from './ContextMenu';
export * from './ContextMenuProvider';
export * from './ContextMenuItem';
1 change: 1 addition & 0 deletions src/components/Pagination/Pagination.test.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import '@testing-library/jest-dom';
import { fireEvent, render, screen } from '@testing-library/react';
import React from 'react';
import { vi } from 'vitest';
Expand Down
12 changes: 11 additions & 1 deletion src/components/Portal/PortalContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,17 @@ export const PortalContent = ({
{showModal &&
createPortal(
<>
{!disabledBG && <PortalBG id="bmates-portal-bg" className="bmates-portal-bg" onClick={close} />}
{!disabledBG && (
<PortalBG
id="bmates-portal-bg"
className="bmates-portal-bg"
onClick={close}
onContextMenu={evt => {
evt.preventDefault();
close();
}}
/>
)}
<Portal
id="bmates-portal"
ref={ref}
Expand Down
4 changes: 4 additions & 0 deletions src/components/Portal/PortalContext.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';

import { AlignType } from '@/types/align';
import { PositionType } from '@/types/position';

export interface PortalContextType {
/*
Expand All @@ -25,5 +26,8 @@ export interface PortalContextType {
* Space Size
*/
space?: number;

reorgPos: PositionType;
setReorgPos: (value: PositionType) => void;
}
export const PortalContext = React.createContext<PortalContextType | null>(null);
4 changes: 4 additions & 0 deletions src/components/Portal/PortalProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';

import useModal from '@/hooks/useModal';
import { PositionType } from '@/types/position';

import { PortalContext } from './PortalContext';
import { PortalType } from './type';
Expand All @@ -12,6 +13,7 @@ export interface PortalProviderProps extends React.PropsWithChildren, PortalType
export const PortalProvider = ({ align, space, enableScroll, children }: PortalProviderProps) => {
const [showModal, _setShowModal] = useModal(enableScroll);
const [toggleElement, setToggleElment] = React.useState<HTMLElement>();
const [reorgPos, setReorgPos] = React.useState<PositionType>({ x: 0, y: 0 });

const setShowModal = (value: boolean) => {
_setShowModal(value);
Expand All @@ -27,6 +29,8 @@ export const PortalProvider = ({ align, space, enableScroll, children }: PortalP
setToggleElment,
align,
space,
reorgPos,
setReorgPos,
}}
>
{children}
Expand Down
16 changes: 10 additions & 6 deletions src/components/Portal/usePortal.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import React from 'react';

import { PortalContext } from '@/components/Portal/PortalContext';
import { PositionType } from '@/types/position';

// import { PositionType } from '@/types/position';

export const usePortal = ({ portalRef }: { portalRef: React.RefObject<HTMLDivElement> }) => {
const {
Expand All @@ -11,8 +12,10 @@ export const usePortal = ({ portalRef }: { portalRef: React.RefObject<HTMLDivEle
setToggleElment,
align,
space = 0,
reorgPos,
setReorgPos,
} = React.useContext(PortalContext)!;
const [reorgPos, setReorgPos] = React.useState<PositionType>({ x: 0, y: 0 });
// const [reorgPos, setReorgPos] = React.useState<PositionType>({ x: 0, y: 0 });

const setShowModal = (value: boolean) => {
setModal(value);
Expand All @@ -21,12 +24,13 @@ export const usePortal = ({ portalRef }: { portalRef: React.RefObject<HTMLDivEle

React.useEffect(() => {
const adjustmentPos = () => {
if (portalRef.current && toggleElement && showModal) {
if (portalRef.current && showModal) {
const rect = portalRef.current;
const toggleRect = toggleElement.getBoundingClientRect();
const toggleRect = toggleElement
? toggleElement.getBoundingClientRect()
: { x: reorgPos.x, y: reorgPos.y, bottom: reorgPos.y, height: 0, width: 0 };

const isOverflowing = rect.offsetHeight + toggleRect.bottom + space >= window.innerHeight;
const reorgPos = { x: 0, y: 0 };

switch (align) {
case 'center':
Expand Down Expand Up @@ -59,7 +63,7 @@ export const usePortal = ({ portalRef }: { portalRef: React.RefObject<HTMLDivEle
return () => {
window.removeEventListener('resize', adjustmentPos);
};
}, [align, showModal, toggleElement, portalRef, space]);
}, [align, showModal, toggleElement, portalRef, space, reorgPos]);

return { showModal, align, toggleElement, setShowModal, setToggleElment, reorgPos };
};
1 change: 0 additions & 1 deletion src/components/Search/SearchInputToggle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ export const SearchInputToggle = React.forwardRef<HTMLInputElement, ComponentPro
};

const handleOnKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
console.log(e);
const handler = ACTIONS[e.key];

if (handler) {
Expand Down
1 change: 1 addition & 0 deletions src/components/Toast/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export { Toast, ToastDescription, ToastTitle } from './Toast';

// eslint-disable-next-line react-refresh/only-export-components
export { useToast } from './useToast';
export type { ToastData } from './type';
1 change: 1 addition & 0 deletions src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@ export * from './Toast';
export * from './Toggle';
export * from './Tooltip';
export * from './Checkbox';
export * from './ContextMenu';

export { default as BMatesProvider } from './Provider/StyledProvider';
42 changes: 42 additions & 0 deletions src/stories/common/ContextMenu.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React from 'react';

import { ContextMenu, ContextMenuItem, ContextMenuProvider } from '../..';

export default {
title: 'common/ContextMenu',
component: ContextMenuProvider,
tags: ['autodocs'],
args: {},
};

const Template = () => {
return (
<ContextMenuProvider>
<InnerComponent />
<ContextMenu>
<ContextMenuItem>GitHub</ContextMenuItem>
<ContextMenuItem>Facebook</ContextMenuItem>
</ContextMenu>
</ContextMenuProvider>
);
};

const InnerComponent = () => {
return (
<div
style={{
height: '1000px',
border: '1px solid #ccc',
background: 'var(--gray-100)',
position: 'relative',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
Right click here!
</div>
);
};

export const Default = Template.bind({});
2 changes: 1 addition & 1 deletion src/stories/common/Toast.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Meta, StoryObj } from '@storybook/react';
import React from 'react';

import { Button, Toaster, useToast } from '../..';
import { ToastData } from '../../components/common/Toast/type';
import { ToastData } from '../../components/Toast';

const ToastForStory = ({ ...props }: ToastData) => {
const { toast } = useToast();
Expand Down

0 comments on commit 778c150

Please sign in to comment.