mitlist/fe/src/components/valerie/VModal.stories.ts

276 lines
9.7 KiB
TypeScript

import VModal from './VModal.vue';
import VButton from './VButton.vue'; // For modal footer actions
import VInput from './VInput.vue'; // For form elements in modal
import VFormField from './VFormField.vue'; // For form layout in modal
import type { Meta, StoryObj } from '@storybook/vue3';
import { ref, watch } from 'vue';
const meta: Meta<typeof VModal> = {
title: 'Valerie/VModal',
component: VModal,
tags: ['autodocs'],
argTypes: {
modelValue: { control: 'boolean', description: 'Controls modal visibility (v-model).' },
title: { control: 'text' },
hideCloseButton: { control: 'boolean' },
persistent: { control: 'boolean' },
size: { control: 'select', options: ['sm', 'md', 'lg'] },
idBase: { control: 'text' },
// Events
'update:modelValue': { action: 'update:modelValue', table: { disable: true } },
close: { action: 'close' },
opened: { action: 'opened' },
closed: { action: 'closed' },
// Slots
header: { table: { disable: true } },
default: { table: { disable: true } }, // Body slot
footer: { table: { disable: true } },
},
parameters: {
docs: {
description: {
component: 'A modal dialog component that teleports to the body. Supports v-model for visibility, custom content via slots, and various interaction options.',
},
},
// To better demonstrate modals, you might want a dark background for stories if not default
// backgrounds: { default: 'dark' },
},
};
export default meta;
type Story = StoryObj<typeof VModal>;
// Template for managing modal visibility in stories
const ModalInteractionTemplate: Story = {
render: (args) => ({
components: { VModal, VButton, VInput, VFormField },
setup() {
// Use a local ref for modelValue to simulate v-model behavior within the story
// This allows Storybook controls to set the initial 'modelValue' arg,
// and then the component and story can interact with this local state.
const isModalOpen = ref(args.modelValue);
// Watch for changes from Storybook controls to update local state
watch(() => args.modelValue, (newVal) => {
isModalOpen.value = newVal;
});
// Function to update Storybook arg when local state changes (simulates emit)
const onUpdateModelValue = (val: boolean) => {
isModalOpen.value = val;
// args.modelValue = val; // This would update the control, but can cause loops if not careful
// Storybook's action logger for 'update:modelValue' will show this.
};
return { ...args, isModalOpen, onUpdateModelValue }; // Spread args to pass all other props
},
// Base template structure, specific content will be overridden by each story
template: `
<div>
<VButton @click="isModalOpen = true">Open Modal</VButton>
<VModal
:modelValue="isModalOpen"
@update:modelValue="onUpdateModelValue"
:title="title"
:hideCloseButton="hideCloseButton"
:persistent="persistent"
:size="size"
:idBase="idBase"
@opened="() => $emit('opened')"
@closed="() => $emit('closed')"
@close="() => $emit('close')"
>
<template #header v-if="args.customHeaderSlot">
<div style="display: flex; justify-content: space-between; align-items: center; width: 100%;">
<h3 style="margin:0;"><em>Custom Header Slot!</em></h3>
<VButton v-if="!hideCloseButton" @click="isModalOpen = false" size="sm" variant="neutral">Close from slot</VButton>
</div>
</template>
<template #default>
<p v-if="args.bodyContent">{{ args.bodyContent }}</p>
<slot name="storyDefaultContent"></slot>
</template>
<template #footer v-if="args.showFooter !== false">
<slot name="storyFooterContent">
<VButton variant="neutral" @click="isModalOpen = false">Cancel</VButton>
<VButton variant="primary" @click="isModalOpen = false">Submit</VButton>
</slot>
</template>
</VModal>
</div>
`,
}),
};
export const Basic: Story = {
...ModalInteractionTemplate,
args: {
modelValue: false, // Initial state for Storybook control
title: 'Basic Modal Title',
bodyContent: 'This is the main content of the modal. You can put any HTML or Vue components here.',
size: 'md',
},
};
export const WithCustomHeader: Story = {
...ModalInteractionTemplate,
args: {
...Basic.args,
title: 'This title is overridden by slot',
customHeaderSlot: true, // Custom arg for story to toggle header slot template
},
};
export const Persistent: Story = {
...ModalInteractionTemplate,
args: {
...Basic.args,
title: 'Persistent Modal',
bodyContent: 'This modal will not close when clicking the backdrop. Use the "Close" button or Escape key.',
persistent: true,
},
};
export const NoCloseButton: Story = {
...ModalInteractionTemplate,
args: {
...Basic.args,
title: 'No "X" Button',
bodyContent: 'The default header close button (X) is hidden. You must provide other means to close it (e.g., footer buttons, Esc key).',
hideCloseButton: true,
},
};
export const SmallSize: Story = {
...ModalInteractionTemplate,
args: {
...Basic.args,
title: 'Small Modal (sm)',
size: 'sm',
bodyContent: 'This modal uses the "sm" size preset for a smaller width.',
},
};
export const LargeSize: Story = {
...ModalInteractionTemplate,
args: {
...Basic.args,
title: 'Large Modal (lg)',
size: 'lg',
bodyContent: 'This modal uses the "lg" size preset for a larger width. Useful for forms or more content.',
},
};
export const WithFormContent: Story = {
...ModalInteractionTemplate,
render: (args) => ({ // Override render for specific slot content
components: { VModal, VButton, VInput, VFormField },
setup() {
const isModalOpen = ref(args.modelValue);
watch(() => args.modelValue, (newVal) => { isModalOpen.value = newVal; });
const onUpdateModelValue = (val: boolean) => { isModalOpen.value = val; };
const username = ref('');
const password = ref('');
return { ...args, isModalOpen, onUpdateModelValue, username, password };
},
template: `
<div>
<VButton @click="isModalOpen = true">Open Form Modal</VButton>
<VModal
:modelValue="isModalOpen"
@update:modelValue="onUpdateModelValue"
:title="title"
:size="size"
>
<VFormField label="Username" forId="modalUser">
<VInput id="modalUser" v-model="username" placeholder="Enter username" />
</VFormField>
<VFormField label="Password" forId="modalPass">
<VInput id="modalPass" type="password" v-model="password" placeholder="Enter password" />
</VFormField>
<template #footer>
<VButton variant="neutral" @click="isModalOpen = false">Cancel</VButton>
<VButton variant="primary" @click="() => { alert('Submitted: ' + username + ' / ' + password); isModalOpen = false; }">Log In</VButton>
</template>
</VModal>
</div>
`,
}),
args: {
...Basic.args,
title: 'Login Form',
showFooter: true, // Ensure default footer with submit/cancel is shown by template logic
},
};
export const ConfirmOnClose: Story = {
render: (args) => ({
components: { VModal, VButton, VInput },
setup() {
const isModalOpen = ref(args.modelValue);
const textInput = ref("Some unsaved data...");
const hasUnsavedChanges = computed(() => textInput.value !== "");
watch(() => args.modelValue, (newVal) => { isModalOpen.value = newVal; });
const requestClose = () => {
if (hasUnsavedChanges.value) {
if (confirm("You have unsaved changes. Are you sure you want to close?")) {
isModalOpen.value = false;
// args.modelValue = false; // Update arg
}
} else {
isModalOpen.value = false;
// args.modelValue = false; // Update arg
}
};
// This simulates the @update:modelValue from VModal,
// but intercepts it for confirmation logic.
const handleModalUpdate = (value: boolean) => {
if (value === false) { // Modal is trying to close
requestClose();
} else {
isModalOpen.value = true;
// args.modelValue = true;
}
};
return { ...args, isModalOpen, textInput, handleModalUpdate, requestClose };
},
template: `
<div>
<VButton @click="isModalOpen = true">Open Modal with Confirmation</VButton>
<VModal
:modelValue="isModalOpen"
@update:modelValue="handleModalUpdate"
:title="title"
:persistent="true"
:hideCloseButton="false"
>
<p>Try to close this modal with text in the input field.</p>
<VInput v-model="textInput" placeholder="Type something here" />
<p v-if="textInput === ''" style="color: green;">No unsaved changes. Modal will close normally.</p>
<p v-else style="color: orange;">Unsaved changes detected!</p>
<template #footer>
<VButton variant="neutral" @click="requestClose">Attempt Close</VButton>
<VButton variant="primary" @click="() => { textInput = ''; alert('Changes saved (simulated)'); }">Save Changes</VButton>
</template>
</VModal>
</div>
`,
}),
args: {
...Basic.args,
title: 'Confirm Close Modal',
bodyContent: '', // Content is in the template for this story
// persistent: true, // Good for confirm on close so backdrop click doesn't bypass confirm
// hideCloseButton: true, // Also good for confirm on close
},
};