mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-30 19:21:54 +00:00
[fix] animate AssetBrowserModal dialog on close
This commit is contained in:
@@ -175,11 +175,17 @@ export default {
|
|||||||
onAssetSelected: (assetPath) => {
|
onAssetSelected: (assetPath) => {
|
||||||
console.log('Selected:', assetPath)
|
console.log('Selected:', assetPath)
|
||||||
// Update your component state
|
// 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>
|
}</code></pre>
|
||||||
</div>
|
</div>
|
||||||
@@ -188,6 +194,12 @@ export default {
|
|||||||
<strong>💡 Try it:</strong> Use the interactive buttons above to see this code in action!
|
<strong>💡 Try it:</strong> Use the interactive buttons above to see this code in action!
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
|
|||||||
@@ -20,15 +20,21 @@ interface AssetBrowserDialogProps {
|
|||||||
export const useAssetBrowserDialog = () => {
|
export const useAssetBrowserDialog = () => {
|
||||||
const dialogStore = useDialogStore()
|
const dialogStore = useDialogStore()
|
||||||
const dialogKey = 'global-asset-browser'
|
const dialogKey = 'global-asset-browser'
|
||||||
|
let onHideComplete: (() => void) | null = null
|
||||||
|
|
||||||
function hide() {
|
function hide(): Promise<void> {
|
||||||
dialogStore.closeDialog({ key: dialogKey })
|
return new Promise((resolve) => {
|
||||||
|
onHideComplete = resolve
|
||||||
|
dialogStore.animateHide({ key: dialogKey })
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function show(props: AssetBrowserDialogProps) {
|
async function show(props: AssetBrowserDialogProps) {
|
||||||
const handleAssetSelected = (assetPath: string) => {
|
const handleAssetSelected = async (assetPath: string) => {
|
||||||
hide() // Auto-close on selection before async operations
|
// Update the widget value immediately - don't wait for animation
|
||||||
props.onAssetSelected?.(assetPath)
|
props.onAssetSelected?.(assetPath)
|
||||||
|
// Then trigger the hide animation
|
||||||
|
await hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default dialog configuration for AssetBrowserModal
|
// Default dialog configuration for AssetBrowserModal
|
||||||
@@ -36,6 +42,13 @@ export const useAssetBrowserDialog = () => {
|
|||||||
headless: true,
|
headless: true,
|
||||||
modal: true,
|
modal: true,
|
||||||
closable: true,
|
closable: true,
|
||||||
|
onAfterHide: () => {
|
||||||
|
// Resolve the hide() promise when animation completes
|
||||||
|
if (!onHideComplete) return
|
||||||
|
|
||||||
|
onHideComplete()
|
||||||
|
onHideComplete = null
|
||||||
|
},
|
||||||
pt: {
|
pt: {
|
||||||
root: {
|
root: {
|
||||||
class: 'rounded-2xl overflow-hidden'
|
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 }) {
|
function closeDialog(options?: { key: string }) {
|
||||||
const targetDialog = options
|
const targetDialog = options
|
||||||
? dialogStack.value.find((d) => d.key === options.key)
|
? dialogStack.value.find((d) => d.key === options.key)
|
||||||
@@ -246,6 +279,7 @@ export const useDialogStore = defineStore('dialog', () => {
|
|||||||
dialogStack,
|
dialogStack,
|
||||||
riseDialog,
|
riseDialog,
|
||||||
showDialog,
|
showDialog,
|
||||||
|
animateHide,
|
||||||
closeDialog,
|
closeDialog,
|
||||||
showExtensionDialog,
|
showExtensionDialog,
|
||||||
isDialogOpen,
|
isDialogOpen,
|
||||||
|
|||||||
@@ -6,6 +6,13 @@ import { useDialogStore } from '@/stores/dialogStore'
|
|||||||
// Mock the dialog store
|
// Mock the dialog store
|
||||||
vi.mock('@/stores/dialogStore')
|
vi.mock('@/stores/dialogStore')
|
||||||
|
|
||||||
|
// Mock the asset service
|
||||||
|
vi.mock('@/platform/assets/services/assetService', () => ({
|
||||||
|
assetService: {
|
||||||
|
getAssetsForNodeType: vi.fn().mockResolvedValue([])
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
// Test factory functions
|
// Test factory functions
|
||||||
interface AssetBrowserProps {
|
interface AssetBrowserProps {
|
||||||
nodeType: string
|
nodeType: string
|
||||||
@@ -25,14 +32,14 @@ function createAssetBrowserProps(
|
|||||||
|
|
||||||
describe('useAssetBrowserDialog', () => {
|
describe('useAssetBrowserDialog', () => {
|
||||||
describe('Asset Selection Flow', () => {
|
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
|
// Create fresh mocks for this test
|
||||||
const mockShowDialog = vi.fn()
|
const mockShowDialog = vi.fn()
|
||||||
const mockCloseDialog = vi.fn()
|
const mockAnimateHide = vi.fn()
|
||||||
|
|
||||||
vi.mocked(useDialogStore).mockReturnValue({
|
vi.mocked(useDialogStore).mockReturnValue({
|
||||||
showDialog: mockShowDialog,
|
showDialog: mockShowDialog,
|
||||||
closeDialog: mockCloseDialog
|
animateHide: mockAnimateHide
|
||||||
} as Partial<ReturnType<typeof useDialogStore>> as ReturnType<
|
} as Partial<ReturnType<typeof useDialogStore>> as ReturnType<
|
||||||
typeof useDialogStore
|
typeof useDialogStore
|
||||||
>)
|
>)
|
||||||
@@ -41,7 +48,7 @@ describe('useAssetBrowserDialog', () => {
|
|||||||
const onAssetSelected = vi.fn()
|
const onAssetSelected = vi.fn()
|
||||||
const props = createAssetBrowserProps({ onAssetSelected })
|
const props = createAssetBrowserProps({ onAssetSelected })
|
||||||
|
|
||||||
assetBrowserDialog.show(props)
|
await assetBrowserDialog.show(props)
|
||||||
|
|
||||||
// Get the onSelect handler that was passed to the dialog
|
// Get the onSelect handler that was passed to the dialog
|
||||||
const dialogCall = mockShowDialog.mock.calls[0][0]
|
const dialogCall = mockShowDialog.mock.calls[0][0]
|
||||||
@@ -50,21 +57,21 @@ describe('useAssetBrowserDialog', () => {
|
|||||||
// Simulate asset selection
|
// Simulate asset selection
|
||||||
onSelectHandler('selected-asset-path')
|
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(onAssetSelected).toHaveBeenCalledWith('selected-asset-path')
|
||||||
expect(mockCloseDialog).toHaveBeenCalledWith({
|
expect(mockAnimateHide).toHaveBeenCalledWith({
|
||||||
key: 'global-asset-browser'
|
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
|
// Create fresh mocks for this test
|
||||||
const mockShowDialog = vi.fn()
|
const mockShowDialog = vi.fn()
|
||||||
const mockCloseDialog = vi.fn()
|
const mockAnimateHide = vi.fn()
|
||||||
|
|
||||||
vi.mocked(useDialogStore).mockReturnValue({
|
vi.mocked(useDialogStore).mockReturnValue({
|
||||||
showDialog: mockShowDialog,
|
showDialog: mockShowDialog,
|
||||||
closeDialog: mockCloseDialog
|
animateHide: mockAnimateHide
|
||||||
} as Partial<ReturnType<typeof useDialogStore>> as ReturnType<
|
} as Partial<ReturnType<typeof useDialogStore>> as ReturnType<
|
||||||
typeof useDialogStore
|
typeof useDialogStore
|
||||||
>)
|
>)
|
||||||
@@ -72,7 +79,7 @@ describe('useAssetBrowserDialog', () => {
|
|||||||
const assetBrowserDialog = useAssetBrowserDialog()
|
const assetBrowserDialog = useAssetBrowserDialog()
|
||||||
const props = createAssetBrowserProps()
|
const props = createAssetBrowserProps()
|
||||||
|
|
||||||
assetBrowserDialog.show(props)
|
await assetBrowserDialog.show(props)
|
||||||
|
|
||||||
// Get the onClose handler that was passed to the dialog
|
// Get the onClose handler that was passed to the dialog
|
||||||
const dialogCall = mockShowDialog.mock.calls[0][0]
|
const dialogCall = mockShowDialog.mock.calls[0][0]
|
||||||
@@ -81,10 +88,9 @@ describe('useAssetBrowserDialog', () => {
|
|||||||
// Simulate dialog close
|
// Simulate dialog close
|
||||||
onCloseHandler()
|
onCloseHandler()
|
||||||
|
|
||||||
expect(mockCloseDialog).toHaveBeenCalledWith({
|
expect(mockAnimateHide).toHaveBeenCalledWith({
|
||||||
key: 'global-asset-browser'
|
key: 'global-asset-browser'
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -146,6 +146,21 @@ describe('dialogStore', () => {
|
|||||||
expect(store.isDialogOpen('test-dialog')).toBe(false)
|
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', () => {
|
it('should reuse existing dialog when showing with same key', () => {
|
||||||
const store = useDialogStore()
|
const store = useDialogStore()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user