Compare commits

...

1 Commits

Author SHA1 Message Date
Matt Miller
4638b2c6bc refactor: extract LocateNodeButton for errors panel
Deduplicate the byte-identical locate icon-button repeated across four
sites in the errors panel (ErrorNodeCard, TabErrors, MissingPackGroupRow
x2) into a single LocateNodeButton component. Standardizes the decorative
icon's aria-hidden and the click .stop modifier, which previously varied
between sites.
2026-07-02 13:09:17 -07:00
5 changed files with 90 additions and 36 deletions

View File

@@ -39,15 +39,10 @@
>
<i class="icon-[lucide--monitor-x] size-4" />
</Button>
<Button
variant="textonly"
size="icon-sm"
class="size-8 shrink-0 text-muted-foreground hover:text-base-foreground focus-visible:ring-inset"
:aria-label="t('rightSidePanel.locateNode')"
@click.stop="handleLocateNode"
>
<i class="icon-[lucide--locate] size-4" />
</Button>
<LocateNodeButton
:label="t('rightSidePanel.locateNode')"
@locate="handleLocateNode"
/>
</div>
</div>
@@ -180,6 +175,7 @@ import { useI18n } from 'vue-i18n'
import Button from '@/components/ui/button/Button.vue'
import { cn } from '@comfyorg/tailwind-utils'
import LocateNodeButton from './LocateNodeButton.vue'
import TransitionCollapse from '../layout/TransitionCollapse.vue'
import type { ErrorCardData, ErrorItem } from './types'

View File

@@ -0,0 +1,48 @@
import { render, screen } from '@testing-library/vue'
import userEvent from '@testing-library/user-event'
import { describe, expect, it } from 'vitest'
import LocateNodeButton from '@/components/rightSidePanel/errors/LocateNodeButton.vue'
describe('LocateNodeButton', () => {
it('exposes the aria-label as the button accessible name', () => {
render(LocateNodeButton, { props: { label: 'Locate node on canvas' } })
expect(
screen.getByRole('button', { name: 'Locate node on canvas' })
).toBeInTheDocument()
})
it('emits locate when clicked', async () => {
const user = userEvent.setup()
const { emitted } = render(LocateNodeButton, {
props: { label: 'Locate node on canvas' }
})
await user.click(screen.getByRole('button'))
expect(emitted().locate).toHaveLength(1)
})
it('stops click propagation so an ancestor handler does not also fire', async () => {
const user = userEvent.setup()
let ancestorClicks = 0
render(
{
components: { LocateNodeButton },
template:
'<div @click="onAncestorClick"><LocateNodeButton label="Locate node on canvas" /></div>',
methods: {
onAncestorClick() {
ancestorClicks++
}
}
},
{}
)
await user.click(screen.getByRole('button'))
expect(ancestorClicks).toBe(0)
})
})

View File

@@ -0,0 +1,23 @@
<template>
<Button
variant="textonly"
size="icon-sm"
class="size-8 shrink-0 text-muted-foreground hover:text-base-foreground focus-visible:ring-inset"
:aria-label="label"
@click.stop="emit('locate')"
>
<i aria-hidden="true" class="icon-[lucide--locate] size-4" />
</Button>
</template>
<script setup lang="ts">
import Button from '@/components/ui/button/Button.vue'
const { label } = defineProps<{
label: string
}>()
const emit = defineEmits<{
locate: []
}>()
</script>

View File

@@ -146,16 +146,11 @@
</span>
</Button>
</div>
<Button
<LocateNodeButton
v-if="primaryLocatableNodeType"
variant="textonly"
size="icon-sm"
class="size-8 shrink-0 text-muted-foreground hover:text-base-foreground focus-visible:ring-inset"
:aria-label="t('rightSidePanel.locateNode')"
@click="handleLocateNode(primaryLocatableNodeType)"
>
<i aria-hidden="true" class="icon-[lucide--locate] size-4" />
</Button>
:label="t('rightSidePanel.locateNode')"
@locate="handleLocateNode(primaryLocatableNodeType)"
/>
</div>
<TransitionCollapse>
@@ -195,16 +190,11 @@
{{ getLabel(nodeType) }}
</span>
</span>
<Button
<LocateNodeButton
v-if="isLocatableNodeType(nodeType)"
variant="textonly"
size="icon-sm"
class="size-8 shrink-0 text-muted-foreground hover:text-base-foreground focus-visible:ring-inset"
:aria-label="t('rightSidePanel.locateNode')"
@click="handleLocateNode(nodeType)"
>
<i aria-hidden="true" class="icon-[lucide--locate] size-4" />
</Button>
:label="t('rightSidePanel.locateNode')"
@locate="handleLocateNode(nodeType)"
/>
</div>
</li>
</ul>
@@ -218,6 +208,7 @@ import { useI18n } from 'vue-i18n'
import { cn } from '@comfyorg/tailwind-utils'
import Button from '@/components/ui/button/Button.vue'
import DotSpinner from '@/components/common/DotSpinner.vue'
import LocateNodeButton from '@/components/rightSidePanel/errors/LocateNodeButton.vue'
import TransitionCollapse from '@/components/rightSidePanel/layout/TransitionCollapse.vue'
import { useMissingNodes } from '@/workbench/extensions/manager/composables/nodePack/useMissingNodes'
import { usePackInstall } from '@/workbench/extensions/manager/composables/nodePack/usePackInstall'

View File

@@ -215,17 +215,12 @@
<i class="icon-[lucide--info] size-3.5" />
</Button>
</span>
<Button
variant="textonly"
size="icon-sm"
class="size-8 shrink-0 text-muted-foreground hover:text-base-foreground focus-visible:ring-inset"
:aria-label="
<LocateNodeButton
:label="
t('rightSidePanel.locateNodeFor', { item: item.label })
"
@click.stop="handleLocateNode(item.nodeId)"
>
<i class="icon-[lucide--locate] size-4" />
</Button>
@locate="handleLocateNode(item.nodeId)"
/>
</div>
<TransitionCollapse>
<p
@@ -323,6 +318,7 @@ import TransitionCollapse from '../layout/TransitionCollapse.vue'
import AsyncSearchInput from '@/components/ui/search-input/AsyncSearchInput.vue'
import ErrorCardSection from './ErrorCardSection.vue'
import ErrorNodeCard from './ErrorNodeCard.vue'
import LocateNodeButton from './LocateNodeButton.vue'
import MissingNodeCard from './MissingNodeCard.vue'
import SwapNodesCard from '@/platform/nodeReplacement/components/SwapNodesCard.vue'
import MissingModelCard from '@/platform/missingModel/components/MissingModelCard.vue'