diff --git a/src/components/Card/Card.tsx b/src/components/Card/Card.tsx index a568d46d3..3103acaeb 100644 --- a/src/components/Card/Card.tsx +++ b/src/components/Card/Card.tsx @@ -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 { +export interface CardProps extends React.HTMLAttributes { children?: React.ReactNode rounded?: Rounded padding?: number @@ -13,50 +13,56 @@ export interface ICardProps extends React.HTMLAttributes { 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( + ( + { + 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 ( -
- {children} -
- ) -} + return ( +
+ {children} +
+ ) + } +) Card.displayName = 'Card' Card.defaultProps = { diff --git a/src/components/ConfirmDialog/ConfirmDialog.test.tsx b/src/components/ConfirmDialog/ConfirmDialog.test.tsx index a3c3f4f1e..9c6f8adeb 100644 --- a/src/components/ConfirmDialog/ConfirmDialog.test.tsx +++ b/src/components/ConfirmDialog/ConfirmDialog.test.tsx @@ -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: ( + + ) } describe('', () => { it('should be rendered with content and title', () => { - const { getByTestId } = render() - const confirmDialog = getByTestId('card-contain') + const { getByTestId, queryByTestId } = render( + + ) - 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() + const { getByRole, getByTestId } = render( + + ) + + const btnOpen = getByTestId('btn-action') + fireEvent.click(btnOpen) + const confirmBtn = getByRole('confirm-btn') fireEvent.click(confirmBtn) diff --git a/src/components/ConfirmDialog/ConfirmDialog.tsx b/src/components/ConfirmDialog/ConfirmDialog.tsx index f8c186f3a..7c4a52900 100644 --- a/src/components/ConfirmDialog/ConfirmDialog.tsx +++ b/src/components/ConfirmDialog/ConfirmDialog.tsx @@ -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 ( - - {position?.show && ( - - {title && ( - - {title} - - )} + 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 -
- {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} + + {isVisible && ( + e.stopPropagation()} + ref={popperElement} + rounded="lg" + className={composeClasses('w-max bg-white', className)} + width={width} + > + {title && ( + + {title} + + )} + {children} +
+ {typeof handleCancel !== 'undefined' && ( + + )} - )} - -
-
- )} - +
+
+ )} + + ) } diff --git a/src/components/Filters/FilterDate.test.tsx b/src/components/Filters/FilterDate.test.tsx index 962b8f5f6..629266c57 100644 --- a/src/components/Filters/FilterDate.test.tsx +++ b/src/components/Filters/FilterDate.test.tsx @@ -1,5 +1,5 @@ -import { fireEvent, render } from '@testing-library/react' import { vi } from 'vitest' +import { RenderResult, fireEvent, render } from '@testing-library/react' import FilterDate, { FilterDateProps } from './FilterDate' const onApply = vi.fn() @@ -8,7 +8,12 @@ const onReset = vi.fn() const defaultProps: FilterDateProps = { onApply, onReset, - position: { show: true, left: 0, top: 0 } + actionContent: +} + +function openDialog(getByTestId: RenderResult['getByTestId']) { + const btnOpen = getByTestId('btn-action') + fireEvent.click(btnOpen) } describe('', () => { @@ -18,13 +23,17 @@ describe('', () => { }) it('should call a function when the apply and reset button are clicked', () => { - const { getByRole } = render() + const { getByRole, getByTestId } = render() + + openDialog(getByTestId) const applyBtn = getByRole('confirm-btn') fireEvent.click(applyBtn) expect(onApply).toHaveBeenCalled() expect(onApply).toHaveBeenCalledTimes(1) + openDialog(getByTestId) + const resetBtn = getByRole('cancel-btn') fireEvent.click(resetBtn) expect(onReset).toHaveBeenCalled() @@ -32,7 +41,11 @@ describe('', () => { }) it('should be call handleChange when changing the value of the date picker', () => { - const { getByRole, getAllByRole } = render() + const { getByRole, getAllByRole, getByTestId } = render( + + ) + + openDialog(getByTestId) fireEvent.click(getByRole('active-calendar')) fireEvent.click(getAllByRole('month')[4]) @@ -46,6 +59,9 @@ describe('', () => { const { getByTestId } = render( ) + + openDialog(getByTestId) + const filter = getByTestId('card-contain') expect(filter).toHaveTextContent('Year') diff --git a/src/components/Filters/FilterDate.tsx b/src/components/Filters/FilterDate.tsx index 350e13aa8..56e494801 100644 --- a/src/components/Filters/FilterDate.tsx +++ b/src/components/Filters/FilterDate.tsx @@ -1,5 +1,7 @@ import { ChangeEvent, useState } from 'react' -import ConfirmDialog from '../ConfirmDialog/ConfirmDialog' +import ConfirmDialog, { + ConfirmDialogAddonsProps +} from '../ConfirmDialog/ConfirmDialog' import MonthInput from '../Form/Input/MonthInput' import YearInput from '../Form/Input/YearInput' import Text from '../Typography/Text' @@ -9,7 +11,7 @@ export interface IFilterDateValue { year: string } -export interface FilterDateProps { +export interface FilterDateProps extends ConfirmDialogAddonsProps { /** * Title displayed in the ConfirmDialog */ @@ -22,10 +24,6 @@ export interface FilterDateProps { * Text displayed on the button to reset */ textResetBtn?: string - /** - * The position in which the ConfirmDialog will be displayed - */ - position?: { show: boolean; left: number; top: number } /** * The class name to apply to the ConfirmDialog */ @@ -53,12 +51,13 @@ const FilterDate = ({ title, textApplyBtn, textResetBtn, - position, className, width, language, onApply, - onReset + onReset, + actionContent, + usePortal }: FilterDateProps) => { const [date, setDate] = useState({ month: '', year: '' }) @@ -80,14 +79,16 @@ const FilterDate = ({ return (
diff --git a/src/components/Filters/FilterRange.test.tsx b/src/components/Filters/FilterRange.test.tsx index f23e42465..c25ce9e5e 100644 --- a/src/components/Filters/FilterRange.test.tsx +++ b/src/components/Filters/FilterRange.test.tsx @@ -1,4 +1,4 @@ -import { fireEvent, render } from '@testing-library/react' +import { RenderResult, fireEvent, render } from '@testing-library/react' import { vi } from 'vitest' import FilterRange, { FilterRangeProps, IRange } from './FilterRange' @@ -8,7 +8,12 @@ const onReset = vi.fn() const defaultProps: FilterRangeProps = { onApply, onReset, - position: { show: true, left: 0, top: 0 } + actionContent: +} + +function openDialog(getByTestId: RenderResult['getByTestId']) { + const btnOpen = getByTestId('btn-action') + fireEvent.click(btnOpen) } describe('', () => { @@ -17,20 +22,24 @@ describe('', () => { }) it('should be rendered', () => { - const { container } = render() + const { container } = render() expect(container).toBeDefined() }) it('should call a function when the apply and reset button are clicked', () => { - const { getByRole } = render( + const { getByRole, getByTestId } = render( ) + openDialog(getByTestId) + const applyBtn = getByRole('confirm-btn') fireEvent.click(applyBtn) expect(onApply).toHaveBeenCalled() expect(onApply).toHaveBeenCalledTimes(1) + openDialog(getByTestId) + const resetBtn = getByRole('cancel-btn') fireEvent.click(resetBtn) expect(onReset).toHaveBeenCalled() @@ -39,6 +48,9 @@ describe('', () => { it('the min and max values should change', () => { const { getByTestId } = render() + + openDialog(getByTestId) + const card = getByTestId('card-contain') const inputMin = card.querySelector( 'input[name="minVal"]' @@ -58,6 +70,9 @@ describe('', () => { const { getByTestId, getByRole } = render( ) + + openDialog(getByTestId) + const card = getByTestId('card-contain') const inputMin = card.querySelector( 'input[name="minVal"]' diff --git a/src/components/Filters/FilterRange.tsx b/src/components/Filters/FilterRange.tsx index dd3516817..2f0725dbc 100644 --- a/src/components/Filters/FilterRange.tsx +++ b/src/components/Filters/FilterRange.tsx @@ -1,5 +1,7 @@ import { ChangeEvent, useState } from 'react' -import ConfirmDialog from 'components/ConfirmDialog/ConfirmDialog' +import ConfirmDialog, { + ConfirmDialogAddonsProps +} from 'components/ConfirmDialog/ConfirmDialog' import Input from 'components/Form/Input' import Text from 'components/Typography/Text' @@ -8,7 +10,7 @@ export interface IRange { maxVal?: number } -export interface FilterRangeProps { +export interface FilterRangeProps extends ConfirmDialogAddonsProps { /** * Title displayed in the ConfirmDialog */ @@ -45,10 +47,6 @@ export interface FilterRangeProps { * Text displayed on the button to reset */ textResetBtn?: string - /** - * The position in which the ConfirmDialog will be displayed - */ - position?: { show: boolean; left: number; top: number } /** * The class name to apply to the ConfirmDialog */ @@ -78,9 +76,10 @@ const FilterRange = ({ defaultMax, textApplyBtn = 'Apply', textResetBtn = 'Reset', - position = { show: false, left: 0, top: 0 }, className, width, + actionContent, + usePortal, onApply, onReset }: FilterRangeProps) => { @@ -130,14 +129,16 @@ const FilterRange = ({ return (
diff --git a/src/components/Filters/FilterRangeSlider.test.tsx b/src/components/Filters/FilterRangeSlider.test.tsx index d347fe6a9..6c14deaf8 100644 --- a/src/components/Filters/FilterRangeSlider.test.tsx +++ b/src/components/Filters/FilterRangeSlider.test.tsx @@ -1,4 +1,4 @@ -import { fireEvent, render } from '@testing-library/react' +import { RenderResult, fireEvent, render } from '@testing-library/react' import { vi } from 'vitest' import { RangeValues } from 'components/RangeSlider/RangeSlider' import FilterRangeSlider, { FilterRangeSliderProps } from './FilterRangeSlider' @@ -8,7 +8,12 @@ const onReset = vi.fn() const defaultProps: FilterRangeSliderProps = { onApply, onReset, - position: { show: true, left: 0, top: 0 } + actionContent: +} + +function openDialog(getByTestId: RenderResult['getByTestId']) { + const btnOpen = getByTestId('btn-action') + fireEvent.click(btnOpen) } describe('', () => { @@ -17,20 +22,24 @@ describe('', () => { }) it('should be rendered', () => { - const { container } = render() + const { container } = render() expect(container).toBeDefined() }) it('should call a function when the apply and reset button are clicked', () => { - const { getByRole } = render( + const { getByRole, getByTestId } = render( ) + openDialog(getByTestId) + const applyBtn = getByRole('confirm-btn') fireEvent.click(applyBtn) expect(onApply).toHaveBeenCalled() expect(onApply).toHaveBeenCalledTimes(1) + openDialog(getByTestId) + const resetBtn = getByRole('cancel-btn') fireEvent.click(resetBtn) expect(onReset).toHaveBeenCalled() diff --git a/src/components/Filters/FilterRangeSlider.tsx b/src/components/Filters/FilterRangeSlider.tsx index 2f8207e90..1b685a2d9 100644 --- a/src/components/Filters/FilterRangeSlider.tsx +++ b/src/components/Filters/FilterRangeSlider.tsx @@ -1,9 +1,11 @@ import { useCallback, useState } from 'react' import RangeSlider, { RangeValues } from '../RangeSlider/RangeSlider' -import ConfirmDialog from '../ConfirmDialog/ConfirmDialog' +import ConfirmDialog, { + ConfirmDialogAddonsProps +} from '../ConfirmDialog/ConfirmDialog' import Text from '../Typography/Text' -export interface FilterRangeSliderProps { +export interface FilterRangeSliderProps extends ConfirmDialogAddonsProps { /** * Title displayed in the ConfirmDialog */ @@ -44,10 +46,6 @@ export interface FilterRangeSliderProps { * Text displayed on the button to reset */ textResetBtn?: string - /** - * The position in which the ConfirmDialog will be displayed - */ - position?: { show: boolean; left: number; top: number } /** * The class name to apply to the ConfirmDialog */ @@ -78,11 +76,12 @@ const FilterRangeSlider = ({ unitName = 'Km', textApplyBtn = 'Apply', textResetBtn = 'Reset', - position = { show: false, left: 0, top: 0 }, className, width, onApply, - onReset + onReset, + actionContent, + usePortal }: FilterRangeSliderProps) => { const initValue = { min: initMinValue ?? min, max: initMaxValue ?? max } const [range, setRange] = useState(initValue) @@ -100,14 +99,16 @@ const FilterRangeSlider = ({ return (
diff --git a/src/components/Filters/FilterSelect.test.tsx b/src/components/Filters/FilterSelect.test.tsx index 557048c25..e03b6af5b 100644 --- a/src/components/Filters/FilterSelect.test.tsx +++ b/src/components/Filters/FilterSelect.test.tsx @@ -1,4 +1,4 @@ -import { fireEvent, render } from '@testing-library/react' +import { RenderResult, fireEvent, render } from '@testing-library/react' import { vi } from 'vitest' import FilterSelect, { IRadioItems } from './FilterSelect' @@ -18,24 +18,39 @@ const onApply = vi.fn((val: string) => console.log(val)) const onReset = vi.fn() const defaultProps = { listItems: list, - position: { show: true, left: 0, top: 0 }, onApply: onApply, - onReset: onReset + onReset: onReset, + actionContent: +} + +function openDialog(getByTestId: RenderResult['getByTestId']) { + const btnOpen = getByTestId('btn-action') + fireEvent.click(btnOpen) } describe('', () => { it('should be rendered with 3 Child Radios', () => { - const { getByRole } = render() - const filterSelect = getByRole('radio-group') + const { getByRole, getByTestId } = render( + + ) + openDialog(getByTestId) + + const filterSelect = getByRole('radio-group') expect(filterSelect).toBeDefined() expect(filterSelect.childElementCount).toEqual(3) }) it('should call a function when the apply and reset button are clicked', () => { - const { getByRole } = render() + const { getByRole, getByTestId } = render( + + ) + + openDialog(getByTestId) const applyBtn = getByRole('confirm-btn') fireEvent.click(applyBtn) + + openDialog(getByTestId) const resetBtn = getByRole('cancel-btn') fireEvent.click(resetBtn) @@ -46,7 +61,12 @@ describe('', () => { }) it('the selected value must be checked', () => { - const { getByRole } = render() + const { getByRole, getByTestId } = render( + + ) + + openDialog(getByTestId) + const radioList = Array.from(getByRole('radio-group').children) const radio = radioList[0].querySelector( 'input[type="radio"]' diff --git a/src/components/Filters/FilterSelect.tsx b/src/components/Filters/FilterSelect.tsx index 00907f258..951d92f9a 100644 --- a/src/components/Filters/FilterSelect.tsx +++ b/src/components/Filters/FilterSelect.tsx @@ -1,5 +1,7 @@ import { ChangeEvent, useState } from 'react' -import ConfirmDialog from 'components/ConfirmDialog/ConfirmDialog' +import ConfirmDialog, { + ConfirmDialogAddonsProps +} from 'components/ConfirmDialog/ConfirmDialog' import Radio from 'components/Radio/Radio' import RadioGroup from 'components/Radio/RadioGroup' @@ -10,7 +12,7 @@ export interface IRadioItems { } } -export interface IFilterSelect { +export interface IFilterSelect extends ConfirmDialogAddonsProps { /** * Title displayed in the ConfirmDialog */ @@ -31,10 +33,6 @@ export interface IFilterSelect { * Text displayed on the button to reset */ textResetBtn?: string - /** - * The position in which the ConfirmDialog will be displayed - */ - position?: { show: boolean; left: number; top: number } /** * The class name to apply to the ConfirmDialog */ @@ -60,11 +58,12 @@ const FilterSelect = ({ selectedValue: defaultValue = '', textApplyBtn = 'Apply', textResetBtn = 'Reset', - position = { show: false, left: 0, top: 0 }, className, width, onApply, - onReset + onReset, + actionContent, + usePortal }: IFilterSelect) => { const [selectedValue, setSelectedValue] = useState(defaultValue) @@ -81,14 +80,16 @@ const FilterSelect = ({ return ( {Object.entries(listItems).map(([key, { label, disabled }]) => ( diff --git a/src/components/Filters/FilterSelectMulti.test.tsx b/src/components/Filters/FilterSelectMulti.test.tsx index ad023ff72..8472b0592 100644 --- a/src/components/Filters/FilterSelectMulti.test.tsx +++ b/src/components/Filters/FilterSelectMulti.test.tsx @@ -1,4 +1,4 @@ -import { fireEvent, render } from '@testing-library/react' +import { RenderResult, fireEvent, render } from '@testing-library/react' import { vi } from 'vitest' import FilterSelectMulti, { ICheckBoxItems } from './FilterSelectMulti' @@ -16,22 +16,39 @@ const defaultProps = { initialItemList: list, position: { show: true, left: 0, top: 0 }, onApply: onApply, - onReset: onReset + onReset: onReset, + actionContent: ( + + ) +} + +function openDialog(getByTestId: RenderResult['getByTestId']) { + const btnOpen = getByTestId('btn-action') + fireEvent.click(btnOpen) } describe('', () => { it('should be rendered with 3 Child Checkbox', () => { - const { getByRole } = render() - const filterSelectMulti = getByRole('checkbox-group') + const { getByRole, getByTestId } = render( + + ) + openDialog(getByTestId) + const filterSelectMulti = getByRole('checkbox-group') expect(filterSelectMulti).toBeDefined() expect(filterSelectMulti.childElementCount).toEqual(3) }) it('should call a function when the apply and reset button are clicked', () => { - const { getByRole } = render() + const { getByRole, getByTestId } = render( + + ) + + openDialog(getByTestId) const applyBtn = getByRole('confirm-btn') fireEvent.click(applyBtn) + + openDialog(getByTestId) const resetBtn = getByRole('cancel-btn') fireEvent.click(resetBtn) @@ -42,7 +59,11 @@ describe('', () => { }) it('the selected value must be checked and unchecked', () => { - const { getByRole } = render() + const { getByRole, getByTestId } = render( + + ) + openDialog(getByTestId) + const checkboxList = Array.from(getByRole('checkbox-group').children) const checkbox = checkboxList[0].querySelector( 'input[type="checkbox"]' diff --git a/src/components/Filters/FilterSelectMulti.tsx b/src/components/Filters/FilterSelectMulti.tsx index 1287a2cf7..ab8700ae9 100644 --- a/src/components/Filters/FilterSelectMulti.tsx +++ b/src/components/Filters/FilterSelectMulti.tsx @@ -1,6 +1,8 @@ import { ChangeEvent, useState } from 'react' import Checkbox from 'components/Checkbox/Checkbox' -import ConfirmDialog from 'components/ConfirmDialog/ConfirmDialog' +import ConfirmDialog, { + ConfirmDialogAddonsProps +} from 'components/ConfirmDialog/ConfirmDialog' import FormControlLabel from 'components/FormControl/FormControlLabel' export interface ICheckBoxItems { @@ -11,7 +13,7 @@ export interface ICheckBoxItems { } } -export interface FilterSelectMultiProps { +export interface FilterSelectMultiProps extends ConfirmDialogAddonsProps { /** * Title displayed in the ConfirmDialog */ @@ -28,10 +30,6 @@ export interface FilterSelectMultiProps { * Text displayed on the button to reset */ textResetBtn?: string - /** - * The position in which the ConfirmDialog will be displayed - */ - position?: { show: boolean; left: number; top: number } /** * The class name to apply to the ConfirmDialog */ @@ -56,11 +54,12 @@ const FilterSelectMulti = ({ initialItemList, textApplyBtn = 'Apply', textResetBtn = 'Reset', - position = { show: false, left: 0, top: 0 }, className, width, onApply, - onReset + onReset, + actionContent, + usePortal }: FilterSelectMultiProps) => { const [itemList, setItemList] = useState(initialItemList) @@ -87,14 +86,16 @@ const FilterSelectMulti = ({ return (
{Object.entries(itemList).map(([key, { label, checked, disabled }]) => ( diff --git a/src/hooks/index.ts b/src/hooks/index.ts index d3fc5a700..4c252900a 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -15,3 +15,6 @@ export * from './useInputFocused' export { default as useLabelScalded } from './useLabelScalded' export * from './useLabelScalded' + +export { default as useIsomorphicEffect } from './useIsomorphicEffect' +export * from './useIsomorphicEffect' diff --git a/src/hooks/useIsomorphicEffect.ts b/src/hooks/useIsomorphicEffect.ts new file mode 100644 index 000000000..f4b6632cc --- /dev/null +++ b/src/hooks/useIsomorphicEffect.ts @@ -0,0 +1,2 @@ +import { useEffect, useLayoutEffect } from 'react' +export default typeof document !== 'undefined' ? useLayoutEffect : useEffect diff --git a/src/hooks/useTooltip.ts b/src/hooks/useTooltip.ts index 144564dfd..36a55ff94 100644 --- a/src/hooks/useTooltip.ts +++ b/src/hooks/useTooltip.ts @@ -1,6 +1,7 @@ -import { useState, useRef, useEffect, useCallback } from 'react' +import { useState, useRef, useEffect, useCallback, useMemo } from 'react' import { createPopper, Instance, preventOverflow, flip } from '@popperjs/core' import { PositionVariants } from '../interfaces/types' +import useIsomorphicEffect from './useIsomorphicEffect' export interface TooltipParams { placement?: PositionVariants @@ -58,50 +59,75 @@ export default function useTooltip(params?: TooltipParams) { entryTimer.current = setTimeout(() => { if (isMounted.current) setIsVisible(true) }, showDelay) - }, [isMounted, showDelay]) + }, [showDelay]) const handleMouseLeave = useCallback(() => { if (leaveTimer?.current) clearTimeout(leaveTimer.current) leaveTimer.current = setTimeout(() => { if (isMounted.current) setIsVisible(false) }, hideDelay) - }, [isMounted, hideDelay]) + }, [hideDelay]) const handleOnClick = useCallback(() => { - if (isMounted.current) setIsVisible((prev) => !prev) - }, [isMounted]) + if (isMounted.current) { + setIsVisible((prev) => !prev) + } + }, []) - useEffect(() => { - if (!isVisible || !refElement.current || !popperElement.current) { - popperInstance.current?.destroy() + const handleSetIsVisible = useCallback((value: boolean) => { + if (isMounted.current) { + setIsVisible(value) + } + }, []) + + const popperOptions = useMemo(() => { + return { + placement: placement, + modifiers: [ + { + name: 'offset', + options: { + offset: [0, 10] + } + }, + preventOverflow, + flip, + ...modifiers + ], + removeOnDestroy: true + } + }, [placement, modifiers]) + + useIsomorphicEffect(() => { + if (!popperInstance.current) return + popperInstance.current.setOptions(popperOptions) + }, [popperOptions]) + + useIsomorphicEffect(() => { + if (!refElement.current || !popperElement.current) { return } - popperInstance.current = createPopper( + const instance = createPopper( refElement.current, popperElement.current, - { - placement: placement, - modifiers: [ - { - name: 'offset', - options: { - offset: [0, 10] - } - }, - preventOverflow, - flip, - ...modifiers - ] - } + popperOptions ) - }, [isVisible, placement]) + + popperInstance.current = instance + + return () => { + instance.destroy() + popperInstance.current = null + } + }, [refElement?.current, popperElement?.current]) return { isVisible, handleMouseEnter, handleMouseLeave, handleOnClick, + handleSetIsVisible, refs: { refElement, popperElement, popperInstance } } } diff --git a/src/interfaces/types.ts b/src/interfaces/types.ts index d7ed3b536..a0e18576a 100644 --- a/src/interfaces/types.ts +++ b/src/interfaces/types.ts @@ -282,4 +282,16 @@ export type ButtonVariant = | 'danger' | 'outlineWhiteRed' -export type PositionVariants = 'right' | 'left' | 'top' | 'bottom' +export type PositionVariants = + | 'right' + | 'right-start' + | 'right-end' + | 'left' + | 'left-start' + | 'left-end' + | 'top' + | 'top-start' + | 'top-end' + | 'bottom' + | 'bottom-start' + | 'bottom-end' diff --git a/src/stories/ConfirmDialog.stories.tsx b/src/stories/ConfirmDialog.stories.tsx new file mode 100644 index 000000000..18dd8646c --- /dev/null +++ b/src/stories/ConfirmDialog.stories.tsx @@ -0,0 +1,28 @@ +import React from 'react' +import { ComponentStory, ComponentMeta } from '@storybook/react' +import ConfirmDialogComponent from '../components/ConfirmDialog' + +export default { + title: 'Components/ConfirmDialog', + component: ConfirmDialogComponent +} as ComponentMeta + +const Template: ComponentStory = (args) => ( + +) + +export const ConfirmDialog = Template.bind({}) +ConfirmDialog.args = { + actionContent: ( + + ), + children: ( +
+

Hi, im the popover

+
+ ), + handleConfirm: () => console.log('Confirmed'), + handleCancel: () => console.log('Canceled') +} diff --git a/src/stories/FilterDate.stories.tsx b/src/stories/FilterDate.stories.tsx index baff273e8..dcb99f20c 100644 --- a/src/stories/FilterDate.stories.tsx +++ b/src/stories/FilterDate.stories.tsx @@ -1,6 +1,5 @@ -import React, { useState } from 'react' +import React from 'react' import { FilterIcon } from '@heroicons/react/outline' -import { useRef } from '@storybook/addons' import { ComponentMeta, ComponentStory } from '@storybook/react' import { FilterDate as FilterDateComponent, Text } from '../components' @@ -14,39 +13,22 @@ const handleChange = (value: string) => { } const Template: ComponentStory = (args) => { - const refButton = useRef(null) - const [position, setPosition] = useState({ show: false, left: 0, top: 0 }) - - const handleClick = () => { - if (refButton.current !== null) { - const { offsetLeft, offsetTop } = refButton.current - setPosition((current) => ({ - ...position, - show: !current.show, - left: offsetLeft + 10, - top: offsetTop + 30 - })) - } - } - - return ( -
- - -
- ) + return } export const FilterDate = Template.bind({}) FilterDate.args = { title: 'Nombre filtro', onApply: handleChange, + actionContent: ( + + ), width: 300 } diff --git a/src/stories/FilterRange.stories.tsx b/src/stories/FilterRange.stories.tsx index 097930216..968e131a6 100644 --- a/src/stories/FilterRange.stories.tsx +++ b/src/stories/FilterRange.stories.tsx @@ -1,4 +1,4 @@ -import React, { useRef, useState } from 'react' +import React from 'react' import { ComponentMeta, ComponentStory } from '@storybook/react' import { FilterRange as FilterRangeComponent, Text } from '../components' import { FilterIcon } from '@heroicons/react/outline' @@ -13,34 +13,7 @@ const handleChange = (value: string) => { } const Template: ComponentStory = (args) => { - const refButton = useRef(null) - const [position, setPosition] = useState({ show: false, left: 0, top: 0 }) - - const handleClick = () => { - if (refButton.current !== null) { - const { offsetLeft, offsetTop } = refButton.current - setPosition((current) => ({ - ...position, - show: !current.show, - left: offsetLeft + 10, - top: offsetTop + 30 - })) - } - } - - return ( -
- - -
- ) + return } export const FilterRange = Template.bind({}) @@ -48,5 +21,15 @@ FilterRange.args = { title: 'Nombre del filtro', min: 30, max: 300, + actionContent: ( + + ), onApply: handleChange } diff --git a/src/stories/FilterRangeSlider.stories.tsx b/src/stories/FilterRangeSlider.stories.tsx index 8fcf0d31c..4cc97196f 100644 --- a/src/stories/FilterRangeSlider.stories.tsx +++ b/src/stories/FilterRangeSlider.stories.tsx @@ -1,6 +1,6 @@ +import React from 'react' import { FilterIcon } from '@heroicons/react/outline' import { ComponentMeta, ComponentStory } from '@storybook/react' -import React, { useRef, useState } from 'react' import { FilterRangeSlider as FilterRangeSliderComponent, IRangeSlider, @@ -13,34 +13,7 @@ export default { } as ComponentMeta const Template: ComponentStory = (args) => { - const refButton = useRef(null) - const [position, setPosition] = useState({ show: false, left: 0, top: 0 }) - - const handleClick = () => { - if (refButton.current !== null) { - const { offsetLeft, offsetTop } = refButton.current - setPosition((current) => ({ - ...position, - show: !current.show, - left: offsetLeft + 10, - top: offsetTop + 30 - })) - } - } - - return ( -
- - -
- ) + return } export const FilterRangeSlider = Template.bind({}) @@ -49,5 +22,15 @@ FilterRangeSlider.args = { min: 50, max: 150, width: 188, + actionContent: ( + + ), onApply: (range: IRangeSlider) => console.log(range) } diff --git a/src/stories/FilterSelect.stories.tsx b/src/stories/FilterSelect.stories.tsx index 637a47e96..515d5adab 100644 --- a/src/stories/FilterSelect.stories.tsx +++ b/src/stories/FilterSelect.stories.tsx @@ -1,4 +1,4 @@ -import React, { useRef, useState } from 'react' +import React from 'react' import { ComponentMeta, ComponentStory } from '@storybook/react' import { FilterSelect as FilterSelectComponent, @@ -30,34 +30,7 @@ const handleChange = (value: string) => { } const Template: ComponentStory = (args) => { - const refButton = useRef(null) - const [position, setPosition] = useState({ show: false, left: 0, top: 0 }) - - const handleClick = () => { - if (refButton.current !== null) { - const { offsetLeft, offsetTop } = refButton.current - setPosition((current) => ({ - ...position, - show: !current.show, - left: offsetLeft + 10, - top: offsetTop + 30 - })) - } - } - - return ( -
- - -
- ) + return } export const FilterSelect = Template.bind({}) @@ -65,5 +38,15 @@ FilterSelect.args = { title: 'Nombre del filtro', listItems: list, selectedValue: 'B', + actionContent: ( + + ), onApply: handleChange } diff --git a/src/stories/FilterSelectMulti.stories.tsx b/src/stories/FilterSelectMulti.stories.tsx index 5650f907b..6eefea97a 100644 --- a/src/stories/FilterSelectMulti.stories.tsx +++ b/src/stories/FilterSelectMulti.stories.tsx @@ -1,4 +1,4 @@ -import React, { useRef, useState } from 'react' +import React from 'react' import { ComponentMeta, ComponentStory } from '@storybook/react' import { FilterSelectMulti as FilterSelectMultiComponent, @@ -36,39 +36,23 @@ const handleChange = (value: string) => { } const Template: ComponentStory = (args) => { - const refButton = useRef(null) - const [position, setPosition] = useState({ show: false, left: 0, top: 0 }) - - const handleClick = () => { - if (refButton.current !== null) { - const { offsetLeft, offsetTop } = refButton.current - setPosition((current) => ({ - ...position, - show: !current.show, - left: offsetLeft + 10, - top: offsetTop + 30 - })) - } - } - - return ( -
- - -
- ) + return } export const FilterSelectMulti = Template.bind({}) FilterSelectMulti.args = { title: 'Nombre del filtro', initialItemList: list, - onApply: handleChange + actionContent: ( + + ), + onApply: handleChange, + usePortal: false }