Compare commits

...

5 Commits

Author SHA1 Message Date
Glary-Bot
ff6ed7117b merge: integrate main into glary/advanced-widget-outline-on-hover 2026-05-12 02:14:31 +00:00
Glary-Bot
47fcab102e fix: add explicit duration-150 to advanced widget opacity transition
Without an explicit duration the transition-opacity utility falls back
to Tailwind's default (no duration), so opacity changes appeared
instant. Adds duration-150 to match the smoother fade christian-byrne
suggested in PR review.
2026-05-11 20:46:16 +00:00
Glary-Bot
2d93546128 fix: dim advanced widgets to 30% on hover instead of outlining them
Design feedback (FE-597): the outline made advanced widgets look
selected, which was confusing. Switching to opacity-30 dims the entire
widget row (label + input) which better communicates 'this row will
disappear if you click the hide button'.

- Remove the showAdvancedRing flow from useProcessedWidgets and revert
  the advanced ring branch in computeProcessedWidgets. Promoted-widget
  styling is unchanged.
- Apply 'opacity-30 transition-opacity' to the .lg-node-widget row in
  NodeWidgets when widget.advanced && isAdvancedHovered.
- Drop the alwaysShowAdvanced fallback: when there is no hide button
  rendered (e.g. AlwaysShowAdvancedWidgets setting on), there is no
  affordance to hint at, so the row stays at full opacity.
- Update tests: composable test asserts no border-style for advanced
  widgets; NodeWidgets test asserts opacity-30 row class is applied
  only when isAdvancedHovered=true and only on advanced widgets.
2026-05-11 19:43:59 +00:00
Glary-Bot
8cd96aa1f1 fix: address review feedback on advanced widget outline prototype
- Use pointerenter/pointerleave + focusin/focusout instead of
  mouse-only events so keyboard users can also reveal the outline.
- Preserve advanced ring when Comfy.Node.AlwaysShowAdvancedWidgets is
  enabled (no hide button is rendered in that mode, so hover gating
  would otherwise hide the outline entirely).
- Rename composable input from isAdvancedHovered to showAdvancedRing
  to reflect that it now covers both hover and the always-show case.
- Add component-level tests for the new advancedHoverChange emit
  (hover + keyboard focus paths).
2026-05-07 05:12:24 +00:00
Glary-Bot
cc94762c0f feat: show advanced widget outline only on hide-button hover
Currently, advanced widgets show a primary-color outline whenever they
are visible, indicating which widgets would be collapsed if the
hide-advanced-inputs button is pressed. Per design feedback (FE-597),
the always-on outline reads as broken/intentional styling rather than a
hint, so this prototype gates the outline behind hover of the
hide/show-advanced button in the node footer.

- NodeFooter emits advancedHoverChange on mouseenter/mouseleave of the
  toggle button (both single-tab and dual-tab error variants).
- LGraphNode tracks the hover state and forwards it to NodeWidgets.
- useProcessedWidgets only assigns the advanced ring class when the
  button is hovered. Promoted-widget styling is unchanged.
2026-05-07 02:25:11 +00:00
7 changed files with 93 additions and 12 deletions

View File

