@@ -22,6 +22,7 @@ import org.junit.Test
2222import org.junit.runner.RunWith
2323import org.robolectric.Robolectric
2424import org.robolectric.RobolectricTestRunner
25+ import org.robolectric.util.ReflectionHelpers
2526
2627@RunWith(RobolectricTestRunner ::class )
2728class ReactViewGroupTest {
@@ -260,6 +261,131 @@ class ReactViewGroupTest {
260261 assertThat(outer.hasOverlappingRendering()).isTrue()
261262 }
262263
264+ @Test
265+ fun `hidden descendants - leave keyboard focus search` () {
266+ val root = ReactViewGroup (context)
267+ val hidden = containerView(top = 0 )
268+ val hiddenControl = focusableView(top = 0 )
269+ val shown = focusableView(top = 20 )
270+ hidden.addView(hiddenControl)
271+ root.addView(hidden)
272+ root.addView(shown)
273+
274+ ReactViewManager ().setImportantForAccessibility(hidden, " no-hide-descendants" )
275+
276+ assertThat(collectKeyboardFocusables(root)).containsExactly(shown)
277+ }
278+
279+ @Test
280+ fun `hidden descendants - return to keyboard focus search once shown again` () {
281+ val root = ReactViewGroup (context)
282+ val hidden = containerView(top = 0 )
283+ val hiddenControl = focusableView(top = 0 )
284+ val shown = focusableView(top = 20 )
285+ hidden.addView(hiddenControl)
286+ root.addView(hidden)
287+ root.addView(shown)
288+ val manager = ReactViewManager ()
289+ manager.setImportantForAccessibility(hidden, " no-hide-descendants" )
290+
291+ manager.setImportantForAccessibility(hidden, null )
292+
293+ assertThat(collectKeyboardFocusables(root)).containsExactly(hiddenControl, shown)
294+ }
295+
296+ @Test
297+ fun `hidden descendants - default focus passes over them` () {
298+ val root = ReactViewGroup (context)
299+ val hidden = containerView(top = 0 )
300+ val hiddenControl = focusableView(top = 0 )
301+ val shown = focusableView(top = 20 )
302+ hidden.addView(hiddenControl)
303+ root.addView(hidden)
304+ root.addView(shown)
305+ ReactViewManager ().setImportantForAccessibility(hidden, " no-hide-descendants" )
306+
307+ assertThat(root.requestFocus(View .FOCUS_DOWN )).isTrue()
308+
309+ assertThat(hiddenControl.isFocused).isFalse()
310+ assertThat(shown.isFocused).isTrue()
311+ }
312+
313+ @Test
314+ fun `hidden descendants - a descendant keyboard focus sits on gives it up as they hide` () {
315+ val root = ReactViewGroup (context)
316+ val hidden = containerView(top = 0 )
317+ val hiddenControl = focusableView(top = 0 )
318+ hidden.addView(hiddenControl)
319+ root.addView(hidden)
320+ Robolectric .buildActivity(Activity ::class .java).setup().get().setContentView(root)
321+ assertThat(hiddenControl.requestFocus()).isTrue()
322+
323+ ReactViewManager ().setImportantForAccessibility(hidden, " no-hide-descendants" )
324+
325+ assertThat(hiddenControl.isFocused).isFalse()
326+ }
327+
328+ @Test
329+ fun `hidden descendants - still take focus directly, as a tap or a focus() call gives it` () {
330+ val root = ReactViewGroup (context)
331+ val hidden = containerView(top = 0 )
332+ val hiddenControl = focusableView(top = 0 )
333+ hidden.addView(hiddenControl)
334+ root.addView(hidden)
335+ Robolectric .buildActivity(Activity ::class .java).setup().get().setContentView(root)
336+ ReactViewManager ().setImportantForAccessibility(hidden, " no-hide-descendants" )
337+
338+ assertThat(hiddenControl.requestFocus()).isTrue()
339+ }
340+
341+ @Test
342+ fun `hidden descendants - a descendant focused by touch keeps its focus as they hide` () {
343+ val root = ReactViewGroup (context)
344+ val hidden = containerView(top = 0 )
345+ val field = focusableView(top = 0 ).apply { isFocusableInTouchMode = true }
346+ hidden.addView(field)
347+ root.addView(hidden)
348+ Robolectric .buildActivity(Activity ::class .java).setup().get().setContentView(root)
349+ enterTouchMode(root)
350+ assertThat(field.requestFocus()).isTrue()
351+
352+ ReactViewManager ().setImportantForAccessibility(hidden, " no-hide-descendants" )
353+
354+ assertThat(field.isFocused).isTrue()
355+ }
356+
357+ @Test
358+ fun `hidden descendants - keyboard navigation moves on from one that keeps its focus` () {
359+ val root = ReactViewGroup (context)
360+ val hidden = containerView(top = 0 )
361+ val field = focusableView(top = 0 ).apply { isFocusableInTouchMode = true }
362+ val otherHiddenControl = focusableView(top = 0 )
363+ val shown = focusableView(top = 20 )
364+ hidden.addView(field)
365+ hidden.addView(otherHiddenControl)
366+ root.addView(hidden)
367+ root.addView(shown)
368+ Robolectric .buildActivity(Activity ::class .java).setup().get().setContentView(root)
369+ enterTouchMode(root)
370+ assertThat(field.requestFocus()).isTrue()
371+
372+ ReactViewManager ().setImportantForAccessibility(hidden, " no-hide-descendants" )
373+
374+ assertThat(collectKeyboardFocusables(root)).containsExactly(field, shown)
375+ }
376+
377+ @Test
378+ fun `hidden descendants - leave the view's own descendant focusability as it was` () {
379+ val hidden =
380+ containerView(top = 0 ).apply { descendantFocusability = ViewGroup .FOCUS_AFTER_DESCENDANTS }
381+ val manager = ReactViewManager ()
382+
383+ manager.setImportantForAccessibility(hidden, " no-hide-descendants" )
384+ manager.setImportantForAccessibility(hidden, null )
385+
386+ assertThat(hidden.descendantFocusability).isEqualTo(ViewGroup .FOCUS_AFTER_DESCENDANTS )
387+ }
388+
263389 // Mirrors the ViewManager mount/unmount hooks: attach/detach the child, then refresh the cache.
264390 private fun mount (parent : ReactViewGroup , child : View ) {
265391 parent.addView(child)
@@ -280,6 +406,27 @@ class ReactViewGroupTest {
280406 }
281407
282408 private fun elevatedChild (): View = View (context).apply { elevation = 10f }
409+
410+ private fun containerView (top : Int ): ReactViewGroup =
411+ ReactViewGroup (context).apply { layout(0 , top, 100 , top + 10 ) }
412+
413+ private fun focusableView (top : Int ): View =
414+ View (context).apply {
415+ isFocusable = true
416+ layout(0 , top, 100 , top + 10 )
417+ }
418+
419+ private fun collectKeyboardFocusables (root : ViewGroup ): List <View > {
420+ val focusables = ArrayList <View >()
421+ root.addFocusables(focusables, View .FOCUS_FORWARD , View .FOCUSABLES_ALL )
422+ return focusables
423+ }
424+
425+ // A touch puts the window in touch mode; Robolectric's windows start out of it.
426+ private fun enterTouchMode (attached : View ) {
427+ val attachInfo = ReflectionHelpers .getField<Any >(attached, " mAttachInfo" )
428+ ReflectionHelpers .setField(attachInfo, " mInTouchMode" , true )
429+ }
283430}
284431
285432class TestView (context : Context , yPos : Int ) : View(context) {
0 commit comments