@@ -981,6 +981,114 @@ TEST_CASE("StateMachineInstance::clearFocus clears internal focus manager",
981981 CHECK (state.expectsKeyboardInput == false );
982982}
983983
984+ TEST_CASE (" FocusManager setFocus on a scope descends to first leaf" ,
985+ " [FocusManager]" )
986+ {
987+ FocusManager manager;
988+ auto scope = make_rcp<FocusNode>();
989+ auto leaf1 = make_rcp<FocusNode>();
990+ auto leaf2 = make_rcp<FocusNode>();
991+
992+ manager.addChild (nullptr , scope);
993+ manager.addChild (scope, leaf1);
994+ manager.addChild (scope, leaf2);
995+
996+ // Focusing the scope resolves to its first eligible leaf.
997+ manager.setFocus (scope);
998+ CHECK (manager.primaryFocus () == leaf1);
999+ }
1000+
1001+ TEST_CASE (" FocusManager setFocus on a scope descends depth-first" ,
1002+ " [FocusManager]" )
1003+ {
1004+ FocusManager manager;
1005+ auto scope = make_rcp<FocusNode>();
1006+ auto row = make_rcp<FocusNode>();
1007+ auto leaf = make_rcp<FocusNode>();
1008+ auto sibling = make_rcp<FocusNode>();
1009+
1010+ manager.addChild (nullptr , scope);
1011+ manager.addChild (scope, row);
1012+ manager.addChild (row, leaf);
1013+ manager.addChild (scope, sibling);
1014+
1015+ // Depth-first: first leaf is the leaf nested under the first child (row).
1016+ manager.setFocus (scope);
1017+ CHECK (manager.primaryFocus () == leaf);
1018+ }
1019+
1020+ TEST_CASE (" FocusManager setFocus on a scope with no eligible leaf falls back" ,
1021+ " [FocusManager]" )
1022+ {
1023+ FocusManager manager;
1024+ auto scope = make_rcp<FocusNode>();
1025+ auto child = make_rcp<FocusNode>();
1026+ // Child cannot be traversed, so the scope has no eligible leaf to descend
1027+ // to. The scope itself remains the focus target (preserves prior behavior).
1028+ child->canTraverse (false );
1029+
1030+ manager.addChild (nullptr , scope);
1031+ manager.addChild (scope, child);
1032+
1033+ manager.setFocus (scope);
1034+ CHECK (manager.primaryFocus () == scope);
1035+ }
1036+
1037+ TEST_CASE (" FocusManager setFocus on an ineligible scope is a no-op" ,
1038+ " [FocusManager]" )
1039+ {
1040+ FocusManager manager;
1041+ auto scope = make_rcp<FocusNode>();
1042+ auto leaf = make_rcp<FocusNode>();
1043+ // The requested target itself cannot be focused. Descent must not reach an
1044+ // eligible descendant — focus stays unchanged (no-op), matching the prior
1045+ // early-return guard behavior.
1046+ scope->canFocus (false );
1047+
1048+ manager.addChild (nullptr , scope);
1049+ manager.addChild (scope, leaf);
1050+
1051+ manager.setFocus (scope);
1052+ CHECK (manager.primaryFocus () == nullptr );
1053+ }
1054+
1055+ TEST_CASE (" FocusManager setFocus on a leaf is unchanged" , " [FocusManager]" )
1056+ {
1057+ FocusManager manager;
1058+ auto scope = make_rcp<FocusNode>();
1059+ auto leaf1 = make_rcp<FocusNode>();
1060+ auto leaf2 = make_rcp<FocusNode>();
1061+
1062+ manager.addChild (nullptr , scope);
1063+ manager.addChild (scope, leaf1);
1064+ manager.addChild (scope, leaf2);
1065+
1066+ // Directly focusing a leaf still focuses that exact leaf (no-op descent).
1067+ manager.setFocus (leaf2);
1068+ CHECK (manager.primaryFocus () == leaf2);
1069+ }
1070+
1071+ TEST_CASE (" FocusManager Tab after focusing a scope traverses leaf siblings" ,
1072+ " [FocusManager]" )
1073+ {
1074+ FocusManager manager;
1075+ auto scope = make_rcp<FocusNode>();
1076+ auto leaf1 = make_rcp<FocusNode>();
1077+ auto leaf2 = make_rcp<FocusNode>();
1078+
1079+ manager.addChild (nullptr , scope);
1080+ manager.addChild (scope, leaf1);
1081+ manager.addChild (scope, leaf2);
1082+
1083+ // Focusing the scope lands on the first leaf; Tab then advances to the
1084+ // scope's next leaf rather than skipping the scope's children.
1085+ manager.setFocus (scope);
1086+ CHECK (manager.primaryFocus () == leaf1);
1087+
1088+ manager.focusNext ();
1089+ CHECK (manager.primaryFocus () == leaf2);
1090+ }
1091+
9841092} // namespace rive
9851093
9861094TEST_CASE (" FocusManager skips collapsed nodes and fully transparent nodes" ,
0 commit comments