Skip to content

Commit

Permalink
I9870 - Consolidate modal rendering stack (#344)
Browse files Browse the repository at this point in the history
* pkp/pkp-lib#9870 Render legacy modals via Vue.js
* pkp/pkp-lib#9870 Update storybook using openSideModal function
  • Loading branch information
jardakotesovec authored Apr 10, 2024
1 parent 8cf5bec commit b2ac693
Show file tree
Hide file tree
Showing 21 changed files with 613 additions and 477 deletions.
6 changes: 3 additions & 3 deletions .storybook/preview.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import Tab from '@/components/Tabs/Tab.vue';
import Tabs from '@/components/Tabs/Tabs.vue';
import FloatingVue from 'floating-vue';

import PkpDialog from '@/components/Modal/Dialog.vue';
import ModalManager from '@/components/Modal/ModalManager.vue';

import VueScrollTo from 'vue-scrollto';

Expand Down Expand Up @@ -116,9 +116,9 @@ const preview = {
/** Globally Available Dialog */
(story) => ({
setup() {},
components: {story, PkpDialog},
components: {story, ModalManager},
template: `<div>
<PkpDialog></PkpDialog>
<ModalManager></ModalManager>
<story />
</div>`,
}),
Expand Down
2 changes: 0 additions & 2 deletions src/components/Container/Page.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
<script type="text/javascript">
import Container from '@/components/Container/Container.vue';
import PkpDialog from '@/components/Modal/Dialog.vue';
import ModalManager from '@/components/Modal/ModalManager.vue';
import PkpAnnouncer from '@/components/Announcer/Announcer.vue';
Expand All @@ -9,7 +8,6 @@ import ReviewerSubmissionPage from '@/pages/reviewerSubmission/ReviewerSubmissio
export default {
name: 'Page',
components: {
PkpDialog,
PkpAnnouncer,
ModalManager,
ReviewerSubmissionPage,
Expand Down
66 changes: 47 additions & 19 deletions src/components/Modal/AjaxModalWrapper.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,18 @@
<div ref="contentDiv" @click="catchInsideClick"></div>
</template>
<script setup>
import {ref, onMounted, inject, defineProps} from 'vue';
/**
* Component to mimick part of AjaxModalHandler which fetches the html content from given url
* and presents it to the user
*/
import {ref, onMounted, inject, defineProps, onBeforeUnmount} from 'vue';
import {useFetch} from '@/composables/useFetch';
const {options} = defineProps({
/**
* Following the object used within AjaxModalHandler
* Particularly important is `url` and `modalHandler`
*/
options: {
type: Object,
default: () => {},
Expand All @@ -16,11 +24,13 @@ const contentDiv = ref(null);
// eslint-disable-next-line no-unused-vars
const pkp = window.pkp;
// Fetches html content from legacy endpoints
const {data: modalData, fetch: fetchAssignParticipantPage} = useFetch(
options.url,
);
const closeModal = inject('closeModal');
// Legacy modal has mechanism where it needs to check with form whether it can close
// Mimicking this behaviour
const registerCloseCallback = inject('registerCloseCallback');
registerCloseCallback(() => {
// eslint-disable-next-line no-unused-vars
Expand Down Expand Up @@ -50,35 +60,53 @@ function catchInsideClick(e) {
}
}
function passToBridge(jQueryEvent) {
// If we have an event bridge configured then re-trigger
// the event on the target object.
if (options.eventBridge) {
$('[id^="' + options.eventBridge + '"]').trigger(
jQueryEvent.type,
jQueryEvent.data,
);
/** The wrapping div element for modal is still created by legacy modal handler, but its not mounted
* only used to keep the legacy event communication going from inside modal to the outside (often its grid component)
*
*/
function passToHandlerElement(...args) {
if (options.modalHandler) {
options.modalHandler.getHtmlElement().trigger(...args);
}
return;
}
onMounted(async () => {
await fetchAssignParticipantPage();
if (modalData.value) {
// TODO CONSIDER REMOVE BINDS ON UNMOUNT
$(contentDiv.value).html(modalData.value.content);
$(contentDiv.value).bind('formSubmitted', closeModal);
$(contentDiv.value).bind('formCanceled', closeModal);
$(contentDiv.value).bind('ajaxHtmlError', closeModal);
$(contentDiv.value).bind('modalFinished', closeModal);
$(contentDiv.value).bind('formSubmitted', passToHandlerElement);
$(contentDiv.value).bind('wizardClose', passToHandlerElement);
$(contentDiv.value).bind('wizardCancel', passToHandlerElement);
$(contentDiv.value).bind('formCanceled', passToHandlerElement);
$(contentDiv.value).bind('ajaxHtmlError', passToHandlerElement);
$(contentDiv.value).bind('modalFinished', passToHandlerElement);
// Publish some otherwise private events triggered
// by nested widgets so that they can be handled by
// the element that opened the modal.
$(contentDiv.value).bind('redirectRequested', passToBridge);
$(contentDiv.value).bind('dataChanged', passToBridge);
$(contentDiv.value).bind('updateHeader', passToBridge);
$(contentDiv.value).bind('gridRefreshRequested', passToBridge);
$(contentDiv.value).bind('redirectRequested', passToHandlerElement);
$(contentDiv.value).bind('dataChanged', passToHandlerElement);
$(contentDiv.value).bind('updateHeader', passToHandlerElement);
$(contentDiv.value).bind('gridRefreshRequested', passToHandlerElement);
}
});
onBeforeUnmount(() => {
$(contentDiv.value).unbind('formSubmitted', passToHandlerElement);
$(contentDiv.value).unbind('wizardClose', passToHandlerElement);
$(contentDiv.value).unbind('wizardCancel', passToHandlerElement);
$(contentDiv.value).unbind('formCanceled', passToHandlerElement);
$(contentDiv.value).unbind('ajaxHtmlError', passToHandlerElement);
$(contentDiv.value).unbind('modalFinished', passToHandlerElement);
$(contentDiv.value).unbind('redirectRequested', passToHandlerElement);
$(contentDiv.value).unbind('dataChanged', passToHandlerElement);
$(contentDiv.value).unbind('updateHeader', passToHandlerElement);
$(contentDiv.value).unbind('gridRefreshRequested', passToHandlerElement);
});
</script>
18 changes: 12 additions & 6 deletions src/components/Modal/Dialog.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<template>
<TransitionRoot as="template" :show="opened">
<HLDialog class="modal" :class="'modal--popup'" @close="onClose">
<HLDialog class="modal" @close="onClose">
<TransitionChild
as="template"
enter="ease-out duration-300"
Expand Down Expand Up @@ -81,7 +81,7 @@
</template>

<script setup>
import {ref, computed, onMounted, onUnmounted} from 'vue';
import {ref, computed, onMounted, onUnmounted, watch} from 'vue';
import {storeToRefs} from 'pinia';
import {
Expand All @@ -106,11 +106,17 @@ const opened = computed(
const isLoading = ref(false);
function onClose() {
if (dialogProps.value.close) {
dialogProps.value.close();
// resetting state after close
// this is not ideal approach, but given how little state the dialog has
// its less complex than splitting dialog into two components to have proper life cycle
// as we do with SideModal and SideModalBody
watch(opened, (prevOpened, nextOpened) => {
if (prevOpened === true && nextOpened === false) {
isLoading.value = false;
}
isLoading.value = false;
});
function onClose() {
closeDialog();
}
Expand Down
69 changes: 40 additions & 29 deletions src/components/Modal/ModalManager.vue
Original file line number Diff line number Diff line change
@@ -1,45 +1,56 @@
<template>
<SideModal close-label="Close" :open="isOpened" @close="close">
<SideModalBodyAjax :options="options" />
<SideModal close-label="Close" :open="isOpened2" @close="close2">
<SideModalBodyAjax :options="options2" />
<SideModal
close-label="Close"
:open="sideModal1?.opened || false"
:modal-level="1"
@close="() => close(sideModal1?.modalId)"
>
<component :is="component1" v-bind="sideModal1?.props" />
<SideModal
close-label="Close"
:modal-level="2"
:open="sideModal2?.opened || false"
@close="() => close(sideModal2?.modalId)"
>
<component :is="component2" v-bind="sideModal2?.props" />
</SideModal>
</SideModal>
<PkpDialog></PkpDialog>
</template>

<script setup>
import {ref} from 'vue';
import {computed} from 'vue';
import {useModalStore} from '@/stores/modalStore';
import {storeToRefs} from 'pinia';
import SideModal from '@/components/Modal/SideModal.vue';
import SideModalBodyAjax from '@/components/Modal/SideModalBodyAjax.vue';
import LegacyAjax from '@/components/Modal/SideModalBodyLegacyAjax.vue';
import PkpDialog from '@/components/Modal/Dialog.vue';
const isOpened = ref(false);
const isOpened2 = ref(false);
const LegacyModals = {LegacyAjax};
const options = ref(null);
const options2 = ref(null);
const modalStore = useModalStore();
const {sideModal1, sideModal2} = storeToRefs(useModalStore());
function close() {
isOpened.value = false;
options.value = null;
}
function close2() {
isOpened2.value = false;
options2.value = null;
}
// pkp.eventBus.$emit('open-tab', tab);
// Component can be either string or actual vue component
const component1 = computed(() => {
if (!sideModal1.value?.component) {
return null;
}
return typeof sideModal1.value.component === 'string'
? LegacyModals[sideModal1.value.component]
: sideModal1.value.component;
});
/** POC, its disabled for now, it will handle legacy modals in future to improve their accessibility */
pkp.eventBus.$on('open-modal-vue', (_options) => {
if (options.value) {
options2.value = _options;
isOpened2.value = true;
return;
const component2 = computed(() => {
if (!sideModal2.value?.component) {
return null;
}
options.value = _options;
isOpened.value = true;
return typeof sideModal2.value.component === 'string'
? LegacyModals[sideModal2.value.component]
: sideModal2.value.component;
});
function close(modalId) {
modalStore.closeSideModal(true, modalId);
}
</script>
40 changes: 18 additions & 22 deletions src/components/Modal/SideModal.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,7 @@ To correctly handle accessibility (title, description) and focus management - [h

## Usage

Important to keep in mind that if you need to nest these modals, its necessary to define them inside each other, which ensures they correctly handle mouse/keyboard events.

We recommend to define modals as individual component files, rather than inline them, that ensures that the `setup` function and any other component life cycle event are triggered as modal is opened/closed. Therefore its easier to control when for example to fetch data from API. Note that in stories the `SideModalBody` is inlined inside `SideModal`, so its easier to see the source code of examples.
We recommend to define modals as individual component files, rather than inline them, that ensures that the `setup` function and any other component life cycle event are triggered as modal is opened/closed. Therefore its easier to control when for example to fetch data from API. We might introduce option to define inline modals for basic use cases in future

### Defining Modal Component

Expand Down Expand Up @@ -55,29 +53,27 @@ We recommend to define modals as individual component files, rather than inline
</script>
```

### Controlling Modal Component
### Opening SideModal

```html
/** SubmissionModal.vue */
<template>
<PkpButton @click="isModalOpened = true">Open Modal</PkpButton>
<SideModal :open="isModalOpened" @close="closeModal">
<SubmissionModal />
</SideModal>
</template>
<script setup>
import {ref} from 'vue';
To open SideModal, use `openSideModal` function from [useModal](../?path=/docs/composables-usemodal--docs#opensidemodal) composable. This ensures that the SideModal is displayed correctly even if the SideModals are nested.

const isModalOpened = ref(true);
function closeModal() {
isModalOpened.value = false;
}
</script>
```
### Closing SideModal

## SideModal Props
To close the modal from the inside of the modal, there is `closeModal` slot prop available. Full example is in SideModalWithTabs story.

<ArgTypes />
```html
<SideModalBody>
<template #title>Title</template>
<template #default="{closeModal}">
<div class="p-4">
<div class="bg-secondary p-4">
content
<PkpButton class="mt-4" @click="closeModal()">Close</PkpButton>
</div>
</div>
</template>
</SideModalBody>
```

## SideModalBody Slots

Expand Down
Loading

0 comments on commit b2ac693

Please sign in to comment.