@@ -162,7 +162,11 @@
>
<NodeSlots :node-data="nodeData" />
<NodeWidgets v-if="nodeData.widgets?.length" :node-data="nodeData" />
<NodeWidgets
v-if="nodeData.widgets?.length"
:node-data="nodeData"
:is-advanced-hovered="isAdvancedButtonHovered"
/>
<div v-if="hasCustomContent" class="flex min-h-0 flex-1 flex-col">
<NodeContent
@@ -203,6 +207,7 @@
@enter-subgraph="handleEnterSubgraph"
@open-errors="handleOpenErrors"
@toggle-advanced="handleToggleAdvanced"
@advanced-hover-change="isAdvancedButtonHovered = $event"
/>
<template
v-if="
@@ -675,6 +680,8 @@ const handleToggleAdvanced = () => {
showAdvancedState.value = !showAdvancedState.value
}
const isAdvancedButtonHovered = ref(false)
const handleEnterSubgraph = () => {
useTelemetry()?.trackUiButtonClicked({
button_id: 'graph_node_open_subgraph_clicked'

View File

@@ -152,6 +152,21 @@ describe('NodeFooter', () => {
expect(emitted()).toHaveProperty('toggleAdvanced')
})
it('emits advancedHoverChange when hovering the advanced tab', async () => {
const { emitted } = renderFooter({ showAdvancedInputsButton: true })
const button = screen.getByRole('button', { name: /show advanced/i })
await user.hover(button)
await user.unhover(button)
expect(emitted().advancedHoverChange).toEqual([[true], [false]])
})
it('emits advancedHoverChange when focusing the advanced tab via keyboard', async () => {
const { emitted } = renderFooter({ showAdvancedInputsButton: true })
await user.tab()
const focusedTrue = emitted().advancedHoverChange?.[0]
expect(focusedTrue).toEqual([true])
})
describe('drag-then-click suppression', () => {
beforeEach(() => {
layoutStore.isDraggingVueNodes.value = false

View File

@@ -83,6 +83,10 @@
:style="headerColorStyle"
@pointerup="snapshotDragOnPointerUp"
@click.stop="emitIfNotDragged('toggleAdvanced')"
@pointerenter="$emit('advancedHoverChange', true)"
@pointerleave="$emit('advancedHoverChange', false)"
@focusin="$emit('advancedHoverChange', true)"
@focusout="$emit('advancedHoverChange', false)"
>
<div class="flex size-full items-center justify-center gap-2">
<span class="truncate">{{
@@ -180,6 +184,10 @@
:style="headerColorStyle"
@pointerup="snapshotDragOnPointerUp"
@click.stop="emitIfNotDragged('toggleAdvanced')"
@pointerenter="$emit('advancedHoverChange', true)"
@pointerleave="$emit('advancedHoverChange', false)"
@focusin="$emit('advancedHoverChange', true)"
@focusout="$emit('advancedHoverChange', false)"
>
<div class="flex size-full items-center justify-center gap-2">
<template v-if="showAdvancedState">
@@ -233,6 +241,7 @@ const emit = defineEmits<{
enterSubgraph: []
openErrors: []
toggleAdvanced: []
advancedHoverChange: [hovered: boolean]
}>()
let suppressNextClick = false

View File

@@ -74,14 +74,19 @@ describe('NodeWidgets', () => {
outputs: []
})
function renderComponent(nodeData?: VueNodeData, setupStores?: () => void) {
function renderComponent(
nodeData?: VueNodeData,
setupStores?: () => void,
extraProps: { isAdvancedHovered?: boolean } = {}
) {
const pinia = createTestingPinia({ stubActions: false })
setActivePinia(pinia)
setupStores?.()
return render(NodeWidgets, {
props: {
nodeData
nodeData,
...extraProps
},
global: {
plugins: [pinia],
@@ -335,4 +340,47 @@ describe('NodeWidgets', () => {
expect(ids).toStrictEqual(['test_node', 'test_node'])
})
describe('advanced widget hover styling', () => {
function renderWithAdvancedWidget(isAdvancedHovered: boolean) {
const advancedWidget = createMockWidget({
name: 'advanced_widget',
type: 'toggle',
options: { advanced: true }
})
const nodeData = {
...createMockNodeData('TestNode', [advancedWidget]),
showAdvanced: true
}
return renderComponent(nodeData, undefined, { isAdvancedHovered })
}
it('dims advanced widget row when isAdvancedHovered is true', () => {
const { container } = renderWithAdvancedWidget(true)
const widgetRow = container.querySelector('.lg-node-widget')
expect(widgetRow).not.toBeNull()
expect(widgetRow!.className).toContain('opacity-30')
})
it('does not dim advanced widget row when isAdvancedHovered is false', () => {
const { container } = renderWithAdvancedWidget(false)
const widgetRow = container.querySelector('.lg-node-widget')
expect(widgetRow).not.toBeNull()
expect(widgetRow!.className).not.toContain('opacity-30')
})
it('does not dim non-advanced widget row even when isAdvancedHovered is true', () => {
const regularWidget = createMockWidget({
name: 'regular_widget',
type: 'combo'
})
const nodeData = createMockNodeData('TestNode', [regularWidget])
const { container } = renderComponent(nodeData, undefined, {
isAdvancedHovered: true
})
const widgetRow = container.querySelector('.lg-node-widget')
expect(widgetRow).not.toBeNull()
expect(widgetRow!.className).not.toContain('opacity-30')
})
})
})

View File

@@ -26,7 +26,12 @@
<div
v-if="!widget.hidden && (!widget.advanced || showAdvanced)"
data-testid="node-widget"
class="lg-node-widget group col-span-full grid grid-cols-subgrid items-stretch"
:class="
cn(
'lg-node-widget group col-span-full grid grid-cols-subgrid items-stretch transition-opacity duration-150',
widget.advanced && isAdvancedHovered && 'opacity-30'
)
"
>
<!-- Widget Input Slot Dot -->
<div
@@ -96,9 +101,10 @@ import InputSlot from './InputSlot.vue'
interface NodeWidgetsProps {
nodeData?: VueNodeData
isAdvancedHovered?: boolean
}
const { nodeData } = defineProps<NodeWidgetsProps>()
const { nodeData, isAdvancedHovered = false } = defineProps<NodeWidgetsProps>()
const { shouldHandleNodePointerEvents, forwardEventToCanvas } =
useCanvasInteractions()

View File

@@ -287,7 +287,7 @@ describe('computeProcessedWidgets borderStyle', () => {
).toBe(false)
})
it('applies advanced border styling to advanced widgets', () => {
it('does not apply border styling to advanced widgets (advanced hint is applied at row level via opacity)', () => {
const advancedWidget = createMockWidget({
name: 'text',
type: 'combo',
@@ -313,9 +313,7 @@ describe('computeProcessedWidgets borderStyle', () => {
ui: noopUi
})
expect(result[0].simplified.borderStyle).toBe(
'ring ring-component-node-widget-advanced'
)
expect(result[0].simplified.borderStyle).toBeUndefined()
})
it('deduplication keeps visible widget over hidden duplicate', () => {

View File

@@ -278,9 +278,7 @@ export function computeProcessedWidgets({
disambiguatingSourceNodeId: promotionSourceNodeId
})
? 'ring ring-component-node-widget-promoted'
: mergedOptions.advanced
? 'ring ring-component-node-widget-advanced'
: undefined
: undefined
const linkedUpstream: LinkedUpstreamInfo | undefined =
slotMetadata?.linked && slotMetadata.originNodeId