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 = { 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; // 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: `
Open Modal
`, }), }; 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: `
Open Form Modal
`, }), 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: `
Open Modal with Confirmation

Try to close this modal with text in the input field.

No unsaved changes. Modal will close normally.

Unsaved changes detected!

`, }), 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 }, };