@@ -323,6 +323,252 @@ describe("Events", () => {
323323 } ) ;
324324} ) ;
325325
326+ describe ( "Keyboard Interactions" , ( ) => {
327+ it ( "Enter key down expands/collapses panel" , ( ) => {
328+ cy . mount ( < Panel headerText = "Panel" onToggle = { cy . stub ( ) . as ( "toggleEvent" ) } >
329+ < Title level = { TitleLevel . H4 } > Content</ Title >
330+ </ Panel > ) ;
331+
332+ cy . get ( "[ui5-panel]" )
333+ . shadow ( )
334+ . find ( ".ui5-panel-header" )
335+ . as ( "header" ) ;
336+
337+ cy . get ( "[ui5-panel]" )
338+ . shadow ( )
339+ . find ( ".ui5-panel-content" )
340+ . as ( "content" ) ;
341+
342+ // Initial state - expanded
343+ cy . get ( "@content" )
344+ . should ( "be.visible" ) ;
345+
346+ // Press Enter - should trigger toggle immediately
347+ cy . get ( "@header" )
348+ . focus ( )
349+ . realPress ( "Enter" ) ;
350+
351+ // eslint-disable-next-line cypress/no-unnecessary-waiting
352+ cy . wait ( 50 ) ;
353+
354+ // Content should be collapsed after Enter
355+ cy . get ( "@content" )
356+ . should ( "not.be.visible" ) ;
357+
358+ cy . get ( "@toggleEvent" )
359+ . should ( "have.been.calledOnce" ) ;
360+
361+ // Press Enter again - should toggle back to expanded
362+ cy . get ( "@header" )
363+ . realPress ( "Enter" ) ;
364+
365+ // eslint-disable-next-line cypress/no-unnecessary-waiting
366+ cy . wait ( 50 ) ;
367+
368+ // Content should be visible again
369+ cy . get ( "@content" )
370+ . should ( "be.visible" ) ;
371+
372+ cy . get ( "@toggleEvent" )
373+ . should ( "have.been.calledTwice" ) ;
374+ } ) ;
375+
376+ it ( "Space key with Escape cancellation prevents toggle" , ( ) => {
377+ cy . mount ( < Panel headerText = "Panel" onToggle = { cy . stub ( ) . as ( "toggleEvent" ) } >
378+ < Title level = { TitleLevel . H4 } > Content</ Title >
379+ </ Panel > ) ;
380+
381+ cy . get ( "[ui5-panel]" )
382+ . shadow ( )
383+ . find ( ".ui5-panel-header" )
384+ . as ( "header" ) ;
385+
386+ cy . get ( "[ui5-panel]" )
387+ . shadow ( )
388+ . find ( ".ui5-panel-content" )
389+ . as ( "content" ) ;
390+
391+ // Initial state - expanded
392+ cy . get ( "@content" )
393+ . should ( "be.visible" ) ;
394+
395+ // Press and hold Space - this should set pending toggle but not execute yet
396+ cy . get ( "@header" )
397+ . focus ( )
398+ . realPress ( [ "Space" , "Escape" ] ) ;
399+
400+ // Content should still be visible (toggle was canceled by Escape)
401+ cy . get ( "@content" )
402+ . should ( "be.visible" ) ;
403+
404+ cy . get ( "@toggleEvent" )
405+ . should ( "not.have.been.called" ) ;
406+
407+ // Verify panel is still in expanded state
408+ cy . get ( "[ui5-panel]" )
409+ . should ( "not.have.attr" , "collapsed" ) ;
410+ } ) ;
411+
412+ it ( "Space key without Escape executes toggle" , ( ) => {
413+ cy . mount ( < Panel headerText = "Panel" onToggle = { cy . stub ( ) . as ( "toggleEvent" ) } >
414+ < Title level = { TitleLevel . H4 } > Content</ Title >
415+ </ Panel > ) ;
416+
417+ cy . get ( "[ui5-panel]" )
418+ . shadow ( )
419+ . find ( ".ui5-panel-header" )
420+ . as ( "header" ) ;
421+
422+ cy . get ( "[ui5-panel]" )
423+ . shadow ( )
424+ . find ( ".ui5-panel-content" )
425+ . as ( "content" ) ;
426+
427+ // Initial state - expanded
428+ cy . get ( "@content" )
429+ . should ( "be.visible" ) ;
430+
431+ // Press Space - should execute the toggle
432+ cy . get ( "@header" )
433+ . focus ( )
434+ . realPress ( "Space" ) ;
435+
436+ // eslint-disable-next-line cypress/no-unnecessary-waiting
437+ cy . wait ( 50 ) ;
438+
439+ // Content should now be collapsed
440+ cy . get ( "@content" )
441+ . should ( "not.be.visible" ) ;
442+
443+ cy . get ( "@toggleEvent" )
444+ . should ( "have.been.calledOnce" ) ;
445+
446+ // Verify panel is in collapsed state
447+ cy . get ( "[ui5-panel]" )
448+ . should ( "have.attr" , "collapsed" ) ;
449+ } ) ;
450+
451+ it ( "Space key interrupted by Escape does not toggle" , ( ) => {
452+ cy . mount ( < Panel headerText = "Panel" onToggle = { cy . stub ( ) . as ( "toggleEvent" ) } >
453+ < Title level = { TitleLevel . H4 } > Content</ Title >
454+ </ Panel > ) ;
455+
456+ cy . get ( "[ui5-panel]" )
457+ . shadow ( )
458+ . find ( ".ui5-panel-header" )
459+ . as ( "header" ) ;
460+
461+ cy . get ( "[ui5-panel]" )
462+ . shadow ( )
463+ . find ( ".ui5-panel-content" )
464+ . as ( "content" ) ;
465+
466+ // Test the Space + Escape cancellation behavior
467+ cy . get ( "@header" )
468+ . focus ( )
469+ . realPress ( [ "Space" , "Escape" ] ) ;
470+
471+ // Should not have toggled because Escape canceled the Space action
472+ cy . get ( "@content" )
473+ . should ( "be.visible" ) ;
474+
475+ cy . get ( "@toggleEvent" )
476+ . should ( "not.have.been.called" ) ;
477+ } ) ;
478+
479+ it ( "Fixed panel (should not toggle)" , ( ) => {
480+ cy . mount ( < Panel headerText = "Fixed Panel" fixed = { true } onToggle = { cy . stub ( ) . as ( "toggleEvent" ) } >
481+ < Title level = { TitleLevel . H4 } > Content</ Title >
482+ </ Panel > ) ;
483+
484+ cy . get ( "[ui5-panel]" )
485+ . shadow ( )
486+ . find ( ".ui5-panel-header" )
487+ . as ( "header" ) ;
488+
489+ cy . get ( "[ui5-panel]" )
490+ . shadow ( )
491+ . find ( ".ui5-panel-content" )
492+ . as ( "content" ) ;
493+
494+ // Content should be visible
495+ cy . get ( "@content" )
496+ . should ( "be.visible" ) ;
497+
498+ // Try Enter - should not toggle fixed panel
499+ cy . get ( "@header" )
500+ . focus ( )
501+ . realPress ( "Enter" ) ;
502+
503+ cy . get ( "@content" )
504+ . should ( "be.visible" ) ;
505+
506+ cy . get ( "@toggleEvent" )
507+ . should ( "not.have.been.called" ) ;
508+
509+ // Try Space - should not toggle fixed panel
510+ cy . get ( "@header" )
511+ . realPress ( "Space" ) ;
512+
513+ cy . get ( "@content" )
514+ . should ( "be.visible" ) ;
515+
516+ cy . get ( "@toggleEvent" )
517+ . should ( "not.have.been.called" ) ;
518+ } ) ;
519+
520+ it ( "Custom header (only button should work)" , ( ) => {
521+ cy . mount ( < Panel onToggle = { cy . stub ( ) . as ( "toggleEvent" ) } >
522+ < div slot = "header" >
523+ < Title level = { TitleLevel . H2 } > Custom Header</ Title >
524+ </ div >
525+ < Title level = { TitleLevel . H3 } > Content</ Title >
526+ </ Panel > ) ;
527+
528+ cy . get ( "[ui5-panel]" )
529+ . shadow ( )
530+ . find ( ".ui5-panel-header" )
531+ . as ( "header" ) ;
532+
533+ cy . get ( "[ui5-panel]" )
534+ . shadow ( )
535+ . find ( ".ui5-panel-header-button" )
536+ . as ( "toggleButton" ) ;
537+
538+ cy . get ( "[ui5-panel]" )
539+ . shadow ( )
540+ . find ( ".ui5-panel-content" )
541+ . as ( "content" ) ;
542+
543+ // Enter on custom header area should not toggle
544+ cy . get ( "@header" )
545+ . focus ( )
546+ . realPress ( "Enter" ) ;
547+
548+ cy . get ( "@content" )
549+ . should ( "be.visible" ) ;
550+
551+ cy . get ( "@toggleEvent" )
552+ . should ( "not.have.been.called" ) ;
553+
554+ // Enter on toggle button should work
555+ cy . get ( "@toggleButton" )
556+ . shadow ( )
557+ . find ( ".ui5-button-root" )
558+ . focus ( )
559+ . realPress ( "Enter" ) ;
560+
561+ // eslint-disable-next-line cypress/no-unnecessary-waiting
562+ cy . wait ( 50 ) ;
563+
564+ cy . get ( "@content" )
565+ . should ( "not.be.visible" ) ;
566+
567+ cy . get ( "@toggleEvent" )
568+ . should ( "have.been.calledOnce" ) ;
569+ } ) ;
570+ } ) ;
571+
326572describe ( "Accessibility" , ( ) => {
327573 it ( "Aria attributes on default header" , ( ) => {
328574 cy . mount ( < Panel headerText = "Panel" headerLevel = { TitleLevel . H3 } >
0 commit comments