fix: prevent confirm dialog buttons from being unreachable on mobile with long text (#8746)

## Summary

Fix confirm dialog buttons becoming unreachable on mobile when text
contains long unbreakable words (e.g. content-hashed filenames with 100+
characters).

<img width="1080" height="2277" alt="image"
src="https://github.com/user-attachments/assets/2f42afc9-c8ec-42aa-89d5-802dbaf788fd"
/>


## Changes

- **What**: Added `overflow-wrap: break-word` and `flex-wrap` to both
confirm dialog systems so long words break properly and buttons wrap on
narrow screens.
- `ConfirmationDialogContent.vue`: Added `overflow-wrap: break-word` to
the existing scoped style and `flex-wrap` to button row.
  - `ConfirmBody.vue`: Added `break-words` tailwind class.
  - `ConfirmFooter.vue`: Added `flex-wrap` to button section.

## Review Focus

Minimal CSS-only fix across both dialog systems (legacy
`dialogService.confirm()` and newer `showConfirmDialog()`). No
behavioral changes.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8746-fix-prevent-confirm-dialog-buttons-from-being-unreachable-on-mobile-with-long-text-3016d73d36508116bf55f0dc5cd89d0b)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
This commit is contained in:
Christian Byrne
2026-02-20 02:20:02 -08:00
committed by GitHub
parent 03f597a496
commit 0792d26f77
6 changed files with 103 additions and 25 deletions

View File

@@ -0,0 +1,32 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.describe('Confirm dialog text wrapping', { tag: ['@mobile'] }, () => {
test('@mobile confirm dialog buttons are visible with long unbreakable text', async ({
comfyPage
}) => {
const longFilename = 'workflow_checkpoint_' + 'a'.repeat(200) + '.json'
await comfyPage.page.evaluate((msg) => {
window
.app!.extensionManager.dialog.confirm({
title: 'Confirm',
type: 'default',
message: msg
})
.catch(() => {})
}, longFilename)
const dialog = comfyPage.page.getByRole('dialog')
await expect(dialog).toBeVisible()
const confirmButton = dialog.getByRole('button', { name: 'Confirm' })
await expect(confirmButton).toBeVisible()
await expect(confirmButton).toBeInViewport()
const cancelButton = dialog.getByRole('button', { name: 'Cancel' })
await expect(cancelButton).toBeVisible()
await expect(cancelButton).toBeInViewport()
})
})

View File

@@ -73,6 +73,10 @@ function getDialogPt(item: {
<style>
@reference '../../assets/css/style.css';
.global-dialog {
max-width: calc(100vw - 1rem);
}
.global-dialog .p-dialog-header {
@apply p-2 2xl:p-[var(--p-dialog-header-padding)];
@apply pb-0;

View File

@@ -1,6 +1,6 @@
<template>
<div
class="flex flex-col px-4 py-2 text-sm text-muted-foreground border-t border-border-default"
class="flex flex-col break-words px-4 py-2 text-sm text-muted-foreground border-t border-border-default"
>
<p v-if="promptTextReal">
{{ promptTextReal }}

View File

@@ -1,5 +1,5 @@
<template>
<section class="w-full flex gap-2 justify-end px-2 pb-2">
<section class="w-full flex flex-wrap gap-2 justify-end px-2 pb-2">
<Button :disabled variant="textonly" autofocus @click="$emit('cancel')">
{{ cancelTextX }}
</Button>

View File

@@ -0,0 +1,45 @@
import { mount } from '@vue/test-utils'
import { createPinia, setActivePinia } from 'pinia'
import PrimeVue from 'primevue/config'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { ComponentProps } from 'vue-component-type-helpers'
import { createI18n } from 'vue-i18n'
import ConfirmationDialogContent from './ConfirmationDialogContent.vue'
type Props = ComponentProps<typeof ConfirmationDialogContent>
const i18n = createI18n({
legacy: false,
locale: 'en',
messages: { en: {} },
missingWarn: false,
fallbackWarn: false
})
describe('ConfirmationDialogContent', () => {
beforeEach(() => {
setActivePinia(createPinia())
})
function mountComponent(props: Partial<Props> = {}) {
return mount(ConfirmationDialogContent, {
global: {
plugins: [PrimeVue, i18n]
},
props: {
message: 'Test message',
type: 'default',
onConfirm: vi.fn(),
...props
} as Props
})
}
it('renders long messages without breaking layout', () => {
const longFilename =
'workflow_checkpoint_' + 'a'.repeat(200) + '.safetensors'
const wrapper = mountComponent({ message: longFilename })
expect(wrapper.text()).toContain(longFilename)
})
})

View File

@@ -1,21 +1,24 @@
<template>
<section class="prompt-dialog-content m-2 mt-4 flex flex-col gap-6">
<span>{{ message }}</span>
<ul v-if="itemList?.length" class="m-0 flex flex-col gap-2 pl-4">
<li v-for="item of itemList" :key="item">
{{ item }}
</li>
</ul>
<Message
v-if="hint"
icon="pi pi-info-circle"
severity="secondary"
size="small"
variant="simple"
>
{{ hint }}
</Message>
<div class="flex justify-end gap-4">
<section class="m-2 mt-4 flex flex-col gap-6 whitespace-pre-wrap break-words">
<div>
<span>{{ message }}</span>
<ul v-if="itemList?.length" class="m-0 mt-2 flex flex-col gap-2 pl-4">
<li v-for="item of itemList" :key="item">
{{ item }}
</li>
</ul>
<Message
v-if="hint"
class="mt-2"
icon="pi pi-info-circle"
severity="secondary"
size="small"
variant="simple"
>
{{ hint }}
</Message>
</div>
<div class="flex shrink-0 flex-wrap justify-end gap-4">
<div
v-if="type === 'overwriteBlueprint'"
class="flex flex-col justify-start gap-1"
@@ -151,9 +154,3 @@ const onConfirm = () => {
useDialogStore().closeDialog()
}
</script>
<style lang="css" scoped>
.prompt-dialog-content {
white-space: pre-wrap;
}
</style>