Skip to content

Commit

Permalink
Merge pull request #11 from Bandmators/feature/dropdown
Browse files Browse the repository at this point in the history
feat(common): dropdown (#10)
  • Loading branch information
kyechan99 authored Jan 31, 2024
2 parents 7545426 + 03ad92c commit e4f9973
Show file tree
Hide file tree
Showing 17 changed files with 587 additions and 39 deletions.
41 changes: 41 additions & 0 deletions src/__tests__/ui.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ import {
DialogHeader,
DialogTitle,
DialogToggle,
Dropdown,
DropdownContent,
DropdownDivider,
DropdownItem,
DropdownLabel,
DropdownShortcut,
DropdownToggle,
Form,
Input,
InputDesc,
Expand Down Expand Up @@ -172,4 +179,38 @@ describe('UI test', () => {

expect(closeBtn).not.toBeInTheDocument();
});

it('Should appear DropdownContent when click DropdownToggle', () => {
render(
<Dropdown>
<DropdownToggle>DropdownToggle</DropdownToggle>
<DropdownContent width={'15rem'}>
<DropdownLabel>Share Social</DropdownLabel>
<DropdownDivider />
<DropdownItem>
GitHub
<DropdownShortcut>⌘+T</DropdownShortcut>
</DropdownItem>

<DropdownItem disabled>Facebook</DropdownItem>
<DropdownItem>Twitter</DropdownItem>
</DropdownContent>
</Dropdown>,
);

const toggleBtn = screen.getByText('DropdownToggle');
expect(toggleBtn).toBeInTheDocument();

fireEvent.click(toggleBtn);

const dropdownLabel = screen.getByText('Share Social');
expect(dropdownLabel).toBeInTheDocument();

const dropdownItem = screen.getByText('Twitter');
expect(dropdownItem).toBeInTheDocument();

fireEvent.click(dropdownItem);

expect(dropdownLabel).not.toBeInTheDocument();
});
});
30 changes: 26 additions & 4 deletions src/components/common/Button/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,32 @@ import React, { ComponentPropsWithoutRef } from 'react';
import { SizeType } from '@/types/size';
import { SpecialVariantType } from '@/types/variant';

