mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-30 01:05:46 +00:00
Compare commits
25 Commits
test/stand
...
feat-widge
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9592e0b0c6 | ||
|
|
c33b30d4a3 | ||
|
|
e4f87a96a4 | ||
|
|
f13e35c89b | ||
|
|
b6ef7b8034 | ||
|
|
43fce00e54 | ||
|
|
fd60f4edb6 | ||
|
|
b6c77706d6 | ||
|
|
e67d1bde5d | ||
|
|
03186ceaf5 | ||
|
|
d94476a7df | ||
|
|
9bb16f6d99 | ||
|
|
c8f52cbcb4 | ||
|
|
dbf4a4c64c | ||
|
|
5d94466d40 | ||
|
|
a1d4e62b87 | ||
|
|
81e4f3cb77 | ||
|
|
9614107e82 | ||
|
|
0e25a2f79b | ||
|
|
dd435f86f5 | ||
|
|
035a0f250c | ||
|
|
795962f3c3 | ||
|
|
5fb3fc5646 | ||
|
|
b16202e3ea | ||
|
|
4fa21b5f0f |
39
src/components/ui/toggle-group/ToggleGroup.vue
Normal file
39
src/components/ui/toggle-group/ToggleGroup.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<script setup lang="ts">
|
||||
/* eslint-disable vue/no-unused-properties */
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import type { ToggleGroupRootEmits, ToggleGroupRootProps } from 'reka-ui'
|
||||
import { ToggleGroupRoot, useForwardPropsEmits } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { provide } from 'vue'
|
||||
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
import { toggleGroupVariants } from './toggleGroup.variants'
|
||||
import type { ToggleGroupItemVariants } from './toggleGroup.variants'
|
||||
|
||||
const props = defineProps<
|
||||
ToggleGroupRootProps & {
|
||||
class?: HTMLAttributes['class']
|
||||
variant?: ToggleGroupItemVariants['variant']
|
||||
}
|
||||
>()
|
||||
const emits = defineEmits<ToggleGroupRootEmits>()
|
||||
|
||||
provide('toggleGroup', {
|
||||
variant: props.variant
|
||||
})
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ToggleGroupRoot
|
||||
v-slot="slotProps"
|
||||
v-bind="forwarded"
|
||||
:class="cn(toggleGroupVariants(), props.class)"
|
||||
>
|
||||
<slot v-bind="slotProps" />
|
||||
</ToggleGroupRoot>
|
||||
</template>
|
||||
47
src/components/ui/toggle-group/ToggleGroupItem.vue
Normal file
47
src/components/ui/toggle-group/ToggleGroupItem.vue
Normal file
@@ -0,0 +1,47 @@
|
||||
<script setup lang="ts">
|
||||
/* eslint-disable vue/no-unused-properties */
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import type { ToggleGroupItemProps } from 'reka-ui'
|
||||
import { ToggleGroupItem, useForwardProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { inject } from 'vue'
|
||||
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
import { toggleGroupItemVariants } from './toggleGroup.variants'
|
||||
import type { ToggleGroupItemVariants } from './toggleGroup.variants'
|
||||
|
||||
const props = defineProps<
|
||||
ToggleGroupItemProps & {
|
||||
class?: HTMLAttributes['class']
|
||||
variant?: ToggleGroupItemVariants['variant']
|
||||
}
|
||||
>()
|
||||
|
||||
const context = inject<{ variant?: ToggleGroupItemVariants['variant'] }>(
|
||||
'toggleGroup'
|
||||
)
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class', 'variant')
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ToggleGroupItem
|
||||
v-slot="slotProps"
|
||||
v-bind="forwardedProps"
|
||||
:class="
|
||||
cn(
|
||||
toggleGroupItemVariants({
|
||||
variant: context?.variant || variant
|
||||
}),
|
||||
props.class
|
||||
)
|
||||
"
|
||||
>
|
||||
<span class="truncate min-w-0">
|
||||
<slot v-bind="slotProps" />
|
||||
</span>
|
||||
</ToggleGroupItem>
|
||||
</template>
|
||||
36
src/components/ui/toggle-group/toggleGroup.variants.ts
Normal file
36
src/components/ui/toggle-group/toggleGroup.variants.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import type { VariantProps } from 'cva'
|
||||
import { cva } from 'cva'
|
||||
|
||||
export const toggleGroupVariants = cva({
|
||||
base: 'flex gap-[var(--primitive-padding-padding-1,4px)] p-[var(--primitive-padding-padding-1,4px)] rounded-[var(--primitive-border-radius-rounded-sm,4px)] bg-component-node-widget-background'
|
||||
})
|
||||
|
||||
export const toggleGroupItemVariants = cva({
|
||||
base: 'flex-1 inline-flex items-center justify-center border-0 rounded-[var(--primitive-border-radius-rounded-sm,4px)] px-[var(--primitive-padding-padding-2,8px)] py-[var(--primitive-padding-padding-1,4px)] text-xs font-inter font-normal transition-colors cursor-pointer overflow-hidden',
|
||||
variants: {
|
||||
variant: {
|
||||
primary: [
|
||||
'data-[state=off]:bg-transparent data-[state=off]:text-muted-foreground',
|
||||
'data-[state=off]:hover:bg-component-node-widget-background-hovered data-[state=off]:hover:text-white',
|
||||
'data-[state=on]:bg-primary-background data-[state=on]:text-white'
|
||||
],
|
||||
secondary: [
|
||||
'data-[state=off]:bg-transparent data-[state=off]:text-muted-foreground',
|
||||
'data-[state=off]:hover:bg-component-node-widget-background-hovered data-[state=off]:hover:text-white',
|
||||
'data-[state=on]:bg-component-node-widget-background-selected data-[state=on]:text-base-foreground'
|
||||
],
|
||||
inverted: [
|
||||
'data-[state=off]:bg-transparent data-[state=off]:text-muted-foreground',
|
||||
'data-[state=off]:hover:bg-component-node-widget-background-hovered data-[state=off]:hover:text-white',
|
||||
'data-[state=on]:bg-white data-[state=on]:text-base-background'
|
||||
]
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'secondary'
|
||||
}
|
||||
})
|
||||
|
||||
export type ToggleGroupItemVariants = VariantProps<
|
||||
typeof toggleGroupItemVariants
|
||||
>
|
||||
@@ -2051,6 +2051,10 @@
|
||||
"Set Group Nodes to Always": "Set Group Nodes to Always"
|
||||
},
|
||||
"widgets": {
|
||||
"boolean": {
|
||||
"true": "true",
|
||||
"false": "false"
|
||||
},
|
||||
"selectModel": "Select model",
|
||||
"uploadSelect": {
|
||||
"placeholder": "Select...",
|
||||
|
||||
@@ -1,17 +1,30 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import ToggleSwitch from 'primevue/toggleswitch'
|
||||
import type { ToggleSwitchProps } from 'primevue/toggleswitch'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import ToggleGroup from '@/components/ui/toggle-group/ToggleGroup.vue'
|
||||
import ToggleGroupItem from '@/components/ui/toggle-group/ToggleGroupItem.vue'
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
|
||||
import WidgetToggleSwitch from './WidgetToggleSwitch.vue'
|
||||
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
t: (key: string) => {
|
||||
const translations: Record<string, string> = {
|
||||
'widgets.boolean.true': 'true',
|
||||
'widgets.boolean.false': 'false'
|
||||
}
|
||||
return translations[key] || key
|
||||
}
|
||||
})
|
||||
}))
|
||||
|
||||
describe('WidgetToggleSwitch Value Binding', () => {
|
||||
const createMockWidget = (
|
||||
value: boolean = false,
|
||||
options: Partial<ToggleSwitchProps> = {},
|
||||
options: Record<string, unknown> = {},
|
||||
callback?: (value: boolean) => void
|
||||
): SimplifiedWidget<boolean> => ({
|
||||
name: 'test_toggle',
|
||||
@@ -34,7 +47,7 @@ describe('WidgetToggleSwitch Value Binding', () => {
|
||||
},
|
||||
global: {
|
||||
plugins: [PrimeVue],
|
||||
components: { ToggleSwitch }
|
||||
components: { ToggleSwitch, ToggleGroup, ToggleGroupItem }
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -149,4 +162,82 @@ describe('WidgetToggleSwitch Value Binding', () => {
|
||||
expect(emitted![3]).toContain(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Label Display', () => {
|
||||
it('uses ToggleGroup when labels are provided', () => {
|
||||
const widget = createMockWidget(false, { on: 'Enabled', off: 'Disabled' })
|
||||
const wrapper = mountComponent(widget, false)
|
||||
|
||||
expect(wrapper.findComponent({ name: 'ToggleGroup' }).exists()).toBe(true)
|
||||
expect(wrapper.findComponent({ name: 'ToggleSwitch' }).exists()).toBe(
|
||||
false
|
||||
)
|
||||
})
|
||||
|
||||
it('uses ToggleSwitch when no labels are provided', () => {
|
||||
const widget = createMockWidget(false, {})
|
||||
const wrapper = mountComponent(widget, false)
|
||||
|
||||
expect(wrapper.findComponent({ name: 'ToggleSwitch' }).exists()).toBe(
|
||||
true
|
||||
)
|
||||
expect(wrapper.findComponent({ name: 'ToggleGroup' }).exists()).toBe(
|
||||
false
|
||||
)
|
||||
})
|
||||
|
||||
it('displays both label_on and label_off in ToggleGroup', () => {
|
||||
const widget = createMockWidget(false, { on: 'Enabled', off: 'Disabled' })
|
||||
const wrapper = mountComponent(widget, false)
|
||||
|
||||
expect(wrapper.text()).toContain('Enabled')
|
||||
expect(wrapper.text()).toContain('Disabled')
|
||||
})
|
||||
|
||||
it('displays correct active state for false', () => {
|
||||
const widget = createMockWidget(false, { on: 'Enabled', off: 'Disabled' })
|
||||
const wrapper = mountComponent(widget, false)
|
||||
|
||||
const toggleGroup = wrapper.findComponent({ name: 'ToggleGroup' })
|
||||
expect(toggleGroup.props('modelValue')).toBe('off')
|
||||
})
|
||||
|
||||
it('displays correct active state for true', () => {
|
||||
const widget = createMockWidget(true, { on: 'Enabled', off: 'Disabled' })
|
||||
const wrapper = mountComponent(widget, true)
|
||||
|
||||
const toggleGroup = wrapper.findComponent({ name: 'ToggleGroup' })
|
||||
expect(toggleGroup.props('modelValue')).toBe('on')
|
||||
})
|
||||
|
||||
it('updates active state when toggled', async () => {
|
||||
const widget = createMockWidget(false, {
|
||||
on: 'Markdown',
|
||||
off: 'Plaintext'
|
||||
})
|
||||
const wrapper = mountComponent(widget, false)
|
||||
|
||||
const toggleGroup = wrapper.findComponent({ name: 'ToggleGroup' })
|
||||
expect(toggleGroup.props('modelValue')).toBe('off')
|
||||
|
||||
await wrapper.setProps({ modelValue: true })
|
||||
|
||||
expect(toggleGroup.props('modelValue')).toBe('on')
|
||||
})
|
||||
|
||||
it('emits update:modelValue when ToggleGroup item is clicked', async () => {
|
||||
const widget = createMockWidget(false, {
|
||||
on: 'Markdown',
|
||||
off: 'Plaintext'
|
||||
})
|
||||
const wrapper = mountComponent(widget, false)
|
||||
|
||||
const toggleGroup = wrapper.findComponent({ name: 'ToggleGroup' })
|
||||
await toggleGroup.vm.$emit('update:modelValue', 'on')
|
||||
|
||||
const emitted = wrapper.emitted('update:modelValue')
|
||||
expect(emitted).toBeDefined()
|
||||
expect(emitted![0]).toContain(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,6 +1,27 @@
|
||||
<template>
|
||||
<WidgetLayoutField :widget>
|
||||
<WidgetLayoutField :widget="widgetWithStyle">
|
||||
<!-- Use ToggleGroup when explicit labels are provided -->
|
||||
<!-- The variant attribute is not necessary here because the default is secondary -->
|
||||
<!-- It was still added to show that a variant (3) can be explicitly set -->
|
||||
<ToggleGroup
|
||||
v-if="hasLabels"
|
||||
type="single"
|
||||
variant="secondary"
|
||||
:model-value="toggleGroupValue"
|
||||
class="flex justify-end w-full mb-[-0.5rem]"
|
||||
@update:model-value="handleToggleGroupChange"
|
||||
>
|
||||
<ToggleGroupItem value="off" :aria-label="`${widget.name}: ${labelOff}`">
|
||||
{{ labelOff }}
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem value="on" :aria-label="`${widget.name}: ${labelOn}`">
|
||||
{{ labelOn }}
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
|
||||
<!-- Use ToggleSwitch for implicit boolean states -->
|
||||
<ToggleSwitch
|
||||
v-else
|
||||
v-model="modelValue"
|
||||
v-bind="filteredProps"
|
||||
class="ml-auto block"
|
||||
@@ -12,7 +33,10 @@
|
||||
<script setup lang="ts">
|
||||
import ToggleSwitch from 'primevue/toggleswitch'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import ToggleGroup from '@/components/ui/toggle-group/ToggleGroup.vue'
|
||||
import ToggleGroupItem from '@/components/ui/toggle-group/ToggleGroupItem.vue'
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
import {
|
||||
STANDARD_EXCLUDED_PROPS,
|
||||
@@ -21,13 +45,50 @@ import {
|
||||
|
||||
import WidgetLayoutField from './layout/WidgetLayoutField.vue'
|
||||
|
||||
interface BooleanWidgetOptions {
|
||||
on?: string
|
||||
off?: string
|
||||
}
|
||||
|
||||
const { widget } = defineProps<{
|
||||
widget: SimplifiedWidget<boolean>
|
||||
widget: SimplifiedWidget<boolean, BooleanWidgetOptions>
|
||||
}>()
|
||||
|
||||
const modelValue = defineModel<boolean>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const filteredProps = computed(() =>
|
||||
filterWidgetProps(widget.options, STANDARD_EXCLUDED_PROPS)
|
||||
)
|
||||
|
||||
const hasLabels = computed(() => {
|
||||
return !!(widget.options?.on || widget.options?.off)
|
||||
})
|
||||
|
||||
const labelOn = computed(() => widget.options?.on ?? t('widgets.boolean.true'))
|
||||
const labelOff = computed(
|
||||
() => widget.options?.off ?? t('widgets.boolean.false')
|
||||
)
|
||||
|
||||
const toggleGroupValue = computed(() => {
|
||||
return modelValue.value ? 'on' : 'off'
|
||||
})
|
||||
|
||||
function handleToggleGroupChange(value: unknown) {
|
||||
if (value === 'on') {
|
||||
modelValue.value = true
|
||||
} else if (value === 'off') {
|
||||
modelValue.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Override WidgetLayoutField styling when using ToggleGroup
|
||||
const widgetWithStyle = computed(() => ({
|
||||
...widget,
|
||||
borderStyle: hasLabels.value
|
||||
? 'focus-within:ring-0 bg-transparent rounded-none focus-within:outline-none'
|
||||
: undefined,
|
||||
labelStyle: hasLabels.value ? 'mb-[-0.5rem]' : undefined
|
||||
}))
|
||||
</script>
|
||||
|
||||
@@ -8,7 +8,9 @@ defineProps<{
|
||||
widget: Pick<
|
||||
SimplifiedWidget<string | number | undefined>,
|
||||
'name' | 'label' | 'borderStyle'
|
||||
>
|
||||
> & {
|
||||
labelStyle?: string
|
||||
}
|
||||
}>()
|
||||
|
||||
const hideLayoutField = inject<boolean>('hideLayoutField', false)
|
||||
@@ -18,7 +20,10 @@ const hideLayoutField = inject<boolean>('hideLayoutField', false)
|
||||
<div
|
||||
class="grid grid-cols-subgrid min-w-0 justify-between gap-1 text-node-component-slot-text"
|
||||
>
|
||||
<div v-if="!hideLayoutField" class="truncate content-center-safe">
|
||||
<div
|
||||
v-if="!hideLayoutField"
|
||||
:class="cn('truncate content-center-safe', widget.labelStyle)"
|
||||
>
|
||||
<template v-if="widget.name">
|
||||
{{ widget.label || widget.name }}
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user