Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
pascalbaljet committed Jan 8, 2025
1 parent 18c0eda commit 1a623da
Show file tree
Hide file tree
Showing 16 changed files with 203 additions and 117 deletions.
10 changes: 10 additions & 0 deletions demo-app/resources/js/Pages/Users.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,16 @@ export default function Users({ users, random, navigate }) {
>
Edit
</ModalLink>
<ModalLink
slideover={true}
navigate={navigate}
dusk={`slideover-user-${user.id}`}
href={`/users/${user.id}/edit`}
className="px-2 py-1 text-xs font-medium text-indigo-600 bg-indigo-100 rounded-md"
onUserGreets={alertGreeting}
>
Slideover
</ModalLink>
</div>
</div>
</li>
Expand Down
10 changes: 10 additions & 0 deletions demo-app/resources/js/Pages/Users.vue
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,16 @@ function alertGreeting(greeting) {
>
Edit
</ModalLink>
<ModalLink
slideover
:navigate="navigate"
:dusk="'slideover-user-' + user.id"
:href="`/users/${user.id}/edit`"
class="px-2 py-1 text-xs font-medium text-indigo-600 bg-indigo-100 rounded-md"
@user-greets="alertGreeting"
>
Slideover
</ModalLink>
</div>
</div>
</li>
Expand Down
1 change: 1 addition & 0 deletions demo-app/tests/Feature/ModalTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ public function it_returns_the_base_page_with_the_modal_as_a_separate_prop_when_
->has('_inertiaui_modal', fn (AssertableInertia $assert) => $assert
->where('component', 'EditUser')
->has('props')
->has('viaInertiaRouter')
->where('props.user.id', $user->id)
->has('version')
->has('url')
Expand Down
5 changes: 4 additions & 1 deletion react/src/ModalContent.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ import { TransitionChild } from '@headlessui/react'
import CloseButton from './CloseButton'
import clsx from 'clsx'
import { useFocusTrap } from './useFocusTrap'
import { useRef } from 'react'

const ModalContent = ({ modalContext, config, children }) => {
const wrapper = useFocusTrap(config?.closeExplicitly, () => modalContext.close())
const wrapper = useRef(null);
const { activate } = useFocusTrap(config?.closeExplicitly, () => modalContext.close());

return (
<div className="im-modal-container fixed inset-0 z-40 overflow-y-auto p-4">
Expand All @@ -22,6 +24,7 @@ const ModalContent = ({ modalContext, config, children }) => {
enterTo="opacity-100 translate-y-0 sm:scale-100"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
afterEnter={() => activate(wrapper.current)}
afterLeave={modalContext.afterLeave}
className={clsx(
'im-modal-wrapper pointer-events-auto w-full transition duration-300 ease-in-out',
Expand Down
50 changes: 27 additions & 23 deletions react/src/ModalRoot.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export const ModalStackProvider = ({ children }) => {

class Modal {
constructor(component, response, config, onClose, afterLeave) {
this.id = Modal.generateId()
this.id = response.id ?? Modal.generateId()
this.isOpen = false
this.shouldRender = false
this.listeners = {}
Expand Down Expand Up @@ -219,7 +219,7 @@ export const ModalStackProvider = ({ children }) => {
'X-Inertia-Partial-Component': this.response.component,
'X-Inertia-Version': this.response.version,
'X-Inertia-Partial-Data': keys.join(','),
'X-InertiaUI-Modal': true,
'X-InertiaUI-Modal': Modal.generateId(),
'X-InertiaUI-Modal-Use-Router': 0,
'X-InertiaUI-Modal-Base-Url': baseUrl,
},
Expand All @@ -234,11 +234,11 @@ export const ModalStackProvider = ({ children }) => {
}
}

const pushFromResponseData = (responseData, config = {}, onClose = null, onAfterLeave = null) => {
return resolveComponent(responseData.component).then((component) => push(component, responseData, config, onClose, onAfterLeave))
const pushFromResponseData = (responseData, config = {}, onClose = null, onAfterLeave = null, viaInertiaRouter = false) => {
return resolveComponent(responseData.component).then((component) => push(component, responseData, config, onClose, onAfterLeave, viaInertiaRouter))
}

const push = (component, response, config, onClose, afterLeave) => {
const push = (component, response, config, onClose, afterLeave, viaInertiaRouter = false) => {
const newModal = new Modal(component, response, config, onClose, afterLeave)
newModal.index = stack.length

Expand Down Expand Up @@ -293,7 +293,10 @@ export const ModalStackProvider = ({ children }) => {
onAfterLeave = null,
queryStringArrayFormat = 'brackets',
useBrowserHistory = false,
modalId = null,
) => {
modalId = modalId ?? generateId()

return new Promise((resolve, reject) => {
if (href.startsWith('#')) {
resolve(pushLocalModal(href.substring(1), config, onClose, onAfterLeave))
Expand All @@ -314,7 +317,7 @@ export const ModalStackProvider = ({ children }) => {
'X-Requested-With': 'XMLHttpRequest',
'X-Inertia': true,
'X-Inertia-Version': pageVersion,
'X-InertiaUI-Modal': true,
'X-InertiaUI-Modal': modalId,
'X-InertiaUI-Modal-Use-Router': useInertiaRouter ? 1 : 0,
'X-InertiaUI-Modal-Base-Url': baseUrl,
}
Expand All @@ -331,22 +334,23 @@ export const ModalStackProvider = ({ children }) => {
onError: reject,
onFinish: () => {
waitFor(() => newModalOnBase).then((modal) => {
const originalOnClose = modal.onCloseCallback
const originalAfterLeave = modal.afterLeaveCallback

modal.update(
config,
() => {
onClose?.()
originalOnClose?.()
},
() => {
onAfterLeave?.()
originalAfterLeave?.()
},
)

resolve(modal)
// const originalOnClose = modal.onCloseCallback
// const originalAfterLeave = modal.afterLeaveCallback

// modal.update(
// config,
// () => {
// onClose?.()
// originalOnClose?.()
// },
// () => {
// onAfterLeave?.()
// originalAfterLeave?.()
// },
// )

// modal.show()
// resolve(modal)
})
},
})
Expand Down Expand Up @@ -491,7 +495,7 @@ export const ModalRoot = ({ children }) => {
preserveState: true,
})
}
})
}, null, modalOnBase.viaInertiaRouter)
.then((newModal) => {
newModalOnBase = newModal
})
Expand Down
9 changes: 6 additions & 3 deletions react/src/SlideoverContent.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,18 @@ import { TransitionChild } from '@headlessui/react'
import CloseButton from './CloseButton'
import clsx from 'clsx'
import { useFocusTrap } from './useFocusTrap'
import { useRef } from 'react'

const SlideoverContent = ({ modalContext, config, children }) => {
const wrapper = useFocusTrap(config?.closeExplicitly, () => modalContext.close())
const wrapper = useRef(null);
const { activate } = useFocusTrap(config?.closeExplicitly, () => modalContext.close());

return (
<div className="im-slideover-container fixed inset-0 z-40 overflow-y-auto overflow-x-hidden">
<div
className={clsx('im-slideover-positioner flex min-h-full items-center', {
'justify-start': config.position === 'left',
'justify-end': config.position === 'right',
'justify-start rtl:justify-end': config?.position === 'left',
'justify-end rtl:justify-start': config?.position === 'right',
})}
>
<TransitionChild
Expand All @@ -21,6 +23,7 @@ const SlideoverContent = ({ modalContext, config, children }) => {
enterTo="opacity-100 translate-x-0"
leaveFrom="opacity-100 translate-x-0"
leaveTo={`opacity-0 ${config.position === 'left' ? '-translate-x-full' : 'translate-x-full'}`}
afterEnter={() => activate(wrapper.current)}
afterLeave={modalContext.afterLeave}
className={clsx(
'im-slideover-wrapper pointer-events-auto w-full transition duration-300 ease-in-out',
Expand Down
4 changes: 2 additions & 2 deletions react/src/helpers.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
import { modalDOMHandler, except, only, rejectNullValues, waitFor, kebabCase } from './../../vue/src/helpers.js'
export { modalDOMHandler, except, only, rejectNullValues, waitFor, kebabCase }
import { modalDOMHandler, generateId, except, only, rejectNullValues, waitFor, kebabCase } from './../../vue/src/helpers.js'
export { modalDOMHandler, generateId, except, only, rejectNullValues, waitFor, kebabCase }
33 changes: 18 additions & 15 deletions react/src/useFocusTrap.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,27 @@ import { useEffect, useRef } from 'react'
import { createFocusTrap } from 'focus-trap'

export function useFocusTrap(closeExplicitly, onDeactivateCallback) {
const wrapperRef = useRef(null)
const trapRef = useRef(null)

useEffect(() => {
if (!wrapperRef.current) {
return
}
const activate = (wrapper) => {
if (wrapper) {
trapRef.current = createFocusTrap(wrapper, {
clickOutsideDeactivates: !closeExplicitly,
escapeDeactivates: !closeExplicitly,
onDeactivate: () => onDeactivateCallback?.(),

fallbackFocus: () => wrapper,
})

const trap = createFocusTrap(wrapperRef.current, {
clickOutsideDeactivates: !closeExplicitly,
escapeDeactivates: !closeExplicitly,
onDeactivate: () => onDeactivateCallback?.(),
fallbackFocus: () => wrapperRef.current,
})
trapRef.current.activate()
}
}

trap.activate()
const deactivate = () => {
trapRef.current?.deactivate()
}

return () => trap.deactivate()
}, [])
useEffect(() => deactivate, [])

return wrapperRef
return { activate, deactivate }
}
5 changes: 5 additions & 0 deletions src/Modal.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,12 @@
use Illuminate\Support\Facades\Response as ResponseFactory;
use Illuminate\View\View;
use Inertia\Response as InertiaResponse;
use Inertia\Support\Header;

class Modal implements Responsable
{
const HEADER_MODAL = 'X-InertiaUI-Modal';

const HEADER_BASE_URL = 'X-InertiaUI-Modal-Base-Url';

const HEADER_USE_ROUTER = 'X-InertiaUI-Modal-Use-Router';
Expand Down Expand Up @@ -96,6 +99,8 @@ public function toResponse($request)
inertia()->share('_inertiaui_modal', [
// @phpstan-ignore-next-line
...$modal->toArray(),
'id' => $request->header(static::HEADER_MODAL),
'viaInertiaRouter' => (bool) $request->header(Header::INERTIA),
'baseUrl' => $baseUrl,
]);

Expand Down
5 changes: 4 additions & 1 deletion vue/src/ModalContent.vue
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<script setup>
import { ref } from 'vue'
import CloseButton from './CloseButton.vue'
import { useFocusTrap } from './useFocusTrap'
Expand All @@ -7,7 +8,8 @@ const props = defineProps({
config: Object,
})
const { wrapper } = useFocusTrap(props.config?.closeExplicitly, () => props.modalContext.close())
const wrapper = ref(null)
const focusTrap = () => useFocusTrap(wrapper.value, props.config?.closeExplicitly, () => props.modalContext.close())
</script>
<template>
Expand All @@ -28,6 +30,7 @@ const { wrapper } = useFocusTrap(props.config?.closeExplicitly, () => props.moda
enter-to-class="opacity-100 translate-y-0 sm:scale-100"
leave-from-class="opacity-100 translate-y-0 sm:scale-100"
leave-to-class="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
@after-enter="focusTrap"
@after-leave="modalContext.afterLeave"
>
<div
Expand Down
24 changes: 21 additions & 3 deletions vue/src/ModalLink.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script setup>
import { modalPropNames, useModalStack } from './modalStack'
import { ref, provide, computed, watch, useAttrs, onBeforeUnmount } from 'vue'
import { only, rejectNullValues } from './helpers'
import { ref, provide, computed, watch, useAttrs, onBeforeUnmount, watchEffect } from 'vue'
import { generateId, only, rejectNullValues } from './helpers'
import { getConfig } from './config'
const props = defineProps({
Expand Down Expand Up @@ -74,6 +74,19 @@ const props = defineProps({
const loading = ref(false)
const modalStack = useModalStack()
const modalContext = ref(null)
const modalId = ref()
watch(
props,
() => {
if (modalId.value) {
modalStack.removePendingModalUpdate(modalId.value)
}
modalId.value = generateId()
},
{ immediate: true },
)
provide('modalContext', modalContext)
Expand All @@ -100,7 +113,11 @@ watch(
)
const unsubscribeEventListeners = ref(null)
onBeforeUnmount(() => unsubscribeEventListeners.value?.())
onBeforeUnmount(() => {
modalStack.removePendingModalUpdate(modalId.value)
unsubscribeEventListeners.value?.()
})
const $attrs = useAttrs()
Expand Down Expand Up @@ -145,6 +162,7 @@ function handle() {
onAfterLeave,
props.queryStringArrayFormat,
shouldNavigate.value,
modalId.value,
)
.then((context) => {
modalContext.value = context
Expand Down
30 changes: 18 additions & 12 deletions vue/src/ModalRoot.vue
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,25 @@ onUnmounted(
previousModalOnBase.value = modalOnBase
modalStack.setBaseUrl(modalOnBase.baseUrl)
modalStack.pushFromResponseData(modalOnBase, {}, () => {
if (!modalOnBase.baseUrl) {
console.error('No base url in modal response data so cannot navigate back')
return
}
modalStack.pushFromResponseData(
modalOnBase,
{},
() => {
if (!modalOnBase.baseUrl) {
console.error('No base url in modal response data so cannot navigate back')
return
}
if (!isNavigating.value && window.location.href !== modalOnBase.baseUrl) {
router.visit(modalOnBase.baseUrl, {
preserveScroll: true,
preserveState: true,
})
}
})
if (!isNavigating.value && window.location.href !== modalOnBase.baseUrl) {
router.visit(modalOnBase.baseUrl, {
preserveScroll: true,
preserveState: true,
})
}
},
null,
modalOnBase.viaInertiaRouter,
)
}),
)
Expand Down
Loading

0 comments on commit 1a623da

Please sign in to comment.