const ButtonVariantStyles = ({ theme, variant }: { theme: Theme; variant: SpecialVariantType }) => {
type ButtonVariantType = SpecialVariantType | 'default';

const ButtonVariantStyles = ({ theme, variant }: { theme: Theme; variant: ButtonVariantType }) => {
switch (variant) {
case 'secondary':
return css`
background-color: ${theme.colors.secondary};
&:hover {
opacity: 0.8;
}
`;
case 'danger':
return css`
color: ${theme.colors.white};
background-color: ${theme.colors.danger};
&:hover {
opacity: 0.8;
}
`;
case 'warning':
return css`
color: ${theme.colors.white};
background-color: ${theme.colors.warning};
&:hover {
opacity: 0.8;
}
`;
case 'outline':
return css`
Expand All @@ -34,10 +45,21 @@ const ButtonVariantStyles = ({ theme, variant }: { theme: Theme; variant: Specia
background-color: transparent;
`;
case 'primary':
default:
return css`
color: white;
background-color: ${theme.colors.primary};
&:hover {
opacity: 0.8;
}
`;
default:
return css`
color: ${theme.colors.black};
background-color: ${theme.colors.background};
border: 1px solid ${theme.colors.gray['300']};
&:hover {
opacity: 0.8;
}
`;
}
};
Expand Down Expand Up @@ -103,7 +125,7 @@ export interface ButtonVariantProps {
/*
Button variant
*/
variant?: SpecialVariantType;
variant?: ButtonVariantType;
/*
Button size
*/
Expand All @@ -116,7 +138,7 @@ export interface ButtonVariantProps {
export interface ButtonProps extends ComponentPropsWithoutRef<'button'>, ButtonVariantProps {}

export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant = 'primary', size = 'md', children, full = false, ...props }, ref) => {
({ className, variant = 'default', size = 'md', children, full = false, ...props }, ref) => {
return (
<StyledButton ref={ref} className={className} variant={variant} full={full} size={size} {...props}>
{children}
Expand Down
6 changes: 3 additions & 3 deletions src/components/common/Dialog/Dialog.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import * as React from 'react';

import useModal from '@/hooks/useModal';

import DialogContext from './DialogContext';

interface DialogProps extends React.ComponentPropsWithoutRef<'div'> {}
Expand All @@ -9,13 +11,11 @@ interface DialogProps extends React.ComponentPropsWithoutRef<'div'> {}
* @returns
*/
export const Dialog = ({ children, ...props }: DialogProps) => {
const [showModal, setShowModal] = React.useState<boolean>(false);
const [showModal, setShowModal] = useModal();

return (
<DialogContext.Provider value={{ showModal, setShowModal }} {...props}>
{children}
</DialogContext.Provider>
);
};

// const DialogProvider = styled.div``;
23 changes: 0 additions & 23 deletions src/components/common/Dialog/DialogContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,29 +71,6 @@ export const DialogContent = React.forwardRef<HTMLDivElement, ModalProps>(
},
);

export const DialogHeader = styled.div`
display: flex;
flex-direction: column;
`;

export const DialogFooter = styled.div<{ justify?: React.CSSProperties['justifyContent'] }>`
display: flex;
flex-direction: row;
gap: 0.5rem;
align-items: flex-start;
justify-content: ${({ justify }) => justify || 'flex-end'};
`;

export const DialogTitle = styled.h2`
font-weight: 600;
line-height: 1;
margin: 0px;
`;
export const DialogDescription = styled.p`
margin: 0.375rem 0px;
font-weight: 300;
`;

const enter = keyframes`
0% { opacity: 0; }
100% { opacity: 1; }
Expand Down
26 changes: 26 additions & 0 deletions src/components/common/Dialog/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,30 @@
import styled from '@emotion/styled';

/* eslint-disable react-refresh/only-export-components */
export * from './Dialog';
export * from './DialogContent';
export * from './DialogToggle';
export * from './DialogClose';

export const DialogHeader = styled.div`
display: flex;
flex-direction: column;
`;

export const DialogFooter = styled.div<{ justify?: React.CSSProperties['justifyContent'] }>`
display: flex;
flex-direction: row;
gap: 0.5rem;
align-items: flex-start;
justify-content: ${({ justify }) => justify || 'flex-end'};
`;

export const DialogTitle = styled.h2`
font-weight: 600;
line-height: 1;
margin: 0px;
`;
export const DialogDescription = styled.p`
margin: 0.375rem 0px;
font-weight: 300;
`;
24 changes: 24 additions & 0 deletions src/components/common/Dropdown/Dropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import * as React from 'react';

import useModal from '@/hooks/useModal';

import DropdownContext, { DropdownAlignType } from './DropdownContext';

interface DropdownProps extends React.ComponentPropsWithoutRef<'div'> {
align?: DropdownAlignType;
}

/**
* Dropdown Context Provider
* @returns
*/
export const Dropdown = ({ children, align = 'center', ...props }: DropdownProps) => {
const [showModal, setShowModal] = useModal();
const [toggleRect, setToggleRect] = React.useState<DOMRect>();

return (
<DropdownContext.Provider value={{ showModal, setShowModal, toggleRect, setToggleRect, align }} {...props}>
{children}
</DropdownContext.Provider>
);
};
109 changes: 109 additions & 0 deletions src/components/common/Dropdown/DropdownContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { keyframes } from '@emotion/react';
import styled from '@emotion/styled';
import * as React from 'react';
import { createPortal } from 'react-dom';

import useContext from '@/hooks/useContext';
import { composeRefs } from '@/libs/ref';

import DropdownContext from './DropdownContext';

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

type PositionType = {
x: number;
y: number;
};

/**
* DropdownContent
* @returns
*/
export const DropdownContent = React.forwardRef<HTMLDivElement, ModalProps>(({ width, ...props }, ref) => {
const { showModal, setShowModal, toggleRect, align } = useContext(DropdownContext);
const modalRef = React.useRef<HTMLDivElement | null>(null);
const [reorgPos, setReorgPos] = React.useState<PositionType>({ x: 0, y: 0 });

const close = () => {
setShowModal(false);
};

React.useEffect(() => {
if (modalRef.current && toggleRect) {
const rect = modalRef.current.getBoundingClientRect();

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

switch (align) {
case 'center':
reorgPos.x = toggleRect.x + toggleRect.width / 2 - rect.width / 2;
break;
case 'start':
reorgPos.x = toggleRect.x;
break;
case 'end':
reorgPos.x = toggleRect.x + toggleRect.width - rect.width;
break;
}

if (isOverflowing) {
reorgPos.y = toggleRect.y - rect.height;
} else {
reorgPos.y = toggleRect.y + toggleRect.height;
}

setReorgPos(reorgPos);
}
}, [align, showModal, toggleRect]);

return (
<>
{showModal &&
createPortal(
<>
<ModalBG onClick={close} className="bmates-modal-bg" />
<Modal ref={composeRefs(modalRef, ref)} width={width} position={reorgPos} {...props}>
{props.children}
</Modal>
</>,
document.body,
)}
</>
);
});
DropdownContent.displayName = 'DropdownContent';

const enter = keyframes`
0% { opacity: 0; }
100% { opacity: 1; }
`;

const ModalBG = styled.div`
position: fixed;
background-color: transparent;
pointer-events: auto;
z-index: 50;
inset: 0;
`;

const Modal = styled.div<{ width?: React.CSSProperties['width']; position: PositionType }>`
display: grid;
min-width: max-content;
${({ width }) => width && `width: ${typeof width === 'string' ? width : `${width}px`};`}
padding: 0.25rem;
border-radius: 0.5rem;
position: fixed;
top: 0px;
left: 0px;
transform: ${({ position }) => `translate(${position.x}px, ${position.y}px)`};
background-color: white;
pointer-events: auto;
z-index: 50;
animation-name: ${enter};
animation-duration: 0.15s;
border: 1px solid ${({ theme }) => theme.colors.gray['300']};
box-shadow: 0px 2px 2px 0px ${({ theme }) => theme.colors.gray['300']};
`;
13 changes: 13 additions & 0 deletions src/components/common/Dropdown/DropdownContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import * as React from 'react';

export type DropdownAlignType = 'start' | 'center' | 'end';

interface DropdownContextType {
showModal: boolean;
setShowModal: (value: boolean) => void;
toggleRect: DOMRect | undefined;
setToggleRect: (value: DOMRect) => void;
align: DropdownAlignType;
}
const DropdownContext = React.createContext<DropdownContextType | null>(null);
export default DropdownContext;
Loading

0 comments on commit e4f9973

Please sign in to comment.