Skip to content

Commit

Permalink
pkp/pkp-lib#9992 Update documentation for SideModal and Dialog, refac…
Browse files Browse the repository at this point in the history
…tor Dialog to expose the props for docs
  • Loading branch information
jardakotesovec committed Jun 25, 2024
1 parent 5cdd839 commit 10ee8ec
Show file tree
Hide file tree
Showing 10 changed files with 192 additions and 94 deletions.
30 changes: 30 additions & 0 deletions src/components/Modal/Dialog.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import {
Primary,
Controls,
Stories,
Meta,
ArgTypes,
Source,
} from '@storybook/blocks';
import Dialog from './Dialog.vue';
import * as DialogStories from './Dialog.stories.js';

<Meta of={DialogStories} />

# Dialog

Dialog purpose is to display simple feedback like success and error messages. Or request confirmation for example before deleting content.

## Usage

Dialog are opened via method `openDialog` from [useModal](../?path=/docs/composables-usemodal--docs#opensidemodal) composable. Check out the props description for details.

## Accessibility

To correctly handle accessibility (title, description) and focus management - [headless-ui](https://headlessui.com) library is used.

### Defining Modal Component

<ArgTypes of={Dialog} />

<Stories />
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import PkpDialog from './Dialog.vue';
import {useModal} from '@/composables/useModal.js';
import {within, userEvent} from '@storybook/test';

import {useModal} from './useModal';
import PkpButton from '@/components/Button/Button.vue';

export default {
title: 'composables/useModal',
title: 'Components/Dialog',
component: PkpDialog,
render: (args) => ({
components: {PkpButton},
setup() {
Expand All @@ -16,6 +17,11 @@ export default {
<PkpButton @click="openDialog(args)">{{args.buttonName}}</PkpButton>
`,
}),
decorators: [
() => ({
template: '<div style="height: 400px"><story/></div>',
}),
],
};

export const DialogBasic = {
Expand Down Expand Up @@ -47,11 +53,51 @@ export const DialogBasic = {

await user.click(canvas.getByText('Basic Example'));
},
decorators: [
() => ({
template: '<div style="height: 400px"><story/></div>',
}),
],
};

const DialogBodyComponent = {
template: `
<p>{{ 'Some DOI(s) could not be updated' }}</p>
<ul>
<li v-for="errorMessage in failedDoiActions" :key="errorMessage.index">
{{ errorMessage }}
</li>
</ul>
`,
props: {
failedDoiActions: {type: Array, required: true},
},
};

export const WithBodyComponent = {
args: {
buttonName: 'With Body Component',
title: 'Submit Article',
bodyComponent: DialogBodyComponent,
bodyProps: {
failedDoiActions: ['First error to display', 'Second error to display'],
},
actions: [
{
label: 'Ok',
isPrimary: true,
callback: (close) => {
// user has confirmed
close();
},
},
],
close: () => {
// dialog has been closed
},
},
play: async ({canvasElement}) => {
// Assigns canvas to the component root element
const canvas = within(canvasElement);
const user = userEvent.setup();

await user.click(canvas.getByText('With Body Component'));
},
};

export const DialogComplex = {
Expand Down Expand Up @@ -91,9 +137,4 @@ export const DialogComplex = {

await user.click(canvas.getByText('Full Example'));
},
decorators: [
() => ({
template: '<div style="height: 400px"><story/></div>',
}),
],
};
84 changes: 37 additions & 47 deletions src/components/Modal/Dialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,10 @@
>
<div class="flex min-h-12 items-center">
<DialogTitle
v-if="dialogProps.title"
v-if="title"
class="m-0 min-w-[1px] overflow-x-hidden overflow-ellipsis whitespace-nowrap px-4 py-2 text-xl-bold"
>
{{ dialogProps.title }}
{{ title }}
</DialogTitle>
<button
class="me-2 ms-auto cursor-pointer border-0 bg-transparent text-center"
Expand All @@ -47,22 +47,22 @@
:aria-hidden="true"
/>
<span class="-screenReader">
{{ dialogProps.closeLabel || t('common.close') }}
{{ t('common.close') }}
</span>
</button>
</div>
<div class="modal-content p-4">
<div v-html="dialogProps.message" />
<div v-html="message" />
<component
:is="dialogProps.bodyComponent"
v-if="dialogProps.bodyComponent"
v-bind="dialogProps.bodyProps"
:is="bodyComponent"
v-if="bodyComponent"
v-bind="bodyProps"
/>
</div>
<div class="flex items-center justify-end p-4">
<spinner v-if="isLoading" />
<pkp-button
v-for="action in dialogProps.actions"
v-for="action in actions"
:key="action.label"
class="ms-2"
:element="action.element || 'button'"
Expand All @@ -86,8 +86,7 @@
</template>

<script setup>
import {ref, computed, onMounted, onUnmounted, watch} from 'vue';
import {storeToRefs} from 'pinia';
import {ref, watch} from 'vue';
import {
Dialog as HLDialog,
Expand All @@ -97,35 +96,45 @@ import {
TransitionChild,
} from '@headlessui/vue';
import {useModalStore} from '@/stores/modalStore';
const myLevel = ref(0);
const modalStore = useModalStore();
const {dialogProps, dialogOpened, dialogLevel} = storeToRefs(modalStore);
const {closeDialog, increaseDialogLevel, decreaseDialogLevel} = modalStore;
const props = defineProps({
/** Used only internally, don't pass this prop via openDialog */
opened: {type: Boolean, default: false},
/** Title of the dialog */
title: {type: String, required: true},
/** Message to be displayed, for more complex messages use bodyComponent&bodyProps */
message: {type: String, default: null},
/** For more complex messages Vue.js component can be passed */
bodyComponent: {type: Object, default: null},
/** Props to be passed to bodyComponent */
bodyProps: {type: Object, default: null},
/** Array of button props to display actions, following props are passed to button component: label, element, href, isPrimary, isWarnable, callback */
actions: {type: Array, default: () => []},
/** Callback when dialog is being closed by close button or clicking outside of the modal */
close: {type: Function, default: null},
});
const opened = computed(
() => dialogOpened.value && myLevel.value === dialogLevel.value,
);
const emit = defineEmits(['close']);
const isLoading = ref(false);
// 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;
}
});
watch(
() => props.opened,
(prevOpened, nextOpened) => {
if (prevOpened === true && nextOpened === false) {
isLoading.value = false;
}
},
);
function onClose() {
if (dialogProps.value.close) {
dialogProps.value.close();
if (props.close) {
props.close();
}
closeDialog();
emit('close');
}
function fireCallback(callback) {
Expand All @@ -136,23 +145,4 @@ function fireCallback(callback) {
});
}
}
onMounted(() => {
increaseDialogLevel();
myLevel.value = dialogLevel.value;
});
onUnmounted(() => {
decreaseDialogLevel();
});
</script>

<style scoped>
.modal-content > p:first-child {
margin-top: 0;
}
.modal-content > p:last-child {
margin-bottom: 0;
}
</style>
2 changes: 1 addition & 1 deletion src/components/Modal/Modal.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import * as ModalStories from './Modal.stories.js';

# Modal

**Deprecated** in favour of SideModal.
**Deprecated** in favour of SideModal. Please migrate to use SideModal and `openSideModal` function from [useModal](../?path=/docs/composables-usemodal--docs#opensidemodal) composable.

## Usage

Expand Down
24 changes: 22 additions & 2 deletions src/components/Modal/ModalManager.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,31 @@
@close="() => close(sideModal1?.modalId)"
>
<component :is="component1" v-bind="sideModal1?.props" />
<PkpDialog
:opened="dialogOpened && dialogLevel === 1"
v-bind="dialogProps"
@close="closeDialog"
></PkpDialog>
<SideModal
close-label="Close"
:modal-level="2"
:open="sideModal2?.opened || false"
@close="() => close(sideModal2?.modalId)"
>
<component :is="component2" v-bind="sideModal2?.props" />
<PkpDialog
:opened="dialogOpened && dialogLevel === 2"
v-bind="dialogProps"
@close="closeDialog"
></PkpDialog>
</SideModal>
</SideModal>
<PkpDialog></PkpDialog>
<PkpDialog
:opened="dialogOpened && dialogLevel === 0"
v-bind="dialogProps"
@close="closeDialog"
></PkpDialog>
</template>
<script setup>
Expand All @@ -30,7 +45,8 @@ import WorkflowLogResponseForModal from '@/pages/workflow/WorkflowLogResponseFor
const GlobalModals = {LegacyAjax, WorkflowLogResponseForModal};
const modalStore = useModalStore();
const {sideModal1, sideModal2} = storeToRefs(useModalStore());
const {sideModal1, sideModal2, dialogProps, dialogOpened, dialogLevel} =
storeToRefs(useModalStore());
// Component can be either string or vue component
const component1 = computed(() => {
Expand All @@ -50,4 +66,8 @@ const component2 = computed(() => {
function close(modalId) {
modalStore.closeSideModalById(true, modalId);
}
function closeDialog() {
modalStore.closeDialog();
}
</script>
14 changes: 7 additions & 7 deletions src/components/Modal/SideModal.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@ import * as SideModalStories from './SideModal.stories.js';

# Side Modal

Side modal has been introduced with new submission listing and will gradually replace both legacy jQuery modal and Vue.js central modal.

To correctly handle accessibility (title, description) and focus management - [headless-ui](https://headlessui.com) library is used.
Side modal has been introduced in 3.5 all existing modals, including jquery legacy ones are now rendered as SideModal.

## Usage

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
Its important to create modals as separate files, which works best with our [useModal](../?path=/docs/composables-usemodal--docs#opensidemodal) composable. Also having them defined as individual components 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.

## Accessibility

To correctly handle accessibility (title, description) and focus management - [headless-ui](https://headlessui.com) library is used.

### Defining Modal Component

Expand All @@ -42,9 +44,7 @@ We recommend to define modals as individual component files, rather than inline
</PkpButton>
</template>

<div class="p-4">
<div class="bg-secondary p-4">CONTENT</div>
</div>
<SideModalLayoutBasic>CONTENT</SideModalLayoutBasic>
</SideModalBody>
</template>

Expand Down
6 changes: 3 additions & 3 deletions src/components/Modal/SideModal.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {ref} from 'vue';
import SideModal from './SideModal.vue';
import {useModal} from '@/composables/useModal.js';
import SideModalBody from './SideModalBody.vue';
import SideModalLayoutBasic from './SideModalLayoutBasic.vue';
import SideModalLayout2Columns from './SideModalLayout2Columns.vue';
import PkpForm from '@/components/Form/Form.vue';
import cloneDeep from 'clone-deep';
Expand All @@ -18,7 +19,7 @@ export default {
};

const SideModalBase = {
components: {SideModalBody},
components: {SideModalBody, SideModalLayoutBasic},
template: `
<SideModalBody>
<template #pre-title>325</template>
Expand All @@ -35,8 +36,7 @@ const SideModalBase = {
</PkpButton>
</template>
<div class="p-4">
<div class="bg-secondary p-4">CONTENT</div>
<SideModalLayoutBasic>CONTENT</SideModalLayoutBasic>
</div>
</SideModalBody>
`,
Expand Down
Loading

0 comments on commit 10ee8ec

Please sign in to comment.