From 6e2f0f2b35c777df0bfdc23ae93960639ba918d8 Mon Sep 17 00:00:00 2001 From: Pascal Baljet Date: Wed, 20 Nov 2024 20:21:18 +0100 Subject: [PATCH 1/3] WIP fix for referring local modal (see #53) --- demo-app/resources/js/Pages/Local.jsx | 12 ++++++++- demo-app/resources/js/Pages/Local.vue | 13 +++++++++- demo-app/tests/Browser/LocalModalTest.php | 12 +++++++++ react/src/HeadlessModal.jsx | 30 ++++++++++++----------- vue/src/HeadlessModal.vue | 26 ++++++++++---------- 5 files changed, 64 insertions(+), 29 deletions(-) diff --git a/demo-app/resources/js/Pages/Local.jsx b/demo-app/resources/js/Pages/Local.jsx index 709899d..73c23cd 100644 --- a/demo-app/resources/js/Pages/Local.jsx +++ b/demo-app/resources/js/Pages/Local.jsx @@ -1,7 +1,14 @@ import { Modal, ModalLink } from '@inertiaui/modal-react'; import Container from './Container'; +import { useRef } from 'react'; export default function Local() { + const modalRef = useRef(null); + + function closeModal() { + modalRef.current.close(); + } + return ( <> @@ -15,11 +22,14 @@ export default function Local() { - + This is a local modal Create Role + ); diff --git a/demo-app/resources/js/Pages/Local.vue b/demo-app/resources/js/Pages/Local.vue index ecdd684..feee565 100644 --- a/demo-app/resources/js/Pages/Local.vue +++ b/demo-app/resources/js/Pages/Local.vue @@ -1,6 +1,13 @@ diff --git a/demo-app/tests/Browser/LocalModalTest.php b/demo-app/tests/Browser/LocalModalTest.php index c862448..2e8c0c8 100644 --- a/demo-app/tests/Browser/LocalModalTest.php +++ b/demo-app/tests/Browser/LocalModalTest.php @@ -20,4 +20,16 @@ public function it_can_open_a_local_modal_and_a_nested_one() ->waitUntilMissingModal(1); }); } + + #[Test] + public function it_can_close_a_local_modal_through_a_template_ref() + { + $this->browse(function (Browser $browser) { + $browser->visit('/local') + ->clickLink('Open Local Modal') + ->waitForTextIn('.im-modal-content', 'This is a local modal') + ->press('Close Modal through Ref') + ->waitUntilMissingModal(1); + }); + } } diff --git a/react/src/HeadlessModal.jsx b/react/src/HeadlessModal.jsx index 7951221..9109a1b 100644 --- a/react/src/HeadlessModal.jsx +++ b/react/src/HeadlessModal.jsx @@ -9,6 +9,7 @@ const HeadlessModal = forwardRef(({ name, children, ...props }, ref) => { const { stack, registerLocalModal, removeLocalModal } = useModalStack() const [localModalContext, setLocalModalContext] = useState(null) + let modalContextCopy = name ? localModalContext : stack[modalIndex] const modalContext = useMemo(() => (name ? localModalContext : stack[modalIndex]), [name, localModalContext, modalIndex, stack]) const nextIndex = useMemo(() => { @@ -38,6 +39,7 @@ const HeadlessModal = forwardRef(({ name, children, ...props }, ref) => { registerLocalModal(name, (localContext) => { removeListeners = localContext.registerEventListenersFromProps(props) setLocalModalContext(localContext) + modalContextCopy = localContext }) return () => { @@ -53,22 +55,22 @@ const HeadlessModal = forwardRef(({ name, children, ...props }, ref) => { useImperativeHandle( ref, () => ({ - afterLeave: () => modalContext.afterLeave(), - close: () => modalContext.close(), + afterLeave: () => modalContextCopy.afterLeave(), + close: () => modalContextCopy.close(), config, - emit: (...args) => modalContext.emit(...args), - getChildModal: () => modalContext.getChildModal(), - getParentModal: () => modalContext.getParentModal(), - id: modalContext?.id, - index: modalContext?.index, - isOpen: modalContext?.isOpen, - modalContext, - onTopOfStack: modalContext?.onTopOfStack, - reload: () => modalContext.reload(), - setOpen: () => modalContext.setOpen(), - shouldRender: modalContext?.shouldRender, + emit: (...args) => modalContextCopy.emit(...args), + getChildModal: () => modalContextCopy.getChildModal(), + getParentModal: () => modalContextCopy.getParentModal(), + id: modalContextCopy?.id, + index: modalContextCopy?.index, + isOpen: modalContextCopy?.isOpen, + modalContext: modalContextCopy, + onTopOfStack: modalContextCopy?.onTopOfStack, + reload: () => modalContextCopy.reload(), + setOpen: () => modalContextCopy.setOpen(), + shouldRender: modalContextCopy?.shouldRender, }), - [modalContext], + [modalContextCopy], ) return ( diff --git a/vue/src/HeadlessModal.vue b/vue/src/HeadlessModal.vue index c39f3e6..2b49d9e 100644 --- a/vue/src/HeadlessModal.vue +++ b/vue/src/HeadlessModal.vue @@ -92,20 +92,20 @@ function emit(event, ...args) { } defineExpose({ - afterLeave: modalContext.value.afterLeave, - close: modalContext.value.close, - config: config.value, + afterLeave: () => modalContext.value?.afterLeave(), + close: () => modalContext.value?.close(), + config: computed(() => modalContext.value?.config), emit, - getChildModal: modalContext.value.getChildModal, - getParentModal: modalContext.value.getParentModal, - id: modalContext.value.id, - index: modalContext.value.index, - isOpen: modalContext.value.isOpen, - modalContext: modalContext.value, - onTopOfStack: modalContext.value.onTopOfStack, - reload: modalContext.value.reload, - setOpen: modalContext.value.setOpen, - shouldRender: modalContext.value.shouldRender, + getChildModal: () => modalContext.value?.getChildModal(), + getParentModal: () => modalContext.value?.getParentModal(), + id: computed(() => modalContext.value?.id), + index: computed(() => modalContext.value?.index), + isOpen: computed(() => modalContext.value?.isOpen), + modalContext: computed(() => modalContext.value?.modalContext), + onTopOfStack: computed(() => modalContext.value?.onTopOfStack), + reload: (...args) => modalContext.value?.reload(...args), + setOpen: (...args) => modalContext.value?.setOpen(...args), + shouldRender: computed(() => modalContext.value?.shouldRender), }) const nextIndex = computed(() => { From f6ff916d71db9fd57f87b1d40b5c746cc3b02071 Mon Sep 17 00:00:00 2001 From: Pascal Baljet Date: Wed, 20 Nov 2024 20:22:53 +0100 Subject: [PATCH 2/3] Updated deps --- demo-app/tests/Feature/DispatchBaseUrlRequestTest.php | 2 +- demo-app/tests/Feature/RedirectorTest.php | 2 +- demo-app/tests/TestCase.php | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/demo-app/tests/Feature/DispatchBaseUrlRequestTest.php b/demo-app/tests/Feature/DispatchBaseUrlRequestTest.php index fcec9c1..fef3710 100644 --- a/demo-app/tests/Feature/DispatchBaseUrlRequestTest.php +++ b/demo-app/tests/Feature/DispatchBaseUrlRequestTest.php @@ -15,7 +15,7 @@ class DispatchBaseUrlRequestTest extends TestCase { protected DispatchBaseUrlRequest $dispatcher; - public function setUp(): void + protected function setUp(): void { parent::setUp(); diff --git a/demo-app/tests/Feature/RedirectorTest.php b/demo-app/tests/Feature/RedirectorTest.php index c7e370b..60589ac 100644 --- a/demo-app/tests/Feature/RedirectorTest.php +++ b/demo-app/tests/Feature/RedirectorTest.php @@ -17,7 +17,7 @@ class RedirectorTest extends TestCase protected Request $request; - public function setUp(): void + protected function setUp(): void { parent::setUp(); $this->urlGenerator = app(UrlGenerator::class); diff --git a/demo-app/tests/TestCase.php b/demo-app/tests/TestCase.php index b66e347..33489d6 100644 --- a/demo-app/tests/TestCase.php +++ b/demo-app/tests/TestCase.php @@ -12,7 +12,7 @@ abstract class TestCase extends BaseTestCase use ModalTestCase; use RefreshDatabase; - public function setUp(): void + protected function setUp(): void { parent::setUp(); @@ -20,7 +20,7 @@ public function setUp(): void Carbon::setTestNow('2024-06-01 12:00:00'); } - public function tearDown(): void + protected function tearDown(): void { Carbon::setTestNow(); parent::tearDown(); From 480367a1bab9a077bb06c5fab5c0e9ecb0b5c6fc Mon Sep 17 00:00:00 2001 From: Pascal Baljet Date: Wed, 20 Nov 2024 22:43:15 +0100 Subject: [PATCH 3/3] Refactor --- demo-app/resources/js/Pages/Local.jsx | 7 +++ demo-app/resources/js/Pages/Local.vue | 8 ++++ demo-app/tests/Browser/LocalModalTest.php | 17 +++++++ react/src/HeadlessModal.jsx | 57 ++++++++++++++++------- vue/src/HeadlessModal.vue | 35 ++++++++++---- vue/src/Modal.vue | 31 ++++++++---- 6 files changed, 119 insertions(+), 36 deletions(-) diff --git a/demo-app/resources/js/Pages/Local.jsx b/demo-app/resources/js/Pages/Local.jsx index 73c23cd..df72e62 100644 --- a/demo-app/resources/js/Pages/Local.jsx +++ b/demo-app/resources/js/Pages/Local.jsx @@ -9,6 +9,10 @@ export default function Local() { modalRef.current.close(); } + function alertModalId() { + alert(modalRef.current.id); + } + return ( <> @@ -30,6 +34,9 @@ export default function Local() { + ); diff --git a/demo-app/resources/js/Pages/Local.vue b/demo-app/resources/js/Pages/Local.vue index feee565..dc6f1ab 100644 --- a/demo-app/resources/js/Pages/Local.vue +++ b/demo-app/resources/js/Pages/Local.vue @@ -8,6 +8,10 @@ const modalRef = ref(null); function closeModal() { modalRef.value.close(); } + +function alertModalId() { + alert(modalRef.value.id); +} diff --git a/demo-app/tests/Browser/LocalModalTest.php b/demo-app/tests/Browser/LocalModalTest.php index 2e8c0c8..c22265c 100644 --- a/demo-app/tests/Browser/LocalModalTest.php +++ b/demo-app/tests/Browser/LocalModalTest.php @@ -21,6 +21,23 @@ public function it_can_open_a_local_modal_and_a_nested_one() }); } + #[Test] + public function it_can_access_a_prop_through_a_template_ref() + { + $this->browse(function (Browser $browser) { + $browser->visit('/local') + ->clickLink('Open Local Modal') + ->waitForTextIn('.im-modal-content', 'This is a local modal') + ->press('Alert Modal ID'); + + $message = $browser->driver->switchTo()->alert()->getText(); + + $this->assertStringStartsWith('inertiaui_modal_', $message); + + $browser->dismissDialog(); + }); + } + #[Test] public function it_can_close_a_local_modal_through_a_template_ref() { diff --git a/react/src/HeadlessModal.jsx b/react/src/HeadlessModal.jsx index 9109a1b..dab2cce 100644 --- a/react/src/HeadlessModal.jsx +++ b/react/src/HeadlessModal.jsx @@ -1,4 +1,4 @@ -import { useMemo, useState, forwardRef, useImperativeHandle, useEffect } from 'react' +import { useMemo, useState, forwardRef, useImperativeHandle, useEffect, useRef } from 'react' import { getConfig, getConfigByType } from './config' import { useModalIndex } from './ModalRenderer.jsx' import { useModalStack } from './ModalRoot.jsx' @@ -9,7 +9,6 @@ const HeadlessModal = forwardRef(({ name, children, ...props }, ref) => { const { stack, registerLocalModal, removeLocalModal } = useModalStack() const [localModalContext, setLocalModalContext] = useState(null) - let modalContextCopy = name ? localModalContext : stack[modalIndex] const modalContext = useMemo(() => (name ? localModalContext : stack[modalIndex]), [name, localModalContext, modalIndex, stack]) const nextIndex = useMemo(() => { @@ -39,7 +38,6 @@ const HeadlessModal = forwardRef(({ name, children, ...props }, ref) => { registerLocalModal(name, (localContext) => { removeListeners = localContext.registerEventListenersFromProps(props) setLocalModalContext(localContext) - modalContextCopy = localContext }) return () => { @@ -52,25 +50,48 @@ const HeadlessModal = forwardRef(({ name, children, ...props }, ref) => { return modalContext.registerEventListenersFromProps(props) }, [name]) + // Store the latest modalContext in a ref to maintain reference + const modalContextRef = useRef(modalContext) + + // Update the ref whenever modalContext changes + useEffect(() => { + modalContextRef.current = modalContext + }, [modalContext]) + useImperativeHandle( ref, () => ({ - afterLeave: () => modalContextCopy.afterLeave(), - close: () => modalContextCopy.close(), - config, - emit: (...args) => modalContextCopy.emit(...args), - getChildModal: () => modalContextCopy.getChildModal(), - getParentModal: () => modalContextCopy.getParentModal(), - id: modalContextCopy?.id, - index: modalContextCopy?.index, - isOpen: modalContextCopy?.isOpen, - modalContext: modalContextCopy, - onTopOfStack: modalContextCopy?.onTopOfStack, - reload: () => modalContextCopy.reload(), - setOpen: () => modalContextCopy.setOpen(), - shouldRender: modalContextCopy?.shouldRender, + afterLeave: () => modalContextRef.current?.afterLeave(), + close: () => modalContextRef.current?.close(), + emit: (...args) => modalContextRef.current?.emit(...args), + getChildModal: () => modalContextRef.current?.getChildModal(), + getParentModal: () => modalContextRef.current?.getParentModal(), + reload: (...args) => modalContextRef.current?.reload(...args), + setOpen: () => modalContextRef.current?.setOpen(), + + get id() { + return modalContextRef.current?.id + }, + get index() { + return modalContextRef.current?.index + }, + get isOpen() { + return modalContextRef.current?.isOpen + }, + get config() { + return modalContextRef.current?.config + }, + get modalContext() { + return modalContextRef.current + }, + get onTopOfStack() { + return modalContextRef.current?.onTopOfStack + }, + get shouldRender() { + return modalContextRef.current?.shouldRender + }, }), - [modalContextCopy], + [modalContext], ) return ( diff --git a/vue/src/HeadlessModal.vue b/vue/src/HeadlessModal.vue index 2b49d9e..a74e85e 100644 --- a/vue/src/HeadlessModal.vue +++ b/vue/src/HeadlessModal.vue @@ -92,20 +92,35 @@ function emit(event, ...args) { } defineExpose({ + emit, afterLeave: () => modalContext.value?.afterLeave(), close: () => modalContext.value?.close(), - config: computed(() => modalContext.value?.config), - emit, - getChildModal: () => modalContext.value?.getChildModal(), - getParentModal: () => modalContext.value?.getParentModal(), - id: computed(() => modalContext.value?.id), - index: computed(() => modalContext.value?.index), - isOpen: computed(() => modalContext.value?.isOpen), - modalContext: computed(() => modalContext.value?.modalContext), - onTopOfStack: computed(() => modalContext.value?.onTopOfStack), reload: (...args) => modalContext.value?.reload(...args), setOpen: (...args) => modalContext.value?.setOpen(...args), - shouldRender: computed(() => modalContext.value?.shouldRender), + getChildModal: () => modalContext.value?.getChildModal(), + getParentModal: () => modalContext.value?.getParentModal(), + + get config() { + return modalContext.value?.config + }, + get id() { + return modalContext.value?.id + }, + get index() { + return modalContext.value?.index + }, + get isOpen() { + return modalContext.value?.isOpen + }, + get modalContext() { + return modalContext.value?.modalContext + }, + get onTopOfStack() { + return modalContext.value?.onTopOfStack + }, + get shouldRender() { + return modalContext.value?.shouldRender + }, }) const nextIndex = computed(() => { diff --git a/vue/src/Modal.vue b/vue/src/Modal.vue index 19414ca..8d5b649 100644 --- a/vue/src/Modal.vue +++ b/vue/src/Modal.vue @@ -3,7 +3,7 @@ import { DialogOverlay, DialogPortal, DialogRoot } from 'radix-vue' import ModalContent from './ModalContent.vue' import HeadlessModal from './HeadlessModal.vue' import SlideoverContent from './SlideoverContent.vue' -import { computed, ref } from 'vue' +import { ref } from 'vue' const modal = ref(null) const rendered = ref(false) @@ -11,18 +11,33 @@ const rendered = ref(false) defineExpose({ afterLeave: () => modal.value?.afterLeave(), close: () => modal.value?.close(), - config: computed(() => modal.value?.config), emit: (...args) => modal.value?.emit(...args), getChildModal: () => modal.value?.getChildModal(), getParentModal: () => modal.value?.getParentModal(), - id: computed(() => modal.value?.id), - index: computed(() => modal.value?.index), - isOpen: computed(() => modal.value?.isOpen), - modalContext: computed(() => modal.value?.modalContext), - onTopOfStack: computed(() => modal.value?.onTopOfStack), reload: (...args) => modal.value?.reload(...args), setOpen: (...args) => modal.value?.setOpen(...args), - shouldRender: computed(() => modal.value?.shouldRender), + + get config() { + return modal.value?.config + }, + get id() { + return modal.value?.id + }, + get index() { + return modal.value?.index + }, + get isOpen() { + return modal.value?.isOpen + }, + get modalContext() { + return modal.value?.modalContext + }, + get onTopOfStack() { + return modal.value?.onTopOfStack + }, + get shouldRender() { + return modal.value?.shouldRender + }, })