diff --git a/src/platform/assets/composables/useAssetBrowserDialog.stories.ts b/src/platform/assets/composables/useAssetBrowserDialog.stories.ts index e0095b6196..aa23fa583d 100644 --- a/src/platform/assets/composables/useAssetBrowserDialog.stories.ts +++ b/src/platform/assets/composables/useAssetBrowserDialog.stories.ts @@ -175,11 +175,17 @@ export default { onAssetSelected: (assetPath) => { console.log('Selected:', assetPath) // Update your component state + // Dialog auto-closes with animation via animateHide() } }) } - return { openBrowser } + // Manual close with animation (if needed) + const closeBrowser = () => { + assetBrowserDialog.hide() // Triggers animateHide() internally + } + + return { openBrowser, closeBrowser } } } @@ -188,6 +194,12 @@ export default { 💡 Try it: Use the interactive buttons above to see this code in action!

+
+

+ ✨ Animation: The close button now uses animateHide() for smooth transitions, + just like pressing ESC. Both auto-close on selection and manual close trigger proper animations. +

+
` diff --git a/src/platform/assets/composables/useAssetBrowserDialog.ts b/src/platform/assets/composables/useAssetBrowserDialog.ts index cc2a217a80..f76998546f 100644 --- a/src/platform/assets/composables/useAssetBrowserDialog.ts +++ b/src/platform/assets/composables/useAssetBrowserDialog.ts @@ -20,15 +20,21 @@ interface AssetBrowserDialogProps { export const useAssetBrowserDialog = () => { const dialogStore = useDialogStore() const dialogKey = 'global-asset-browser' + let onHideComplete: (() => void) | null = null - function hide() { - dialogStore.closeDialog({ key: dialogKey }) + function hide(): Promise { + return new Promise((resolve) => { + onHideComplete = resolve + dialogStore.animateHide({ key: dialogKey }) + }) } async function show(props: AssetBrowserDialogProps) { - const handleAssetSelected = (assetPath: string) => { - hide() // Auto-close on selection before async operations + const handleAssetSelected = async (assetPath: string) => { + // Update the widget value immediately - don't wait for animation props.onAssetSelected?.(assetPath) + // Then trigger the hide animation + await hide() } // Default dialog configuration for AssetBrowserModal @@ -36,6 +42,13 @@ export const useAssetBrowserDialog = () => { headless: true, modal: true, closable: true, + onAfterHide: () => { + // Resolve the hide() promise when animation completes + if (!onHideComplete) return + + onHideComplete() + onHideComplete = null + }, pt: { root: { class: 'rounded-2xl overflow-hidden' diff --git a/src/stores/dialogStore.ts b/src/stores/dialogStore.ts index f74e44c7d2..754fb67ad2 100644 --- a/src/stores/dialogStore.ts +++ b/src/stores/dialogStore.ts @@ -104,6 +104,39 @@ export const useDialogStore = defineStore('dialog', () => { } } + /** + * Triggers the dialog hide animation without immediately removing from stack. + * This is the preferred way to hide dialogs as it provides smooth visual transitions. + * + * Flow: animateHide() → PrimeVue animation → PrimeVue calls onAfterHide → closeDialog() + * + * Use this when: + * - User clicks close button + * - Programmatically hiding a dialog + * - You want the same smooth animation as ESC key + */ + function animateHide(options?: { key: string }) { + const targetDialog = options + ? dialogStack.value.find((d) => d.key === options.key) + : dialogStack.value.find((d) => d.key === activeKey.value) + if (!targetDialog) return + + // Set visible to false to trigger PrimeVue's close animation + // PrimeVue will call onAfterHide when animation completes, which calls closeDialog() + targetDialog.visible = false + } + + /** + * Immediately removes dialog from stack without animation. + * This is called internally after animations complete. + * + * Use this when: + * - Called from onAfterHide callback (PrimeVue animation already done) + * - Force-closing without animation (rare) + * - Cleaning up dialog state + * + * For user-initiated closes, prefer animateClose() instead. + */ function closeDialog(options?: { key: string }) { const targetDialog = options ? dialogStack.value.find((d) => d.key === options.key) @@ -246,6 +279,7 @@ export const useDialogStore = defineStore('dialog', () => { dialogStack, riseDialog, showDialog, + animateHide, closeDialog, showExtensionDialog, isDialogOpen, diff --git a/tests-ui/platform/assets/composables/useAssetBrowserDialog.test.ts b/tests-ui/platform/assets/composables/useAssetBrowserDialog.test.ts index 06bbbd71a7..e947fdc3cd 100644 --- a/tests-ui/platform/assets/composables/useAssetBrowserDialog.test.ts +++ b/tests-ui/platform/assets/composables/useAssetBrowserDialog.test.ts @@ -6,6 +6,13 @@ import { useDialogStore } from '@/stores/dialogStore' // Mock the dialog store vi.mock('@/stores/dialogStore') +// Mock the asset service +vi.mock('@/platform/assets/services/assetService', () => ({ + assetService: { + getAssetsForNodeType: vi.fn().mockResolvedValue([]) + } +})) + // Test factory functions interface AssetBrowserProps { nodeType: string @@ -25,14 +32,14 @@ function createAssetBrowserProps( describe('useAssetBrowserDialog', () => { describe('Asset Selection Flow', () => { - it('auto-closes dialog when asset is selected', () => { + it('auto-closes dialog when asset is selected', async () => { // Create fresh mocks for this test const mockShowDialog = vi.fn() - const mockCloseDialog = vi.fn() + const mockAnimateHide = vi.fn() vi.mocked(useDialogStore).mockReturnValue({ showDialog: mockShowDialog, - closeDialog: mockCloseDialog + animateHide: mockAnimateHide } as Partial> as ReturnType< typeof useDialogStore >) @@ -41,7 +48,7 @@ describe('useAssetBrowserDialog', () => { const onAssetSelected = vi.fn() const props = createAssetBrowserProps({ onAssetSelected }) - assetBrowserDialog.show(props) + await assetBrowserDialog.show(props) // Get the onSelect handler that was passed to the dialog const dialogCall = mockShowDialog.mock.calls[0][0] @@ -50,21 +57,21 @@ describe('useAssetBrowserDialog', () => { // Simulate asset selection onSelectHandler('selected-asset-path') - // Should call the original callback and close dialog + // Should call the original callback and trigger hide animation expect(onAssetSelected).toHaveBeenCalledWith('selected-asset-path') - expect(mockCloseDialog).toHaveBeenCalledWith({ + expect(mockAnimateHide).toHaveBeenCalledWith({ key: 'global-asset-browser' }) }) - it('closes dialog when close handler is called', () => { + it('closes dialog when close handler is called', async () => { // Create fresh mocks for this test const mockShowDialog = vi.fn() - const mockCloseDialog = vi.fn() + const mockAnimateHide = vi.fn() vi.mocked(useDialogStore).mockReturnValue({ showDialog: mockShowDialog, - closeDialog: mockCloseDialog + animateHide: mockAnimateHide } as Partial> as ReturnType< typeof useDialogStore >) @@ -72,7 +79,7 @@ describe('useAssetBrowserDialog', () => { const assetBrowserDialog = useAssetBrowserDialog() const props = createAssetBrowserProps() - assetBrowserDialog.show(props) + await assetBrowserDialog.show(props) // Get the onClose handler that was passed to the dialog const dialogCall = mockShowDialog.mock.calls[0][0] @@ -81,10 +88,9 @@ describe('useAssetBrowserDialog', () => { // Simulate dialog close onCloseHandler() - expect(mockCloseDialog).toHaveBeenCalledWith({ + expect(mockAnimateHide).toHaveBeenCalledWith({ key: 'global-asset-browser' }) }) - }) }) diff --git a/tests-ui/tests/store/dialogStore.test.ts b/tests-ui/tests/store/dialogStore.test.ts index 3d21ff695b..64260822c9 100644 --- a/tests-ui/tests/store/dialogStore.test.ts +++ b/tests-ui/tests/store/dialogStore.test.ts @@ -146,6 +146,21 @@ describe('dialogStore', () => { expect(store.isDialogOpen('test-dialog')).toBe(false) }) + it('should hide dialog by setting visible to false', () => { + const store = useDialogStore() + store.showDialog({ key: 'test-dialog', component: MockComponent }) + + const dialog = store.dialogStack[0] + expect(dialog.visible).toBe(true) + + store.animateHide({ key: 'test-dialog' }) + + // Dialog should be hidden but still in stack + expect(dialog.visible).toBe(false) + expect(store.dialogStack).toHaveLength(1) + expect(store.isDialogOpen('test-dialog')).toBe(true) + }) + it('should reuse existing dialog when showing with same key', () => { const store = useDialogStore()