diff --git a/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/BrowserMenuController.kt b/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/BrowserMenuController.kt index 2d7ab77e4c4b..29c51996533f 100644 --- a/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/BrowserMenuController.kt +++ b/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/BrowserMenuController.kt @@ -40,10 +40,13 @@ class BrowserMenuController( /** * @param anchor The view on which to pin the popup window. * @param orientation The preferred orientation to show the popup window. + * @param forceOrientation When set to true, the orientation will be respected even when the + * menu doesn't fully fit. */ override fun show( anchor: View, - orientation: Orientation? + orientation: Orientation?, + forceOrientation: Boolean, ): PopupWindow { val view = MenuView(anchor.context).apply { // Show nested list if present, or the standard menu candidates list. @@ -56,7 +59,11 @@ class BrowserMenuController( view.onDismiss = ::dismiss view.onReopenMenu = ::reopenMenu setOnDismissListener(menuDismissListener) - displayPopup(view, anchor, orientation) + displayPopup(view, anchor, orientation, forceOrientation) + + if (orientation == Orientation.UP && forceOrientation) { + view.scrollOnceToTheBottom() + } }.also { currentPopupInfo = PopupMenuInfo( window = it, diff --git a/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/ext/PopupWindow.kt b/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/ext/PopupWindow.kt index 24aba9ee3bc7..f69209cc3448 100644 --- a/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/ext/PopupWindow.kt +++ b/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/ext/PopupWindow.kt @@ -16,6 +16,7 @@ internal fun PopupWindow.displayPopup( containerView: View, anchor: View, preferredOrientation: Orientation? = null, + forceOrientation: Boolean = false, ) { // Measure menu val spec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED) @@ -34,9 +35,9 @@ internal fun PopupWindow.displayPopup( // Try to use the preferred orientation, if doesn't fit fallback to the best fit. when { - preferredOrientation == Orientation.DOWN && fitsDown -> + preferredOrientation == Orientation.DOWN && (fitsDown || forceOrientation) -> showPopupWithDownOrientation(anchor, reversed) - preferredOrientation == Orientation.UP && fitsUp -> + preferredOrientation == Orientation.UP && (fitsUp || forceOrientation) -> showPopupWithUpOrientation(anchor, containerHeight, reversed) else -> { showPopupWhereBestFits( diff --git a/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/view/MenuView.kt b/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/view/MenuView.kt index 2100b6bedae8..bf62ecd2092d 100644 --- a/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/view/MenuView.kt +++ b/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/view/MenuView.kt @@ -11,7 +11,6 @@ import android.util.AttributeSet import android.view.LayoutInflater import android.view.View import android.widget.FrameLayout -import androidx.annotation.VisibleForTesting import androidx.cardview.widget.CardView import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView @@ -75,7 +74,7 @@ class MenuView @JvmOverloads constructor( // In devices with Android 6 and below stackFromEnd is not working properly, // as a result, we have to provided a backwards support. // See: https://github.com/mozilla-mobile/android-components/issues/3211 - if (side == Side.END) scrollOnceToTheBottom(recyclerView) + if (side == Side.END) scrollOnceToTheBottom() } } @@ -86,8 +85,10 @@ class MenuView @JvmOverloads constructor( style.backgroundColor?.let { cardView.setCardBackgroundColor(it) } } - @VisibleForTesting - internal fun scrollOnceToTheBottom(recyclerView: RecyclerView) { + /** + * Scroll to the bottom of the menu view. + */ + fun scrollOnceToTheBottom() { recyclerView.onNextGlobalLayout { recyclerView.adapter?.let { recyclerView.scrollToPosition(it.itemCount - 1) } } diff --git a/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/ext/PopupWindowTest.kt b/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/ext/PopupWindowTest.kt index 31162029b261..4b15b9518aba 100644 --- a/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/ext/PopupWindowTest.kt +++ b/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/ext/PopupWindowTest.kt @@ -134,6 +134,26 @@ class PopupWindowTest { verify(popupWindow).showAtLocation(anchor, Gravity.START or Gravity.TOP, 0, 10) } + @Test + fun `WHEN displaying force up popup from bottom left THEN show popup up and to the left`() { + anchor = createMockViewWith(x = 0, y = 190, false) + doReturn(300).`when`(menuContentView).measuredHeight + + popupWindow.displayPopup(menuContentView, anchor, Orientation.UP, true) + assertEquals(R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeftBottom, popupWindow.animationStyle) + verify(popupWindow).showAsDropDown(anchor, 0, -300) + } + + @Test + fun `WHEN displaying force down popup from top left THEN show popup down and to the right`() { + anchor = createMockViewWith(x = 0, y = 0, false) + doReturn(300).`when`(menuContentView).measuredHeight + + popupWindow.displayPopup(menuContentView, anchor, Orientation.DOWN, true) + assertEquals(R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeftTop, popupWindow.animationStyle) + verify(popupWindow).showAsDropDown(anchor, 0, 0) + } + private fun createMockViewWith(x: Int, y: Int, isRTL: Boolean): View { val view = spy(View(testContext)) doAnswer { invocation -> diff --git a/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/view/MenuViewTest.kt b/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/view/MenuViewTest.kt index 3732a5e4efc8..ef4ef3f1d4af 100644 --- a/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/view/MenuViewTest.kt +++ b/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/view/MenuViewTest.kt @@ -15,7 +15,6 @@ import mozilla.components.browser.menu2.R import mozilla.components.concept.menu.MenuStyle import mozilla.components.concept.menu.Side import mozilla.components.concept.menu.candidate.DecorativeTextMenuCandidate -import mozilla.components.support.test.any import mozilla.components.support.test.robolectric.testContext import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse @@ -63,25 +62,25 @@ class MenuViewTest { @Test @Config(sdk = [Build.VERSION_CODES.M]) fun `setVisibleSide will be forwarded to scrollOnceToTheBottom on devices with Android M and below`() { - doNothing().`when`(menuView).scrollOnceToTheBottom(any()) + doNothing().`when`(menuView).scrollOnceToTheBottom() menuView.setVisibleSide(Side.END) val layoutManager = recyclerView.layoutManager as LinearLayoutManager assertFalse(layoutManager.stackFromEnd) - verify(menuView).scrollOnceToTheBottom(any()) + verify(menuView).scrollOnceToTheBottom() } @Test @Config(sdk = [Build.VERSION_CODES.N]) fun `setVisibleSide changes stackFromEnd on devices with Android N and above`() { - doNothing().`when`(menuView).scrollOnceToTheBottom(any()) + doNothing().`when`(menuView).scrollOnceToTheBottom() menuView.setVisibleSide(Side.END) val layoutManager = recyclerView.layoutManager as LinearLayoutManager assertTrue(layoutManager.stackFromEnd) - verify(menuView, never()).scrollOnceToTheBottom(any()) + verify(menuView, never()).scrollOnceToTheBottom() } @Test diff --git a/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/MenuController.kt b/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/MenuController.kt index 3bc2f23c0677..727768bf4aba 100644 --- a/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/MenuController.kt +++ b/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/MenuController.kt @@ -17,8 +17,10 @@ interface MenuController : Observable { /** * @param anchor The view on which to pin the popup window. * @param orientation The preferred orientation to show the popup window. + * @param forceOrientation When set to true, the orientation will be respected even when the + * menu doesn't fully fit. */ - fun show(anchor: View, orientation: Orientation? = null): PopupWindow + fun show(anchor: View, orientation: Orientation? = null, forceOrientation: Boolean = false): PopupWindow /** * Dismiss the menu popup if the menu is visible.