diff --git a/packages/design-system/src/css/style.css b/packages/design-system/src/css/style.css index 767e8c25bc..b1634beaac 100644 --- a/packages/design-system/src/css/style.css +++ b/packages/design-system/src/css/style.css @@ -1894,3 +1894,17 @@ audio.comfy-audio.empty-audio-widget { 300% 14px; background-attachment: local, local, scroll, scroll; } + +/* + PrimeVue overlays teleport to body. When a Reka modal dialog is open it sets + body { pointer-events: none } via DismissableLayer, which propagates to the + body-portaled overlays and makes them unclickable. PrimeVue's own Dialog + sets pointer-events inline, but Select / ColorPicker / Popover / Autocomplete + overlays do not, so they need to opt in here. +*/ +.p-select-overlay, +.p-colorpicker-panel, +.p-popover, +.p-autocomplete-overlay { + pointer-events: auto; +} diff --git a/src/components/dialog/GlobalDialog.test.ts b/src/components/dialog/GlobalDialog.test.ts index 1fa839772d..a11f5bb17e 100644 --- a/src/components/dialog/GlobalDialog.test.ts +++ b/src/components/dialog/GlobalDialog.test.ts @@ -8,6 +8,7 @@ import { defineComponent, h } from 'vue' import { createI18n } from 'vue-i18n' import GlobalDialog from '@/components/dialog/GlobalDialog.vue' +import { onRekaPointerDownOutside } from '@/components/dialog/rekaPrimeVueBridge' import { useDialogStore } from '@/stores/dialogStore' const i18n = createI18n({ @@ -190,3 +191,53 @@ describe('GlobalDialog Reka parity with PrimeVue', () => { expect(store.isDialogOpen('reka-esc-blocked')).toBe(true) }) }) + +describe('shouldPreventRekaDismiss', () => { + function makeEvent(target: Element | null) { + let prevented = false + return { + detail: { originalEvent: { target } }, + preventDefault: () => { + prevented = true + }, + get defaultPrevented() { + return prevented + } + } as unknown as CustomEvent<{ originalEvent: PointerEvent }> & { + defaultPrevented: boolean + } + } + + it.for([ + 'p-select-overlay', + 'p-colorpicker-panel', + 'p-popover', + 'p-autocomplete-overlay', + 'p-overlay-mask', + 'p-dialog' + ])('prevents dismiss when target is inside %s', (className) => { + const overlay = document.createElement('div') + overlay.className = className + const inner = document.createElement('button') + overlay.appendChild(inner) + document.body.appendChild(overlay) + + const event = makeEvent(inner) + onRekaPointerDownOutside({ dismissableMask: undefined }, event) + + expect(event.defaultPrevented).toBe(true) + overlay.remove() + }) + + it('allows dismiss when target is outside any PrimeVue overlay', () => { + const event = makeEvent(document.body) + onRekaPointerDownOutside({ dismissableMask: undefined }, event) + expect(event.defaultPrevented).toBe(false) + }) + + it('prevents dismiss when dismissableMask is false even outside an overlay', () => { + const event = makeEvent(document.body) + onRekaPointerDownOutside({ dismissableMask: false }, event) + expect(event.defaultPrevented).toBe(true) + }) +}) diff --git a/src/components/dialog/GlobalDialog.vue b/src/components/dialog/GlobalDialog.vue index 6abf62db22..85dc162a7d 100644 --- a/src/components/dialog/GlobalDialog.vue +++ b/src/components/dialog/GlobalDialog.vue @@ -20,9 +20,7 @@ e.preventDefault() " @pointer-down-outside=" - (e) => - item.dialogComponentProps.dismissableMask === false && - e.preventDefault() + (e) => onRekaPointerDownOutside(item.dialogComponentProps, e) " @mousedown="() => dialogStore.riseDialog({ key: item.key })" > @@ -115,6 +113,7 @@ import DialogMaximize from '@/components/ui/dialog/DialogMaximize.vue' import DialogOverlay from '@/components/ui/dialog/DialogOverlay.vue' import DialogPortal from '@/components/ui/dialog/DialogPortal.vue' import DialogTitle from '@/components/ui/dialog/DialogTitle.vue' +import { onRekaPointerDownOutside } from '@/components/dialog/rekaPrimeVueBridge' import type { DialogInstance } from '@/stores/dialogStore' import { useDialogStore } from '@/stores/dialogStore' diff --git a/src/components/dialog/rekaPrimeVueBridge.ts b/src/components/dialog/rekaPrimeVueBridge.ts new file mode 100644 index 0000000000..781e1ca4de --- /dev/null +++ b/src/components/dialog/rekaPrimeVueBridge.ts @@ -0,0 +1,23 @@ +// PrimeVue overlays (Select, ColorPicker, Popover, Autocomplete, stacked +// PrimeVue Dialogs) teleport to body. Reka treats clicks on body-portaled +// elements as outside its dialog and would auto-dismiss on the first +// interaction, tearing the overlay down mid-interaction. Treat any +// PrimeVue overlay click as inside. +const PRIMEVUE_OVERLAY_SELECTORS = + '.p-select-overlay, .p-colorpicker-panel, .p-popover, .p-autocomplete-overlay, .p-overlay, .p-overlay-mask, .p-dialog' + +type PointerDownOutsideEvent = CustomEvent<{ originalEvent: PointerEvent }> + +export function onRekaPointerDownOutside( + options: { dismissableMask?: boolean }, + event: PointerDownOutsideEvent +) { + const target = event.detail.originalEvent.target + if (target instanceof Element && target.closest(PRIMEVUE_OVERLAY_SELECTORS)) { + event.preventDefault() + return + } + if (options.dismissableMask === false) { + event.preventDefault() + } +}