@@ -250,4 +250,234 @@ describe('FocusManager', () => {
250250
251251 expect ( focusManager . focusPath ) . toEqual ( [ parent ] ) ;
252252 } ) ;
253+
254+ describe ( 'Layer Management (Modal Support)' , ( ) => {
255+ it ( 'should create a new layer when pushLayer is called' , ( ) => {
256+ const mainElement = createMockElement ( 1 , 'main' ) ;
257+ const modalElement = createMockElement ( 2 , 'modal' ) ;
258+
259+ // Add element to main layer
260+ focusManager . addElement ( mainElement , null , { autoFocus : true } ) ;
261+ expect ( focusManager . focusPath ) . toEqual ( [ mainElement ] ) ;
262+
263+ // Push new layer with modal
264+ focusManager . pushLayer ( modalElement ) ;
265+ expect ( focusManager . focusPath ) . toEqual ( [ modalElement ] ) ;
266+
267+ // Main element should still be focused on its layer, but modal takes precedence
268+ expect ( mainElement . focused ) . toBe ( false ) ;
269+ expect ( modalElement . focused ) . toBe ( true ) ;
270+ } ) ;
271+
272+ it ( 'should maintain separate focus paths for different layers' , ( ) => {
273+ const mainElement = createMockElement ( 1 , 'main' ) ;
274+ const modalElement = createMockElement ( 2 , 'modal' ) ;
275+ const modalChild = createMockElement ( 3 , 'modalChild' ) ;
276+
277+ // Setup main layer
278+ focusManager . addElement ( mainElement , null , { autoFocus : true } ) ;
279+ expect ( focusManager . focusPath ) . toEqual ( [ mainElement ] ) ;
280+
281+ // Push modal layer
282+ focusManager . pushLayer ( modalElement ) ;
283+ expect ( focusManager . focusPath ) . toEqual ( [ modalElement ] ) ;
284+
285+ // Add child to modal
286+ focusManager . addElement ( modalChild , modalElement , { autoFocus : true } ) ;
287+ expect ( focusManager . focusPath ) . toEqual ( [ modalElement , modalChild ] ) ;
288+ } ) ;
289+
290+ it ( 'should not allow focusing elements outside the active layer' , ( ) => {
291+ const mainElement = createMockElement ( 1 , 'main' ) ;
292+ const modalElement = createMockElement ( 2 , 'modal' ) ;
293+
294+ // Setup main layer
295+ focusManager . addElement ( mainElement , null , { autoFocus : true } ) ;
296+ expect ( focusManager . focusPath ) . toEqual ( [ mainElement ] ) ;
297+
298+ // Push modal layer
299+ focusManager . pushLayer ( modalElement ) ;
300+ expect ( focusManager . focusPath ) . toEqual ( [ modalElement ] ) ;
301+
302+ // Try to focus element from main layer - should be blocked
303+ const consoleSpy = vi . spyOn ( console , 'warn' ) . mockImplementation ( ( ) => { } ) ;
304+ focusManager . focus ( mainElement ) ;
305+
306+ expect ( consoleSpy ) . toHaveBeenCalledWith (
307+ 'FocusManager: Cannot focus element outside of active focus layer' ,
308+ ) ;
309+ expect ( focusManager . focusPath ) . toEqual ( [ modalElement ] ) ;
310+ expect ( mainElement . focused ) . toBe ( false ) ;
311+
312+ consoleSpy . mockRestore ( ) ;
313+ } ) ;
314+
315+ it ( 'should restore previous layer focus when popLayer is called' , ( ) => {
316+ const mainElement = createMockElement ( 1 , 'main' ) ;
317+ const modalElement = createMockElement ( 2 , 'modal' ) ;
318+
319+ // Setup main layer
320+ focusManager . addElement ( mainElement , null , { autoFocus : true } ) ;
321+ expect ( focusManager . focusPath ) . toEqual ( [ mainElement ] ) ;
322+ expect ( mainElement . focused ) . toBe ( true ) ;
323+
324+ // Push modal layer
325+ focusManager . pushLayer ( modalElement ) ;
326+ expect ( focusManager . focusPath ) . toEqual ( [ modalElement ] ) ;
327+ expect ( mainElement . focused ) . toBe ( false ) ;
328+ expect ( modalElement . focused ) . toBe ( true ) ;
329+
330+ // Pop modal layer
331+ focusManager . popLayer ( ) ;
332+ expect ( focusManager . focusPath ) . toEqual ( [ mainElement ] ) ;
333+ expect ( modalElement . focused ) . toBe ( false ) ;
334+ expect ( mainElement . focused ) . toBe ( true ) ;
335+ } ) ;
336+
337+ it ( 'should handle multiple nested layers' , ( ) => {
338+ const mainElement = createMockElement ( 1 , 'main' ) ;
339+ const modal1Element = createMockElement ( 2 , 'modal1' ) ;
340+ const modal2Element = createMockElement ( 3 , 'modal2' ) ;
341+
342+ // Setup main layer
343+ focusManager . addElement ( mainElement , null , { autoFocus : true } ) ;
344+
345+ // Push first modal
346+ focusManager . pushLayer ( modal1Element ) ;
347+ expect ( focusManager . focusPath ) . toEqual ( [ modal1Element ] ) ;
348+
349+ // Push second modal
350+ focusManager . pushLayer ( modal2Element ) ;
351+ expect ( focusManager . focusPath ) . toEqual ( [ modal2Element ] ) ;
352+
353+ // Pop second modal
354+ focusManager . popLayer ( ) ;
355+ expect ( focusManager . focusPath ) . toEqual ( [ modal1Element ] ) ;
356+
357+ // Pop first modal
358+ focusManager . popLayer ( ) ;
359+ expect ( focusManager . focusPath ) . toEqual ( [ mainElement ] ) ;
360+ } ) ;
361+
362+ it ( 'should not close the main layer when popLayer is called' , ( ) => {
363+ const mainElement = createMockElement ( 1 , 'main' ) ;
364+
365+ // Setup main layer
366+ focusManager . addElement ( mainElement , null , { autoFocus : true } ) ;
367+ expect ( focusManager . focusPath ) . toEqual ( [ mainElement ] ) ;
368+
369+ // Try to pop the main layer - should not do anything
370+ focusManager . popLayer ( ) ;
371+ expect ( focusManager . focusPath ) . toEqual ( [ mainElement ] ) ;
372+ expect ( mainElement . focused ) . toBe ( true ) ;
373+ } ) ;
374+
375+ it ( 'should close all layers except main when popAllLayers is called' , ( ) => {
376+ const mainElement = createMockElement ( 1 , 'main' ) ;
377+ const modal1Element = createMockElement ( 2 , 'modal1' ) ;
378+ const modal2Element = createMockElement ( 3 , 'modal2' ) ;
379+ const modal3Element = createMockElement ( 4 , 'modal3' ) ;
380+
381+ // Setup main layer
382+ focusManager . addElement ( mainElement , null , { autoFocus : true } ) ;
383+
384+ // Push multiple modals
385+ focusManager . pushLayer ( modal1Element ) ;
386+ focusManager . pushLayer ( modal2Element ) ;
387+ focusManager . pushLayer ( modal3Element ) ;
388+ expect ( focusManager . focusPath ) . toEqual ( [ modal3Element ] ) ;
389+
390+ // Pop all layers
391+ focusManager . popAllLayers ( ) ;
392+ expect ( focusManager . focusPath ) . toEqual ( [ mainElement ] ) ;
393+ expect ( modal1Element . focused ) . toBe ( false ) ;
394+ expect ( modal2Element . focused ) . toBe ( false ) ;
395+ expect ( modal3Element . focused ) . toBe ( false ) ;
396+ expect ( mainElement . focused ) . toBe ( true ) ;
397+ } ) ;
398+
399+ it ( 'should emit modalOpened and modalClosed events' , ( ) => {
400+ const mainElement = createMockElement ( 1 , 'main' ) ;
401+ const modalElement = createMockElement ( 2 , 'modal' ) ;
402+ const modalOpenedSpy = vi . fn ( ) ;
403+ const modalClosedSpy = vi . fn ( ) ;
404+
405+ focusManager . on ( 'modalOpened' , modalOpenedSpy ) ;
406+ focusManager . on ( 'modalClosed' , modalClosedSpy ) ;
407+
408+ // Setup main layer
409+ focusManager . addElement ( mainElement , null , { autoFocus : true } ) ;
410+
411+ // Push modal layer
412+ focusManager . pushLayer ( modalElement ) ;
413+ expect ( modalOpenedSpy ) . toHaveBeenCalledWith (
414+ modalElement ,
415+ undefined ,
416+ undefined ,
417+ undefined ,
418+ undefined ,
419+ ) ;
420+
421+ // Pop modal layer
422+ focusManager . popLayer ( ) ;
423+ expect ( modalClosedSpy ) . toHaveBeenCalledWith (
424+ modalElement ,
425+ undefined ,
426+ undefined ,
427+ undefined ,
428+ undefined ,
429+ ) ;
430+ } ) ;
431+
432+ it ( 'should handle complex hierarchies within layers' , ( ) => {
433+ const mainParent = createMockElement ( 1 , 'mainParent' ) ;
434+ const mainChild = createMockElement ( 2 , 'mainChild' ) ;
435+ const modalParent = createMockElement ( 3 , 'modalParent' ) ;
436+ const modalChild1 = createMockElement ( 4 , 'modalChild1' ) ;
437+ const modalChild2 = createMockElement ( 5 , 'modalChild2' ) ;
438+
439+ // Setup main layer hierarchy
440+ focusManager . addElement ( mainParent , null , { autoFocus : true } ) ;
441+ focusManager . addElement ( mainChild , mainParent , { autoFocus : true } ) ;
442+ expect ( focusManager . focusPath ) . toEqual ( [ mainParent , mainChild ] ) ;
443+
444+ // Push modal with hierarchy
445+ focusManager . pushLayer ( modalParent ) ;
446+ focusManager . addElement ( modalChild1 , modalParent , { autoFocus : true } ) ;
447+ focusManager . addElement ( modalChild2 , modalParent , { autoFocus : false } ) ;
448+ expect ( focusManager . focusPath ) . toEqual ( [ modalParent , modalChild1 ] ) ;
449+
450+ // Focus different child in modal
451+ focusManager . focus ( modalChild2 ) ;
452+ expect ( focusManager . focusPath ) . toEqual ( [ modalParent , modalChild2 ] ) ;
453+
454+ // Pop modal and verify main layer is restored
455+ focusManager . popLayer ( ) ;
456+ expect ( focusManager . focusPath ) . toEqual ( [ mainParent , mainChild ] ) ;
457+ } ) ;
458+
459+ it ( 'should properly handle element removal within layers' , ( ) => {
460+ const mainElement = createMockElement ( 1 , 'main' ) ;
461+ const modalParent = createMockElement ( 2 , 'modalParent' ) ;
462+ const modalChild1 = createMockElement ( 3 , 'modalChild1' ) ;
463+ const modalChild2 = createMockElement ( 4 , 'modalChild2' ) ;
464+
465+ // Setup main layer
466+ focusManager . addElement ( mainElement , null , { autoFocus : true } ) ;
467+
468+ // Push modal with children
469+ focusManager . pushLayer ( modalParent ) ;
470+ focusManager . addElement ( modalChild1 , modalParent , { autoFocus : true } ) ;
471+ focusManager . addElement ( modalChild2 , modalParent , { autoFocus : false } ) ;
472+ expect ( focusManager . focusPath ) . toEqual ( [ modalParent , modalChild1 ] ) ;
473+
474+ // Remove focused child, should focus next available
475+ focusManager . removeElement ( modalChild1 ) ;
476+ expect ( focusManager . focusPath ) . toEqual ( [ modalParent , modalChild2 ] ) ;
477+
478+ // Remove last child, should focus parent
479+ focusManager . removeElement ( modalChild2 ) ;
480+ expect ( focusManager . focusPath ) . toEqual ( [ modalParent ] ) ;
481+ } ) ;
482+ } ) ;
253483} ) ;
0 commit comments