@@ -48,6 +48,7 @@ vi.mock("./ZplCodeMirror", () => {
4848 return { default : MockCodeMirror } ;
4949} ) ;
5050import { ZPLOutput } from "./ZPLOutput" ;
51+ import { DialogShell } from "../ui/DialogShell" ;
5152import { useLabelStore } from "../../store/labelStore" ;
5253import type { LabelObject , Page } from "@zplab/core/types/Group" ;
5354
@@ -234,7 +235,7 @@ describe("leaving the editor", () => {
234235 const confirm = ( ) => within ( screen . getByRole ( "alertdialog" ) ) ;
235236 expect ( confirm ( ) . getByText ( t ( ) . output . editSourceDiscardBody ) ) . toBeTruthy ( ) ;
236237 expect ( review ( ) . getByRole ( "button" , { name : t ( ) . output . editSourceConfirmApply } ) ) . toBeTruthy ( ) ;
237- // The deferred blur apply stays suspended across the handover. No apply, no second review.
238+ // Focus crossing from the review into the question must not apply, or a second review opens .
238239 await act ( async ( ) => {
239240 await new Promise ( ( r ) => setTimeout ( r , 0 ) ) ;
240241 } ) ;
@@ -266,7 +267,7 @@ describe("leaving the editor", () => {
266267 expect ( copiedTexts ) . toEqual ( [ "^XA^FDedited^FS^XZ" ] ) ;
267268 } ) ;
268269
269- it ( "asks before discarding a modified buffer, and the question suspends the blur apply" , async ( ) => {
270+ it ( "asks before discarding a modified buffer, and the question holds the blur apply" , async ( ) => {
270271 const { container } = render ( < ZPLOutput onResizeMouseDown = { vi . fn ( ) } /> ) ;
271272 typeDraft ( "^XA^JZY^FO10,10^A0N,30,30^FDX^FS^XZ" ) ;
272273 fireEvent . click ( screen . getByRole ( "button" , { name : t ( ) . app . cancel } ) ) ;
@@ -593,6 +594,54 @@ describe("outside pointerdown", () => {
593594 } ) ;
594595} ) ;
595596
597+ describe ( "a modal above the panel" , ( ) => {
598+ const Modal = ( { onAction } : { onAction : ( ) => void } ) => (
599+ < DialogShell portal onClose = { vi . fn ( ) } describedBy = "modal-body" boxClassName = "" >
600+ < p id = "modal-body" > settings</ p >
601+ < button onClick = { onAction } > setting</ button >
602+ </ DialogShell >
603+ ) ;
604+
605+ it ( "takes clicks while an unappliable buffer holds the session" , ( ) => {
606+ const action = vi . fn ( ) ;
607+ render (
608+ < >
609+ < Modal onAction = { action } />
610+ < ZPLOutput onResizeMouseDown = { vi . fn ( ) } />
611+ </ > ,
612+ ) ;
613+ typeDraft ( "^XA^FO10,10^A0N,30,30^FDbroken^FS" ) ;
614+ const setting = screen . getByRole ( "button" , { name : "setting" } ) ;
615+ fireEvent . pointerDown ( setting ) ;
616+ fireEvent . click ( setting ) ;
617+ expect ( action ) . toHaveBeenCalledTimes ( 1 ) ;
618+ expect ( useLabelStore . getState ( ) . sourceEdit . status ) . toBe ( "editing" ) ;
619+ expect ( useLabelStore . getState ( ) . sourceShadow ?. refusal ?? null ) . toBeNull ( ) ;
620+ } ) ;
621+
622+ it ( "keeps the focus it took instead of handing it back to the editor" , async ( ) => {
623+ const { container, rerender } = render ( < ZPLOutput onResizeMouseDown = { vi . fn ( ) } /> ) ;
624+ typeDraft ( "^XA^FO10,10^A0N,30,30^FDbroken^FS" ) ;
625+ editor ( ) . focus ( ) ;
626+ rerender (
627+ < >
628+ < Modal onAction = { vi . fn ( ) } />
629+ < ZPLOutput onResizeMouseDown = { vi . fn ( ) } />
630+ </ > ,
631+ ) ;
632+ vi . spyOn ( document , "hasFocus" ) . mockReturnValue ( true ) ;
633+ const setting = screen . getByRole ( "button" , { name : "setting" } ) ;
634+ setting . focus ( ) ;
635+ fireEvent . focusOut ( panelRoot ( container ) ) ;
636+ await act ( async ( ) => {
637+ await new Promise ( ( r ) => setTimeout ( r , 0 ) ) ;
638+ } ) ;
639+ expect ( useLabelStore . getState ( ) . sourceEdit . status ) . toBe ( "editing" ) ;
640+ expect ( useLabelStore . getState ( ) . sourceShadow ?. refusal ?? null ) . toBeNull ( ) ;
641+ expect ( document . activeElement ) . toBe ( setting ) ;
642+ } ) ;
643+ } ) ;
644+
596645describe ( "keyboard focus after button exits" , ( ) => {
597646 it ( "returns to the editor on a clean footer apply" , ( ) => {
598647 render ( < ZPLOutput onResizeMouseDown = { vi . fn ( ) } /> ) ;
0 commit comments