[fix] animate AssetBrowserModal dialog on close

This commit is contained in:
Arjan Singh
2025-09-17 17:56:13 -07:00
parent b5a6a4fe83
commit cdd77f9cd8
5 changed files with 97 additions and 17 deletions

View File

@@ -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 }
}
}</code></pre>
</div>
@@ -188,6 +194,12 @@ export default {
<strong>💡 Try it:</strong> Use the interactive buttons above to see this code in action!
</p>
</div>
<div class="mt-4 p-3 bg-green-50 border border-green-200 rounded">
<p class="text-sm text-green-800">
<strong>✨ Animation:</strong> The close button now uses <code>animateHide()</code> for smooth transitions,
just like pressing ESC. Both auto-close on selection and manual close trigger proper animations.
</p>
</div>
</div>
</div>
`

View File

@@ -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<void> {
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'

View File

@@ -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,

View File

@@ -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<ReturnType<typeof useDialogStore>> 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<ReturnType<typeof useDialogStore>> 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'
})
})
})
})

View File

@@ -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()