-
-
Notifications
You must be signed in to change notification settings - Fork 475
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(ui): global confirm modal (#2018)
* feat(ui): global confirm modal gloabl configm modal * fix(ui): cr update cr update
- Loading branch information
Showing
14 changed files
with
271 additions
and
20 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
118 changes: 118 additions & 0 deletions
118
packages/ui/src/containers/ConfirmModalProvider/index.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
import { Nullable } from '@silverhand/essentials'; | ||
import { useState, useRef, useMemo, createContext, useCallback } from 'react'; | ||
|
||
import { WebModal, MobileModal, ModalProps } from '@/components/ConfirmModal'; | ||
import usePlatform from '@/hooks/use-platform'; | ||
|
||
export type ChildRenderProps = { | ||
confirm: (data?: unknown) => void; | ||
cancel: (data?: unknown) => void; | ||
}; | ||
|
||
type ConfirmModalType = 'alert' | 'confirm'; | ||
|
||
type ConfirmModalState = Omit<ModalProps, 'onClose' | 'onConfirm' | 'children'> & { | ||
ModalContent: string | ((props: ChildRenderProps) => Nullable<JSX.Element>); | ||
type: ConfirmModalType; | ||
}; | ||
|
||
type ConfirmModalProps = Omit<ConfirmModalState, 'isOpen' | 'type'> & { type?: ConfirmModalType }; | ||
|
||
type ConfirmModalContextType = { | ||
show: (props: ConfirmModalProps) => Promise<[boolean, unknown?]>; | ||
confirm: (data?: unknown) => void; | ||
cancel: (data?: unknown) => void; | ||
}; | ||
|
||
const noop = () => { | ||
throw new Error('Context provider not found'); | ||
}; | ||
|
||
export const ConfirmModalContext = createContext<ConfirmModalContextType>({ | ||
show: async () => [true], | ||
confirm: noop, | ||
cancel: noop, | ||
}); | ||
|
||
type Props = { | ||
children?: React.ReactNode; | ||
}; | ||
|
||
const defaultModalState: ConfirmModalState = { | ||
isOpen: false, | ||
type: 'confirm', | ||
ModalContent: () => null, | ||
}; | ||
|
||
const ConfirmModalProvider = ({ children }: Props) => { | ||
const [modalState, setModalState] = useState<ConfirmModalState>(defaultModalState); | ||
|
||
const resolver = useRef<(value: [result: boolean, data?: unknown]) => void>(); | ||
|
||
const { isMobile } = usePlatform(); | ||
|
||
const ConfirmModal = isMobile ? MobileModal : WebModal; | ||
|
||
const handleShow = useCallback(async ({ type = 'confirm', ...props }: ConfirmModalProps) => { | ||
resolver.current?.([false]); | ||
|
||
setModalState({ | ||
isOpen: true, | ||
type, | ||
...props, | ||
}); | ||
|
||
return new Promise<[result: boolean, data?: unknown]>((resolve) => { | ||
// eslint-disable-next-line @silverhand/fp/no-mutation | ||
resolver.current = resolve; | ||
}); | ||
}, []); | ||
|
||
const handleConfirm = useCallback((data?: unknown) => { | ||
resolver.current?.([true, data]); | ||
setModalState(defaultModalState); | ||
}, []); | ||
|
||
const handleCancel = useCallback((data?: unknown) => { | ||
resolver.current?.([false, data]); | ||
setModalState(defaultModalState); | ||
}, []); | ||
|
||
const contextValue = useMemo( | ||
() => ({ | ||
show: handleShow, | ||
confirm: handleConfirm, | ||
cancel: handleCancel, | ||
}), | ||
[handleCancel, handleConfirm, handleShow] | ||
); | ||
|
||
const { ModalContent, type, ...restProps } = modalState; | ||
|
||
return ( | ||
<ConfirmModalContext.Provider value={contextValue}> | ||
{children} | ||
<ConfirmModal | ||
{...restProps} | ||
onConfirm={ | ||
type === 'confirm' | ||
? () => { | ||
handleConfirm(); | ||
} | ||
: undefined | ||
} | ||
onClose={() => { | ||
handleCancel(); | ||
}} | ||
> | ||
{typeof ModalContent === 'string' ? ( | ||
ModalContent | ||
) : ( | ||
<ModalContent confirm={handleConfirm} cancel={handleCancel} /> | ||
)} | ||
</ConfirmModal> | ||
</ConfirmModalContext.Provider> | ||
); | ||
}; | ||
|
||
export default ConfirmModalProvider; |
107 changes: 107 additions & 0 deletions
107
packages/ui/src/containers/ConfirmModalProvider/indext.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
import { render, fireEvent, waitFor } from '@testing-library/react'; | ||
import { act } from 'react-dom/test-utils'; | ||
|
||
import { useConfirmModal } from '@/hooks/use-confirm-modal'; | ||
|
||
import ConfirmModalProvider from '.'; | ||
|
||
const confirmHandler = jest.fn(); | ||
const cancelHandler = jest.fn(); | ||
|
||
const ConfirmModalTestComponent = () => { | ||
const { show } = useConfirmModal(); | ||
|
||
const onClick = async () => { | ||
const [result] = await show({ ModalContent: 'confirm modal content' }); | ||
|
||
if (result) { | ||
confirmHandler(); | ||
|
||
return; | ||
} | ||
|
||
cancelHandler(); | ||
}; | ||
|
||
return <button onClick={onClick}>show modal</button>; | ||
}; | ||
|
||
describe('confirm modal provider', () => { | ||
it('render confirm modal', async () => { | ||
const { queryByText, getByText } = render( | ||
<ConfirmModalProvider> | ||
<ConfirmModalTestComponent /> | ||
</ConfirmModalProvider> | ||
); | ||
|
||
const trigger = getByText('show modal'); | ||
|
||
act(() => { | ||
fireEvent.click(trigger); | ||
}); | ||
|
||
await waitFor(() => { | ||
expect(queryByText('confirm modal content')).not.toBeNull(); | ||
expect(queryByText('action.confirm')).not.toBeNull(); | ||
expect(queryByText('action.cancel')).not.toBeNull(); | ||
}); | ||
}); | ||
|
||
it('confirm callback of confirm modal', async () => { | ||
const { queryByText, getByText } = render( | ||
<ConfirmModalProvider> | ||
<ConfirmModalTestComponent /> | ||
</ConfirmModalProvider> | ||
); | ||
|
||
const trigger = getByText('show modal'); | ||
|
||
act(() => { | ||
fireEvent.click(trigger); | ||
}); | ||
|
||
await waitFor(() => { | ||
expect(queryByText('confirm modal content')).not.toBeNull(); | ||
expect(queryByText('action.confirm')).not.toBeNull(); | ||
}); | ||
|
||
const confirm = getByText('action.confirm'); | ||
|
||
act(() => { | ||
fireEvent.click(confirm); | ||
}); | ||
|
||
await waitFor(() => { | ||
expect(confirmHandler).toBeCalled(); | ||
}); | ||
}); | ||
|
||
it('cancel callback of confirm modal', async () => { | ||
const { queryByText, getByText } = render( | ||
<ConfirmModalProvider> | ||
<ConfirmModalTestComponent /> | ||
</ConfirmModalProvider> | ||
); | ||
|
||
const trigger = getByText('show modal'); | ||
|
||
act(() => { | ||
fireEvent.click(trigger); | ||
}); | ||
|
||
await waitFor(() => { | ||
expect(queryByText('confirm modal content')).not.toBeNull(); | ||
expect(queryByText('action.cancel')).not.toBeNull(); | ||
}); | ||
|
||
const cancel = getByText('action.cancel'); | ||
|
||
act(() => { | ||
fireEvent.click(cancel); | ||
}); | ||
|
||
await waitFor(() => { | ||
expect(cancelHandler).toBeCalled(); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.