276 lines
9.7 KiB
TypeScript
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
|
|
},
|
|
};
|