mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-22 07:19:41 +00:00
Compare commits
2 Commits
bl/fix-ply
...
bl/pr-1030
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
31cb321d0d | ||
|
|
97c8be9ce9 |
57
src/components/ui/context-menu/ContextMenu.test.ts
Normal file
57
src/components/ui/context-menu/ContextMenu.test.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { defineComponent, nextTick, ref } from 'vue'
|
||||
|
||||
import ContextMenu from './ContextMenu.vue'
|
||||
import ContextMenuContent from './ContextMenuContent.vue'
|
||||
import ContextMenuItem from './ContextMenuItem.vue'
|
||||
import ContextMenuSeparator from './ContextMenuSeparator.vue'
|
||||
import ContextMenuTrigger from './ContextMenuTrigger.vue'
|
||||
|
||||
const TestContextMenu = defineComponent({
|
||||
components: {
|
||||
ContextMenu,
|
||||
ContextMenuTrigger,
|
||||
ContextMenuContent,
|
||||
ContextMenuItem,
|
||||
ContextMenuSeparator
|
||||
},
|
||||
setup() {
|
||||
const open = ref(false)
|
||||
|
||||
return { open }
|
||||
},
|
||||
template: `
|
||||
<ContextMenu v-model:open="open" :modal="false">
|
||||
<ContextMenuTrigger as-child>
|
||||
<button type="button">Trigger</button>
|
||||
</ContextMenuTrigger>
|
||||
<ContextMenuContent close-on-scroll>
|
||||
<ContextMenuItem text-value="First item">First item</ContextMenuItem>
|
||||
<ContextMenuSeparator />
|
||||
</ContextMenuContent>
|
||||
</ContextMenu>
|
||||
`
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = ''
|
||||
})
|
||||
|
||||
describe('ContextMenu', () => {
|
||||
it('closes the content on scroll when close-on-scroll is enabled', async () => {
|
||||
const wrapper = mount(TestContextMenu, {
|
||||
attachTo: document.body
|
||||
})
|
||||
|
||||
await wrapper.find('button').trigger('contextmenu')
|
||||
await nextTick()
|
||||
|
||||
expect(wrapper.vm.open).toBe(true)
|
||||
|
||||
window.dispatchEvent(new Event('scroll'))
|
||||
await nextTick()
|
||||
|
||||
expect(wrapper.vm.open).toBe(false)
|
||||
})
|
||||
})
|
||||
16
src/components/ui/context-menu/ContextMenu.vue
Normal file
16
src/components/ui/context-menu/ContextMenu.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
/* eslint-disable vue/no-unused-properties */
|
||||
import type { ContextMenuRootEmits, ContextMenuRootProps } from 'reka-ui'
|
||||
import { ContextMenuRoot, useForwardPropsEmits } from 'reka-ui'
|
||||
|
||||
const props = defineProps<ContextMenuRootProps>()
|
||||
const emits = defineEmits<ContextMenuRootEmits>()
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ContextMenuRoot v-bind="forwarded">
|
||||
<slot />
|
||||
</ContextMenuRoot>
|
||||
</template>
|
||||
58
src/components/ui/context-menu/ContextMenuContent.vue
Normal file
58
src/components/ui/context-menu/ContextMenuContent.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<script setup lang="ts">
|
||||
/* eslint-disable vue/no-unused-properties */
|
||||
import { reactiveOmit, useEventListener } from '@vueuse/core'
|
||||
import type { ContextMenuContentEmits, ContextMenuContentProps } from 'reka-ui'
|
||||
import {
|
||||
ContextMenuContent,
|
||||
ContextMenuPortal,
|
||||
injectContextMenuRootContext,
|
||||
useForwardPropsEmits
|
||||
} from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<
|
||||
ContextMenuContentProps & {
|
||||
class?: HTMLAttributes['class']
|
||||
closeOnScroll?: boolean
|
||||
}
|
||||
>(),
|
||||
{
|
||||
closeOnScroll: false
|
||||
}
|
||||
)
|
||||
const emits = defineEmits<ContextMenuContentEmits>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class', 'closeOnScroll')
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
const rootContext = injectContextMenuRootContext()
|
||||
|
||||
useEventListener(
|
||||
window,
|
||||
'scroll',
|
||||
() => {
|
||||
if (props.closeOnScroll) {
|
||||
rootContext.onOpenChange(false)
|
||||
}
|
||||
},
|
||||
{ capture: true, passive: true }
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ContextMenuPortal>
|
||||
<ContextMenuContent
|
||||
v-bind="forwarded"
|
||||
:class="
|
||||
cn(
|
||||
'bg-popover text-popover-foreground z-1700 min-w-32 overflow-hidden rounded-md border p-1 shadow-md data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</ContextMenuContent>
|
||||
</ContextMenuPortal>
|
||||
</template>
|
||||
32
src/components/ui/context-menu/ContextMenuItem.vue
Normal file
32
src/components/ui/context-menu/ContextMenuItem.vue
Normal file
@@ -0,0 +1,32 @@
|
||||
<script setup lang="ts">
|
||||
/* eslint-disable vue/no-unused-properties */
|
||||
import type { ContextMenuItemEmits, ContextMenuItemProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { ContextMenuItem, useForwardPropsEmits } from 'reka-ui'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
const props = defineProps<
|
||||
ContextMenuItemProps & { class?: HTMLAttributes['class']; inset?: boolean }
|
||||
>()
|
||||
const emits = defineEmits<ContextMenuItemEmits>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ContextMenuItem
|
||||
v-bind="forwarded"
|
||||
:class="
|
||||
cn(
|
||||
'focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-none select-none data-disabled:pointer-events-none data-disabled:opacity-50',
|
||||
inset && 'pl-8',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</ContextMenuItem>
|
||||
</template>
|
||||
21
src/components/ui/context-menu/ContextMenuSeparator.vue
Normal file
21
src/components/ui/context-menu/ContextMenuSeparator.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
/* eslint-disable vue/no-unused-properties */
|
||||
import type { ContextMenuSeparatorProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { ContextMenuSeparator } from 'reka-ui'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
const props = defineProps<
|
||||
ContextMenuSeparatorProps & { class?: HTMLAttributes['class'] }
|
||||
>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ContextMenuSeparator
|
||||
v-bind="delegatedProps"
|
||||
:class="cn('bg-border -mx-1 my-1 h-px', props.class)"
|
||||
/>
|
||||
</template>
|
||||
15
src/components/ui/context-menu/ContextMenuTrigger.vue
Normal file
15
src/components/ui/context-menu/ContextMenuTrigger.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
/* eslint-disable vue/no-unused-properties */
|
||||
import type { ContextMenuTriggerProps } from 'reka-ui'
|
||||
import { ContextMenuTrigger, useForwardProps } from 'reka-ui'
|
||||
|
||||
const props = defineProps<ContextMenuTriggerProps>()
|
||||
|
||||
const forwardedProps = useForwardProps(props)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ContextMenuTrigger v-bind="forwardedProps">
|
||||
<slot />
|
||||
</ContextMenuTrigger>
|
||||
</template>
|
||||
57
src/components/ui/dropdown-menu/DropdownMenu.test.ts
Normal file
57
src/components/ui/dropdown-menu/DropdownMenu.test.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { defineComponent, nextTick, ref } from 'vue'
|
||||
|
||||
import DropdownMenu from './DropdownMenu.vue'
|
||||
import DropdownMenuContent from './DropdownMenuContent.vue'
|
||||
import DropdownMenuItem from './DropdownMenuItem.vue'
|
||||
import DropdownMenuSeparator from './DropdownMenuSeparator.vue'
|
||||
import DropdownMenuTrigger from './DropdownMenuTrigger.vue'
|
||||
|
||||
const TestDropdownMenu = defineComponent({
|
||||
components: {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator
|
||||
},
|
||||
setup() {
|
||||
const open = ref(false)
|
||||
|
||||
return { open }
|
||||
},
|
||||
template: `
|
||||
<DropdownMenu v-model:open="open">
|
||||
<DropdownMenuTrigger as-child>
|
||||
<button type="button">Trigger</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent close-on-scroll>
|
||||
<DropdownMenuItem text-value="First item">First item</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
`
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = ''
|
||||
})
|
||||
|
||||
describe('DropdownMenu', () => {
|
||||
it('closes the content on scroll when close-on-scroll is enabled', async () => {
|
||||
const wrapper = mount(TestDropdownMenu, {
|
||||
attachTo: document.body
|
||||
})
|
||||
|
||||
await wrapper.find('button').trigger('click')
|
||||
await nextTick()
|
||||
|
||||
expect(wrapper.vm.open).toBe(true)
|
||||
|
||||
window.dispatchEvent(new Event('scroll'))
|
||||
await nextTick()
|
||||
|
||||
expect(wrapper.vm.open).toBe(false)
|
||||
})
|
||||
})
|
||||
16
src/components/ui/dropdown-menu/DropdownMenu.vue
Normal file
16
src/components/ui/dropdown-menu/DropdownMenu.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
/* eslint-disable vue/no-unused-properties */
|
||||
import type { DropdownMenuRootEmits, DropdownMenuRootProps } from 'reka-ui'
|
||||
import { DropdownMenuRoot, useForwardPropsEmits } from 'reka-ui'
|
||||
|
||||
const props = defineProps<DropdownMenuRootProps>()
|
||||
const emits = defineEmits<DropdownMenuRootEmits>()
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuRoot v-bind="forwarded">
|
||||
<slot />
|
||||
</DropdownMenuRoot>
|
||||
</template>
|
||||
62
src/components/ui/dropdown-menu/DropdownMenuContent.vue
Normal file
62
src/components/ui/dropdown-menu/DropdownMenuContent.vue
Normal file
@@ -0,0 +1,62 @@
|
||||
<script setup lang="ts">
|
||||
/* eslint-disable vue/no-unused-properties */
|
||||
import { reactiveOmit, useEventListener } from '@vueuse/core'
|
||||
import type {
|
||||
DropdownMenuContentEmits,
|
||||
DropdownMenuContentProps
|
||||
} from 'reka-ui'
|
||||
import {
|
||||
DropdownMenuContent,
|
||||
DropdownMenuPortal,
|
||||
injectDropdownMenuRootContext,
|
||||
useForwardPropsEmits
|
||||
} from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<
|
||||
DropdownMenuContentProps & {
|
||||
class?: HTMLAttributes['class']
|
||||
closeOnScroll?: boolean
|
||||
}
|
||||
>(),
|
||||
{
|
||||
closeOnScroll: false,
|
||||
sideOffset: 4
|
||||
}
|
||||
)
|
||||
const emits = defineEmits<DropdownMenuContentEmits>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class', 'closeOnScroll')
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
const rootContext = injectDropdownMenuRootContext()
|
||||
|
||||
useEventListener(
|
||||
window,
|
||||
'scroll',
|
||||
() => {
|
||||
if (props.closeOnScroll) {
|
||||
rootContext.onOpenChange(false)
|
||||
}
|
||||
},
|
||||
{ capture: true, passive: true }
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuContent
|
||||
v-bind="forwarded"
|
||||
:class="
|
||||
cn(
|
||||
'bg-popover text-popover-foreground z-1700 min-w-32 overflow-hidden rounded-md border p-1 shadow-md data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenuPortal>
|
||||
</template>
|
||||
31
src/components/ui/dropdown-menu/DropdownMenuItem.vue
Normal file
31
src/components/ui/dropdown-menu/DropdownMenuItem.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
/* eslint-disable vue/no-unused-properties */
|
||||
import type { DropdownMenuItemProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { DropdownMenuItem, useForwardProps } from 'reka-ui'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
const props = defineProps<
|
||||
DropdownMenuItemProps & { class?: HTMLAttributes['class']; inset?: boolean }
|
||||
>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuItem
|
||||
v-bind="forwardedProps"
|
||||
:class="
|
||||
cn(
|
||||
'focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm transition-colors outline-none select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0',
|
||||
inset && 'pl-8',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</DropdownMenuItem>
|
||||
</template>
|
||||
23
src/components/ui/dropdown-menu/DropdownMenuSeparator.vue
Normal file
23
src/components/ui/dropdown-menu/DropdownMenuSeparator.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
/* eslint-disable vue/no-unused-properties */
|
||||
import type { DropdownMenuSeparatorProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { DropdownMenuSeparator } from 'reka-ui'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
const props = defineProps<
|
||||
DropdownMenuSeparatorProps & {
|
||||
class?: HTMLAttributes['class']
|
||||
}
|
||||
>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuSeparator
|
||||
v-bind="delegatedProps"
|
||||
:class="cn('-mx-1 my-1 h-px bg-muted', props.class)"
|
||||
/>
|
||||
</template>
|
||||
15
src/components/ui/dropdown-menu/DropdownMenuTrigger.vue
Normal file
15
src/components/ui/dropdown-menu/DropdownMenuTrigger.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
/* eslint-disable vue/no-unused-properties */
|
||||
import type { DropdownMenuTriggerProps } from 'reka-ui'
|
||||
import { DropdownMenuTrigger, useForwardProps } from 'reka-ui'
|
||||
|
||||
const props = defineProps<DropdownMenuTriggerProps>()
|
||||
|
||||
const forwardedProps = useForwardProps(props)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuTrigger class="outline-none" v-bind="forwardedProps">
|
||||
<slot />
|
||||
</DropdownMenuTrigger>
|
||||
</template>
|
||||
Reference in New Issue
Block a user