Skip to content
Open
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
90 changes: 48 additions & 42 deletions src/components/Card/Card.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { useCallback } from 'react'
import { forwardRef, useCallback } from 'react'
import { composeClasses } from 'lib/classes'
import { Rounded } from '../../interfaces/types'

export interface ICardProps extends React.HTMLAttributes<HTMLDivElement> {
export interface CardProps extends React.HTMLAttributes<HTMLDivElement> {
children?: React.ReactNode
rounded?: Rounded
padding?: number
Expand All @@ -13,50 +13,56 @@ export interface ICardProps extends React.HTMLAttributes<HTMLDivElement> {
width?: number | string
}

const Card = ({
children,
rounded,
height,
width,
padding,
paddingX,
paddingY,
className,
style,
...otherProps
}: ICardProps) => {
const getPadding = useCallback(() => {
if (paddingX && paddingY) {
return `px-${paddingX} py-${paddingY}`
}
const Card = forwardRef<HTMLDivElement, CardProps>(
(
{
children,
rounded,
height,
width,
padding,
paddingX,
paddingY,
className,
style,
...otherProps
},
ref
) => {
const getPadding = useCallback(() => {
if (paddingX && paddingY) {
return `px-${paddingX} py-${paddingY}`
}

if (paddingX) {
return `px-${paddingX}`
}
if (paddingX) {
return `px-${paddingX}`
}

if (paddingY) {
return `py-${paddingY}`
}
if (paddingY) {
return `py-${paddingY}`
}

return `p-${padding}`
}, [padding, paddingY, paddingX])
return `p-${padding}`
}, [padding, paddingY, paddingX])

return (
<div
data-testid="card-contain"
style={{ ...style, height, width }}
className={composeClasses(
'shadow-sm border',
rounded && `rounded-${rounded}`,
getPadding(),
className
)}
{...otherProps}
>
{children}
</div>
)
}
return (
<div
ref={ref}
data-testid="card-contain"
style={{ ...style, height, width }}
className={composeClasses(
'shadow-sm border',
rounded && `rounded-${rounded}`,
getPadding(),
className
)}
{...otherProps}
>
{children}
</div>
)
}
)

Card.displayName = 'Card'
Card.defaultProps = {
Expand Down
36 changes: 27 additions & 9 deletions src/components/ConfirmDialog/ConfirmDialog.test.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,44 @@
import { fireEvent, render } from '@testing-library/react'
import { vi } from 'vitest'
import ConfirmDialog, { IConfirmDialog } from './ConfirmDialog'
import { expect, vi } from 'vitest'
import ConfirmDialog, { ConfirmDialogProps } from './ConfirmDialog'

const onClick = vi.fn(() => 0)
const defaultProps: IConfirmDialog = {
const defaultProps: ConfirmDialogProps = {
title: 'This is a title',
children: 'Content',
onConfirm: onClick,
position: { show: true, left: 0, top: 0 }
handleConfirm: onClick,
actionContent: (
<button className="w-52" data-testid="btn-action">
This is the action content
</button>
)
}

describe('<FilterSelect/>', () => {
it('should be rendered with content and title', () => {
const { getByTestId } = render(<ConfirmDialog {...defaultProps} />)
const confirmDialog = getByTestId('card-contain')
const { getByTestId, queryByTestId } = render(
<ConfirmDialog {...defaultProps} />
)

expect(confirmDialog.children[0]).toHaveTextContent('This is a title')
const btnOpen = getByTestId('btn-action')

expect(queryByTestId('card-contain')).toBeNull()
fireEvent.click(btnOpen)

const confirmDialog = queryByTestId('card-contain')

expect(confirmDialog?.children[0]).toHaveTextContent('This is a title')
expect(confirmDialog).toHaveTextContent('Content')
})

it('should call a function when the apply button is clicked', () => {
const { getByRole } = render(<ConfirmDialog {...defaultProps} />)
const { getByRole, getByTestId } = render(
<ConfirmDialog {...defaultProps} />
)

const btnOpen = getByTestId('btn-action')
fireEvent.click(btnOpen)

const confirmBtn = getByRole('confirm-btn')
fireEvent.click(confirmBtn)

Expand Down
179 changes: 132 additions & 47 deletions src/components/ConfirmDialog/ConfirmDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,77 +1,162 @@
import { ReactNode } from 'react'
import {
Fragment,
ReactElement,
ReactNode,
cloneElement,
createElement,
useCallback,
useEffect
} from 'react'
import { composeClasses } from 'lib/classes'
import { Portal } from '../../common/Portal/Portal'
import useTooltip from 'hooks/useTooltip'
import { Portal } from 'common/Portal/Portal'
import Card from '../Card/Card'
import Text from '../Typography/Text'
import Button from '../Buttons/Button'

export interface IConfirmDialog {
export interface ConfirmDialogAddonsProps {
actionContent: ReactElement
usePortal?: boolean
}

export interface ConfirmDialogProps extends ConfirmDialogAddonsProps {
title?: string
children: ReactNode
textConfirmBtn?: string
textCancelBtn?: string
onConfirm: () => void
onCancel?: () => void
position?: { show: boolean; left: number; top: number }
handleCancel?: () => void
handleConfirm: () => void
preventCloseHandleCancel?: boolean
preventCloseHandleConfirm?: boolean
className?: string
width?: number | string
idRoot?: string
}

function generateUniqueId() {
const timestamp = new Date().getUTCMilliseconds()
return timestamp + Math.random().toString(36)
}

const DialogWrapper = ({
usePortal,
children
}: Partial<{
children: ReactNode
usePortal?: ConfirmDialogProps['usePortal']
}>) => {
const genericId = generateUniqueId()
const element = usePortal ? Portal : Fragment
return createElement(
element,
usePortal && ({ idRoot: `dialog-${genericId}` } as any),
children
)
}

const ConfirmDialog = ({
title,
actionContent,
children,
textConfirmBtn = 'Apply',
textCancelBtn = 'Reset',
position,
className,
width,
onConfirm,
onCancel,
idRoot
}: IConfirmDialog) => {
return (
<Portal idRoot={idRoot}>
{position?.show && (
<Card
rounded="lg"
className={composeClasses('w-max absolute bg-white', className)}
width={width}
style={{ left: position?.left, top: position?.top }}
>
{title && (
<Text variant="p" className="text-info mb-4 text-xxs font-semibold">
{title}
</Text>
)}
handleConfirm,
handleCancel,
usePortal = true,
preventCloseHandleCancel = false,
preventCloseHandleConfirm = false
}: ConfirmDialogProps) => {
const { refs, isVisible, handleOnClick, handleSetIsVisible } = useTooltip({
placement: 'bottom-end'
})

const { refElement, popperElement, popperInstance } = refs
const clonedChildren = cloneElement(actionContent, {
ref: refElement,
onClick: handleOnClick
})

{children}
const handleClickOutside = useCallback((e: globalThis.MouseEvent) => {
if (!popperInstance?.current) return

<div className="flex gap-4 justify-end mt-1.5">
{onCancel && (
if (
popperElement.current &&
!popperElement.current.contains(e.target as Node) &&
refElement.current &&
!refElement.current.contains(e.target as Node)
) {
handleSetIsVisible(false)
}
}, [])

useEffect(() => {
document.addEventListener('click', handleClickOutside)

return () => {
document.removeEventListener('click', handleClickOutside)
}
}, [])

return (
<>
{clonedChildren}
<DialogWrapper usePortal={usePortal}>
{isVisible && (
<Card
onClick={(e) => e.stopPropagation()}
ref={popperElement}
rounded="lg"
className={composeClasses('w-max bg-white', className)}
width={width}
>
{title && (
<Text
variant="p"
className="text-info mb-4 text-xxs font-semibold"
>
{title}
</Text>
)}
{children}
<div className="flex gap-4 justify-end mt-1.5">
{typeof handleCancel !== 'undefined' && (
<Button
role="cancel-btn"
variant="link"
className="underline text-xs disabled:opacity-75 text-black"
style={{ padding: 0, fontWeight: 600 }}
onClick={() => {
if (!preventCloseHandleCancel) {
handleSetIsVisible(false)
}

handleCancel()
}}
>
{textCancelBtn}
</Button>
)}
<Button
role="cancel-btn"
role="confirm-btn"
variant="link"
className="underline text-xs disabled:opacity-75 text-black"
className="underline text-xs disabled:opacity-75 text-blue-500"
style={{ padding: 0, fontWeight: 600 }}
onClick={onCancel}
onClick={() => {
if (!preventCloseHandleConfirm) {
handleSetIsVisible(false)
}

handleConfirm()
}}
>
{textCancelBtn}
{textConfirmBtn}
</Button>
)}
<Button
role="confirm-btn"
variant="link"
className="underline text-xs disabled:opacity-75 text-blue-500"
style={{ padding: 0, fontWeight: 600 }}
onClick={onConfirm}
>
{textConfirmBtn}
</Button>
</div>
</Card>
)}
</Portal>
</div>
</Card>
)}
</DialogWrapper>
</>
)
}

Expand Down
Loading