diff --git a/resources/assets/src/components/Modal.tsx b/resources/assets/src/components/Modal.tsx index f4206243..89fa754e 100644 --- a/resources/assets/src/components/Modal.tsx +++ b/resources/assets/src/components/Modal.tsx @@ -33,115 +33,131 @@ type Props = { footer?: React.ReactNode onConfirm?(payload: { value: string }): void onDismiss?(): void + onClose?(): void } export type ModalResult = { value: string } -const Modal = React.forwardRef( - (props, forwardedRef) => { - const [hidden, setHidden] = useState(false) - const [value, setValue] = useState(props.input!) - const [valid, setValid] = useState(true) - const [validatorMessage, setValidatorMessage] = useState('') - const ref = (forwardedRef ?? - useRef(null)) as React.RefObject +const Modal: React.FC = props => { + const [value, setValue] = useState(props.input!) + const [valid, setValid] = useState(true) + const [validatorMessage, setValidatorMessage] = useState('') + const ref = useRef(null) - const handleInputChange = (event: React.ChangeEvent) => { - setValue(event.target.value) + const handleInputChange = (event: React.ChangeEvent) => { + setValue(event.target.value) + } + + const confirm = () => { + const { validator } = props + if (typeof validator === 'function') { + const result = validator(value) + if (typeof result === 'string') { + setValidatorMessage(result) + setValid(false) + return + } } - const confirm = () => { - const { validator } = props - if (typeof validator === 'function') { - const result = validator(value) - if (typeof result === 'string') { - setValidatorMessage(result) - setValid(false) - return - } - } + props.onConfirm?.({ value }) + $(ref.current!).modal('hide') - setHidden(true) - props.onConfirm?.({ value }) - $(ref.current!).modal('hide') + // The "hidden.bs.modal" event can't be trigged automatically when testing. + /* istanbul ignore next */ + if (process.env.NODE_ENV === 'test') { + $(ref.current!).trigger('hidden.bs.modal') + } + } + + const dismiss = () => { + props.onDismiss?.() + $(ref.current!).modal('hide') + + /* istanbul ignore next */ + if (process.env.NODE_ENV === 'test') { + $(ref.current!).trigger('hidden.bs.modal') + } + } + + useEffect(() => { + if (!props.show) { + return } - const dismiss = () => { - setHidden(true) - props.onDismiss?.() + const onHidden = () => props.onClose?.() + + const el = $(ref.current!) + el.on('hidden.bs.modal', onHidden) + + return () => { + el.off('hidden.bs.modal', onHidden) } + }, [props.onClose, props.show]) - useEffect(() => { - const onHide = () => { - if (!hidden) { - dismiss() - } - } - const onHidden = () => setHidden(false) + useEffect(() => { + if (props.show) { + setTimeout(() => $(ref.current!).modal('show'), 50) + } + }, [props.show]) - const el = $(ref.current!) - el.on('hide.bs.modal', onHide).on('hidden.bs.modal', onHidden) + if (!props.show) { + return null + } - return () => { - el.off('hide.bs.modal', onHide).off('hidden.bs.modal', onHidden) - } - }, [hidden, props.onDismiss]) - - return ( + return ( + - + ) } diff --git a/resources/assets/tests/components/Modal.test.tsx b/resources/assets/tests/components/Modal.test.tsx index 4554f235..a3aa8760 100644 --- a/resources/assets/tests/components/Modal.test.tsx +++ b/resources/assets/tests/components/Modal.test.tsx @@ -24,26 +24,15 @@ test('background color', () => { expect(container.querySelector('.modal-content')).toHaveClass('bg-primary') }) -test('forward ref', () => { - const ref = React.createRef() - render() - expect(ref.current).not.toBeNull() -}) - test('jQuery events', () => { - const ref = React.createRef() - const { getByText } = render() + const { getByText } = render() act(() => { - $(ref.current!) - .trigger('hide.bs.modal') - .trigger('hidden.bs.modal') + $('.modal').trigger('hidden.bs.modal') }) fireEvent.click(getByText(trans('general.cancel'))) act(() => { - $(ref.current!) - .trigger('hide.bs.modal') - .trigger('hidden.bs.modal') + $('.modal').trigger('hidden.bs.modal') }) }) @@ -280,3 +269,23 @@ describe('"prompt" mode', () => { expect(reject).not.toBeCalled() }) }) + +describe('"onClose" event', () => { + it('button confirm', () => { + const mock = jest.fn() + const { getByText } = render() + fireEvent.click(getByText(trans('general.confirm'))) + jest.runAllTimers() + $('.modal').trigger('hidden.bs.modal') + expect(mock).toBeCalled() + }) + + it('button cancel', () => { + const mock = jest.fn() + const { getByText } = render() + fireEvent.click(getByText(trans('general.cancel'))) + jest.runAllTimers() + $('.modal').trigger('hidden.bs.modal') + expect(mock).toBeCalled() + }) +}) diff --git a/resources/assets/tests/scripts/modal.test.ts b/resources/assets/tests/scripts/modal.test.ts index e3c5a7c0..26ccc6ff 100644 --- a/resources/assets/tests/scripts/modal.test.ts +++ b/resources/assets/tests/scripts/modal.test.ts @@ -1,21 +1,13 @@ -import $ from 'jquery' -import { act } from 'react-dom/test-utils' import { trans } from '@/scripts/i18n' import { showModal } from '@/scripts/modal' test('show modal', async () => { process.nextTick(() => { - expect( - document.querySelector('.modal-title')!.textContent, - ).toBe(trans('general.tip')) + expect(document.querySelector('.modal-title')!.textContent).toBe( + trans('general.tip'), + ) document.querySelector('.btn-primary')!.click() }) const { value } = await showModal() expect(value).toBe('') - - act(() => { - $('.modal').trigger('hidden.bs.modal') - jest.runAllTimers() - }) - expect(document.querySelector('.modal')).toBeNull() })