mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-25 15:15:47 +00:00
Compare commits
16 Commits
feat/toolt
...
fix/dropdo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
07e892bcc5 | ||
|
|
b77f0551bd | ||
|
|
04a172c880 | ||
|
|
b29c56dd55 | ||
|
|
8f000fe8da | ||
|
|
8618661bab | ||
|
|
43a24cc869 | ||
|
|
3f3d6e4ebe | ||
|
|
0a4d3e307b | ||
|
|
83da733a5f | ||
|
|
eed1fbbb8a | ||
|
|
c514b6a825 | ||
|
|
88dfe6d749 | ||
|
|
e98df0a577 | ||
|
|
15226f7730 | ||
|
|
b495372511 |
@@ -112,7 +112,9 @@ export const TestIds = {
|
|||||||
decrement: 'decrement',
|
decrement: 'decrement',
|
||||||
increment: 'increment',
|
increment: 'increment',
|
||||||
domWidgetTextarea: 'dom-widget-textarea',
|
domWidgetTextarea: 'dom-widget-textarea',
|
||||||
subgraphEnterButton: 'subgraph-enter-button'
|
subgraphEnterButton: 'subgraph-enter-button',
|
||||||
|
formDropdownMenu: 'form-dropdown-menu',
|
||||||
|
formDropdownTrigger: 'form-dropdown-trigger'
|
||||||
},
|
},
|
||||||
builder: {
|
builder: {
|
||||||
footerNav: 'builder-footer-nav',
|
footerNav: 'builder-footer-nav',
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
comfyPageFixture as test,
|
comfyPageFixture as test,
|
||||||
comfyExpect as expect
|
comfyExpect as expect
|
||||||
} from '@e2e/fixtures/ComfyPage'
|
} from '@e2e/fixtures/ComfyPage'
|
||||||
|
import { TestIds } from '@e2e/fixtures/selectors'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default workflow widget inputs as [nodeId, widgetName] tuples.
|
* Default workflow widget inputs as [nodeId, widgetName] tuples.
|
||||||
@@ -137,12 +138,12 @@ test.describe('App mode dropdown clipping', { tag: '@ui' }, () => {
|
|||||||
const dropdownButton = imageRow.locator('button:has(> span)').first()
|
const dropdownButton = imageRow.locator('button:has(> span)').first()
|
||||||
await dropdownButton.click()
|
await dropdownButton.click()
|
||||||
|
|
||||||
// The unstyled PrimeVue Popover renders with role="dialog".
|
const menu = comfyPage.page
|
||||||
// Locate the one containing the image grid (filter buttons like "All", "Inputs").
|
.getByTestId(TestIds.widgets.formDropdownMenu)
|
||||||
const popover = comfyPage.appMode.imagePickerPopover
|
.first()
|
||||||
await expect(popover).toBeVisible({ timeout: 5000 })
|
await expect(menu).toBeVisible({ timeout: 5000 })
|
||||||
|
|
||||||
const isInViewport = await popover.evaluate((el) => {
|
const isInViewport = await menu.evaluate((el) => {
|
||||||
const rect = el.getBoundingClientRect()
|
const rect = el.getBoundingClientRect()
|
||||||
return (
|
return (
|
||||||
rect.top >= 0 &&
|
rect.top >= 0 &&
|
||||||
@@ -153,7 +154,7 @@ test.describe('App mode dropdown clipping', { tag: '@ui' }, () => {
|
|||||||
})
|
})
|
||||||
expect(isInViewport).toBe(true)
|
expect(isInViewport).toBe(true)
|
||||||
|
|
||||||
const isClipped = await popover.evaluate(isClippedByAnyAncestor)
|
const isClipped = await menu.evaluate(isClippedByAnyAncestor)
|
||||||
expect(isClipped).toBe(false)
|
expect(isClipped).toBe(false)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
import {
|
||||||
|
comfyExpect as expect,
|
||||||
|
comfyPageFixture as test
|
||||||
|
} from '../../../../fixtures/ComfyPage'
|
||||||
|
import { TestIds } from '../../../../fixtures/selectors'
|
||||||
|
|
||||||
|
test.describe(
|
||||||
|
'FormDropdown positioning in Vue nodes',
|
||||||
|
{ tag: ['@widget', '@node'] },
|
||||||
|
() => {
|
||||||
|
test.beforeEach(async ({ comfyPage }) => {
|
||||||
|
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true)
|
||||||
|
await comfyPage.workflow.loadWorkflow('widgets/load_image_widget')
|
||||||
|
await comfyPage.vueNodes.waitForNodes()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('dropdown menu appears directly below the trigger', async ({
|
||||||
|
comfyPage
|
||||||
|
}) => {
|
||||||
|
const node = comfyPage.vueNodes.getNodeByTitle('Load Image')
|
||||||
|
await expect(node).toBeVisible()
|
||||||
|
|
||||||
|
const trigger = node.getByTestId(TestIds.widgets.formDropdownTrigger)
|
||||||
|
await trigger.first().click()
|
||||||
|
|
||||||
|
const menu = comfyPage.page.getByTestId(TestIds.widgets.formDropdownMenu)
|
||||||
|
await expect(menu).toBeVisible({ timeout: 5000 })
|
||||||
|
|
||||||
|
const triggerBox = await trigger.first().boundingBox()
|
||||||
|
const menuBox = await menu.boundingBox()
|
||||||
|
|
||||||
|
expect(triggerBox).toBeTruthy()
|
||||||
|
expect(menuBox).toBeTruthy()
|
||||||
|
|
||||||
|
// Menu top should be near the trigger bottom (within 20px tolerance for padding)
|
||||||
|
expect(menuBox!.y).toBeGreaterThanOrEqual(
|
||||||
|
triggerBox!.y + triggerBox!.height - 5
|
||||||
|
)
|
||||||
|
expect(menuBox!.y).toBeLessThanOrEqual(
|
||||||
|
triggerBox!.y + triggerBox!.height + 20
|
||||||
|
)
|
||||||
|
|
||||||
|
// Menu left should be near the trigger left (within 10px tolerance)
|
||||||
|
expect(menuBox!.x).toBeGreaterThanOrEqual(triggerBox!.x - 10)
|
||||||
|
expect(menuBox!.x).toBeLessThanOrEqual(triggerBox!.x + 10)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('dropdown menu appears correctly at different zoom levels', async ({
|
||||||
|
comfyPage
|
||||||
|
}) => {
|
||||||
|
for (const zoom of [0.75, 1.5]) {
|
||||||
|
// Set zoom via canvas
|
||||||
|
await comfyPage.page.evaluate((scale) => {
|
||||||
|
const canvas = window.app!.canvas
|
||||||
|
canvas.ds.scale = scale
|
||||||
|
canvas.setDirty(true, true)
|
||||||
|
}, zoom)
|
||||||
|
await comfyPage.nextFrame()
|
||||||
|
|
||||||
|
const node = comfyPage.vueNodes.getNodeByTitle('Load Image')
|
||||||
|
await expect(node).toBeVisible()
|
||||||
|
|
||||||
|
const trigger = node.getByTestId(TestIds.widgets.formDropdownTrigger)
|
||||||
|
await trigger.first().click()
|
||||||
|
|
||||||
|
const menu = comfyPage.page.getByTestId(
|
||||||
|
TestIds.widgets.formDropdownMenu
|
||||||
|
)
|
||||||
|
await expect(menu).toBeVisible({ timeout: 5000 })
|
||||||
|
|
||||||
|
const triggerBox = await trigger.first().boundingBox()
|
||||||
|
const menuBox = await menu.boundingBox()
|
||||||
|
|
||||||
|
expect(triggerBox).toBeTruthy()
|
||||||
|
expect(menuBox).toBeTruthy()
|
||||||
|
|
||||||
|
// Menu top should still be near trigger bottom regardless of zoom
|
||||||
|
expect(menuBox!.y).toBeGreaterThanOrEqual(
|
||||||
|
triggerBox!.y + triggerBox!.height - 5
|
||||||
|
)
|
||||||
|
expect(menuBox!.y).toBeLessThanOrEqual(
|
||||||
|
triggerBox!.y + triggerBox!.height + 20 * zoom
|
||||||
|
)
|
||||||
|
|
||||||
|
// Close dropdown before next iteration
|
||||||
|
await comfyPage.page.keyboard.press('Escape')
|
||||||
|
await expect(menu).not.toBeVisible()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('dropdown closes on outside click', async ({ comfyPage }) => {
|
||||||
|
const node = comfyPage.vueNodes.getNodeByTitle('Load Image')
|
||||||
|
const trigger = node.getByTestId(TestIds.widgets.formDropdownTrigger)
|
||||||
|
await trigger.first().click()
|
||||||
|
|
||||||
|
const menu = comfyPage.page.getByTestId(TestIds.widgets.formDropdownMenu)
|
||||||
|
await expect(menu).toBeVisible({ timeout: 5000 })
|
||||||
|
|
||||||
|
// Click outside the node
|
||||||
|
await comfyPage.page.mouse.click(10, 10)
|
||||||
|
await expect(menu).not.toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('dropdown closes on Escape key', async ({ comfyPage }) => {
|
||||||
|
const node = comfyPage.vueNodes.getNodeByTitle('Load Image')
|
||||||
|
const trigger = node.getByTestId(TestIds.widgets.formDropdownTrigger)
|
||||||
|
await trigger.first().click()
|
||||||
|
|
||||||
|
const menu = comfyPage.page.getByTestId(TestIds.widgets.formDropdownMenu)
|
||||||
|
await expect(menu).toBeVisible({ timeout: 5000 })
|
||||||
|
|
||||||
|
await comfyPage.page.keyboard.press('Escape')
|
||||||
|
await expect(menu).not.toBeVisible()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -43,11 +43,6 @@ const MockFormDropdownInput = {
|
|||||||
'<button class="mock-dropdown-trigger" @click="$emit(\'select-click\', $event)">Open</button>'
|
'<button class="mock-dropdown-trigger" @click="$emit(\'select-click\', $event)">Open</button>'
|
||||||
}
|
}
|
||||||
|
|
||||||
const MockPopover = {
|
|
||||||
name: 'Popover',
|
|
||||||
template: '<div><slot /></div>'
|
|
||||||
}
|
|
||||||
|
|
||||||
interface MountDropdownOptions {
|
interface MountDropdownOptions {
|
||||||
searcher?: (
|
searcher?: (
|
||||||
query: string,
|
query: string,
|
||||||
@@ -72,7 +67,6 @@ function mountDropdown(
|
|||||||
plugins: [PrimeVue, i18n],
|
plugins: [PrimeVue, i18n],
|
||||||
stubs: {
|
stubs: {
|
||||||
FormDropdownInput: MockFormDropdownInput,
|
FormDropdownInput: MockFormDropdownInput,
|
||||||
Popover: MockPopover,
|
|
||||||
FormDropdownMenu: MockFormDropdownMenu
|
FormDropdownMenu: MockFormDropdownMenu
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -80,6 +74,18 @@ function mountDropdown(
|
|||||||
return { ...result, user }
|
return { ...result, user }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function openDropdown(
|
||||||
|
container: Element,
|
||||||
|
user: ReturnType<typeof userEvent.setup>
|
||||||
|
) {
|
||||||
|
// eslint-disable-next-line testing-library/no-node-access
|
||||||
|
const trigger = container.querySelector(
|
||||||
|
'.mock-dropdown-trigger'
|
||||||
|
) as HTMLElement
|
||||||
|
await user.click(trigger)
|
||||||
|
await flushPromises()
|
||||||
|
}
|
||||||
|
|
||||||
function getMenuItems(): FormDropdownItem[] {
|
function getMenuItems(): FormDropdownItem[] {
|
||||||
const menuEl = screen.getByTestId('dropdown-menu')
|
const menuEl = screen.getByTestId('dropdown-menu')
|
||||||
return JSON.parse(menuEl.getAttribute('data-items') ?? '[]')
|
return JSON.parse(menuEl.getAttribute('data-items') ?? '[]')
|
||||||
@@ -88,11 +94,11 @@ function getMenuItems(): FormDropdownItem[] {
|
|||||||
describe('FormDropdown', () => {
|
describe('FormDropdown', () => {
|
||||||
describe('filteredItems updates when items prop changes', () => {
|
describe('filteredItems updates when items prop changes', () => {
|
||||||
it('updates displayed items when items prop changes', async () => {
|
it('updates displayed items when items prop changes', async () => {
|
||||||
const { rerender } = mountDropdown([
|
const { rerender, container, user } = mountDropdown([
|
||||||
createItem('input-0', 'video1.mp4'),
|
createItem('input-0', 'video1.mp4'),
|
||||||
createItem('input-1', 'video2.mp4')
|
createItem('input-1', 'video2.mp4')
|
||||||
])
|
])
|
||||||
await flushPromises()
|
await openDropdown(container, user)
|
||||||
|
|
||||||
expect(getMenuItems()).toHaveLength(2)
|
expect(getMenuItems()).toHaveLength(2)
|
||||||
|
|
||||||
@@ -110,8 +116,10 @@ describe('FormDropdown', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('updates when items change but IDs stay the same', async () => {
|
it('updates when items change but IDs stay the same', async () => {
|
||||||
const { rerender } = mountDropdown([createItem('1', 'alpha')])
|
const { rerender, container, user } = mountDropdown([
|
||||||
await flushPromises()
|
createItem('1', 'alpha')
|
||||||
|
])
|
||||||
|
await openDropdown(container, user)
|
||||||
|
|
||||||
await rerender({ items: [createItem('1', 'beta')] })
|
await rerender({ items: [createItem('1', 'beta')] })
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
@@ -120,8 +128,8 @@ describe('FormDropdown', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('updates when switching between empty and non-empty items', async () => {
|
it('updates when switching between empty and non-empty items', async () => {
|
||||||
const { rerender } = mountDropdown([])
|
const { rerender, container, user } = mountDropdown([])
|
||||||
await flushPromises()
|
await openDropdown(container, user)
|
||||||
|
|
||||||
expect(getMenuItems()).toHaveLength(0)
|
expect(getMenuItems()).toHaveLength(0)
|
||||||
|
|
||||||
@@ -165,7 +173,6 @@ describe('FormDropdown', () => {
|
|||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
|
||||||
expect(searcher).not.toHaveBeenCalled()
|
expect(searcher).not.toHaveBeenCalled()
|
||||||
expect(getMenuItems().map((item) => item.id)).toEqual(['3', '4'])
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('runs filtering when dropdown opens', async () => {
|
it('runs filtering when dropdown opens', async () => {
|
||||||
@@ -180,9 +187,7 @@ describe('FormDropdown', () => {
|
|||||||
)
|
)
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
|
||||||
// eslint-disable-next-line testing-library/no-node-access
|
await openDropdown(container, user)
|
||||||
await user.click(container.querySelector('.mock-dropdown-trigger')!)
|
|
||||||
await flushPromises()
|
|
||||||
|
|
||||||
expect(searcher).toHaveBeenCalled()
|
expect(searcher).toHaveBeenCalled()
|
||||||
expect(getMenuItems().map((item) => item.id)).toEqual(['keep'])
|
expect(getMenuItems().map((item) => item.id)).toEqual(['keep'])
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computedAsync, refDebounced } from '@vueuse/core'
|
import { computedAsync, onClickOutside, refDebounced } from '@vueuse/core'
|
||||||
import Popover from 'primevue/popover'
|
import type { CSSProperties } from 'vue'
|
||||||
import { computed, ref, useTemplateRef } from 'vue'
|
import { computed, inject, ref, useTemplateRef } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
import { useTransformCompatOverlayProps } from '@/composables/useTransformCompatOverlayProps'
|
import { OverlayAppendToKey } from '@/composables/useTransformCompatOverlayProps'
|
||||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||||
|
import { cn } from '@/utils/tailwindUtil'
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
FilterOption,
|
FilterOption,
|
||||||
@@ -16,6 +17,7 @@ import type {
|
|||||||
import FormDropdownInput from './FormDropdownInput.vue'
|
import FormDropdownInput from './FormDropdownInput.vue'
|
||||||
import FormDropdownMenu from './FormDropdownMenu.vue'
|
import FormDropdownMenu from './FormDropdownMenu.vue'
|
||||||
import { defaultSearcher, getDefaultSortOptions } from './shared'
|
import { defaultSearcher, getDefaultSortOptions } from './shared'
|
||||||
|
import { MENU_HEIGHT, MENU_WIDTH } from './types'
|
||||||
import type { FormDropdownItem, LayoutMode, SortOption } from './types'
|
import type { FormDropdownItem, LayoutMode, SortOption } from './types'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -51,7 +53,6 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const overlayProps = useTransformCompatOverlayProps()
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
placeholder,
|
placeholder,
|
||||||
@@ -95,8 +96,10 @@ const baseModelSelected = defineModel<Set<string>>('baseModelSelected', {
|
|||||||
const isOpen = defineModel<boolean>('isOpen', { default: false })
|
const isOpen = defineModel<boolean>('isOpen', { default: false })
|
||||||
|
|
||||||
const toastStore = useToastStore()
|
const toastStore = useToastStore()
|
||||||
const popoverRef = ref<InstanceType<typeof Popover>>()
|
|
||||||
const triggerRef = useTemplateRef('triggerRef')
|
const triggerRef = useTemplateRef('triggerRef')
|
||||||
|
const dropdownRef = useTemplateRef('dropdownRef')
|
||||||
|
|
||||||
|
const shouldTeleport = inject(OverlayAppendToKey, undefined) === 'body'
|
||||||
|
|
||||||
const maxSelectable = computed(() => {
|
const maxSelectable = computed(() => {
|
||||||
if (multiple === true) return Infinity
|
if (multiple === true) return Infinity
|
||||||
@@ -142,18 +145,63 @@ function internalIsSelected(item: FormDropdownItem, index: number): boolean {
|
|||||||
return isSelected(selected.value, item, index)
|
return isSelected(selected.value, item, index)
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleDropdown = (event: Event) => {
|
const MENU_HEIGHT_WITH_GAP = MENU_HEIGHT + 8
|
||||||
|
const openUpward = ref(false)
|
||||||
|
const fixedPosition = ref({ top: 0, left: 0 })
|
||||||
|
|
||||||
|
const teleportStyle = computed<CSSProperties | undefined>(() => {
|
||||||
|
if (!shouldTeleport) return undefined
|
||||||
|
const pos = fixedPosition.value
|
||||||
|
return openUpward.value
|
||||||
|
? {
|
||||||
|
position: 'fixed',
|
||||||
|
left: `${pos.left}px`,
|
||||||
|
bottom: `${window.innerHeight - pos.top}px`,
|
||||||
|
paddingBottom: '0.5rem'
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
position: 'fixed',
|
||||||
|
left: `${pos.left}px`,
|
||||||
|
top: `${pos.top}px`,
|
||||||
|
paddingTop: '0.5rem'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function toggleDropdown() {
|
||||||
if (disabled) return
|
if (disabled) return
|
||||||
if (popoverRef.value && triggerRef.value) {
|
if (!isOpen.value && triggerRef.value) {
|
||||||
popoverRef.value.toggle?.(event, triggerRef.value)
|
const rect = triggerRef.value.getBoundingClientRect()
|
||||||
isOpen.value = !isOpen.value
|
|
||||||
|
const spaceBelow = window.innerHeight - rect.bottom
|
||||||
|
const spaceAbove = rect.top
|
||||||
|
openUpward.value =
|
||||||
|
spaceBelow < MENU_HEIGHT_WITH_GAP && spaceAbove > spaceBelow
|
||||||
|
|
||||||
|
if (shouldTeleport) {
|
||||||
|
const left = Math.max(
|
||||||
|
0,
|
||||||
|
Math.min(rect.right - MENU_WIDTH, window.innerWidth - MENU_WIDTH)
|
||||||
|
)
|
||||||
|
fixedPosition.value = {
|
||||||
|
top: openUpward.value
|
||||||
|
? Math.max(MENU_HEIGHT_WITH_GAP, rect.top)
|
||||||
|
: Math.min(rect.bottom, window.innerHeight - MENU_HEIGHT_WITH_GAP),
|
||||||
|
left
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
isOpen.value = !isOpen.value
|
||||||
}
|
}
|
||||||
|
|
||||||
const closeDropdown = () => {
|
function closeDropdown() {
|
||||||
if (popoverRef.value) {
|
isOpen.value = false
|
||||||
popoverRef.value.hide?.()
|
}
|
||||||
isOpen.value = false
|
|
||||||
|
onClickOutside(triggerRef, closeDropdown, { ignore: [dropdownRef] })
|
||||||
|
|
||||||
|
function handleEscape(event: KeyboardEvent) {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
closeDropdown()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,7 +240,7 @@ function handleSelection(item: FormDropdownItem, index: number) {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div ref="triggerRef">
|
<div ref="triggerRef" class="relative" @keydown="handleEscape">
|
||||||
<FormDropdownInput
|
<FormDropdownInput
|
||||||
:files
|
:files
|
||||||
:is-open
|
:is-open
|
||||||
@@ -207,42 +255,41 @@ function handleSelection(item: FormDropdownItem, index: number) {
|
|||||||
@select-click="toggleDropdown"
|
@select-click="toggleDropdown"
|
||||||
@file-change="handleFileChange"
|
@file-change="handleFileChange"
|
||||||
/>
|
/>
|
||||||
<Popover
|
<Teleport to="body" :disabled="!shouldTeleport">
|
||||||
ref="popoverRef"
|
<div
|
||||||
:dismissable="true"
|
v-if="isOpen"
|
||||||
:close-on-escape="true"
|
ref="dropdownRef"
|
||||||
:append-to="overlayProps.appendTo"
|
:class="
|
||||||
unstyled
|
cn(
|
||||||
:pt="{
|
'z-50 rounded-lg border-none bg-transparent p-0 shadow-lg',
|
||||||
root: {
|
!shouldTeleport && 'absolute left-0',
|
||||||
class: 'absolute z-50'
|
!shouldTeleport &&
|
||||||
},
|
(openUpward ? 'bottom-full pb-2' : 'top-full pt-2')
|
||||||
content: {
|
)
|
||||||
class: ['bg-transparent border-none p-0 pt-2 rounded-lg shadow-lg']
|
"
|
||||||
}
|
:style="teleportStyle"
|
||||||
}"
|
>
|
||||||
@hide="isOpen = false"
|
<FormDropdownMenu
|
||||||
>
|
v-model:filter-selected="filterSelected"
|
||||||
<FormDropdownMenu
|
v-model:layout-mode="layoutMode"
|
||||||
v-model:filter-selected="filterSelected"
|
v-model:sort-selected="sortSelected"
|
||||||
v-model:layout-mode="layoutMode"
|
v-model:search-query="searchQuery"
|
||||||
v-model:sort-selected="sortSelected"
|
v-model:ownership-selected="ownershipSelected"
|
||||||
v-model:search-query="searchQuery"
|
v-model:base-model-selected="baseModelSelected"
|
||||||
v-model:ownership-selected="ownershipSelected"
|
:filter-options
|
||||||
v-model:base-model-selected="baseModelSelected"
|
:sort-options
|
||||||
:filter-options
|
:show-ownership-filter
|
||||||
:sort-options
|
:ownership-options
|
||||||
:show-ownership-filter
|
:show-base-model-filter
|
||||||
:ownership-options
|
:base-model-options
|
||||||
:show-base-model-filter
|
:disabled
|
||||||
:base-model-options
|
:items="sortedItems"
|
||||||
:disabled
|
:is-selected="internalIsSelected"
|
||||||
:items="sortedItems"
|
:max-selectable
|
||||||
:is-selected="internalIsSelected"
|
@close="closeDropdown"
|
||||||
:max-selectable
|
@item-click="handleSelection"
|
||||||
@close="closeDropdown"
|
/>
|
||||||
@item-click="handleSelection"
|
</div>
|
||||||
/>
|
</Teleport>
|
||||||
</Popover>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ const theButtonStyle = computed(() =>
|
|||||||
"
|
"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
|
data-testid="form-dropdown-trigger"
|
||||||
:class="
|
:class="
|
||||||
cn(
|
cn(
|
||||||
theButtonStyle,
|
theButtonStyle,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import type {
|
|||||||
import FormDropdownMenuActions from './FormDropdownMenuActions.vue'
|
import FormDropdownMenuActions from './FormDropdownMenuActions.vue'
|
||||||
import FormDropdownMenuFilter from './FormDropdownMenuFilter.vue'
|
import FormDropdownMenuFilter from './FormDropdownMenuFilter.vue'
|
||||||
import FormDropdownMenuItem from './FormDropdownMenuItem.vue'
|
import FormDropdownMenuItem from './FormDropdownMenuItem.vue'
|
||||||
|
import { MENU_HEIGHT, MENU_WIDTH } from './types'
|
||||||
import type { FormDropdownItem, LayoutMode, SortOption } from './types'
|
import type { FormDropdownItem, LayoutMode, SortOption } from './types'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -97,7 +98,9 @@ const virtualItems = computed<VirtualDropdownItem[]>(() =>
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="flex h-[640px] w-103 flex-col rounded-lg bg-component-node-background pt-4 outline -outline-offset-1 outline-node-component-border"
|
data-testid="form-dropdown-menu"
|
||||||
|
class="flex flex-col rounded-lg bg-component-node-background pt-4 outline -outline-offset-1 outline-node-component-border"
|
||||||
|
:style="{ height: `${MENU_HEIGHT}px`, width: `${MENU_WIDTH}px` }"
|
||||||
data-capture-wheel="true"
|
data-capture-wheel="true"
|
||||||
>
|
>
|
||||||
<FormDropdownMenuFilter
|
<FormDropdownMenuFilter
|
||||||
|
|||||||
@@ -28,5 +28,10 @@ export interface SortOption<TId extends string = string> {
|
|||||||
|
|
||||||
export type LayoutMode = 'list' | 'grid' | 'list-small'
|
export type LayoutMode = 'list' | 'grid' | 'list-small'
|
||||||
|
|
||||||
|
/** Height of FormDropdownMenu in pixels (matches h-[640px] in template). */
|
||||||
|
export const MENU_HEIGHT = 640
|
||||||
|
/** Width of FormDropdownMenu in pixels (matches w-103 = 26rem = 416px in template). */
|
||||||
|
export const MENU_WIDTH = 412
|
||||||
|
|
||||||
export const AssetKindKey: InjectionKey<ComputedRef<AssetKind | undefined>> =
|
export const AssetKindKey: InjectionKey<ComputedRef<AssetKind | undefined>> =
|
||||||
Symbol('assetKind')
|
Symbol('assetKind')
|
||||||
|
|||||||
Reference in New Issue
Block a user