mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-01 19:20:10 +00:00
[fix] animate AssetBrowserModal dialog on close
This commit is contained in:
@@ -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>
|
||||
`
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user